Merge branch 'refs/heads/dev/前端页面修改'

This commit is contained in:
lin 2025-04-28 17:36:39 +08:00
commit 6b3423b932
341 changed files with 46314 additions and 311 deletions

View File

@ -12,12 +12,13 @@ WEB VIDEO PLATFORM是一个基于GB28181-2016标准实现的开箱即用的网
流媒体服务基于@夏楚 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)
前端页面基于@Kyle MediaServerUI [https://gitee.com/kkkkk5G/MediaServerUI](https://gitee.com/kkkkk5G/MediaServerUI) 进行修改.
播放器使用@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等)设备接入
支持非国标(onvif, rtsp, rtmp直播设备等等)设备接入,充分利旧。
支持rtsp, rtmp直播设备设备接入充分利旧。
支持国标级联。多平台级联。跨网视频预览。
支持跨网网闸平台互联。
@ -29,19 +30,31 @@ ZLM使用文档 [https://github.com/ZLMediaKit/ZLMediaKit](https://github.com/ZL
# 付费社群
[![社群](doc/_media/shequ.png "shequ")](https://t.zsxq.com/0d8VAD3Dm)
> 收费是为了提供更好的服务,也是对作者更大的激励。加入星球的用户三天后可以私信我留下微信号,我会拉大家入群。加入三天内不满意可以直接自行推出,星球会直接退款给大家。
> 星球还提供了基于主线master分支的打包, 会随时更新。
# gitee同步仓库
> 星球还提供了包括闭源的全功能试用包, 会随时更新。
# gitee仓库
https://gitee.com/pan648540858/wvp-GB28181-pro.git
# 截图
![index](doc/_media/index.png "index.png")
![2](doc/_media/2.png "2.png")
![3](doc/_media/3.png "3.png")
![3-1](doc/_media/3-1.png "3-1.png")
![3-2](doc/_media/3-2.png "3-2.png")
![build_1](https://images.gitee.com/uploads/images/2022/0304/101919_ee5b8c79_1018729.png "2022-03-04_10-13.png")
![运维中心](doc/_media/log.jpg "log.jpg")
<table>
<tr>
<td ><center><img src="doc/_media/1.png" >登录页面 </center></td>
<td ><center><img src="doc/_media/2.png" >首页</center></td>
</tr>
<tr>
<td ><center><img src="doc/_media/3.png" >分屏播放 </center></td>
<td ><center><img src="doc/_media/4.png" >国标设备列表</center></td>
</tr>
<tr>
<td ><center><img src="doc/_media/5.png" >行政区划管理 </center></td>
<td ><center><img src="doc/_media/8.png" >业务分组管理</center></td>
</tr>
<tr>
<td ><center><img src="doc/_media/6.png" >录制计划</center></td>
<td ><center><img src="doc/_media/7.png" >平台信息</center></td>
</tr>
</table>
# 功能特性
- [X] 集成web界面
@ -117,10 +130,10 @@ https://gitee.com/pan648540858/wvp-GB28181-pro.git
- [X] 支持国标信令集群
# 非开源的内容
- [X] ONVIF设备的接入支持点播云台控制国标级联点播自动点播。试用安装包以及使用教程: [知识星球](https://t.zsxq.com/10WAnH2MP),没有使用时间限制,需要源码可以星球私信我或者邮箱联系。
# 闭源内容
- [X] ONVIF设备的接入支持点播云台控制国标级联点播自动点播。
- [X] 支持部标1078+808协议支持点播云台控制录像回放位置上报自动点播。
- [X] 支持国标28181-2022协议支持巡航轨迹查询PTZ精准控制存储卡格式化设备软件升级OSD配置h265+aac支持辅码流录像倒放等。具体的功能列表可在[知识星球](https://t.zsxq.com/18GXkpkqs)查看,试用安装包: [知识星球](https://t.zsxq.com/UJ6V3),没有使用时间限制,需要源码可以星球私信我或者邮箱联系。
- [X] 支持国标28181-2022协议支持巡航轨迹查询PTZ精准控制存储卡格式化设备软件升级OSD配置h265+aac支持辅码流录像倒放等。
# 授权协议
@ -131,19 +144,14 @@ https://gitee.com/pan648540858/wvp-GB28181-pro.git
[知识星球](https://t.zsxq.com/0d8VAD3Dm)专栏列表:,
- [使用入门系列一WVP-PRO能做什么](https://t.zsxq.com/0dLguVoSp)
有偿技术支持请发送邮件到648540858@qq.com
有偿技术支持,一对一开发辅导,闭源内容合作请发送邮件到648540858@qq.com咨询
# 致谢
感谢作者[夏楚](https://github.com/xia-chu) 提供这么棒的开源流媒体服务框架,并在开发过程中给予支持与帮助。
感谢作者[dexter langhuihui](https://github.com/langhuihui) 开源这么好用的WEB播放器。
感谢作者[Kyle](https://gitee.com/kkkkk5G) 开源了好用的前端页面
感谢作者[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)
同时感谢JetBrains对开源项目的支持本项目使用IntelliJ IDEA开发与调试
![JetBrains](https://resources.jetbrains.com/storage/products/company/brand/logos/IntelliJ_IDEA_icon.svg?_ga=2.143694769.529214288.1712023294-439039083.1711422571&_gl=1*102dv9n*_ga*NDM5MDM5MDgzLjE3MTE0MjI1NzE.*_ga_9J976DJZ68*MTcxMjEyNjg4NC45LjEuMTcxMjEyNzc2My4zMy4wLjA.)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 134 KiB

After

Width:  |  Height:  |  Size: 146 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 96 KiB

After

Width:  |  Height:  |  Size: 122 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 84 KiB

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 81 KiB

After

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 124 KiB

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 KiB

After

Width:  |  Height:  |  Size: 214 KiB

View File

@ -8,15 +8,18 @@
左侧树结构为行政区划结构, 通过数据鼠标右键可以操作,包括: 刷新节点,新建节点,编辑节点,删除节点,添加设备(
可以将某个国标设备下的通道全部添加道某一个节点下),移除设备(可以将某个国标设备下的通道全部从这个节点移除)
右侧伪通道列表, 对于非国标接入的设备只有配置了国标编号后才可以在这里进行操作, 添加状态选择"未添加"可以进行添加操作,选择"
已添加"进行移除操作
![刷新](_media/img_21.png)
右侧伪通道列表, 对于非国标接入的设备只有配置了国标编号后才可以在这里进行操作
选择左侧的节点后,可以点击右侧的“添加通道”, 选择需要的通道添加到改节点下,如果找不到通道, 可以选择“异常挂载通道”,点击清理后重新回来选择。
![行政区划](_media/img_21.png)
## 业务分组
左侧树结构为业务分组结构, 通过数据鼠标右键可以操作,包括: 刷新节点,新建节点,编辑节点,删除节点,添加设备(
可以将某个国标设备下的通道全部添加道某一个节点下),移除设备(可以将某个国标设备下的通道全部从这个节点移除)
业务分组下不能挂载设备,所以没有选择该节点的单选框.
右侧伪通道列表, 对于非国标接入的设备只有配置了国标编号后才可以在这里进行操作, 添加状态选择"未添加"可以进行添加操作,选择"
已添加"进行移除操作
![刷新](_media/img_22.png)
右侧为通道列表, 对于非国标接入的设备只有配置了国标编号后才可以在这里进行操作。
选择左侧的节点后,可以点击右侧的“添加通道”, 选择需要的通道添加到改节点下。
如果找不到通道, 可以选择“异常挂载通道”,点击清理后重新回来选择。
注意,根资源组下的那一级为业务分组类型不可以直接挂载设备,需要继续建立节点,后续的节点的都是虚拟组织类型, 就可以挂载通道了。
![业务分组](_media/img_22.png)

View File

@ -2,7 +2,7 @@
# 云端录像
![云端录像](_media/img_23.png)
![云端录像](_media/img_26.png)
云端录像是对录制在zlm服务下的录像文件的管理录像的文件路径默认在ZLM/www/record下。
- 国标设备是否录像: 可以再WVP的配置中user-settings.record-sip设置为true那么每次点播以及录像回放都会录像;

View File

@ -2,7 +2,7 @@
# 节点管理
![节点管理](_media/img_23.png)
![节点管理](_media/img_26.png)
WVP支持单个WVP多个ZLM的方案来扩展WVP的视频并发能力并发点播是因为带宽和性能的原因单个ZLM节点能支持的路数有限所以WVP增加了ZLM集群来扩展并发并且保证ZLM的高可用。

Binary file not shown.

Before

Width:  |  Height:  |  Size: 112 KiB

After

Width:  |  Height:  |  Size: 180 KiB

View File

@ -87,9 +87,9 @@ git clone https://github.com/648540858/wvp-GB28181-pro.git
### 5.2 编译前端页面
```shell script
cd wvp-GB28181-pro/web_src/
cd wvp-GB28181-pro/web/
npm --registry=https://registry.npmmirror.com install
npm run build
npm npm run build:prod
```
编译如果报错, 一般都是网络问题, 导致的依赖包下载失败

View File

@ -44,15 +44,12 @@ wvp支持多种数据库包括MysqlPostgresql金仓等配置任选
```yaml
spring:
dynamic:
primary: master
datasource:
master:
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
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数据库配置
@ -61,14 +58,12 @@ spring:
```yaml
spring:
dynamic:
primary: master
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
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
@ -80,14 +75,13 @@ pagehelper:
```yaml
spring:
dynamic:
primary: master
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
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

View File

@ -41,19 +41,7 @@ nohup ./MediaServer -d -m 3 &
### 前后端分离部署
前后端部署目前在最新的版本已经支持请使用3月15日之后的版本部署
前端编译后的文件在`src/main/resources/static`中,将此目录下的文件部署。
WVP默认开启全部接口支持跨域。部署前端文件到WEB容器并将访问的地址设置为WVP的地址即可。
**配置前端服务器**
1. 在`src/main/resources/static/static/js/config.js`下配置服务器的地址也就是wvp服务的地址
```javascript
window.baseUrl = "http://xxx.com:18080"
```
`这里的地址是需要客户电脑能访问到的,因为请求是客户端电脑发起,与代理不同`
[接入设备](./_content/ability/device.md)
前端基于 [vue-admin-template](https://github.com/PanJiaChen/vue-admin-template/blob/master/README-zh.md) 构建, 参考这儿即可。
### 默认账号和密码

BIN
doc/_media/1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 938 KiB

After

Width:  |  Height:  |  Size: 101 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 125 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 920 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 146 KiB

After

Width:  |  Height:  |  Size: 1.0 MiB

BIN
doc/_media/4.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 166 KiB

BIN
doc/_media/5.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 103 KiB

BIN
doc/_media/6.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

BIN
doc/_media/7.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 110 KiB

BIN
doc/_media/8.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 96 KiB

View File

@ -10,7 +10,7 @@
* [推流列表](_content/ability/push.md)
* [拉流代理](_content/ability/proxy.md)
* [云端录像](_content/ability/cloud_record.md)
* [节点管理](_content/ability/node_manger.md)
* [节点管理](_content/ability/node_manager.md)
* [通道管理](_content/ability/channel.md)
* [国标级联](_content/ability/cascade2.md)
* **流程与原理**

57
install.sh Normal file
View File

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

View File

@ -372,6 +372,7 @@
<version>2.7.2</version>
<configuration>
<includeSystemScope>true</includeSystemScope>
<executable>true</executable>
</configuration>
</plugin>
@ -422,6 +423,7 @@
<exclude>**/application.yml</exclude>
<exclude>**/application-*.yml</exclude>
<exclude>**/local.jks</exclude>
<exclude>**/install.sh</exclude>
</excludes>
</configuration>
</plugin>
@ -441,6 +443,7 @@
<includes>
<include>application.yml</include>
<include>application-*.yml</include>
<include>install.sh</include>
</includes>
</resource>
</resources>

View File

@ -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 + '\'' +
'}';
}
}

View File

@ -23,7 +23,7 @@ import java.nio.file.Files;
*/
@Slf4j
@Configuration
@Order(value=14)
@Order(value=15)
public class CivilCodeFileConf implements CommandLineRunner {
@Autowired

View File

@ -1,6 +1,7 @@
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;
@ -59,11 +60,14 @@ public class CloudRecordTimer {
// TODO 后续可以删除空了的过期日期文件夹
for (CloudRecordItem cloudRecordItem : cloudRecordItemList) {
String date = new File(cloudRecordItem.getFilePath()).getParentFile().getName();
boolean deleteResult = mediaServerService.deleteRecordDirectory(mediaServerItem, cloudRecordItem.getApp(),
cloudRecordItem.getStream(), date, cloudRecordItem.getFileName());
if (deleteResult) {
log.warn("[录像文件定时清理] 删除磁盘文件成功: {}", cloudRecordItem.getFilePath());
}
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);
}

View File

@ -30,6 +30,7 @@ public class SpringDocConfig {
Contact contact = new Contact();
contact.setName("pan");
contact.setEmail("648540858@qq.com");
return new OpenAPI()
.components(new Components()
.addSecuritySchemes(JwtUtils.HEADER, new SecurityScheme()
@ -37,7 +38,11 @@ public class SpringDocConfig {
.bearerFormat("JWT")))
.info(new Info().title("WVP-PRO 接口文档")
.contact(contact)
.description("开箱即用的28181协议视频平台")
.description("开箱即用的28181协议视频平台。 <br/>" +
"1. 打开http://127.0.0.1:18080/doc.html#/1.%20全部/用户管理/login_1" +
" 登录成功后返回AccessToken。 <br/>" +
"2. 填写到AccessToken到参数值 http://127.0.0.1:18080/doc.html#/Authorize/1.%20全部 <br/>" +
"后续接口就可以直接测试了")
.version("v3.1.0")
.license(new License().name("Apache 2.0").url("http://springdoc.org")));
}

View File

@ -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<Object, Object> 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){}
}
}

View File

@ -178,7 +178,7 @@ public class UserSetting {
/**
* 登录超时时间(分钟)
*/
private long loginTimeout = 30;
private long loginTimeout = 60;
/**
* jwk文件路径若不指定则使用resources目录下的jwk.json

View File

@ -97,7 +97,6 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter {
// return;
default:
}
// 构建UsernamePasswordAuthenticationToken,这里密码为null是因为提供了正确的JWT,实现自动登录
User user = new User();
user.setId(jwtUser.getUserId());

View File

@ -11,7 +11,6 @@ import org.springframework.security.authentication.dao.DaoAuthenticationProvider
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
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.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
@ -24,7 +23,6 @@ import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
/**
@ -58,33 +56,6 @@ public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
private JwtAuthenticationFilter jwtAuthenticationFilter;
/**
* 描述: 静态资源放行这里的放行是不走 Spring Security 过滤器链
**/
@Override
public void configure(WebSecurity web) {
if (userSetting.getInterfaceAuthentication()) {
ArrayList<String> matchers = new ArrayList<>();
matchers.add("/");
matchers.add("/#/**");
matchers.add("/static/**");
matchers.add("/swagger-ui.html");
matchers.add("/swagger-ui/");
matchers.add("/index.html");
matchers.add("/doc.html");
matchers.add("/webjars/**");
matchers.add("/swagger-resources/**");
matchers.add("/v3/api-docs/**");
matchers.add("/js/**");
matchers.add("/api/device/query/snap/**");
matchers.add("/record_proxy/*/**");
matchers.add("/api/emit");
matchers.add("/favicon.ico");
// 可以直接访问的静态数据
web.ignoring().antMatchers(matchers.toArray(new String[0]));
}
}
/**
* 配置认证方式
*
@ -105,15 +76,36 @@ public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
List<String> defaultExcludes = new ArrayList<>();
defaultExcludes.add("/");
defaultExcludes.add("/#/**");
defaultExcludes.add("/static/**");
List<String> defaultExcludes = userSetting.getInterfaceAuthenticationExcludes();
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("/swagger-ui/**");
defaultExcludes.add("/doc.html#/**");
// defaultExcludes.add("/channel/log");
if (userSetting.getInterfaceAuthentication() && !userSetting.getInterfaceAuthenticationExcludes().isEmpty()) {
defaultExcludes.addAll(userSetting.getInterfaceAuthenticationExcludes());
}
http.headers().contentTypeOptions().disable()
.and().cors().configurationSource(configurationSource())

View File

@ -2,6 +2,8 @@ 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;
@ -19,8 +21,13 @@ public class LoginUser implements UserDetails, CredentialsContainer {
*/
private User user;
@Getter
@Setter
private String accessToken;
@Setter
@Getter
private String serverId;
/**
* 登录时间
@ -104,11 +111,4 @@ public class LoginUser implements UserDetails, CredentialsContainer {
return user.getPushKey();
}
public String getAccessToken() {
return accessToken;
}
public void setAccessToken(String accessToken) {
this.accessToken = accessToken;
}
}

View File

@ -79,7 +79,7 @@ public class DeviceQuery {
@Parameter(name = "deviceId", description = "设备国标编号", required = true)
@GetMapping("/devices/{deviceId}")
public Device devices(@PathVariable String deviceId){
return deviceService.getDeviceByDeviceId(deviceId);
}
@ -224,16 +224,19 @@ public class DeviceQuery {
"UDPudp传输TCP-ACTIVEtcp主动模式TCP-PASSIVEtcp被动模式", 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);
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(Device device){
@PostMapping("/device/add")
public void addDevice(@RequestBody Device device){
if (device == null || device.getDeviceId() == null) {
throw new ControllerException(ErrorCode.ERROR400);
@ -250,8 +253,8 @@ public class DeviceQuery {
@Operation(summary = "更新设备信息", security = @SecurityRequirement(name = JwtUtils.HEADER))
@Parameter(name = "device", description = "设备", required = true)
@PostMapping("/device/update/")
public void updateDevice(Device device){
@PostMapping("/device/update")
public void updateDevice(@RequestBody Device device){
if (device == null || device.getDeviceId() == null || device.getId() <= 0) {
throw new ControllerException(ErrorCode.ERROR400);
}

View File

@ -17,7 +17,6 @@ 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.stereotype.Controller;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletRequest;
@ -89,7 +88,7 @@ public class MediaController {
}
if (streamInfo != null){
return new StreamContent(streamInfo);
return new StreamContent(streamInfo);
}else {
//获取流失败重启拉流后重试一次
streamProxyService.stopByAppAndStream(app,stream);

View File

@ -215,7 +215,7 @@ public class PlaybackController {
@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", 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) {
log.info("playSpeed: "+streamId+", "+speed);
@ -225,10 +225,6 @@ public class PlaybackController {
log.warn("streamId不存在!");
throw new ControllerException(ErrorCode.ERROR400.getCode(), "streamId不存在");
}
if(speed != 0.25 && speed != 0.5 && speed != 1 && speed != 2.0 && speed != 4.0) {
log.warn("不支持的speed " + speed);
throw new ControllerException(ErrorCode.ERROR100.getCode(), "不支持的speed0.25 0.5 1、2、4");
}
Device device = deviceService.getDeviceByDeviceId(inviteInfo.getDeviceId());
DeviceChannel channel = channelService.getOneById(inviteInfo.getChannelId());
try {

View File

@ -23,7 +23,6 @@ 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.service.bean.ErrorCallback;
import com.genersoft.iot.vmp.storager.IRedisCatchStorage;
import com.genersoft.iot.vmp.utils.DateUtil;
import com.genersoft.iot.vmp.vmanager.bean.ErrorCode;
@ -41,9 +40,6 @@ import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.util.ObjectUtils;
import javax.sip.InvalidArgumentException;
import javax.sip.SipException;
import java.text.ParseException;
import javax.sip.InvalidArgumentException;
import javax.sip.SipException;
import javax.sip.message.Response;
@ -442,7 +438,7 @@ public class DeviceChannelServiceImpl implements IDeviceChannelService {
deviceMobilePositionMapper.insertNewPosition(mobilePosition);
}
if (deviceChannel.getDeviceId().equals(deviceChannel.getDeviceId())) {
if (deviceChannel.getDeviceId().equals(device.getDeviceId())) {
deviceChannel.setDeviceId(null);
}
if (deviceChannel.getGpsTime() == null) {

View File

@ -28,6 +28,7 @@ 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.*;
@ -1047,8 +1048,8 @@ public class PlayServiceImpl implements IPlayService {
null);
return;
}
log.info("[录像下载] deviceId: {}, channelId: {}, 下载速度:{}, 收流端口:{}, 收流模式:{}, SSRC: {}({}), SSRC校验{}",
device.getDeviceId(), channel.getDeviceId(), downloadSpeed, ssrcInfo.getPort(), device.getStreamMode(),
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());

View File

@ -44,7 +44,7 @@ public class MediaInfo {
private Integer audioChannels;
@Schema(description = "音频采样率")
private Integer audioSampleRate;
@Schema(description = "音频采样率")
@Schema(description = "时长")
private Long duration;
@Schema(description = "在线")
private Boolean online;

View File

@ -1,7 +1,9 @@
package com.genersoft.iot.vmp.media.bean;
import com.genersoft.iot.vmp.media.zlm.dto.hook.OnRecordMp4HookParam;
import lombok.Data;
@Data
public class RecordInfo {
private String fileName;
private String filePath;
@ -24,70 +26,6 @@ public class RecordInfo {
return recordInfo;
}
public String getFileName() {
return fileName;
}
public void setFileName(String fileName) {
this.fileName = fileName;
}
public String getFilePath() {
return filePath;
}
public void setFilePath(String filePath) {
this.filePath = filePath;
}
public long getFileSize() {
return fileSize;
}
public void setFileSize(long fileSize) {
this.fileSize = fileSize;
}
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 long getStartTime() {
return startTime;
}
public void setStartTime(long startTime) {
this.startTime = startTime;
}
public double getTimeLen() {
return timeLen;
}
public void setTimeLen(double timeLen) {
this.timeLen = timeLen;
}
public String getParams() {
return params;
}
public void setParams(String params) {
this.params = params;
}
@Override
public String toString() {
return "RecordInfo{" +

View File

@ -70,4 +70,9 @@ public interface IMediaNodeServerService {
List<String> listRtpServer(MediaServer mediaServer);
void loadMP4File(MediaServer mediaServer, String app, String stream, String datePath);
void seekRecordStamp(MediaServer mediaServer, String app, String stream, Double stamp, String schema);
void setRecordSpeed(MediaServer mediaServer, String app, String stream, Integer speed, String schema);
}

View File

@ -159,4 +159,10 @@ public interface IMediaServerService {
int createRTPServer(MediaServer mediaServerItem, String streamId, long ssrc, Integer port, boolean onlyAuto, boolean disableAudio, boolean reUsePort, Integer tcpMode);
List<String> listRtpServer(MediaServer mediaServer);
StreamInfo loadMP4File(MediaServer mediaServer, String app, String stream, String datePath);
void seekRecordStamp(MediaServer mediaServer, String app, String stream, Double stamp, String schema);
void setRecordSpeed(MediaServer mediaServer, String app, String stream, Integer speed, String schema);
}

View File

@ -311,6 +311,10 @@ public class MediaServerServiceImpl implements IMediaServerService {
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()) {
@ -966,4 +970,35 @@ public class MediaServerServiceImpl implements IMediaServerService {
}
mediaNodeServerService.stopProxy(mediaServer, streamKey);
}
@Override
public StreamInfo loadMP4File(MediaServer mediaServer, String app, String stream, String datePath) {
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, datePath);
return getStreamInfoByAppAndStream(mediaServer, app, stream, null, null);
}
@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);
}
}

View File

@ -126,7 +126,6 @@ public class ZLMHttpHookListener {
@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();
@ -190,6 +189,8 @@ public class ZLMHttpHookListener {
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;

View File

@ -169,7 +169,7 @@ public class ZLMMediaNodeServerService implements IMediaNodeServerService {
return true;
}else {
log.info("[zlm-deleteRecordDirectory] 删除磁盘文件错误, server: {} {}:{}->{}/{}, 结果: {}", mediaServer.getId(), app, stream, date, fileName, jsonObject);
return false;
throw new ControllerException(ErrorCode.ERROR100.getCode(), "删除磁盘文件失败");
}
}
@ -549,4 +549,37 @@ public class ZLMMediaNodeServerService implements IMediaNodeServerService {
}
return result;
}
@Override
public void loadMP4File(MediaServer mediaServer, String app, String stream, String datePath) {
JSONObject jsonObject = zlmresTfulUtils.loadMP4File(mediaServer, app, stream, datePath);
if (jsonObject == null) {
throw new ControllerException(ErrorCode.ERROR100.getCode(), "请求失败");
}
if (jsonObject.getInteger("code") != 0) {
throw new ControllerException(jsonObject.getInteger("code"), jsonObject.getString("msg"));
}
}
@Override
public void seekRecordStamp(MediaServer mediaServer, String app, String stream, Double stamp, String schema) {
JSONObject jsonObject = zlmresTfulUtils.seekRecordStamp(mediaServer, app, stream, stamp, schema);
if (jsonObject == null) {
throw new ControllerException(ErrorCode.ERROR100.getCode(), "请求失败");
}
if (jsonObject.getInteger("code") != 0) {
throw new ControllerException(jsonObject.getInteger("code"), jsonObject.getString("msg"));
}
}
@Override
public void setRecordSpeed(MediaServer mediaServer, String app, String stream, Integer speed, String schema) {
JSONObject jsonObject = zlmresTfulUtils.setRecordSpeed(mediaServer, app, stream, speed, schema);
if (jsonObject == null) {
throw new ControllerException(ErrorCode.ERROR100.getCode(), "请求失败");
}
if (jsonObject.getInteger("code") != 0) {
throw new ControllerException(jsonObject.getInteger("code"), jsonObject.getString("msg"));
}
}
}

View File

@ -25,6 +25,7 @@ public class ZLMRESTfulUtils {
private OkHttpClient client;
public interface RequestCallback{
void run(JSONObject response);
}
@ -415,4 +416,34 @@ public class ZLMRESTfulUtils {
param.put("name", fileName);
return sendPost(mediaServerItem, "deleteRecordDirectory",param, null);
}
public JSONObject loadMP4File(MediaServer mediaServer, String app, String stream, String datePath) {
Map<String, Object> 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");
return sendPost(mediaServer, "loadMP4File",param, null);
}
public JSONObject setRecordSpeed(MediaServer mediaServer, String app, String stream, int speed, String schema) {
Map<String, Object> param = new HashMap<>(1);
param.put("vhost", "__defaultVhost__");
param.put("app", app);
param.put("stream", stream);
param.put("speed", speed);
param.put("schema", schema);
return sendPost(mediaServer, "setRecordSpeed",param, null);
}
public JSONObject seekRecordStamp(MediaServer mediaServer, String app, String stream, Double stamp, String schema) {
Map<String, Object> param = new HashMap<>(1);
param.put("vhost", "__defaultVhost__");
param.put("app", app);
param.put("stream", stream);
param.put("stamp", stamp);
param.put("schema", schema);
return sendPost(mediaServer, "seekRecordStamp",param, null);
}
}

View File

@ -1,12 +1,15 @@
package com.genersoft.iot.vmp.gb28181.service;
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.github.pagehelper.PageInfo;
import java.util.List;
import java.util.Set;
/**
* 云端录像管理
@ -17,7 +20,7 @@ public interface ICloudRecordService {
/**
* 分页回去云端录像列表
*/
PageInfo<CloudRecordItem> getList(int page, int count, String query, String app, String stream, String startTime, String endTime, List<MediaServer> mediaServerItems, String callId);
PageInfo<CloudRecordItem> getList(int page, int count, String query, String app, String stream, String startTime, String endTime, List<MediaServer> mediaServerItems, String callId, Boolean ascOrder);
/**
* 获取所有的日期
@ -52,4 +55,15 @@ public interface ICloudRecordService {
DownloadFileInfo getPlayUrlPath(Integer recordId);
List<CloudRecordItem> getAllList(String query, String app, String stream, String startTime, String endTime, List<MediaServer> mediaServerItems, String callId, List<Integer> ids);
/**
* 加载录像文件形成录像流
*/
void loadRecord(String app, String stream, String date, ErrorCallback<StreamInfo> 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<Integer> ids);
}

View File

@ -15,52 +15,52 @@ 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;
/**
* 收藏收藏的文件不移除
*/
@ -70,16 +70,16 @@ public class CloudRecordItem {
* 保留收藏的文件不移除
*/
private Boolean reserve;
/**
* 文件大小
*/
private long fileSize;
/**
* 文件时长
*/
private long timeLen;
private double timeLen;
/**
* 所属服务ID
@ -96,7 +96,7 @@ public class CloudRecordItem {
cloudRecordItem.setFileSize(param.getRecordInfo().getFileSize());
cloudRecordItem.setFilePath(param.getRecordInfo().getFilePath());
cloudRecordItem.setMediaServerId(param.getMediaServer().getId());
cloudRecordItem.setTimeLen((long) param.getRecordInfo().getTimeLen() * 1000);
cloudRecordItem.setTimeLen(param.getRecordInfo().getTimeLen() * 1000);
cloudRecordItem.setEndTime((param.getRecordInfo().getStartTime() + (long)param.getRecordInfo().getTimeLen()) * 1000);
Map<String, String> paramsMap = MediaServerUtils.urlParamToMap(param.getRecordInfo().getParams());
if (paramsMap.get("callId") != null) {

View File

@ -2,16 +2,22 @@ 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.gb28181.service.ICloudRecordService;
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.HookSubscribe;
import com.genersoft.iot.vmp.media.event.hook.HookType;
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;
@ -28,12 +34,10 @@ 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;
import java.util.*;
@Slf4j
@Service
@ -57,9 +61,12 @@ public class CloudRecordServiceImpl implements ICloudRecordService {
@Autowired
private IRedisRpcPlayService redisRpcPlayService;
@Autowired
private HookSubscribe subscribe;
@Override
public PageInfo<CloudRecordItem> getList(int page, int count, String query, String app, String stream, String startTime,
String endTime, List<MediaServer> mediaServerItems, String callId) {
String endTime, List<MediaServer> mediaServerItems, String callId, Boolean ascOrder) {
// 开始时间和结束时间在数据库中都是以秒为单位的
Long startTimeStamp = null;
Long endTimeStamp = null;
@ -84,7 +91,7 @@ public class CloudRecordServiceImpl implements ICloudRecordService {
.replaceAll("_", "/_");
}
List<CloudRecordItem> all = cloudRecordServiceMapper.getList(query, app, stream, startTimeStamp, endTimeStamp,
callId, mediaServerItems, null);
callId, mediaServerItems, null, ascOrder);
return new PageInfo<>(all);
}
@ -100,7 +107,7 @@ public class CloudRecordServiceImpl implements ICloudRecordService {
long startTimeStamp = startDate.atStartOfDay().toInstant(ZoneOffset.ofHours(8)).toEpochMilli();
long endTimeStamp = endDate.atStartOfDay().toInstant(ZoneOffset.ofHours(8)).toEpochMilli();
List<CloudRecordItem> cloudRecordItemList = cloudRecordServiceMapper.getList(null, app, stream, startTimeStamp,
endTimeStamp, null, mediaServerItems, null);
endTimeStamp, null, mediaServerItems, null, null);
if (cloudRecordItemList.isEmpty()) {
return new ArrayList<>();
}
@ -213,7 +220,7 @@ public class CloudRecordServiceImpl implements ICloudRecordService {
}
List<CloudRecordItem> all = cloudRecordServiceMapper.getList(null, app, stream, startTimeStamp, endTimeStamp,
callId, mediaServerItems, null);
callId, mediaServerItems, null, null);
if (all.isEmpty()) {
throw new ControllerException(ErrorCode.ERROR100.getCode(), "未找到待收藏的视频");
}
@ -273,6 +280,96 @@ public class CloudRecordServiceImpl implements ICloudRecordService {
}
return cloudRecordServiceMapper.getList(query, app, stream, startTimeStamp, endTimeStamp,
callId, mediaServerItems, ids);
callId, mediaServerItems, ids, null);
}
@Override
public void loadRecord(String app, String stream, String date, ErrorCallback<StreamInfo> callback) {
long startTimestamp = DateUtil.yyyy_MM_dd_HH_mm_ssToTimestampMs(date + " 00:00:00");
long endTimestamp = startTimestamp + 24 * 60 * 60 * 1000;
List<CloudRecordItem> 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 buildApp = "mp4_record";
String buildStream = app + "_" + stream + "_" + date;
MediaInfo mediaInfo = mediaServerService.getMediaInfo(mediaServer, buildApp, buildStream);
if (mediaInfo != null) {
if (callback != null) {
StreamInfo streamInfo = mediaServerService.getStreamInfoByAppAndStream(mediaServer, buildApp, buildStream, mediaInfo, null);
callback.run(ErrorCode.SUCCESS.getCode(), ErrorCode.SUCCESS.getMsg(), streamInfo);
}
return;
}
Hook hook = Hook.getInstance(HookType.on_media_arrival, buildApp, buildStream, mediaServerId);
subscribe.addSubscribe(hook, (hookData) -> {
StreamInfo streamInfo = mediaServerService.getStreamInfoByAppAndStream(mediaServer, buildApp, buildStream, hookData.getMediaInfo(), null);
if (callback != null) {
callback.run(ErrorCode.SUCCESS.getCode(), ErrorCode.SUCCESS.getMsg(), streamInfo);
}
});
String dateDir = recordItemList.get(0).getFilePath().substring(0, recordItemList.get(0).getFilePath().lastIndexOf("/"));
mediaServerService.loadMP4File(mediaServer, buildApp, buildStream, dateDir);
}
@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<Integer> ids) {
log.info("[删除录像文件] ids: {}", ids.toArray());
List<CloudRecordItem> cloudRecordItemList = cloudRecordServiceMapper.queryRecordByIds(ids);
if (cloudRecordItemList.isEmpty()) {
return;
}
List<CloudRecordItem> 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());
}
}
}

View File

@ -79,9 +79,8 @@ public class RecordPlanServiceImpl implements IRecordPlanService {
Map<Integer, StreamInfo> recordStreamMap = new HashMap<>();
@Scheduled(fixedRate = 10, timeUnit = TimeUnit.MINUTES)
@Scheduled(fixedRate = 1, timeUnit = TimeUnit.MINUTES)
public void execution() {
log.info("[录制计划] 执行");
// 查询现在需要录像的通道Id
List<Integer> startChannelIdList = queryCurrentChannelRecord();
@ -133,7 +132,7 @@ public class RecordPlanServiceImpl implements IRecordPlanService {
// 获取当前时间在一周内的序号, 数据库存储的从第几个30分钟开始, 0-47, 包括首尾
LocalDateTime now = LocalDateTime.now();
int week = now.getDayOfWeek().getValue();
int index = now.getHour() * 2 + (now.getMinute() > 30?1:0);
int index = now.getHour() * 60 + now.getMinute();
// 查询现在需要录像的通道Id
return recordPlanMapper.queryRecordIng(week, index);
@ -221,7 +220,7 @@ public class RecordPlanServiceImpl implements IRecordPlanService {
}
}
// TODO 更新录像队列
}
@Override

View File

@ -6,7 +6,7 @@ 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.service.ICloudRecordService;
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;

View File

@ -5,6 +5,7 @@ import com.genersoft.iot.vmp.service.bean.CloudRecordItem;
import org.apache.ibatis.annotations.*;
import java.util.List;
import java.util.Set;
@Mapper
public interface CloudRecordServiceMapper {
@ -55,12 +56,13 @@ public interface CloudRecordServiceMapper {
" <if test= 'ids != null ' > and id in " +
" <foreach collection='ids' item='item' open='(' separator=',' close=')' > #{item}</foreach>" +
" </if>" +
" order by start_time desc" +
" <if test= 'ascOrder != null and ascOrder == true'> order by start_time asc</if>" +
" <if test= 'ascOrder == null or ascOrder == false'> order by start_time desc</if>" +
" </script>")
List<CloudRecordItem> 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<MediaServer> mediaServerItemList,
List<Integer> ids);
List<Integer> ids, @Param("ascOrder") Boolean ascOrder);
@Select(" <script>" +
@ -124,4 +126,26 @@ public interface CloudRecordServiceMapper {
"where id = #{id}" +
" </script>")
CloudRecordItem queryOne(@Param("id") Integer id);
@Select(" <script>" +
"select media_server_id " +
" from wvp_cloud_record " +
" where 0 = 0" +
" <if test= 'app != null '> and app=#{app}</if>" +
" <if test= 'stream != null '> and stream=#{stream}</if>" +
" <if test= 'startTimeStamp != null '> and end_time &gt;= #{startTimeStamp}</if>" +
" <if test= 'endTimeStamp != null '> and start_time &lt;= #{endTimeStamp}</if>" +
" group by media_server_id" +
" </script>")
List<String> queryMediaServerId(@Param("app") String app,
@Param("stream") String stream,
@Param("startTimeStamp")Long startTimeStamp,
@Param("endTimeStamp")Long endTimeStamp);
@Select(" <script>" +
"select * " +
" from wvp_cloud_record where id in " +
" <foreach collection='ids' item='item' open='(' separator=',' close=')' > #{item}</foreach>" +
" </script>")
List<CloudRecordItem> queryRecordByIds(Set<Integer> ids);
}

View File

@ -1,6 +1,7 @@
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;
@ -9,6 +10,7 @@ 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;
@ -142,4 +144,19 @@ public class SystemInfoUtils {
}
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));
}
}

View File

@ -1,15 +1,22 @@
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.gb28181.service.ICloudRecordService;
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.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;
@ -20,12 +27,15 @@ 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 javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
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;
@ -46,6 +56,9 @@ public class CloudRecordController {
@Autowired
private IMediaServerService mediaServerService;
@Autowired
private UserSetting userSetting;
@ResponseBody
@GetMapping("/date/list")
@ -101,6 +114,7 @@ public class CloudRecordController {
@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<CloudRecordItem> openRtpServer(@RequestParam(required = false) String query,
@RequestParam(required = false) String app,
@RequestParam(required = false) String stream,
@ -109,7 +123,8 @@ public class CloudRecordController {
@RequestParam(required = false) String startTime,
@RequestParam(required = false) String endTime,
@RequestParam(required = false) String mediaServerId,
@RequestParam(required = false) String callId
@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);
@ -143,7 +158,7 @@ public class CloudRecordController {
if (callId != null && ObjectUtils.isEmpty(callId.trim())) {
callId = null;
}
return cloudRecordService.getList(page, count, query, app, stream, startTime, endTime, mediaServers, callId);
return cloudRecordService.getList(page, count, query, app, stream, startTime, endTime, mediaServers, callId, ascOrder);
}
@ResponseBody
@ -233,6 +248,113 @@ public class CloudRecordController {
return cloudRecordService.getPlayUrlPath(recordId);
}
@ResponseBody
@GetMapping("/loadRecord")
@Operation(summary = "加载录像文件形成播放地址")
@Parameter(name = "app", description = "应用名", required = true)
@Parameter(name = "stream", description = "流ID", required = true)
@Parameter(name = "date", description = "日期, 例如 2025-04-10", required = true)
public DeferredResult<WVPResult<StreamContent>> loadRecord(
HttpServletRequest request,
@RequestParam(required = true) String app,
@RequestParam(required = true) String stream,
@RequestParam(required = true) String date
) {
DeferredResult<WVPResult<StreamContent>> result = new DeferredResult<>();
result.onTimeout(()->{
log.info("[加载录像文件超时] app={}, stream={}, date={}", app, stream, date);
WVPResult<StreamContent> wvpResult = new WVPResult<>();
wvpResult.setCode(ErrorCode.ERROR100.getCode());
wvpResult.setMsg("加载录像文件超时");
result.setResult(wvpResult);
});
ErrorCallback<StreamInfo> callback = (code, msg, streamInfo) -> {
WVPResult<StreamContent> 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.loadRecord(app, stream, date, callback);
return result;
}
@ResponseBody
@GetMapping("/seek")
@Operation(summary = "定位录像播放到制定位置")
@Parameter(name = "mediaServerId", description = "使用的节点Id", 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 = "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());
}
/************************* 以下这些接口只适合wvp和zlm部署在同一台服务器的情况且wvp只有一个zlm节点的情况 ***************************************/
/**
@ -295,7 +417,7 @@ public class CloudRecordController {
try {
ZipOutputStream zos = new ZipOutputStream(response.getOutputStream());
for (CloudRecordItem cloudRecordItem : cloudRecordItemList) {
zos.putNextEntry(new ZipEntry(DateUtil.timestampMsToUrlToyyyy_MM_dd_HH_mm_ss(cloudRecordItem.getStartTime()) + ".mp4"));
zos.putNextEntry(new ZipEntry(DateUtil.timestampMsToUrlToyyyy_MM_dd_HH_mm_ss((long)cloudRecordItem.getStartTime()) + ".mp4"));
File file = new File(cloudRecordItem.getFilePath());
if (!file.exists() || file.isDirectory()) {
continue;
@ -379,7 +501,7 @@ public class CloudRecordController {
if (remoteHost == null) {
remoteHost = request.getScheme() + "://" + request.getLocalAddr() + ":" + (request.getScheme().equals("https") ? mediaServer.getHttpSSlPort() : mediaServer.getHttpPort());
}
PageInfo<CloudRecordItem> cloudRecordItemPageInfo = cloudRecordService.getList(page, count, query, app, stream, startTime, endTime, mediaServers, callId);
PageInfo<CloudRecordItem> cloudRecordItemPageInfo = cloudRecordService.getList(page, count, query, app, stream, startTime, endTime, mediaServers, callId, null);
PageInfo<CloudRecordUrl> cloudRecordUrlPageInfo = new PageInfo<>();
if (!ObjectUtils.isEmpty(cloudRecordItemPageInfo)) {
cloudRecordUrlPageInfo.setPageNum(cloudRecordItemPageInfo.getPageNum());
@ -404,7 +526,7 @@ public class CloudRecordController {
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(cloudRecordItem.getStartTime()));
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);
}

View File

@ -1,21 +1,10 @@
package com.genersoft.iot.vmp.vmanager.log;
import ch.qos.logback.classic.Logger;
import ch.qos.logback.core.rolling.RollingFileAppender;
import com.alibaba.fastjson2.JSONArray;
import com.genersoft.iot.vmp.conf.exception.ControllerException;
import com.genersoft.iot.vmp.conf.security.JwtUtils;
import com.genersoft.iot.vmp.gb28181.service.ICloudRecordService;
import com.genersoft.iot.vmp.media.bean.MediaServer;
import com.genersoft.iot.vmp.media.service.IMediaServerService;
import com.genersoft.iot.vmp.service.ILogService;
import com.genersoft.iot.vmp.service.bean.CloudRecordItem;
import com.genersoft.iot.vmp.service.bean.DownloadFileInfo;
import com.genersoft.iot.vmp.service.bean.LogFileInfo;
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.PageInfo;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
@ -23,24 +12,17 @@ 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.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.*;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
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 = "日志文件查询接口")

View File

@ -28,6 +28,7 @@ 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.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
@ -41,13 +42,14 @@ import oshi.hardware.HardwareAbstractionLayer;
import oshi.hardware.NetworkIF;
import oshi.software.os.OperatingSystem;
import javax.servlet.http.HttpServletRequest;
import java.io.File;
import java.text.DecimalFormat;
import java.util.*;
@SuppressWarnings("rawtypes")
@Tag(name = "服务控制")
@Slf4j
@RestController
@RequestMapping("/api/server")
public class ServerController {
@ -80,6 +82,7 @@ public class ServerController {
@Value("${server.port}")
private int serverPort;
@Autowired
private IRedisCatchStorage redisCatchStorage;
@ -176,33 +179,14 @@ public class ServerController {
}
@Operation(summary = "重启服务", security = @SecurityRequirement(name = JwtUtils.HEADER))
@GetMapping(value = "/restart")
@Operation(summary = "关闭服务", security = @SecurityRequirement(name = JwtUtils.HEADER))
@GetMapping(value = "/shutdown")
@ResponseBody
public void restart() {
// taskExecutor.execute(()-> {
// try {
// Thread.sleep(3000);
// SipProvider up = (SipProvider) SpringBeanFactory.getBean("udpSipProvider");
// SipStackImpl stack = (SipStackImpl) up.getSipStack();
// stack.stop();
// Iterator listener = stack.getListeningPoints();
// while (listener.hasNext()) {
// stack.deleteListeningPoint((ListeningPoint) listener.next());
// }
// Iterator providers = stack.getSipProviders();
// while (providers.hasNext()) {
// stack.deleteSipProvider((SipProvider) providers.next());
// }
// VManageBootstrap.restart();
// } catch (InterruptedException | ObjectInUseException e) {
// throw new ControllerException(ErrorCode.ERROR100.getCode(), e.getMessage());
// }
// });
public void shutdown() {
log.info("正在关闭服务。。。");
System.exit(1);
}
;
@Operation(summary = "获取系统配置信息", security = @SecurityRequirement(name = JwtUtils.HEADER))
@GetMapping(value = "/system/configInfo")
@ResponseBody
@ -293,7 +277,7 @@ public class ServerController {
@GetMapping(value = "/info")
@ResponseBody
@Operation(summary = "获取系统信息", security = @SecurityRequirement(name = JwtUtils.HEADER))
public Map<String, Map<String, String>> getInfo() {
public Map<String, Map<String, String>> getInfo(HttpServletRequest request) {
Map<String, Map<String, String>> result = new LinkedHashMap<>();
Map<String, String> hardwareMap = new LinkedHashMap<>();
result.put("硬件信息", hardwareMap);
@ -341,6 +325,11 @@ public class ServerController {
platformMap.put("GIT版本", version.getGIT_Revision_SHORT());
platformMap.put("DOCKER环境", new File("/.dockerenv").exists()?"":"");
Map<String, String> docmap = new LinkedHashMap<>();
result.put("文档地址", docmap);
docmap.put("部署文档", String.format("%s://%s:%s/doc.html", request.getScheme(), request.getServerName(), request.getServerPort()));
docmap.put("接口文档", "https://doc.wvp-pro.cn");
return result;
}

View File

@ -63,9 +63,9 @@ public class UserApiKeyController {
Long expirationTime = null;
if (expiresAt != null) {
long timestamp = DateUtil.yyyy_MM_dd_HH_mm_ssToTimestampMs(expiresAt);
expirationTime = (timestamp - System.currentTimeMillis()) / (60 * 1000);
if (expirationTime < 0) {
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(), "过期时间不能早于当前时间");
}
}

View File

@ -1,5 +1,6 @@
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;
@ -42,6 +43,9 @@ public class UserController {
@Autowired
private IRoleService roleService;
@Autowired
private UserSetting userSetting;
@GetMapping("/login")
@PostMapping("/login")
@Operation(summary = "登录", description = "登录成功后返回AccessToken 可以从返回值获取到也可以从响应头中获取到," +
@ -62,6 +66,7 @@ public class UserController {
String jwt = JwtUtils.createToken(username);
response.setHeader(JwtUtils.getHeader(), jwt);
user.setAccessToken(jwt);
user.setServerId(userSetting.getServerId());
}
return user;
}

View File

@ -2,4 +2,4 @@ spring:
application:
name: wvp
profiles:
active: dev
active: 273

57
src/main/resources/install.sh Executable file
View File

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

View File

@ -5,6 +5,10 @@
spring:
cache:
type: redis
thymeleaf:
cache: false
# 设置接口超时时间
mvc:
async:
@ -113,8 +117,8 @@ sip:
# keepalliveToOnline: false
# 是否存储alarm信息
alarm: false
# 命令发送等待回复的超时时间, 单位:
timeout: 15
# 命令发送等待回复的超时时间, 单位:
timeout: 1000
# 做为JT1078服务器的配置
jt1078:

14
web/.editorconfig Normal file
View File

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

5
web/.env.development Normal file
View File

@ -0,0 +1,5 @@
# just a flag
ENV = 'development'
# base api
VUE_APP_BASE_API = '/dev-api'

6
web/.env.production Normal file
View File

@ -0,0 +1,6 @@
# just a flag
ENV = 'production'
# base api
VUE_APP_BASE_API = ''

8
web/.env.staging Normal file
View File

@ -0,0 +1,8 @@
NODE_ENV = production
# just a flag
ENV = 'staging'
# base api
VUE_APP_BASE_API = '/stage-api'

4
web/.eslintignore Normal file
View File

@ -0,0 +1,4 @@
build/*.js
src/assets
public
dist

198
web/.eslintrc.js Normal file
View File

@ -0,0 +1,198 @@
module.exports = {
root: true,
parserOptions: {
parser: 'babel-eslint',
sourceType: 'module'
},
env: {
browser: true,
node: true,
es6: true,
},
extends: ['plugin:vue/recommended', 'eslint:recommended'],
// add your custom rules here
//it is base on https://github.com/vuejs/eslint-config-vue
rules: {
"vue/max-attributes-per-line": [2, {
"singleline": 10,
"multiline": {
"max": 1,
"allowFirstLine": false
}
}],
"vue/singleline-html-element-content-newline": "off",
"vue/multiline-html-element-content-newline":"off",
"vue/name-property-casing": ["error", "PascalCase"],
"vue/no-v-html": "off",
'accessor-pairs': 2,
'arrow-spacing': [2, {
'before': true,
'after': true
}],
'block-spacing': [2, 'always'],
'brace-style': [2, '1tbs', {
'allowSingleLine': true
}],
'camelcase': [0, {
'properties': 'always'
}],
'comma-dangle': [2, 'never'],
'comma-spacing': [2, {
'before': false,
'after': true
}],
'comma-style': [2, 'last'],
'constructor-super': 2,
'curly': [2, 'multi-line'],
'dot-location': [2, 'property'],
'eol-last': 2,
'eqeqeq': ["error", "always", {"null": "ignore"}],
'generator-star-spacing': [2, {
'before': true,
'after': true
}],
'handle-callback-err': [2, '^(err|error)$'],
'indent': [2, 2, {
'SwitchCase': 1
}],
'jsx-quotes': [2, 'prefer-single'],
'key-spacing': [2, {
'beforeColon': false,
'afterColon': true
}],
'keyword-spacing': [2, {
'before': true,
'after': true
}],
'new-cap': [2, {
'newIsCap': true,
'capIsNew': false
}],
'new-parens': 2,
'no-array-constructor': 2,
'no-caller': 2,
'no-console': 'off',
'no-class-assign': 2,
'no-cond-assign': 2,
'no-const-assign': 2,
'no-control-regex': 0,
'no-delete-var': 2,
'no-dupe-args': 2,
'no-dupe-class-members': 2,
'no-dupe-keys': 2,
'no-duplicate-case': 2,
'no-empty-character-class': 2,
'no-empty-pattern': 2,
'no-eval': 2,
'no-ex-assign': 2,
'no-extend-native': 2,
'no-extra-bind': 2,
'no-extra-boolean-cast': 2,
'no-extra-parens': [2, 'functions'],
'no-fallthrough': 2,
'no-floating-decimal': 2,
'no-func-assign': 2,
'no-implied-eval': 2,
'no-inner-declarations': [2, 'functions'],
'no-invalid-regexp': 2,
'no-irregular-whitespace': 2,
'no-iterator': 2,
'no-label-var': 2,
'no-labels': [2, {
'allowLoop': false,
'allowSwitch': false
}],
'no-lone-blocks': 2,
'no-mixed-spaces-and-tabs': 2,
'no-multi-spaces': 2,
'no-multi-str': 2,
'no-multiple-empty-lines': [2, {
'max': 1
}],
'no-native-reassign': 2,
'no-negated-in-lhs': 2,
'no-new-object': 2,
'no-new-require': 2,
'no-new-symbol': 2,
'no-new-wrappers': 2,
'no-obj-calls': 2,
'no-octal': 2,
'no-octal-escape': 2,
'no-path-concat': 2,
'no-proto': 2,
'no-redeclare': 2,
'no-regex-spaces': 2,
'no-return-assign': [2, 'except-parens'],
'no-self-assign': 2,
'no-self-compare': 2,
'no-sequences': 2,
'no-shadow-restricted-names': 2,
'no-spaced-func': 2,
'no-sparse-arrays': 2,
'no-this-before-super': 2,
'no-throw-literal': 2,
'no-trailing-spaces': 2,
'no-undef': 2,
'no-undef-init': 2,
'no-unexpected-multiline': 2,
'no-unmodified-loop-condition': 2,
'no-unneeded-ternary': [2, {
'defaultAssignment': false
}],
'no-unreachable': 2,
'no-unsafe-finally': 2,
'no-unused-vars': [2, {
'vars': 'all',
'args': 'none'
}],
'no-useless-call': 2,
'no-useless-computed-key': 2,
'no-useless-constructor': 2,
'no-useless-escape': 0,
'no-whitespace-before-property': 2,
'no-with': 2,
'one-var': [2, {
'initialized': 'never'
}],
'operator-linebreak': [2, 'after', {
'overrides': {
'?': 'before',
':': 'before'
}
}],
'padded-blocks': [2, 'never'],
'quotes': [2, 'single', {
'avoidEscape': true,
'allowTemplateLiterals': true
}],
'semi': [2, 'never'],
'semi-spacing': [2, {
'before': false,
'after': true
}],
'space-before-blocks': [2, 'always'],
'space-before-function-paren': [2, 'never'],
'space-in-parens': [2, 'never'],
'space-infix-ops': 2,
'space-unary-ops': [2, {
'words': true,
'nonwords': false
}],
'spaced-comment': [2, 'always', {
'markers': ['global', 'globals', 'eslint', 'eslint-disable', '*package', '!', ',']
}],
'template-curly-spacing': [2, 'never'],
'use-isnan': 2,
'valid-typeof': 2,
'wrap-iife': [2, 'any'],
'yield-star-spacing': [2, 'both'],
'yoda': [2, 'never'],
'prefer-const': 2,
'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0,
'object-curly-spacing': [2, 'always', {
objectsInObjects: false
}],
'array-bracket-spacing': [2, 'never']
}
}

16
web/.gitignore vendored Normal file
View File

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

5
web/.travis.yml Normal file
View File

@ -0,0 +1,5 @@
language: node_js
node_js: 10
script: npm run test
notifications:
email: false

21
web/LICENSE Normal file
View File

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

111
web/README-zh.md Normal file
View File

@ -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`
<p align="center">
<b>SPONSORED BY</b>
</p>
<p align="center">
<a href="https://finclip.com?from=vue_element" title="FinClip" target="_blank">
<img height="200px" src="https://gitee.com/panjiachen/gitee-cdn/raw/master/vue%E8%B5%9E%E5%8A%A9.png" title="FinClip">
</a>
</p>
## 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+.
| [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/edge/edge_48x48.png" alt="IE / Edge" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>IE / Edge | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/firefox/firefox_48x48.png" alt="Firefox" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Firefox | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/chrome/chrome_48x48.png" alt="Chrome" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Chrome | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/safari/safari_48x48.png" alt="Safari" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>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

99
web/README.md Normal file
View File

@ -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`**
<p align="center">
<b>SPONSORED BY</b>
</p>
<p align="center">
<a href="https://finclip.com?from=vue_element" title="FinClip" target="_blank">
<img height="200px" src="https://gitee.com/panjiachen/gitee-cdn/raw/master/vue%E8%B5%9E%E5%8A%A9.png" title="FinClip">
</a>
</p>
## 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+.
| [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/edge/edge_48x48.png" alt="IE / Edge" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>IE / Edge | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/firefox/firefox_48x48.png" alt="Firefox" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Firefox | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/chrome/chrome_48x48.png" alt="Chrome" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Chrome | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/safari/safari_48x48.png" alt="Safari" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>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

14
web/babel.config.js Normal file
View File

@ -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']
}
}
}

35
web/build/index.js Normal file
View File

@ -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}`)
}

24
web/jest.config.js Normal file
View File

@ -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: {
'^@/(.*)$': '<rootDir>/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: '<rootDir>/tests/unit/coverage',
// 'collectCoverage': true,
'coverageReporters': [
'lcov',
'text-summary'
],
testURL: 'http://localhost/'
}

9
web/jsconfig.json Normal file
View File

@ -0,0 +1,9 @@
{
"compilerOptions": {
"baseUrl": "./",
"paths": {
"@/*": ["src/*"]
}
},
"exclude": ["node_modules", "dist"]
}

57
web/mock/index.js Normal file
View File

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

81
web/mock/mock-server.js Normal file
View File

@ -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))
}
}
})
}

29
web/mock/table.js Normal file
View File

@ -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
}
}
}
}
]

84
web/mock/user.js Normal file
View File

@ -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'
}
}
}
]

25
web/mock/utils.js Normal file
View File

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

74
web/package.json Normal file
View File

@ -0,0 +1,74 @@
{
"name": "vue-admin-template",
"version": "4.4.0",
"description": "A vue admin template with Element UI & axios & iconfont & permission control & lint",
"author": "Pan <panfree23@gmail.com>",
"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": {
"@wchbrad/vue-easy-tree": "^1.0.13",
"@femessage/log-viewer": "^1.5.0",
"axios": "^0.24.0",
"core-js": "3.6.5",
"dayjs": "^1.11.13",
"echarts": "^4.9.0",
"element-ui": "^2.15.14",
"js-cookie": "2.2.0",
"moment": "^2.29.1",
"normalize.css": "7.0.0",
"nprogress": "0.2.0",
"ol": "^6.14.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-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"
},
"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"
}

8
web/postcss.config.js Normal file
View File

@ -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': {}
}
}

BIN
web/public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

22
web/public/index.html Normal file
View File

@ -0,0 +1,22 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
<title><%= webpackConfig.name %></title>
</head>
<body>
<script type="text/javascript" src="./static/js/jessibuca/jessibuca.js"></script>
<script type="text/javascript" src="./static/js/ZLMRTCClient.js"></script>
<script type="text/javascript" src="./static/js/config.js"></script>
<script type="text/javascript" src="./static/js/h265web/h265webjs-v20221106.js"></script>
<script type="text/javascript" src="./static/js/h265web/missile.js"></script>
<noscript>
<strong>We're sorry but <%= webpackConfig.name %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
</html>

BIN
web/public/libDecoder.wasm Normal file

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 876 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Some files were not shown because too many files have changed in this diff Show More