Compare commits

...

184 Commits

Author SHA1 Message Date
4799d83014 调整视频回放/下载 新增 prefetch 参数 指定是否提前启动预拉取 2024-04-29 00:37:01 +08:00
d1829901bf 修改 report 时间 字段类型 2024-03-28 09:45:20 +08:00
dda2d2fc07 修正 ffmpegRecord 时间范围 2024-03-27 15:25:01 +08:00
b4be3825bc 视频下载 添加 调用日志上报 2024-03-26 16:50:19 +08:00
5eed5e0eca 视频下载 添加 调用日志上报 2024-03-26 15:51:11 +08:00
bc5ff7505a 视频下载 添加 调用日志上报 2024-03-26 15:09:56 +08:00
5003b1268b 视频下载 添加 调用日志上报 2024-03-26 15:08:56 +08:00
ddd13510a8 更新 2024-03-19 09:37:20 +08:00
db293c8430 更新 2024-03-19 08:51:25 +08:00
33185bdc1f Merge remote-tracking branch 'origin/master' 2024-03-18 08:39:10 +08:00
86f056a01b 调整 2024-03-17 02:34:39 +08:00
daccfb48ca 更新 2024-03-15 16:08:01 +08:00
279ae44a25 更新 2024-03-15 16:01:27 +08:00
5061c3fdda 更新 2024-03-15 15:55:32 +08:00
41c6c887f5 更新 2024-03-15 15:16:33 +08:00
b4423f9084 调整 2024-03-15 11:53:26 +08:00
b699f1fb3a 调整 2024-03-15 11:40:40 +08:00
6c52ede51b 调整 2024-03-15 11:33:15 +08:00
c69ee9af05 调整 2024-03-15 11:26:35 +08:00
f8ce65d15b 调整 2024-03-15 11:26:10 +08:00
04cfcfd22a 异常处理 2024-03-15 11:09:03 +08:00
054ab25205 downloadToStream 添加 -t 参数
/video ffmpegRecord 调整
2024-03-15 10:48:03 +08:00
8c5b6f6021 downloadToStream 添加 -t 参数
/video ffmpegRecord 调整
2024-03-15 10:32:25 +08:00
c9af1a4dc3 添加测试脚本 2024-03-15 10:13:31 +08:00
87423c734f ffmpegRecord调整 2024-03-14 15:26:57 +08:00
0b3a80bce1 超时 处理 2024-03-14 01:17:12 +08:00
e1d0e355a0 支持临时目录配置
录像查询超时 处理
2024-03-13 22:02:18 +08:00
79fc31dd6e 完善 时长统计 及 临时文件回收 2024-03-13 21:41:35 +08:00
5152136f97 配置 ffmpegRecord 写入临时文件 2024-03-13 17:18:25 +08:00
5611bbbe38 配置 ffmpegRecord 写入临时文件 2024-03-13 17:14:27 +08:00
ccbb1ad186 配置 ffmpegRecord 写入临时文件 2024-03-13 16:53:35 +08:00
ef13720181 调整 ffmpegRecord 2024-03-13 16:22:50 +08:00
3ec2f90961 调整 ffmpegRecord 2024-03-13 16:22:12 +08:00
ac10573dc8 先用 PipedInputStream 接收数据 再 写入 http 响应 2024-03-13 16:14:50 +08:00
d4cf7e076a 调整 ack 2024-03-13 13:36:26 +08:00
e37e9b2677 调整 ffmpeg 启动时机 2024-03-13 12:36:25 +08:00
fd8a711662 调整 ffmpeg 启动时机 2024-03-13 12:30:14 +08:00
2dc1f43401 调整 ffmpeg 启动时机 2024-03-13 12:14:25 +08:00
fdc826e246 调整 ffmpeg 启动时机 2024-03-13 11:57:48 +08:00
9ac5a2adad videoUrl 返回 .mp4 视频流 2024-03-05 16:34:53 +08:00
8a907dd68a 修正返回的 url 2024-02-28 10:50:09 +08:00
1a1fe4e89b 修正返回的 url 2024-02-28 10:43:31 +08:00
c456d2e1ea 提前到 发起 invite 请求时 启动 ffmpeg 预备录制 2024-02-26 09:48:49 +08:00
0feb5ad242 ffmpeg download 参数调整 2024-02-25 16:03:36 +08:00
205c903294 添加 rtmp-port 参数
调整 视频下载 download
尽可能以最快速度收流
2024-02-25 03:27:07 +08:00
071e2a2491 新增可配置 获取历史视频之前 是否 先发起 RecordInfo 请求 2024-02-05 16:43:43 +08:00
028c178ef5 默认参数 和 偏移量 调整 2024-01-17 16:29:18 +08:00
d3a828fa13 添加 useDownload 参数 2024-01-16 10:30:03 +08:00
9c8883823a 更新本地测试配置 2024-01-15 15:51:07 +08:00
1c2b05b0ef 更新 gb28181-docking-platform 依赖版本 2024-01-11 16:36:41 +08:00
8c68d9dfa1 添加 /recordInfo api 2024-01-11 14:15:49 +08:00
e6819b2a15 更新 gb28181-docking-platform 依赖版本 2024-01-11 11:33:58 +08:00
e36befe7ef 新增 deviceControl/recordCmd 2024-01-09 11:31:02 +08:00
440b316c4c playStart 补充参数 isSubStream 2023-12-26 08:41:27 +08:00
b35db44aeb 单次实时点播持续时间可配置 2023-12-25 14:23:23 +08:00
21f2a50952 添加重试 2023-12-25 14:11:17 +08:00
f1db3e3d1d 添加重试 2023-12-25 14:10:31 +08:00
b5068617bb 调整 实时视频获取 改为 调用 wvp 接口 获取 2023-12-21 15:33:13 +08:00
da98ec41c3 调整 2023-12-18 22:26:32 +08:00
b2d6a5fe4c 调整 2023-12-18 21:55:32 +08:00
c8bfdfb67f 调整 2023-12-18 21:40:32 +08:00
7ccb3db8bc 调整 2023-12-18 21:40:11 +08:00
091776c30e 调整实时点播实现 2023-12-18 21:16:21 +08:00
00a5ab9dde 调整 2023-12-18 15:10:52 +08:00
b0733e985b 修正 2023-12-18 14:17:05 +08:00
4e0b1d6c31 修正 2023-12-18 13:28:32 +08:00
92dee06429 实时点播信息缓存到 redis 2023-12-15 09:42:20 +08:00
7528fdbb4b 调整 2023-12-14 17:14:52 +08:00
2bb56f5bbc 调整 2023-12-14 17:09:00 +08:00
a5faf10138 调整 2023-12-14 16:47:16 +08:00
cbba1c7be7 调整 2023-12-14 16:03:21 +08:00
9c5a2c7b26 调整 2023-12-14 15:48:44 +08:00
db58ea8b95 调整 2023-12-14 15:44:04 +08:00
5ae7fc9972 实时视频 2023-12-14 14:43:27 +08:00
ff690aaaef 屏蔽测试 2023-10-18 11:27:54 +08:00
613be42216 调整 2023-10-18 01:45:13 +08:00
4c84da13bc 添加 proxy-media-url
支持替换 返回的 url
2023-10-17 23:31:39 +08:00
e55bd2aea5 添加 proxy-media-url
支持替换 返回的 url
2023-10-17 23:20:13 +08:00
d7837a1975 调整 2023-10-17 19:56:40 +08:00
c1655bdeb5 修正 2023-10-17 17:20:53 +08:00
81822ff553 剔除 其他实现 2023-10-17 17:00:06 +08:00
9bcade0b2e /device/video.mp4 改为 返回 url 2023-10-17 11:25:41 +08:00
71b75038e2 同一设备限制 点播数 重复点播则关闭已有点播 2023-10-16 17:22:02 +08:00
37cfd012e8 添加对 mediaStatus 消息处理 2023-10-16 15:09:40 +08:00
a66c66119b 调整 2023-10-13 20:29:02 +08:00
44470dddb7 添加 cors 策略 允许跨域 2023-10-10 17:04:16 +08:00
3ea5ac5deb 添加 cors 策略 允许跨域 2023-10-10 16:58:10 +08:00
765b1f6fe8 mediaStatus byeRequest 修复 2023-10-10 14:13:30 +08:00
5a35e37687 /device/video.mp4 contentType 改为 application/octet-stream 2023-10-10 10:05:30 +08:00
0bf1eb6615 /device/video.mp4 添加偏移量配置 2023-10-10 09:27:50 +08:00
a9eab491d5 /device/video.mp4 添加偏移量配置 2023-10-10 09:22:06 +08:00
6d1dfc6445 添加 设备视频下载接口 /device/video.mp4 2023-10-09 16:03:26 +08:00
845d049ad5 /video 支持某些特殊情况下使用 实时回放 代替下载 2023-10-09 15:07:36 +08:00
4fc4d3a863 /video 支持某些特殊情况下使用 实时回放 代替下载 2023-10-09 14:52:36 +08:00
c2dcf6d931 修复 添加设备接口 2023-10-09 14:34:09 +08:00
91b488d9a6 默认媒体流 改为 TCP_PASSIVE 2023-10-06 13:59:35 +08:00
f36ed68607 支持配置流传输模式
已知 zlm rtp udp 收流有问题, 故提供选项切换为 tcp 收流/下载
2023-10-05 03:11:59 +08:00
410de78a1c 配置调整 2023-09-28 10:57:00 +08:00
53c4b69f0e 配置调整 2023-09-26 11:32:19 +08:00
a24e9fc910 配置调整 2023-09-26 11:31:51 +08:00
9586f05a6d 调整配置 2023-09-25 14:39:32 +08:00
c20a1b4534 调整配置 2023-09-25 14:33:26 +08:00
510319cd65 测试 拉流结束发送bye 2023-09-25 14:00:48 +08:00
2ec9dad3c8 测试 fmp4 流 2023-09-25 13:21:03 +08:00
94df487a93 下载 收流参数调整
修复 下载不完整问题
调整 Gb28181DownloadService.AsyncContext 默认超时时间
2023-09-22 22:45:29 +08:00
60afe2aaed Merge remote-tracking branch 'sk/master'
# Conflicts:
#	gb28181-wvp-proxy-service/src/main/java/cn/skcks/docking/gb28181/wvp/service/video/VideoService.java
#	gb28181-wvp-proxy-starter/src/main/resources/application-local.yml
#	gb28181-wvp-proxy-starter/src/main/resources/application.yml
2023-09-22 21:23:48 +08:00
0de97d39a8 下载 收流参数调整
修复 下载不完整问题
调整 Gb28181DownloadService.AsyncContext 默认超时时间
2023-09-22 21:17:53 +08:00
d847e06ec3 下载 调整 2023-09-22 17:30:06 +08:00
c11f5c9ef0 设备信息修改api
对接信息查询api
2023-09-22 15:31:39 +08:00
3e422e384b 推流参数测试 2023-09-22 13:50:18 +08:00
6ecbf83cdc docking api 2023-09-22 13:50:04 +08:00
bff7ad1b37 docker 添加默认时区 2023-09-22 08:41:43 +08:00
6c619f6605 下载调整 2023-09-21 16:31:41 +08:00
efdb6b1e02 下载调整 2023-09-21 16:15:47 +08:00
51baef0318 下载调整 2023-09-21 15:50:10 +08:00
0ce07bbcfe 下载调整 2023-09-21 15:35:08 +08:00
edf174c2f5 下载调整 2023-09-21 15:04:34 +08:00
f5da6cf084 可选是否要文件头 2023-09-21 14:58:15 +08:00
b12cdba22f 可选是否要文件头 2023-09-21 14:53:17 +08:00
8a7c1e7edd 可选是否要文件头 2023-09-21 14:31:06 +08:00
0e586a75bb 完成 video sip信令下载重构, 配置文件 调整 2023-09-21 14:27:06 +08:00
b372fd8d8e 处理 sip 信令后续请求 2023-09-21 12:10:38 +08:00
6fdc9c5c3b sip 下载信令 构造 (未完)
待实现 notify=MediaStatus, BYE 事件
2023-09-21 02:55:28 +08:00
a5ef517477 sip 下载信令 构造 (未完)
待实现 notify=MediaStatus, BYE 事件
2023-09-21 02:54:52 +08:00
a3a23db8df sip 下载信令 构造 (未完)
待实现 notify=MediaStatus, BYE 事件
2023-09-21 02:48:13 +08:00
5d7e30eec9 Merge remote-tracking branch 'sk/master' 2023-09-21 00:50:18 +08:00
506f67cb0c 测试 2023-09-21 00:49:46 +08:00
387451afb2 日志配置 2023-09-21 00:28:46 +08:00
e2aa0a5b0c 根据 catalog 更新 proxy 表 2023-09-20 13:10:11 +08:00
e0974681f9 结构调整 2023-09-20 11:00:55 +08:00
f2835262fd 目录查询 2023-09-20 10:24:10 +08:00
65b492f8e7 SipRequestBuilder 修正 2023-09-20 09:39:58 +08:00
39b65a2aee 尝试整合sip信令 实现国标级联上级(未完) 2023-09-20 03:16:28 +08:00
7c6f0a612c 可切换是否使用ffmpeg处理视频 2023-09-20 01:04:35 +08:00
00b2d42a68 可切换是否使用ffmpeg处理视频 2023-09-20 01:03:56 +08:00
7d71478baf 下载调整 2023-09-18 17:05:47 +08:00
704e9eb9a0 Content-Disposition 2023-09-18 16:25:00 +08:00
bbabf0d695 端口号 改为 18186 -> 18183 2023-09-18 15:31:33 +08:00
b98acc9cd4 调整 playbackStart 重试策略 2023-09-18 14:05:42 +08:00
b0cdba12f7 去除 /video http 头 Content-Disposition 2023-09-18 11:37:58 +08:00
5cbd6a6b49 infoByGbDeviceId 调整 2023-09-12 09:56:50 +08:00
2e42490440 修改录制文件格式为浏览器支持的 fmp4(Fragmented MP4) 格式 2023-09-11 14:10:13 +08:00
6a86ab806f 设备编码 (支持参数名: deviceCode device_code deviceId device_id)
开始时间 (支持参数名: startTime start_time beginTime begin_time)
结束时间 (支持参数名: endTime end_time endTime end_time)
2023-09-11 13:37:07 +08:00
e5e2a65c18 /video 接口参数兼容 device_id 2023-09-11 13:29:24 +08:00
30e96ed6cb /video 接口参数兼容 蛇形参数 2023-09-11 12:33:45 +08:00
f7b8960a98 添加 proxy.wvp.use-wvp-assist 配置
支持配置 是否 尝试使用 wvp-assist 下载视频回放
否则使用 playback 回放录制视频
2023-09-11 11:37:57 +08:00
37fc276f3d 修改设备表 索引定义 与 添加设备 逻辑
一个下级平台 有多个设备, 每个设备为一个通道
2023-09-11 10:25:36 +08:00
8d1bc71170 修改设备表 索引定义 与 添加设备 逻辑
一个下级平台 有多个设备, 每个设备为一个通道
2023-09-11 10:20:43 +08:00
ffac3458fd 修改设备表 索引定义 与 添加设备 逻辑
一个下级平台 有多个设备, 每个设备为一个通道
2023-09-11 10:17:58 +08:00
84e0addf53 补充信息/完善 2023-09-11 09:42:41 +08:00
34a6ba040d 录像补充 http 响应头 2023-09-10 05:07:19 +08:00
b625dcff7d 如果 无法通过 wvp-assist 下载则 使用 回放实时录制 2023-09-10 04:57:11 +08:00
dfec2e8754 初步完成下载流程, 未测试 2023-09-10 04:33:28 +08:00
330e279347 测试 2023-09-10 02:43:00 +08:00
5f16ba0371 完善重试日志 2023-09-09 23:13:23 +08:00
c7d79b48ab 完善重试日志 2023-09-09 23:13:12 +08:00
707c38da72 修复重试逻辑 2023-09-09 23:05:30 +08:00
ef6c164c0f 修复重试逻辑 2023-09-09 23:03:54 +08:00
62cd5e2016 完善重试日志
查询历史录像
2023-09-09 22:49:36 +08:00
b7b70fe42f 查询历史录像 2023-09-09 22:17:03 +08:00
9d1da22aa0 调整 feign 日期格式问题 2023-09-09 22:13:38 +08:00
6a25f48852 调整/调试 重试逻辑 2023-09-09 21:01:29 +08:00
45ac5d5d56 调整/调试 重试逻辑 2023-09-09 20:53:52 +08:00
85962d1373 调整 2023-09-09 00:21:59 +08:00
66dc724317 调整 2023-09-09 00:12:45 +08:00
2641b866be 简单测试 2023-09-08 17:20:08 +08:00
55de64069b 补充注释 2023-09-08 16:38:39 +08:00
7666446ed8 下载代理 2023-09-08 16:33:53 +08:00
0a7fefaead feign wvp 接口定义 2023-09-08 12:21:23 +08:00
d2d278ae76 根据主键id 根据设备编码(21位) 国标设备id(20位) 删除指定设备 2023-09-07 17:15:11 +08:00
cd43cc9fa8 根据设备编码(21位) 国标设备id(20位) 查询指定设备信息 2023-09-07 16:58:20 +08:00
d4ae48d963 异常捕获调整 2023-09-07 16:45:15 +08:00
8db1726e10 设备查询/添加 api 2023-09-07 15:43:11 +08:00
a7e453fbda DeviceService 简单封装 2023-09-07 14:12:28 +08:00
bd9e15f232 video 测试改为 拉flv 不支持 mp4 2023-09-07 12:01:58 +08:00
dc56cbd65e record 文件头设置 2023-09-07 11:43:24 +08:00
af40be890e 改为 flv 格式 实时流下载 不再需要临时文件转储 2023-09-07 11:37:15 +08:00
951bffea4e 改为 flv 格式 实时流下载 不再需要临时文件转储 2023-09-07 11:32:27 +08:00
0bcebc8c30 调整 录制时长 和 超时控制 2023-09-07 10:36:55 +08:00
c4ebda0713 调整 录制时长 和 超时控制 2023-09-07 10:35:35 +08:00
67a891ada8 调整 录制时长 和 超时控制 2023-09-07 10:35:00 +08:00
310ec451fb AsyncContext 超时调整 2023-09-07 09:52:11 +08:00
0ece1e2915 readme 补充 2023-09-07 09:25:24 +08:00
c1e9ce147f 调整为异步 context 2023-09-07 03:09:25 +08:00
95 changed files with 6353 additions and 261 deletions

View File

@ -45,3 +45,14 @@ docker run --name gb28181-wvp-proxy --rm \
mvn deploy -DaltDeploymentRepository=amleixun-mvn-reop::default::file:H:/Repository/skcks.cn/gb28181-docking-platform-mvn-repo
```
git push 推送即可
### docker ffmpeg 推流
```shell
docker run --rm jrottenberg/ffmpeg \
-re -i \
"http://192.168.1.241:5080/rtp/44050100001180000001_44050100001310000001.live.flv" \
-vcodec h264 \
-acodec aac \
-f flv \
rtmp://192.168.1.241:1935/live/test?secret=035c73f7-bb6b-4889-a715-d9eb2d1925cc
```

Binary file not shown.

View File

@ -5,6 +5,8 @@ import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import java.io.IOException;
/**
* 全局异常处理类
*
@ -13,7 +15,7 @@ import org.springframework.web.bind.annotation.ExceptionHandler;
@Slf4j
@ControllerAdvice
public class BasicExceptionAdvice {
@ExceptionHandler(Exception.class)
@ExceptionHandler(IOException.class)
public void exception(HttpServletRequest request, Exception e) {
if(request.getRequestURI().equals("/video")){
return;

View File

@ -1,5 +1,6 @@
package cn.skcks.docking.gb28181.wvp.advice;
import cn.skcks.docking.gb28181.common.json.JsonException;
import cn.skcks.docking.gb28181.common.json.JsonResponse;
import jakarta.validation.ConstraintViolationException;
import lombok.extern.slf4j.Slf4j;
@ -72,6 +73,11 @@ public class ExceptionAdvice {
return JsonResponse.error("参数异常");
}
@ExceptionHandler(JsonException.class)
public JsonResponse<String> handleJsonException(JsonException e){
return JsonResponse.error(e.getMessage());
}
@ExceptionHandler(Exception.class)
public JsonResponse<String> exception(Exception e) {
e.printStackTrace();

View File

@ -1,91 +0,0 @@
package cn.skcks.docking.gb28181.wvp.api;
import cn.hutool.core.io.IoUtil;
import cn.hutool.core.util.IdUtil;
import cn.skcks.docking.gb28181.media.config.ZlmMediaConfig;
import cn.skcks.docking.gb28181.wvp.config.SwaggerConfig;
import cn.skcks.docking.gb28181.wvp.proxy.ZlmProxyClient;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.bytedeco.ffmpeg.global.avcodec;
import org.bytedeco.javacv.*;
import org.springdoc.core.models.GroupedOpenApi;
import org.springframework.context.annotation.Bean;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import java.io.*;
import java.nio.file.Path;
import static org.bytedeco.ffmpeg.global.avutil.AVMEDIA_TYPE_VIDEO;
@Slf4j
@Tag(name = "视频api")
@Controller
@RequestMapping("/video")
@RequiredArgsConstructor
public class VideoController {
private final ZlmProxyClient zlmProxyClient;
private final ZlmMediaConfig config;
@Bean
public GroupedOpenApi videoApi() {
return SwaggerConfig.api("VideoApi", "/video");
}
@Operation(summary = "获取视频")
@GetMapping(produces = MediaType.APPLICATION_OCTET_STREAM_VALUE)
@ResponseBody
public void video(HttpServletResponse response) throws IOException {
response.reset();
// response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE);
response.setContentType("video/mp4");
Path tmp = Path.of(System.getProperty("java.io.tmpdir"), IdUtil.getSnowflakeNextIdStr()).toAbsolutePath();
File file = new File(tmp + ".mp4");
log.info("创建文件 {}, {}", file, file.createNewFile());
String url = StringUtils.joinWith("/", config.getUrl(), "live", "test.live.mp4");
log.info("url {}", url);
try (
FFmpegFrameGrabber grabber = new FFmpegFrameGrabber(url);
FFmpegFrameRecorder recorder = new FFmpegFrameRecorder(file, 1920, 1080, 0)
) {
grabber.start();
recorder.start();
log.info("开始录像");
log.info("{}", file);
recorder.setVideoCodec(avcodec.AV_CODEC_ID_H264);
// recorder.setPixelFormat(avutil.AV_PIX_FMT_YUV420P); //视频源数据yuv
recorder.setAudioCodec(avcodec.AV_CODEC_ID_AAC); //设置音频压缩方式
recorder.setFormat("mp4");
recorder.setVideoOption("threads", String.valueOf(Runtime.getRuntime().availableProcessors())); //解码线程数
try {
Frame frame;
while ((frame = grabber.grab()) != null) {
if (frame.streamIndex == AVMEDIA_TYPE_VIDEO) {
recorder.record(frame);
}
}
grabber.stop();
grabber.release();
recorder.stop();
} catch (FFmpegFrameRecorder.Exception | FrameGrabber.Exception e) {
throw new RuntimeException(e);
}
}
log.info("录像结束");
InputStream inputStream = new BufferedInputStream(new FileInputStream(file));
OutputStream outputStream = new BufferedOutputStream(response.getOutputStream());
IoUtil.copy(inputStream, outputStream);
log.info("临时文件 {} 写入 响应 完成", file);
log.info("删除临时文件 {} {}", file, file.delete());
}
}

View File

@ -0,0 +1,95 @@
package cn.skcks.docking.gb28181.wvp.api.device;
import cn.skcks.docking.gb28181.annotation.web.JsonMapping;
import cn.skcks.docking.gb28181.annotation.web.methods.GetJson;
import cn.skcks.docking.gb28181.annotation.web.methods.PostJson;
import cn.skcks.docking.gb28181.common.json.JsonResponse;
import cn.skcks.docking.gb28181.common.page.PageWrapper;
import cn.skcks.docking.gb28181.wvp.api.device.convertor.DeviceDTOConvertor;
import cn.skcks.docking.gb28181.wvp.api.device.dto.AddDeviceDTO;
import cn.skcks.docking.gb28181.wvp.api.device.dto.DevicePageDTO;
import cn.skcks.docking.gb28181.wvp.config.SwaggerConfig;
import cn.skcks.docking.gb28181.wvp.orm.mybatis.dynamic.model.WvpProxyDevice;
import cn.skcks.docking.gb28181.wvp.service.device.DeviceService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springdoc.core.annotations.ParameterObject;
import org.springdoc.core.models.GroupedOpenApi;
import org.springframework.context.annotation.Bean;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Optional;
@Slf4j
@Tag(name = "设备信息")
@RestController
@RequestMapping("/device")
@RequiredArgsConstructor
public class DeviceController {
private final DeviceService deviceService;
@Bean
public GroupedOpenApi deviceApi() {
return SwaggerConfig.api("DeviceApi", "/device");
}
@Operation(summary = "分页查询设备列表")
@GetJson("/page")
public JsonResponse<PageWrapper<WvpProxyDevice>> getDevicesWithPagePage(@ParameterObject @Validated DevicePageDTO dto) {
return JsonResponse.success(PageWrapper.of(deviceService.getDevicesWithPage(dto.getPage(), dto.getSize())));
}
@Operation(summary = "添加设备")
@PostJson("/add")
public JsonResponse<Boolean> addDevice(@RequestBody AddDeviceDTO dto) {
return JsonResponse.success(deviceService.addDevice(DeviceDTOConvertor.INSTANCE.dto2dao(dto)));
}
@Operation(summary = "根据设备编码(21位) 查询指定设备信息")
@GetJson("/info/deviceCode")
public JsonResponse<WvpProxyDevice> infoByDeviceCode(@RequestParam String deviceCode) {
Optional<WvpProxyDevice> wvpProxyDevice = deviceService.getDeviceByDeviceCode(deviceCode);
return JsonResponse.success(wvpProxyDevice.orElse(null));
}
@Operation(summary = "根据国标id(20位) 查询指定设备信息")
@GetJson("/info/gbDeviceId")
public JsonResponse<List<WvpProxyDevice>> infoByGbDeviceId(@RequestParam String gbDeviceId) {
List<WvpProxyDevice> wvpProxyDevice = deviceService.getDeviceByGbDeviceId(gbDeviceId);
return JsonResponse.success(wvpProxyDevice);
}
@Operation(summary = "根据设备编码(21位) 删除指定设备")
@JsonMapping(value = "/delete/deviceCode",method = {RequestMethod.GET,RequestMethod.DELETE})
public JsonResponse<Boolean> deleteByDeviceCode(@RequestParam String deviceCode){
WvpProxyDevice wvpProxyDevice = new WvpProxyDevice();
wvpProxyDevice.setDeviceCode(deviceCode);
return JsonResponse.success(deviceService.deleteDevice(wvpProxyDevice));
}
@Operation(summary = "根据国标id(20位) 删除指定设备")
@JsonMapping(value = "/delete/gbDeviceId",method = {RequestMethod.GET,RequestMethod.DELETE})
public JsonResponse<Boolean> deleteByGbDeviceId(@RequestParam String gbDeviceId){
WvpProxyDevice wvpProxyDevice = new WvpProxyDevice();
wvpProxyDevice.setGbDeviceId(gbDeviceId);
return JsonResponse.success(deviceService.deleteDevice(wvpProxyDevice));
}
@Operation(summary = "根据主键 id 删除指定设备")
@JsonMapping(value = "/delete/id",method = {RequestMethod.GET,RequestMethod.DELETE})
public JsonResponse<Boolean> deleteById(@RequestParam Long id){
WvpProxyDevice wvpProxyDevice = new WvpProxyDevice();
wvpProxyDevice.setId(id);
return JsonResponse.success(deviceService.deleteDevice(wvpProxyDevice));
}
@Operation(summary = "根据主键id修改设备信息")
@PostJson("/modify")
public JsonResponse<Boolean> updateById(@RequestBody WvpProxyDevice device){
return JsonResponse.success(deviceService.modifyDevice(device));
}
}

View File

@ -0,0 +1,13 @@
package cn.skcks.docking.gb28181.wvp.api.device.convertor;
import cn.skcks.docking.gb28181.wvp.api.device.dto.AddDeviceDTO;
import cn.skcks.docking.gb28181.wvp.orm.mybatis.dynamic.model.WvpProxyDevice;
import org.mapstruct.Mapper;
import org.mapstruct.factory.Mappers;
@Mapper
public abstract class DeviceDTOConvertor {
public static final DeviceDTOConvertor INSTANCE = Mappers.getMapper(DeviceDTOConvertor.class);
abstract public WvpProxyDevice dto2dao(AddDeviceDTO dto);
}

View File

@ -0,0 +1,24 @@
package cn.skcks.docking.gb28181.wvp.api.device.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
@Data
public class AddDeviceDTO {
@NotBlank(message = "设备编码 不能为空")
@Schema(description = "设备编码")
private String deviceCode;
@NotBlank(message = "国标编码 不能为空")
@Schema(description = "国标编码")
private String gbDeviceId;
@NotBlank(message = "国标通道id 不能为空")
@Schema(description = "国标通道id")
private String gbDeviceChannelId;
@NotBlank(message = "设备名称 不能为空")
@Schema(description = "设备名称")
private String name;
@Schema(description = "地址")
private String address;
}

View File

@ -0,0 +1,19 @@
package cn.skcks.docking.gb28181.wvp.api.device.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
@Data
public class DevicePageDTO {
@Schema(description = "页数", example = "1")
@NotNull(message = "page 不能为空")
@Min(value = 1, message = "page 必须为正整数")
Integer page;
@Schema(description = "每页条数", example = "10")
@NotNull(message = "size 不能为空")
@Min(value = 1, message = "size 必须为正整数")
Integer size;
}

View File

@ -0,0 +1,36 @@
package cn.skcks.docking.gb28181.wvp.api.docking;
import cn.skcks.docking.gb28181.annotation.web.JsonMapping;
import cn.skcks.docking.gb28181.annotation.web.methods.GetJson;
import cn.skcks.docking.gb28181.common.json.JsonResponse;
import cn.skcks.docking.gb28181.common.page.PageWrapper;
import cn.skcks.docking.gb28181.wvp.api.docking.dto.DockingPageDTO;
import cn.skcks.docking.gb28181.wvp.config.SwaggerConfig;
import cn.skcks.docking.gb28181.wvp.orm.mybatis.dynamic.model.WvpProxyDocking;
import cn.skcks.docking.gb28181.wvp.service.docking.DockingService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springdoc.core.annotations.ParameterObject;
import org.springdoc.core.models.GroupedOpenApi;
import org.springframework.context.annotation.Bean;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.RestController;
@Tag(name = "设备/平台对接")
@RestController
@JsonMapping("/docking")
@RequiredArgsConstructor
public class DockingController {
private final DockingService dockingService;
@Bean
public GroupedOpenApi dockingApi() {
return SwaggerConfig.api("Docking", "/docking");
}
@Operation(summary = "分页查询对接的设备/平台列表")
@GetJson("/page")
private JsonResponse<PageWrapper<WvpProxyDocking>> page(@ParameterObject @Validated DockingPageDTO dto){
return JsonResponse.success(PageWrapper.of(dockingService.getDockingWithPage(dto.getPage(), dto.getSize())));
}
}

View File

@ -0,0 +1,19 @@
package cn.skcks.docking.gb28181.wvp.api.docking.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
@Data
public class DockingPageDTO {
@Schema(description = "页数", example = "1")
@NotNull(message = "page 不能为空")
@Min(value = 1, message = "page 必须为正整数")
Integer page;
@Schema(description = "每页条数", example = "10")
@NotNull(message = "size 不能为空")
@Min(value = 1, message = "size 必须为正整数")
Integer size;
}

View File

@ -0,0 +1,39 @@
package cn.skcks.docking.gb28181.wvp.api.download;
import cn.skcks.docking.gb28181.wvp.config.SwaggerConfig;
import cn.skcks.docking.gb28181.wvp.service.download.DownloadService;
import cn.skcks.docking.gb28181.wvp.service.gb28181.Gb28181DownloadService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.springdoc.core.models.GroupedOpenApi;
import org.springframework.context.annotation.Bean;
import org.springframework.web.bind.annotation.*;
@RequiredArgsConstructor
@Tag(name = "下载")
@RestController
@RequestMapping("/download")
public class DownloadController {
private final DownloadService downloadService;
private final Gb28181DownloadService gb28181DownloadService;
@Bean
public GroupedOpenApi downloadApi() {
return SwaggerConfig.api("DownloadApi", "/download");
}
@Operation(summary = "下载代理")
@RequestMapping(method = RequestMethod.HEAD, value = "/proxy")
public void downloadProxyHeader(HttpServletRequest request, HttpServletResponse response, @RequestParam String url) {
downloadService.header(request, response, url);
}
@Operation(summary = "下载代理")
@GetMapping("/proxy")
public void downloadProxy(HttpServletRequest request, HttpServletResponse response, @RequestParam String url) {
downloadService.download(request, response, url);
}
}

View File

@ -0,0 +1,63 @@
package cn.skcks.docking.gb28181.wvp.api.gb28181;
import cn.skcks.docking.gb28181.annotation.web.JsonMapping;
import cn.skcks.docking.gb28181.annotation.web.methods.GetJson;
import cn.skcks.docking.gb28181.annotation.web.methods.PostJson;
import cn.skcks.docking.gb28181.common.json.JsonResponse;
import cn.skcks.docking.gb28181.service.record.vo.RecordInfoItemVO;
import cn.skcks.docking.gb28181.wvp.config.SwaggerConfig;
import cn.skcks.docking.gb28181.wvp.service.catalog.CatalogService;
import cn.skcks.docking.gb28181.wvp.service.device.control.DeviceControlService;
import cn.skcks.docking.gb28181.wvp.service.record.RecordInfoService;
import cn.skcks.docking.gb28181.wvp.service.record.dto.RecordInfoDTO;
import cn.skcks.docking.gb28181.wvp.sip.message.message.catalog.dto.CatalogItemDTO;
import cn.skcks.docking.gb28181.wvp.sip.message.message.catalog.dto.CatalogResponseDTO;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import org.springdoc.core.models.GroupedOpenApi;
import org.springframework.context.annotation.Bean;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.context.request.async.DeferredResult;
import java.util.List;
import java.util.concurrent.CompletableFuture;
@RequiredArgsConstructor
@RestController
@JsonMapping("/gb28181")
public class Gb28181Controller {
private final CatalogService catalogService;
private final DeviceControlService deviceControlService;
private final RecordInfoService recordInfoService;
@Bean
public GroupedOpenApi gb28181Api() {
return SwaggerConfig.api("Gb28181Api", "/gb28181");
}
@SneakyThrows
@GetJson("/catalog")
public DeferredResult<JsonResponse<List<CatalogItemDTO>>> catalog(@RequestParam("gbDeviceId") String id){
DeferredResult<JsonResponse<List<CatalogItemDTO>>> result = new DeferredResult<>();
CompletableFuture<CatalogResponseDTO> catalog = catalogService.getCatalog(id);
catalog.thenApplyAsync((dto)->{
List<CatalogItemDTO> deviceList = dto.getDeviceList().getDeviceList();
result.setResult(JsonResponse.success(deviceList));
return null;
});
return result;
}
@SneakyThrows
@GetJson("/deviceControl/recordCmd")
public JsonResponse<Void> recordCmd(@RequestParam String deviceCode,@RequestParam String cmd){
deviceControlService.sendRecordControl(deviceCode, cmd);
return JsonResponse.success(null);
}
@PostJson("/recordInfo")
public DeferredResult<JsonResponse<List<RecordInfoItemVO>>> recordInfo(RecordInfoDTO dto){
return recordInfoService.requestRecordInfo(dto);
}
}

View File

@ -1,25 +1,31 @@
package cn.skcks.docking.gb28181.wvp.api.video;
import cn.skcks.docking.gb28181.wvp.service.video.RecordService;
import cn.skcks.docking.gb28181.wvp.service.video.VideoService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
@Slf4j
@Tag(name = "录像api")
@RequiredArgsConstructor
@RestController
@RequestMapping("/video/record")
public class RecordController {
private final RecordService recordService;
private final VideoService videoService;
@Operation(summary = "返回文件下载 http 头信息",description = "禁止多线程下载, 默认文件名为 record.mp4")
@RequestMapping(method = {RequestMethod.HEAD,RequestMethod.OPTIONS})
public void record(HttpServletResponse response){
recordService.header(response);
videoService.header(response);
}
@Operation(summary = "录制flv视频流, 并下载为flv文件")
@GetMapping
public void record(HttpServletResponse response, @RequestParam String url,@RequestParam long time){
recordService.record(response,url,time);
public void record(HttpServletRequest request, HttpServletResponse response, @RequestParam String url, @RequestParam long time){
videoService.record(request, response,url,time);
}
}

View File

@ -0,0 +1,95 @@
package cn.skcks.docking.gb28181.wvp.api.video;
import cn.hutool.core.date.DateTime;
import cn.hutool.core.date.DateUtil;
import cn.skcks.docking.gb28181.common.json.JsonResponse;
import cn.skcks.docking.gb28181.media.config.ZlmMediaConfig;
import cn.skcks.docking.gb28181.wvp.api.video.dto.RealtimeVideoReq;
import cn.skcks.docking.gb28181.wvp.api.video.dto.VideoMp4Req;
import cn.skcks.docking.gb28181.wvp.api.video.dto.VideoReq;
import cn.skcks.docking.gb28181.wvp.config.Gb28181DeviceVideoApiConfig;
import cn.skcks.docking.gb28181.wvp.config.SwaggerConfig;
import cn.skcks.docking.gb28181.wvp.config.WvpProxyConfig;
import cn.skcks.docking.gb28181.wvp.service.gb28181.Gb28181DownloadService;
import cn.skcks.docking.gb28181.wvp.service.video.VideoService;
import cn.skcks.docking.gb28181.wvp.service.wvp.WvpService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springdoc.core.annotations.ParameterObject;
import org.springdoc.core.models.GroupedOpenApi;
import org.springframework.context.annotation.Bean;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.context.request.async.DeferredResult;
import java.util.Date;
@Slf4j
@Tag(name = "视频api")
@Controller
@RequestMapping("/video")
@RequiredArgsConstructor
public class VideoController {
private final ZlmMediaConfig config;
private final VideoService videoService;
private final WvpService wvpService;
private final WvpProxyConfig proxyConfig;
private final Gb28181DownloadService gb28181DownloadService;
private final Gb28181DeviceVideoApiConfig gb28181DeviceVideoApiConfig;
@Bean
public GroupedOpenApi videoApi() {
return SwaggerConfig.api("VideoApi", "/video");
}
@Operation(summary = "获取视频 (返回视频流)")
@GetMapping(produces = MediaType.APPLICATION_OCTET_STREAM_VALUE)
@ResponseBody
public void video(HttpServletRequest request, HttpServletResponse response, @ParameterObject VideoReq req) {
if(proxyConfig.getEnable()){
wvpService.video(request,response,req.getDeviceCode(), req.getStartTime(), req.getEndTime());
} else {
gb28181DownloadService.video(request,response,req.getDeviceCode(), req.getStartTime(), req.getEndTime(), req.getFileHeader(), req.getUseDownload());
}
}
@Operation(summary = "获取视频 (返回视频url)")
@GetMapping(value = "/device/video.mp4")
@ResponseBody
public DeferredResult<JsonResponse<String>> video(@ParameterObject VideoMp4Req req) {
long forward = gb28181DeviceVideoApiConfig.getOffset().getForward().toMillis();
long back = gb28181DeviceVideoApiConfig.getOffset().getBack().toMillis();
DateTime reqStartTime = DateUtil.date(req.getStartTime());
DateTime reqEndTime = DateUtil.date(req.getEndTime());
Date startTime = DateUtil.offsetMillisecond(reqStartTime, (int) -forward);
Date endTime = DateUtil.offsetMillisecond(reqEndTime, (int) back);
log.info("请求的时间范围 {} ~ {}", reqStartTime, reqEndTime);
log.info("偏移后的时间范围 {} ~ {}", startTime, endTime);
return gb28181DownloadService.videoUrl(req.getDeviceCode(), startTime, endTime);
}
@Operation(summary = "获取实时视频 (返回视频url)")
@GetMapping(value = "/device/realtime")
@ResponseBody
public DeferredResult<JsonResponse<String>> realtime(@ParameterObject RealtimeVideoReq req) {
return wvpService.realtimeVideoUrl(req.getDeviceCode());
}
@Operation(summary = "关闭实时视频")
@GetMapping(value = "/device/realtime/close")
@ResponseBody
public JsonResponse<Void> close(@ParameterObject RealtimeVideoReq req) {
wvpService.closeRealtimeVideo(req.getDeviceCode());
return JsonResponse.success(null);
}
}

View File

@ -0,0 +1,14 @@
package cn.skcks.docking.gb28181.wvp.api.video.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
import lombok.NoArgsConstructor;
@NoArgsConstructor
@Data
public class RealtimeVideoReq {
@NotBlank(message = "设备编码 不能为空")
@Schema(description = "设备编码")
private String deviceCode;
}

View File

@ -0,0 +1,26 @@
package cn.skcks.docking.gb28181.wvp.api.video.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
import lombok.NoArgsConstructor;
@NoArgsConstructor
@Data
public class VideoMp4Req {
@NotBlank(message = "设备编码 不能为空")
@Schema(description = "设备编码")
private String deviceCode;
@Schema(description = "开始时间 (毫秒) (proxy.gb28181.device-api.offset 可额外设置偏移量)",example = "1695593100207")
@NotBlank(message = "开始时间 不能为空")
private Long startTime;
@Schema(description = "结束时间 (毫秒) (proxy.gb28181.device-api.offset 可额外设置偏移量)",example = "1695593190207")
@NotBlank(message = "结束时间 不能为空")
private Long endTime;
@Schema(description = "http 头是否需要文件名 (没有文件名时浏览器会试图直接播放,会导致短时间内重复访问同一设备,导致失败)")
private Boolean fileHeader = true;
}

View File

@ -0,0 +1,68 @@
package cn.skcks.docking.gb28181.wvp.api.video.dto;
import cn.hutool.core.date.DatePattern;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.format.annotation.DateTimeFormat;
import java.util.Date;
@NoArgsConstructor
@Data
public class VideoReq {
@NotBlank(message = "设备编码 不能为空")
@Schema(description = "设备编码 (支持参数名: deviceCode device_code deviceId device_id)")
private String deviceCode;
@Schema(description = "开始时间 (支持参数名: startTime start_time beginTime begin_time)",example = "20230909000000")
@NotBlank(message = "开始时间 不能为空")
@DateTimeFormat(pattern= DatePattern.PURE_DATETIME_PATTERN)
@JsonFormat(pattern = DatePattern.PURE_DATETIME_PATTERN)
private Date startTime;
@Schema(description = "结束时间 (支持参数名: endTime end_time endTime end_time)",example = "20230909000500")
@NotBlank(message = "结束时间 不能为空")
@DateTimeFormat(pattern= DatePattern.PURE_DATETIME_PATTERN)
@JsonFormat(pattern = DatePattern.PURE_DATETIME_PATTERN)
private Date endTime;
@Schema(description = "http 头是否需要文件名 (没有文件名时浏览器会试图直接播放,会导致短时间内重复访问同一设备,导致失败)")
private Boolean fileHeader = true;
@Schema(description = "使用哪种方式拉取历史视频 (true 为 使用 Download 方式拉取 4倍速流, false 为 使用 Playback 原始速率拉取 视频回放)")
private Boolean useDownload = true;
public void setDevice_id(String deviceCode){
this.deviceCode = deviceCode;
}
public void setDevice_code(String deviceCode){
this.deviceCode = deviceCode;
}
@DateTimeFormat(pattern= DatePattern.PURE_DATETIME_PATTERN)
@JsonFormat(pattern = DatePattern.PURE_DATETIME_PATTERN)
public void setBegin_time(Date startTime){
this.startTime = startTime;
}
@DateTimeFormat(pattern= DatePattern.PURE_DATETIME_PATTERN)
@JsonFormat(pattern = DatePattern.PURE_DATETIME_PATTERN)
public void setStart_time(Date startTime){
this.startTime = startTime;
}
@DateTimeFormat(pattern= DatePattern.PURE_DATETIME_PATTERN)
@JsonFormat(pattern = DatePattern.PURE_DATETIME_PATTERN)
public void setEnd_time(Date endTime){
this.endTime = endTime;
}
public void setUse_download(Boolean useDownload){
this.useDownload = useDownload;
}
}

View File

@ -0,0 +1,17 @@
package cn.skcks.docking.gb28181.wvp.common.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotNull;
public class PageDTO {
@Schema(description = "页数", example = "10")
@NotNull(message = "page 不能为空")
@Min(value = 1, message = "page 必须为正整数")
int page;
@Schema(description = "每页条数", example = "10")
@NotNull(message = "size 不能为空")
@Min(value = 1, message = "size 必须为正整数")
int size;
}

View File

@ -5,6 +5,7 @@ import jakarta.validation.constraints.NotNull;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@ -19,4 +20,14 @@ public class WebConfig implements WebMvcConfigurer {
.excludePathPatterns("/swagger-ui/**","/v3/api-docs/**")
.addPathPatterns("/**");
}
@Override
public void addCorsMappings(CorsRegistry corsRegistry){
corsRegistry.addMapping("/**")
.allowedOriginPatterns("*")
.allowCredentials(true)
.allowedMethods("GET","POST","HEAD","DELETE","PUT")
.allowedHeaders("*")
.maxAge(3600);
}
}

View File

@ -1,5 +1,6 @@
package cn.skcks.docking.gb28181.wvp.orm.mybatis.dynamic.mapper;
import static cn.skcks.docking.gb28181.wvp.orm.mybatis.dynamic.mapper.WvpProxyDeviceDynamicSqlSupport.*;
import static org.mybatis.dynamic.sql.SqlBuilder.isEqualTo;
import cn.skcks.docking.gb28181.wvp.orm.mybatis.dynamic.model.WvpProxyDevice;
@ -31,7 +32,7 @@ import org.mybatis.dynamic.sql.util.mybatis3.MyBatis3Utils;
@Mapper
public interface WvpProxyDeviceMapper extends CommonCountMapper, CommonDeleteMapper, CommonInsertMapper<WvpProxyDevice>, CommonUpdateMapper {
@Generated(value="org.mybatis.generator.api.MyBatisGenerator", comments="Source Table: wvp_proxy_device")
BasicColumn[] selectList = BasicColumn.columnList(WvpProxyDeviceDynamicSqlSupport.id, WvpProxyDeviceDynamicSqlSupport.deviceCode, WvpProxyDeviceDynamicSqlSupport.gbDeviceId, WvpProxyDeviceDynamicSqlSupport.gbDeviceChannelId, WvpProxyDeviceDynamicSqlSupport.name, WvpProxyDeviceDynamicSqlSupport.address);
BasicColumn[] selectList = BasicColumn.columnList(id, deviceCode, gbDeviceId, gbDeviceChannelId, name, address);
@Generated(value="org.mybatis.generator.api.MyBatisGenerator", comments="Source Table: wvp_proxy_device")
@SelectProvider(type=SqlProviderAdapter.class, method="select")
@ -52,125 +53,125 @@ public interface WvpProxyDeviceMapper extends CommonCountMapper, CommonDeleteMap
@Generated(value="org.mybatis.generator.api.MyBatisGenerator", comments="Source Table: wvp_proxy_device")
default long count(CountDSLCompleter completer) {
return MyBatis3Utils.countFrom(this::count, WvpProxyDeviceDynamicSqlSupport.wvpProxyDevice, completer);
return MyBatis3Utils.countFrom(this::count, wvpProxyDevice, completer);
}
@Generated(value="org.mybatis.generator.api.MyBatisGenerator", comments="Source Table: wvp_proxy_device")
default int delete(DeleteDSLCompleter completer) {
return MyBatis3Utils.deleteFrom(this::delete, WvpProxyDeviceDynamicSqlSupport.wvpProxyDevice, completer);
return MyBatis3Utils.deleteFrom(this::delete, wvpProxyDevice, completer);
}
@Generated(value="org.mybatis.generator.api.MyBatisGenerator", comments="Source Table: wvp_proxy_device")
default int deleteByPrimaryKey(Long id_) {
return delete(c ->
c.where(WvpProxyDeviceDynamicSqlSupport.id, isEqualTo(id_))
c.where(id, isEqualTo(id_))
);
}
@Generated(value="org.mybatis.generator.api.MyBatisGenerator", comments="Source Table: wvp_proxy_device")
default int insert(WvpProxyDevice row) {
return MyBatis3Utils.insert(this::insert, row, WvpProxyDeviceDynamicSqlSupport.wvpProxyDevice, c ->
c.map(WvpProxyDeviceDynamicSqlSupport.id).toProperty("id")
.map(WvpProxyDeviceDynamicSqlSupport.deviceCode).toProperty("deviceCode")
.map(WvpProxyDeviceDynamicSqlSupport.gbDeviceId).toProperty("gbDeviceId")
.map(WvpProxyDeviceDynamicSqlSupport.gbDeviceChannelId).toProperty("gbDeviceChannelId")
.map(WvpProxyDeviceDynamicSqlSupport.name).toProperty("name")
.map(WvpProxyDeviceDynamicSqlSupport.address).toProperty("address")
return MyBatis3Utils.insert(this::insert, row, wvpProxyDevice, c ->
c.map(id).toProperty("id")
.map(deviceCode).toProperty("deviceCode")
.map(gbDeviceId).toProperty("gbDeviceId")
.map(gbDeviceChannelId).toProperty("gbDeviceChannelId")
.map(name).toProperty("name")
.map(address).toProperty("address")
);
}
@Generated(value="org.mybatis.generator.api.MyBatisGenerator", comments="Source Table: wvp_proxy_device")
default int insertMultiple(Collection<WvpProxyDevice> records) {
return MyBatis3Utils.insertMultiple(this::insertMultiple, records, WvpProxyDeviceDynamicSqlSupport.wvpProxyDevice, c ->
c.map(WvpProxyDeviceDynamicSqlSupport.id).toProperty("id")
.map(WvpProxyDeviceDynamicSqlSupport.deviceCode).toProperty("deviceCode")
.map(WvpProxyDeviceDynamicSqlSupport.gbDeviceId).toProperty("gbDeviceId")
.map(WvpProxyDeviceDynamicSqlSupport.gbDeviceChannelId).toProperty("gbDeviceChannelId")
.map(WvpProxyDeviceDynamicSqlSupport.name).toProperty("name")
.map(WvpProxyDeviceDynamicSqlSupport.address).toProperty("address")
return MyBatis3Utils.insertMultiple(this::insertMultiple, records, wvpProxyDevice, c ->
c.map(id).toProperty("id")
.map(deviceCode).toProperty("deviceCode")
.map(gbDeviceId).toProperty("gbDeviceId")
.map(gbDeviceChannelId).toProperty("gbDeviceChannelId")
.map(name).toProperty("name")
.map(address).toProperty("address")
);
}
@Generated(value="org.mybatis.generator.api.MyBatisGenerator", comments="Source Table: wvp_proxy_device")
default int insertSelective(WvpProxyDevice row) {
return MyBatis3Utils.insert(this::insert, row, WvpProxyDeviceDynamicSqlSupport.wvpProxyDevice, c ->
c.map(WvpProxyDeviceDynamicSqlSupport.id).toPropertyWhenPresent("id", row::getId)
.map(WvpProxyDeviceDynamicSqlSupport.deviceCode).toPropertyWhenPresent("deviceCode", row::getDeviceCode)
.map(WvpProxyDeviceDynamicSqlSupport.gbDeviceId).toPropertyWhenPresent("gbDeviceId", row::getGbDeviceId)
.map(WvpProxyDeviceDynamicSqlSupport.gbDeviceChannelId).toPropertyWhenPresent("gbDeviceChannelId", row::getGbDeviceChannelId)
.map(WvpProxyDeviceDynamicSqlSupport.name).toPropertyWhenPresent("name", row::getName)
.map(WvpProxyDeviceDynamicSqlSupport.address).toPropertyWhenPresent("address", row::getAddress)
return MyBatis3Utils.insert(this::insert, row, wvpProxyDevice, c ->
c.map(id).toPropertyWhenPresent("id", row::getId)
.map(deviceCode).toPropertyWhenPresent("deviceCode", row::getDeviceCode)
.map(gbDeviceId).toPropertyWhenPresent("gbDeviceId", row::getGbDeviceId)
.map(gbDeviceChannelId).toPropertyWhenPresent("gbDeviceChannelId", row::getGbDeviceChannelId)
.map(name).toPropertyWhenPresent("name", row::getName)
.map(address).toPropertyWhenPresent("address", row::getAddress)
);
}
@Generated(value="org.mybatis.generator.api.MyBatisGenerator", comments="Source Table: wvp_proxy_device")
default Optional<WvpProxyDevice> selectOne(SelectDSLCompleter completer) {
return MyBatis3Utils.selectOne(this::selectOne, selectList, WvpProxyDeviceDynamicSqlSupport.wvpProxyDevice, completer);
return MyBatis3Utils.selectOne(this::selectOne, selectList, wvpProxyDevice, completer);
}
@Generated(value="org.mybatis.generator.api.MyBatisGenerator", comments="Source Table: wvp_proxy_device")
default List<WvpProxyDevice> select(SelectDSLCompleter completer) {
return MyBatis3Utils.selectList(this::selectMany, selectList, WvpProxyDeviceDynamicSqlSupport.wvpProxyDevice, completer);
return MyBatis3Utils.selectList(this::selectMany, selectList, wvpProxyDevice, completer);
}
@Generated(value="org.mybatis.generator.api.MyBatisGenerator", comments="Source Table: wvp_proxy_device")
default List<WvpProxyDevice> selectDistinct(SelectDSLCompleter completer) {
return MyBatis3Utils.selectDistinct(this::selectMany, selectList, WvpProxyDeviceDynamicSqlSupport.wvpProxyDevice, completer);
return MyBatis3Utils.selectDistinct(this::selectMany, selectList, wvpProxyDevice, completer);
}
@Generated(value="org.mybatis.generator.api.MyBatisGenerator", comments="Source Table: wvp_proxy_device")
default Optional<WvpProxyDevice> selectByPrimaryKey(Long id_) {
return selectOne(c ->
c.where(WvpProxyDeviceDynamicSqlSupport.id, isEqualTo(id_))
c.where(id, isEqualTo(id_))
);
}
@Generated(value="org.mybatis.generator.api.MyBatisGenerator", comments="Source Table: wvp_proxy_device")
default int update(UpdateDSLCompleter completer) {
return MyBatis3Utils.update(this::update, WvpProxyDeviceDynamicSqlSupport.wvpProxyDevice, completer);
return MyBatis3Utils.update(this::update, wvpProxyDevice, completer);
}
@Generated(value="org.mybatis.generator.api.MyBatisGenerator", comments="Source Table: wvp_proxy_device")
static UpdateDSL<UpdateModel> updateAllColumns(WvpProxyDevice row, UpdateDSL<UpdateModel> dsl) {
return dsl.set(WvpProxyDeviceDynamicSqlSupport.id).equalTo(row::getId)
.set(WvpProxyDeviceDynamicSqlSupport.deviceCode).equalTo(row::getDeviceCode)
.set(WvpProxyDeviceDynamicSqlSupport.gbDeviceId).equalTo(row::getGbDeviceId)
.set(WvpProxyDeviceDynamicSqlSupport.gbDeviceChannelId).equalTo(row::getGbDeviceChannelId)
.set(WvpProxyDeviceDynamicSqlSupport.name).equalTo(row::getName)
.set(WvpProxyDeviceDynamicSqlSupport.address).equalTo(row::getAddress);
return dsl.set(id).equalTo(row::getId)
.set(deviceCode).equalTo(row::getDeviceCode)
.set(gbDeviceId).equalTo(row::getGbDeviceId)
.set(gbDeviceChannelId).equalTo(row::getGbDeviceChannelId)
.set(name).equalTo(row::getName)
.set(address).equalTo(row::getAddress);
}
@Generated(value="org.mybatis.generator.api.MyBatisGenerator", comments="Source Table: wvp_proxy_device")
static UpdateDSL<UpdateModel> updateSelectiveColumns(WvpProxyDevice row, UpdateDSL<UpdateModel> dsl) {
return dsl.set(WvpProxyDeviceDynamicSqlSupport.id).equalToWhenPresent(row::getId)
.set(WvpProxyDeviceDynamicSqlSupport.deviceCode).equalToWhenPresent(row::getDeviceCode)
.set(WvpProxyDeviceDynamicSqlSupport.gbDeviceId).equalToWhenPresent(row::getGbDeviceId)
.set(WvpProxyDeviceDynamicSqlSupport.gbDeviceChannelId).equalToWhenPresent(row::getGbDeviceChannelId)
.set(WvpProxyDeviceDynamicSqlSupport.name).equalToWhenPresent(row::getName)
.set(WvpProxyDeviceDynamicSqlSupport.address).equalToWhenPresent(row::getAddress);
return dsl.set(id).equalToWhenPresent(row::getId)
.set(deviceCode).equalToWhenPresent(row::getDeviceCode)
.set(gbDeviceId).equalToWhenPresent(row::getGbDeviceId)
.set(gbDeviceChannelId).equalToWhenPresent(row::getGbDeviceChannelId)
.set(name).equalToWhenPresent(row::getName)
.set(address).equalToWhenPresent(row::getAddress);
}
@Generated(value="org.mybatis.generator.api.MyBatisGenerator", comments="Source Table: wvp_proxy_device")
default int updateByPrimaryKey(WvpProxyDevice row) {
return update(c ->
c.set(WvpProxyDeviceDynamicSqlSupport.deviceCode).equalTo(row::getDeviceCode)
.set(WvpProxyDeviceDynamicSqlSupport.gbDeviceId).equalTo(row::getGbDeviceId)
.set(WvpProxyDeviceDynamicSqlSupport.gbDeviceChannelId).equalTo(row::getGbDeviceChannelId)
.set(WvpProxyDeviceDynamicSqlSupport.name).equalTo(row::getName)
.set(WvpProxyDeviceDynamicSqlSupport.address).equalTo(row::getAddress)
.where(WvpProxyDeviceDynamicSqlSupport.id, isEqualTo(row::getId))
c.set(deviceCode).equalTo(row::getDeviceCode)
.set(gbDeviceId).equalTo(row::getGbDeviceId)
.set(gbDeviceChannelId).equalTo(row::getGbDeviceChannelId)
.set(name).equalTo(row::getName)
.set(address).equalTo(row::getAddress)
.where(id, isEqualTo(row::getId))
);
}
@Generated(value="org.mybatis.generator.api.MyBatisGenerator", comments="Source Table: wvp_proxy_device")
default int updateByPrimaryKeySelective(WvpProxyDevice row) {
return update(c ->
c.set(WvpProxyDeviceDynamicSqlSupport.deviceCode).equalToWhenPresent(row::getDeviceCode)
.set(WvpProxyDeviceDynamicSqlSupport.gbDeviceId).equalToWhenPresent(row::getGbDeviceId)
.set(WvpProxyDeviceDynamicSqlSupport.gbDeviceChannelId).equalToWhenPresent(row::getGbDeviceChannelId)
.set(WvpProxyDeviceDynamicSqlSupport.name).equalToWhenPresent(row::getName)
.set(WvpProxyDeviceDynamicSqlSupport.address).equalToWhenPresent(row::getAddress)
.where(WvpProxyDeviceDynamicSqlSupport.id, isEqualTo(row::getId))
c.set(deviceCode).equalToWhenPresent(row::getDeviceCode)
.set(gbDeviceId).equalToWhenPresent(row::getGbDeviceId)
.set(gbDeviceChannelId).equalToWhenPresent(row::getGbDeviceChannelId)
.set(name).equalToWhenPresent(row::getName)
.set(address).equalToWhenPresent(row::getAddress)
.where(id, isEqualTo(row::getId))
);
}
}

View File

@ -0,0 +1,38 @@
package cn.skcks.docking.gb28181.wvp.orm.mybatis.dynamic.mapper;
import jakarta.annotation.Generated;
import java.sql.JDBCType;
import org.mybatis.dynamic.sql.AliasableSqlTable;
import org.mybatis.dynamic.sql.SqlColumn;
public final class WvpProxyDockingDynamicSqlSupport {
@Generated(value="org.mybatis.generator.api.MyBatisGenerator", comments="Source Table: wvp_proxy_docking")
public static final WvpProxyDocking wvpProxyDocking = new WvpProxyDocking();
@Generated(value="org.mybatis.generator.api.MyBatisGenerator", comments="Source field: wvp_proxy_docking.id")
public static final SqlColumn<Long> id = wvpProxyDocking.id;
@Generated(value="org.mybatis.generator.api.MyBatisGenerator", comments="Source field: wvp_proxy_docking.gb_device_id")
public static final SqlColumn<String> gbDeviceId = wvpProxyDocking.gbDeviceId;
@Generated(value="org.mybatis.generator.api.MyBatisGenerator", comments="Source field: wvp_proxy_docking.ip")
public static final SqlColumn<String> ip = wvpProxyDocking.ip;
@Generated(value="org.mybatis.generator.api.MyBatisGenerator", comments="Source field: wvp_proxy_docking.port")
public static final SqlColumn<String> port = wvpProxyDocking.port;
@Generated(value="org.mybatis.generator.api.MyBatisGenerator", comments="Source Table: wvp_proxy_docking")
public static final class WvpProxyDocking extends AliasableSqlTable<WvpProxyDocking> {
public final SqlColumn<Long> id = column("id", JDBCType.BIGINT);
public final SqlColumn<String> gbDeviceId = column("gb_device_id", JDBCType.VARCHAR);
public final SqlColumn<String> ip = column("ip", JDBCType.VARCHAR);
public final SqlColumn<String> port = column("port", JDBCType.VARCHAR);
public WvpProxyDocking() {
super("wvp_proxy_docking", WvpProxyDocking::new);
}
}
}

View File

@ -0,0 +1,161 @@
package cn.skcks.docking.gb28181.wvp.orm.mybatis.dynamic.mapper;
import static cn.skcks.docking.gb28181.wvp.orm.mybatis.dynamic.mapper.WvpProxyDockingDynamicSqlSupport.*;
import static org.mybatis.dynamic.sql.SqlBuilder.isEqualTo;
import cn.skcks.docking.gb28181.wvp.orm.mybatis.dynamic.model.WvpProxyDocking;
import jakarta.annotation.Generated;
import java.util.Collection;
import java.util.List;
import java.util.Optional;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Result;
import org.apache.ibatis.annotations.ResultMap;
import org.apache.ibatis.annotations.Results;
import org.apache.ibatis.annotations.SelectProvider;
import org.apache.ibatis.type.JdbcType;
import org.mybatis.dynamic.sql.BasicColumn;
import org.mybatis.dynamic.sql.delete.DeleteDSLCompleter;
import org.mybatis.dynamic.sql.select.CountDSLCompleter;
import org.mybatis.dynamic.sql.select.SelectDSLCompleter;
import org.mybatis.dynamic.sql.select.render.SelectStatementProvider;
import org.mybatis.dynamic.sql.update.UpdateDSL;
import org.mybatis.dynamic.sql.update.UpdateDSLCompleter;
import org.mybatis.dynamic.sql.update.UpdateModel;
import org.mybatis.dynamic.sql.util.SqlProviderAdapter;
import org.mybatis.dynamic.sql.util.mybatis3.CommonCountMapper;
import org.mybatis.dynamic.sql.util.mybatis3.CommonDeleteMapper;
import org.mybatis.dynamic.sql.util.mybatis3.CommonInsertMapper;
import org.mybatis.dynamic.sql.util.mybatis3.CommonUpdateMapper;
import org.mybatis.dynamic.sql.util.mybatis3.MyBatis3Utils;
@Mapper
public interface WvpProxyDockingMapper extends CommonCountMapper, CommonDeleteMapper, CommonInsertMapper<WvpProxyDocking>, CommonUpdateMapper {
@Generated(value="org.mybatis.generator.api.MyBatisGenerator", comments="Source Table: wvp_proxy_docking")
BasicColumn[] selectList = BasicColumn.columnList(id, gbDeviceId, ip, port);
@Generated(value="org.mybatis.generator.api.MyBatisGenerator", comments="Source Table: wvp_proxy_docking")
@SelectProvider(type=SqlProviderAdapter.class, method="select")
@Results(id="WvpProxyDockingResult", value = {
@Result(column="id", property="id", jdbcType=JdbcType.BIGINT, id=true),
@Result(column="gb_device_id", property="gbDeviceId", jdbcType=JdbcType.VARCHAR),
@Result(column="ip", property="ip", jdbcType=JdbcType.VARCHAR),
@Result(column="port", property="port", jdbcType=JdbcType.VARCHAR)
})
List<WvpProxyDocking> selectMany(SelectStatementProvider selectStatement);
@Generated(value="org.mybatis.generator.api.MyBatisGenerator", comments="Source Table: wvp_proxy_docking")
@SelectProvider(type=SqlProviderAdapter.class, method="select")
@ResultMap("WvpProxyDockingResult")
Optional<WvpProxyDocking> selectOne(SelectStatementProvider selectStatement);
@Generated(value="org.mybatis.generator.api.MyBatisGenerator", comments="Source Table: wvp_proxy_docking")
default long count(CountDSLCompleter completer) {
return MyBatis3Utils.countFrom(this::count, wvpProxyDocking, completer);
}
@Generated(value="org.mybatis.generator.api.MyBatisGenerator", comments="Source Table: wvp_proxy_docking")
default int delete(DeleteDSLCompleter completer) {
return MyBatis3Utils.deleteFrom(this::delete, wvpProxyDocking, completer);
}
@Generated(value="org.mybatis.generator.api.MyBatisGenerator", comments="Source Table: wvp_proxy_docking")
default int deleteByPrimaryKey(Long id_) {
return delete(c ->
c.where(id, isEqualTo(id_))
);
}
@Generated(value="org.mybatis.generator.api.MyBatisGenerator", comments="Source Table: wvp_proxy_docking")
default int insert(WvpProxyDocking row) {
return MyBatis3Utils.insert(this::insert, row, wvpProxyDocking, c ->
c.map(id).toProperty("id")
.map(gbDeviceId).toProperty("gbDeviceId")
.map(ip).toProperty("ip")
.map(port).toProperty("port")
);
}
@Generated(value="org.mybatis.generator.api.MyBatisGenerator", comments="Source Table: wvp_proxy_docking")
default int insertMultiple(Collection<WvpProxyDocking> records) {
return MyBatis3Utils.insertMultiple(this::insertMultiple, records, wvpProxyDocking, c ->
c.map(id).toProperty("id")
.map(gbDeviceId).toProperty("gbDeviceId")
.map(ip).toProperty("ip")
.map(port).toProperty("port")
);
}
@Generated(value="org.mybatis.generator.api.MyBatisGenerator", comments="Source Table: wvp_proxy_docking")
default int insertSelective(WvpProxyDocking row) {
return MyBatis3Utils.insert(this::insert, row, wvpProxyDocking, c ->
c.map(id).toPropertyWhenPresent("id", row::getId)
.map(gbDeviceId).toPropertyWhenPresent("gbDeviceId", row::getGbDeviceId)
.map(ip).toPropertyWhenPresent("ip", row::getIp)
.map(port).toPropertyWhenPresent("port", row::getPort)
);
}
@Generated(value="org.mybatis.generator.api.MyBatisGenerator", comments="Source Table: wvp_proxy_docking")
default Optional<WvpProxyDocking> selectOne(SelectDSLCompleter completer) {
return MyBatis3Utils.selectOne(this::selectOne, selectList, wvpProxyDocking, completer);
}
@Generated(value="org.mybatis.generator.api.MyBatisGenerator", comments="Source Table: wvp_proxy_docking")
default List<WvpProxyDocking> select(SelectDSLCompleter completer) {
return MyBatis3Utils.selectList(this::selectMany, selectList, wvpProxyDocking, completer);
}
@Generated(value="org.mybatis.generator.api.MyBatisGenerator", comments="Source Table: wvp_proxy_docking")
default List<WvpProxyDocking> selectDistinct(SelectDSLCompleter completer) {
return MyBatis3Utils.selectDistinct(this::selectMany, selectList, wvpProxyDocking, completer);
}
@Generated(value="org.mybatis.generator.api.MyBatisGenerator", comments="Source Table: wvp_proxy_docking")
default Optional<WvpProxyDocking> selectByPrimaryKey(Long id_) {
return selectOne(c ->
c.where(id, isEqualTo(id_))
);
}
@Generated(value="org.mybatis.generator.api.MyBatisGenerator", comments="Source Table: wvp_proxy_docking")
default int update(UpdateDSLCompleter completer) {
return MyBatis3Utils.update(this::update, wvpProxyDocking, completer);
}
@Generated(value="org.mybatis.generator.api.MyBatisGenerator", comments="Source Table: wvp_proxy_docking")
static UpdateDSL<UpdateModel> updateAllColumns(WvpProxyDocking row, UpdateDSL<UpdateModel> dsl) {
return dsl.set(id).equalTo(row::getId)
.set(gbDeviceId).equalTo(row::getGbDeviceId)
.set(ip).equalTo(row::getIp)
.set(port).equalTo(row::getPort);
}
@Generated(value="org.mybatis.generator.api.MyBatisGenerator", comments="Source Table: wvp_proxy_docking")
static UpdateDSL<UpdateModel> updateSelectiveColumns(WvpProxyDocking row, UpdateDSL<UpdateModel> dsl) {
return dsl.set(id).equalToWhenPresent(row::getId)
.set(gbDeviceId).equalToWhenPresent(row::getGbDeviceId)
.set(ip).equalToWhenPresent(row::getIp)
.set(port).equalToWhenPresent(row::getPort);
}
@Generated(value="org.mybatis.generator.api.MyBatisGenerator", comments="Source Table: wvp_proxy_docking")
default int updateByPrimaryKey(WvpProxyDocking row) {
return update(c ->
c.set(gbDeviceId).equalTo(row::getGbDeviceId)
.set(ip).equalTo(row::getIp)
.set(port).equalTo(row::getPort)
.where(id, isEqualTo(row::getId))
);
}
@Generated(value="org.mybatis.generator.api.MyBatisGenerator", comments="Source Table: wvp_proxy_docking")
default int updateByPrimaryKeySelective(WvpProxyDocking row) {
return update(c ->
c.set(gbDeviceId).equalToWhenPresent(row::getGbDeviceId)
.set(ip).equalToWhenPresent(row::getIp)
.set(port).equalToWhenPresent(row::getPort)
.where(id, isEqualTo(row::getId))
);
}
}

View File

@ -0,0 +1,62 @@
package cn.skcks.docking.gb28181.wvp.orm.mybatis.dynamic.model;
import jakarta.annotation.Generated;
/**
*
* This class was generated by MyBatis Generator.
* This class corresponds to the database table wvp_proxy_docking
*/
public class WvpProxyDocking {
@Generated(value="org.mybatis.generator.api.MyBatisGenerator", comments="Source field: wvp_proxy_docking.id")
private Long id;
@Generated(value="org.mybatis.generator.api.MyBatisGenerator", comments="Source field: wvp_proxy_docking.gb_device_id")
private String gbDeviceId;
@Generated(value="org.mybatis.generator.api.MyBatisGenerator", comments="Source field: wvp_proxy_docking.ip")
private String ip;
@Generated(value="org.mybatis.generator.api.MyBatisGenerator", comments="Source field: wvp_proxy_docking.port")
private String port;
@Generated(value="org.mybatis.generator.api.MyBatisGenerator", comments="Source field: wvp_proxy_docking.id")
public Long getId() {
return id;
}
@Generated(value="org.mybatis.generator.api.MyBatisGenerator", comments="Source field: wvp_proxy_docking.id")
public void setId(Long id) {
this.id = id;
}
@Generated(value="org.mybatis.generator.api.MyBatisGenerator", comments="Source field: wvp_proxy_docking.gb_device_id")
public String getGbDeviceId() {
return gbDeviceId;
}
@Generated(value="org.mybatis.generator.api.MyBatisGenerator", comments="Source field: wvp_proxy_docking.gb_device_id")
public void setGbDeviceId(String gbDeviceId) {
this.gbDeviceId = gbDeviceId == null ? null : gbDeviceId.trim();
}
@Generated(value="org.mybatis.generator.api.MyBatisGenerator", comments="Source field: wvp_proxy_docking.ip")
public String getIp() {
return ip;
}
@Generated(value="org.mybatis.generator.api.MyBatisGenerator", comments="Source field: wvp_proxy_docking.ip")
public void setIp(String ip) {
this.ip = ip == null ? null : ip.trim();
}
@Generated(value="org.mybatis.generator.api.MyBatisGenerator", comments="Source field: wvp_proxy_docking.port")
public String getPort() {
return port;
}
@Generated(value="org.mybatis.generator.api.MyBatisGenerator", comments="Source field: wvp_proxy_docking.port")
public void setPort(String port) {
this.port = port == null ? null : port.trim();
}
}

View File

@ -6,4 +6,6 @@ import org.apache.ibatis.annotations.Mapper;
public interface WvpProxyOperateTableMapper {
// int createNewTable(@Param("tableName")String tableName);
void createDeviceTable();
void createDockingTable();
}

View File

@ -6,18 +6,28 @@
<update id="createDeviceTable">
CREATE TABLE IF NOT EXISTS `wvp_proxy_device`
(
`id` bigint NOT NULL AUTO_INCREMENT,
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`device_code` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL,
`gb_device_id` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL,
`gb_device_channel_id` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL,
`name` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`address` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `gb_device_id` (`gb_device_id`),
UNIQUE KEY `device_code` (`device_code`),
UNIQUE KEY `gb_device_id_2` (`gb_device_id`, `gb_device_channel_id`)
UNIQUE KEY `device_code_2` (`device_code`, `gb_device_id`,
`gb_device_channel_id`),
KEY `device_code` (`device_code`) USING BTREE
) ENGINE = InnoDB
AUTO_INCREMENT = 6
DEFAULT CHARSET = utf8mb4
COLLATE = utf8mb4_unicode_ci;
</update>
<update id="createDockingTable">
CREATE TABLE IF NOT EXISTS `wvp_proxy_docking` (
`id` bigint NOT NULL AUTO_INCREMENT,
`gb_device_id` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL,
`ip` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL,
`port` varchar(5) COLLATE utf8mb4_unicode_ci NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
</update>
</mapper>

View File

@ -29,6 +29,11 @@
<artifactId>common</artifactId>
</dependency>
<dependency>
<groupId>cn.skcks.docking.gb28181</groupId>
<artifactId>gb28181-service</artifactId>
</dependency>
<dependency>
<groupId>cn.skcks.docking</groupId>
<artifactId>zlmediakit-service</artifactId>
@ -168,6 +173,47 @@
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents.client5</groupId>
<artifactId>httpclient5</artifactId>
</dependency>
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-httpclient</artifactId>
</dependency>
<!-- https://mvnrepository.com/artifact/com.google.guava/guava -->
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>32.1.2-jre</version>
</dependency>
<dependency>
<groupId>com.github.rholder</groupId>
<artifactId>guava-retrying</artifactId>
<version>2.0.0</version>
<exclusions>
<exclusion>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- https://mvnrepository.com/artifact/org.apache.commons/commons-exec -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-exec</artifactId>
<version>1.3</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-collections4</artifactId>
<version>4.4</version>
</dependency>
</dependencies>
<build>

View File

@ -0,0 +1,36 @@
package cn.skcks.docking.gb28181.wvp.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
@ConfigurationProperties(prefix = "ffmpeg-support")
@Configuration
@Data
public class FfmpegConfig {
private String ffmpeg;
private String ffprobe;
private Rtp rtp;
@Data
public static class Rtp {
private String download = "-i";
private String input = "-re -i";
private String output = "-vcodec h264 -acodec aac -f mp4";
private String logLevel = "fatal";
}
private Debug debug;
@Data
public static class Debug {
private Boolean download = false;
private Boolean input = false;
private Boolean output = false;
}
private Boolean useTmpFile = true;
private String tmpDir = System.getProperty("java.io.tmpdir");
}

View File

@ -0,0 +1,27 @@
package cn.skcks.docking.gb28181.wvp.config;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import java.time.Duration;
import java.time.temporal.ChronoUnit;
@Component
@ConfigurationProperties(prefix = "proxy.gb28181.device-api", ignoreInvalidFields = true)
@Order(0)
@Data
public class Gb28181DeviceVideoApiConfig {
private Offset offset = new Offset();
@Data
@NoArgsConstructor
@AllArgsConstructor
public static class Offset {
private Duration forward = Duration.of(0, ChronoUnit.SECONDS);
private Duration back= Duration.of(0, ChronoUnit.SECONDS);
}
}

View File

@ -0,0 +1,12 @@
package cn.skcks.docking.gb28181.wvp.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
@Data
@Configuration
@ConfigurationProperties(prefix = "media")
public class MediaRtmpConfig {
private int rtmpPort = 1935;
}

View File

@ -0,0 +1,67 @@
package cn.skcks.docking.gb28181.wvp.config;
import cn.skcks.docking.gb28181.config.sip.SipConfig;
import cn.skcks.docking.gb28181.sdp.media.MediaStreamMode;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import javax.sip.ListeningPoint;
import java.util.List;
import java.util.concurrent.TimeUnit;
@Component
@ConfigurationProperties(prefix = "proxy.gb28181.sip", ignoreInvalidFields = true)
@Order(0)
@Data
public class ProxySipConfig {
private List<String> ip;
private List<String> showIp;
private Integer port;
private String domain;
private String id;
private String password;
private String transport = ListeningPoint.UDP;
private MediaStreamMode streamMode = MediaStreamMode.TCP_PASSIVE;
/**
* 某些特殊情况下 使用 视频回放点播 代替 下载
*/
private boolean usePlaybackToDownload = false;
/**
* 代理 zlm 地址, 用于 /video/device/video.mp4 替换返回值地址
* <p>: http://127.0.0.1:5080</p>
*/
private String proxyMediaUrl = "";
/**
* 调用 视频下载之前 是否使用 recordInfo 查询
*/
private boolean useRecordInfoQueryBeforeDownload = true;
private int retryRecordInfoQueryBeforeDownloadTimes = 20;
private long retryRecordInfoQueryBeforeDownloadInterval = 3;
private TimeUnit retryRecordInfoQueryBeforeDownloadIntervalUnit = TimeUnit.SECONDS;
@Bean
public SipConfig sipConfig(){
SipConfig sipConfig = new SipConfig();
sipConfig.setIp(ip);
sipConfig.setShowIp(showIp);
sipConfig.setPort(port);
sipConfig.setDomain(domain);
sipConfig.setId(id);
sipConfig.setPassword(password);
return sipConfig;
}
}

View File

@ -0,0 +1,17 @@
package cn.skcks.docking.gb28181.wvp.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import java.util.HashMap;
import java.util.Map;
@Data
@Configuration
@ConfigurationProperties(prefix = "report")
public class ReportConfig {
private Boolean enabled = false;
private String url;
private Map<String, String> customHeaders = new HashMap<>();
}

View File

@ -4,11 +4,40 @@ import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
@Configuration
@ConfigurationProperties(prefix = "proxy.wvp")
@Data
public class WvpProxyConfig {
private Boolean enable = true;
private String url;
private String user;
private String passwd;
/**
* 是否尝试通过 wvp-assist 服务下载
*/
private Boolean useWvpAssist = true;
/**
* 是否使用 ffmpeg /解码, 否则使用内置 javacv
*/
private Boolean useFfmpeg = false;
/**
* 需要通过 wvp 代理的 (wvp的上级) 上级平台
*/
private List<String> parents = new ArrayList<>();
/**
* 用于生成 代理 wvp 视频流 ws-flv 地址
*/
private String proxyMediaUrl = "";
/**
* 实时视频单次点播持续时间 (默认: 15分钟)
*/
private Duration realtimeVideoDuration = Duration.ofMinutes(15);
}

View File

@ -0,0 +1,21 @@
package cn.skcks.docking.gb28181.wvp.dto.common;
import cn.hutool.core.date.DatePattern;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.format.annotation.DateTimeFormat;
@NoArgsConstructor
@AllArgsConstructor
@Data
public class GeneralTimeReq {
@DateTimeFormat(pattern= DatePattern.NORM_DATETIME_PATTERN)
@JsonFormat(pattern = DatePattern.NORM_DATETIME_PATTERN)
private String startTime;
@DateTimeFormat(pattern= DatePattern.NORM_DATETIME_PATTERN)
@JsonFormat(pattern = DatePattern.NORM_DATETIME_PATTERN)
private String endTime;
}

View File

@ -0,0 +1,26 @@
package cn.skcks.docking.gb28181.wvp.dto.download;
import cn.hutool.core.date.DatePattern;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.format.annotation.DateTimeFormat;
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Data
public class DownloadStartReq {
@DateTimeFormat(pattern= DatePattern.NORM_DATETIME_PATTERN)
@JsonFormat(pattern = DatePattern.NORM_DATETIME_PATTERN)
private String startTime;
@DateTimeFormat(pattern= DatePattern.NORM_DATETIME_PATTERN)
@JsonFormat(pattern = DatePattern.NORM_DATETIME_PATTERN)
private String endTime;
@Builder.Default
private int downloadSpeed = 4;
}

View File

@ -0,0 +1,61 @@
package cn.skcks.docking.gb28181.wvp.dto.download;
import cn.hutool.core.date.DatePattern;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.format.annotation.DateTimeFormat;
import java.util.List;
@NoArgsConstructor
@Data
public class DownloadStartResp {
private String app;
@DateTimeFormat(pattern= DatePattern.NORM_DATETIME_PATTERN)
@JsonFormat(pattern = DatePattern.NORM_DATETIME_PATTERN)
private String endTime;
private String flv;
private String fmp4;
private String hls;
private String httpsFlv;
private String httpsFmp4;
private String httpsHls;
private String httpsTs;
private String mediaServerId;
private Double progress;
private String rtc;
private String rtcs;
private String rtmp;
private String rtsp;
@DateTimeFormat(pattern= DatePattern.NORM_DATETIME_PATTERN)
@JsonFormat(pattern = DatePattern.NORM_DATETIME_PATTERN)
private String startTime;
private String stream;
private List<TracksDTO> tracks;
private String ts;
private String wsFlv;
private String wsFmp4;
private String wsHls;
private String wsTs;
private String wssFlv;
private String wssFmp4;
private String wssHls;
@NoArgsConstructor
@Data
public static class TracksDTO {
private Integer channels;
private Integer codecId;
private Integer codecType;
private Integer fps;
private Integer height;
private Boolean ready;
private Integer sampleBit;
private Integer sampleRate;
private Integer width;
}
}

View File

@ -0,0 +1,22 @@
package cn.skcks.docking.gb28181.wvp.dto.media.proxy;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@AllArgsConstructor
@NoArgsConstructor
@Data
public class AddDownloadTaskReq {
/**
* 应用id
* <p>WvpProxyClient.downloadStart WvpProxyClient.downloadProgress 返回值中获取的 app 字段</p>
*/
private String app;
/**
* 流id
* <p>WvpProxyClient.downloadStart WvpProxyClient.downloadProgress 返回值中获取的 stream 字段</p>
*/
private String stream;
}

View File

@ -0,0 +1,28 @@
package cn.skcks.docking.gb28181.wvp.dto.media.proxy;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@NoArgsConstructor
@AllArgsConstructor
@Data
public class GetDownloadTaskReq {
/**
* 应用id
* <p>WvpProxyClient.downloadStart WvpProxyClient.downloadProgress 返回值中获取的 app 字段</p>
*/
private String app;
/**
* 流id
* <p>WvpProxyClient.downloadStart WvpProxyClient.downloadProgress 返回值中获取的 stream 字段</p>
*/
private String stream;
private String taskId;
@JsonProperty("isEnd")
private Boolean isEnd;
}

View File

@ -0,0 +1,30 @@
package cn.skcks.docking.gb28181.wvp.dto.media.proxy;
import cn.hutool.core.date.DatePattern;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.format.annotation.DateTimeFormat;
@NoArgsConstructor
@Data
public class GetDownloadTaskResp {
private String id;
private String app;
private String stream;
@DateTimeFormat(pattern= DatePattern.NORM_DATETIME_PATTERN)
@JsonFormat(pattern = DatePattern.NORM_DATETIME_PATTERN)
private String startTime;
@DateTimeFormat(pattern= DatePattern.NORM_DATETIME_PATTERN)
@JsonFormat(pattern = DatePattern.NORM_DATETIME_PATTERN)
private String endTime;
@DateTimeFormat(pattern= DatePattern.NORM_DATETIME_PATTERN)
@JsonFormat(pattern = DatePattern.NORM_DATETIME_PATTERN)
private String createTime;
private String percentage;
private String recordFile;
private String downloadFile;
private String playFile;
}

View File

@ -0,0 +1,21 @@
package cn.skcks.docking.gb28181.wvp.dto.record;
import cn.hutool.core.date.DatePattern;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.format.annotation.DateTimeFormat;
@AllArgsConstructor
@NoArgsConstructor
@Data
public class QueryRecordReq {
@DateTimeFormat(pattern= DatePattern.NORM_DATETIME_PATTERN)
@JsonFormat(pattern = DatePattern.NORM_DATETIME_PATTERN)
private String startTime;
@DateTimeFormat(pattern= DatePattern.NORM_DATETIME_PATTERN)
@JsonFormat(pattern = DatePattern.NORM_DATETIME_PATTERN)
private String endTime;
}

View File

@ -0,0 +1,40 @@
package cn.skcks.docking.gb28181.wvp.dto.record;
import cn.hutool.core.date.DatePattern;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.format.annotation.DateTimeFormat;
import java.util.List;
@NoArgsConstructor
@Data
public class QueryRecordResp {
private String channelId;
private Integer count;
private String deviceId;
private String name;
private List<RecordListDTO> recordList;
private String sn;
private Integer sumNum;
@NoArgsConstructor
@Data
public static class RecordListDTO {
private String deviceId;
@DateTimeFormat(pattern= DatePattern.NORM_DATETIME_PATTERN)
@JsonFormat(pattern = DatePattern.NORM_DATETIME_PATTERN)
private String endTime;
private String filePath;
private String name;
private Integer secrecy;
@DateTimeFormat(pattern= DatePattern.NORM_DATETIME_PATTERN)
@JsonFormat(pattern = DatePattern.NORM_DATETIME_PATTERN)
private String startTime;
private String type;
}
}

View File

@ -0,0 +1,40 @@
package cn.skcks.docking.gb28181.wvp.dto.report;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@AllArgsConstructor
@NoArgsConstructor
@Data
@Schema(description = "上报信息")
public class ReportReq {
@Schema(description = "上报消息id")
private String id;
@Schema(description = "设备编码")
private String deviceCode;
@Schema(description = "设备id/通道id")
private String deviceId;
@Schema(description = "点播时长")
private String durationTime;
@Schema(description = "点播时长")
private TimeRange timeRange;
@Schema(description = "日志记录时间")
private String logTime;
@AllArgsConstructor
@NoArgsConstructor
@Data
public static class TimeRange {
@Schema(description = "开始时间")
private String startTime;
@Schema(description = "结束时间")
private String endTime;
}
@Schema(description = "文件大小, 未知大小为 -1")
private String fileSize;
}

View File

@ -0,0 +1,62 @@
package cn.skcks.docking.gb28181.wvp.dto.stream;
import cn.hutool.core.date.DatePattern;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.databind.PropertyNamingStrategies;
import com.fasterxml.jackson.databind.annotation.JsonNaming;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.format.annotation.DateTimeFormat;
import java.util.List;
@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
@NoArgsConstructor
@Data
public class StreamContent {
private String app;
@DateTimeFormat(pattern= DatePattern.NORM_DATETIME_PATTERN)
@JsonFormat(pattern = DatePattern.NORM_DATETIME_PATTERN)
private String endTime;
private String flv;
private String fmp4;
private String hls;
private String httpsFlv;
private String httpsFmp4;
private String httpsHls;
private String httpsTs;
private String mediaServerId;
private Double progress;
private String rtc;
private String rtcs;
private String rtmp;
private String rtsp;
@DateTimeFormat(pattern= DatePattern.NORM_DATETIME_PATTERN)
@JsonFormat(pattern = DatePattern.NORM_DATETIME_PATTERN)
private String startTime;
private String stream;
private List<TracksDTO> tracks;
private String ts;
private String wsFlv;
private String wsFmp4;
private String wsHls;
private String wsTs;
private String wssFlv;
private String wssFmp4;
private String wssHls;
@NoArgsConstructor
@Data
public static class TracksDTO {
private Integer channels;
private Integer codecId;
private Integer codecType;
private Integer fps;
private Integer height;
private Boolean ready;
private Integer sampleBit;
private Integer sampleRate;
private Integer width;
}
}

View File

@ -24,5 +24,6 @@ public class WvpProxyOrmInitService {
public void init(){
log.info("[orm] 自动建表");
mapper.createDeviceTable();
mapper.createDockingTable();
}
}

View File

@ -1,27 +1,126 @@
package cn.skcks.docking.gb28181.wvp.proxy;
import cn.skcks.docking.gb28181.common.json.JsonResponse;
import cn.skcks.docking.gb28181.wvp.dto.common.GeneralTimeReq;
import cn.skcks.docking.gb28181.wvp.dto.device.GetDeviceChannelsReq;
import cn.skcks.docking.gb28181.wvp.dto.device.GetDeviceChannelsResp;
import cn.skcks.docking.gb28181.wvp.dto.device.GetDeviceTreeReq;
import cn.skcks.docking.gb28181.wvp.dto.device.GetDeviceTreeResp;
import cn.skcks.docking.gb28181.wvp.dto.download.DownloadStartReq;
import cn.skcks.docking.gb28181.wvp.dto.login.WvpLoginReq;
import cn.skcks.docking.gb28181.wvp.dto.login.WvpLoginResp;
import cn.skcks.docking.gb28181.wvp.dto.media.proxy.AddDownloadTaskReq;
import cn.skcks.docking.gb28181.wvp.dto.media.proxy.GetDownloadTaskReq;
import cn.skcks.docking.gb28181.wvp.dto.media.proxy.GetDownloadTaskResp;
import cn.skcks.docking.gb28181.wvp.dto.record.QueryRecordReq;
import cn.skcks.docking.gb28181.wvp.dto.record.QueryRecordResp;
import cn.skcks.docking.gb28181.wvp.dto.stream.StreamContent;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.cloud.openfeign.SpringQueryMap;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestParam;
@FeignClient(name="wvpProxyClient", url = "${proxy.wvp.url}")
import java.util.List;
@FeignClient(name = "wvpProxyClient", url = "${proxy.wvp.url}")
public interface WvpProxyClient {
@GetMapping(value = "/api/user/login", consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE)
JsonResponse<WvpLoginResp> login(@SpringQueryMap WvpLoginReq req);
@GetMapping(value = "/api/device/query/tree/channel/{deviceId}")
JsonResponse<GetDeviceTreeResp> getDeviceTree(@RequestHeader("access-token") String token, @PathVariable String deviceId, @SpringQueryMap GetDeviceTreeReq req);
@GetMapping("/api/device/query/tree/channel/{deviceId}")
JsonResponse<GetDeviceTreeResp> getDeviceTree(@RequestHeader("access-token") String token,
@PathVariable String deviceId,
@SpringQueryMap GetDeviceTreeReq req);
@GetMapping(value = "/api/device/query/devices/{deviceId}/channels")
JsonResponse<GetDeviceChannelsResp> getDeviceChannels(@RequestHeader("access-token") String token, @PathVariable String deviceId, @SpringQueryMap GetDeviceChannelsReq req);
@GetMapping("/api/device/query/devices/{deviceId}/channels")
JsonResponse<GetDeviceChannelsResp> getDeviceChannels(@RequestHeader("access-token") String token,
@PathVariable String deviceId,
@SpringQueryMap GetDeviceChannelsReq req);
@GetMapping("/api/gb_record/query/{deviceId}/{channelId}")
JsonResponse<QueryRecordResp> queryRecord(@RequestHeader("access-token") String token,
@PathVariable String deviceId,
@PathVariable String channelId,
@SpringQueryMap QueryRecordReq req);
// /api/playback/start/44050100001180000001/44050100001310000001?startTime=2023-09-07%2000:20:01&endTime=2023-09-07%2000:25:01
@GetMapping("/api/playback/start/{deviceId}/{channelId}")
JsonResponse<StreamContent> playbackStart(@RequestHeader("access-token") String token,
@PathVariable String deviceId,
@PathVariable String channelId,
@SpringQueryMap GeneralTimeReq req);
@GetMapping("/api/playback/stop/{deviceId}/{channelId}/{stream}")
JsonResponse<StreamContent> playbackStop(@RequestHeader("access-token") String token,
@PathVariable String deviceId,
@PathVariable String channelId,
@PathVariable String stream);
// /api/playback/stop/44050100001180000001/44050100001310000001/44050100001180000001_44050100001310000001_20230908083001_20230908083501
@GetMapping("/api/gb_record/download/start/{deviceId}/{channelId}")
JsonResponse<StreamContent> downloadStart(@RequestHeader("access-token") String token,
@PathVariable String deviceId,
@PathVariable String channelId,
@SpringQueryMap DownloadStartReq req);
// /api/gb_record/download/progress/44050100001180000001/44050100001310000001/59777645
@GetMapping("/api/gb_record/download/progress/{deviceId}/{channelId}/{stream}")
JsonResponse<StreamContent> downloadProgress(@RequestHeader("access-token") String token,
@PathVariable String deviceId,
@PathVariable String channelId,
@PathVariable String stream);
/**
* 停止下载, 部分设备会出现长时间卡住不动 wvp 会出现 100 错误, 故调用即可无需理会返回值
* @param token 认证token
* @param deviceId 国标设备id/下级平台id
* @param channelId 通道id
* @param stream downloadStart中的 stream
*/
// /api/gb_record/download/stop/44050100001180000001/44050100001310000001/59777645
@GetMapping("/api/gb_record/download/stop/{deviceId}/{channelId}/{stream}")
JsonResponse<Void> downloadStop(@RequestHeader("access-token") String token,
@PathVariable String deviceId,
@PathVariable String channelId,
@PathVariable String stream);
/**
* 添加视频裁剪合并 任务 wvp-assist 服务, 返回 taskId 用于后续操作
* @param token 认证token
* @param mediaServerId 流媒体服务id, downloadStart/downloadProgress 获得
* @return 返回 taskId 用于后续操作
*/
// /record_proxy/your_server_id/api/record/file/download/task/add?app=rtp&stream=59777645
@GetMapping("/record_proxy/{mediaServerId}/api/record/file/download/task/add")
JsonResponse<String> addDownloadTask2MediaServer(@RequestHeader("access-token") String token,
@PathVariable String mediaServerId,
@SpringQueryMap AddDownloadTaskReq req);
/**
* 获取视频裁剪合并任务列表
* <p>如果 GetDownloadTaskResp.isEnd = true, 就是已经处理完成, 可以直接用里面的 playUrl / downloadUrl下载</p>
* @param token 认证token
* @param mediaServerId 流媒体服务id, downloadStart/downloadProgress 获得
* @param req GetDownloadTaskReq 请求参数
* @return GetDownloadTaskResp 任务列表
*/
// /record_proxy/your_server_id/api/record/file/download/task/list?app=rtp&stream=59777645&taskId=0490d767d94ce20aedce57c862b6bfe9&isEnd=true
@GetMapping("/record_proxy/{mediaServerId}/api/record/file/download/task/list")
JsonResponse<List<GetDownloadTaskResp>> getDownloadTask4MediaServer(@RequestHeader("access-token") String token,
@PathVariable String mediaServerId,
@SpringQueryMap GetDownloadTaskReq req);
@GetMapping("/api/play/start/{deviceId}/{channelId}")
JsonResponse<StreamContent> playStart(@RequestHeader("access-token") String token,
@PathVariable String deviceId,
@PathVariable String channelId,
@RequestParam boolean isSubStream);
@GetMapping("/api/play/stop/{deviceId}/{channelId}")
JsonResponse<Void> playStop(@RequestHeader("access-token") String token,
@PathVariable String deviceId,
@PathVariable String channelId,
@RequestParam boolean isSubStream);
}

View File

@ -0,0 +1,129 @@
package cn.skcks.docking.gb28181.wvp.service.catalog;
import cn.skcks.docking.gb28181.common.json.JsonException;
import cn.skcks.docking.gb28181.common.xml.XmlUtils;
import cn.skcks.docking.gb28181.core.sip.gb28181.constant.CmdType;
import cn.skcks.docking.gb28181.core.sip.gb28181.constant.GB28181Constant;
import cn.skcks.docking.gb28181.core.sip.message.subscribe.GenericSubscribe;
import cn.skcks.docking.gb28181.core.sip.utils.SipUtil;
import cn.skcks.docking.gb28181.wvp.orm.mybatis.dynamic.model.WvpProxyDevice;
import cn.skcks.docking.gb28181.wvp.orm.mybatis.dynamic.model.WvpProxyDocking;
import cn.skcks.docking.gb28181.wvp.service.device.DeviceService;
import cn.skcks.docking.gb28181.wvp.service.docking.DockingService;
import cn.skcks.docking.gb28181.wvp.sip.message.message.catalog.dto.CatalogItemDTO;
import cn.skcks.docking.gb28181.wvp.sip.message.message.catalog.dto.CatalogRequestDTO;
import cn.skcks.docking.gb28181.wvp.sip.message.message.catalog.dto.CatalogResponseDTO;
import cn.skcks.docking.gb28181.wvp.sip.request.SipRequestBuilder;
import cn.skcks.docking.gb28181.wvp.sip.sender.SipSender;
import cn.skcks.docking.gb28181.wvp.sip.subscribe.SipSubscribe;
import gov.nist.javax.sip.message.SIPRequest;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.ListUtils;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicLong;
@Slf4j
@Service
@RequiredArgsConstructor
public class CatalogService {
private final SipSender sipSender;
private final SipSubscribe sipSubscribe;
private final ScheduledExecutorService scheduledExecutorService = Executors.newSingleThreadScheduledExecutor();
private final DockingService dockingService;
private final DeviceService deviceService;
@SneakyThrows
public CompletableFuture<CatalogResponseDTO> getCatalog(String deviceId){
CompletableFuture<CatalogResponseDTO> result = new CompletableFuture<>();
WvpProxyDocking device = dockingService.getDeviceByDeviceCode(deviceId).orElse(null);
if (device == null){
throw new JsonException("设备不存在");
}
CatalogRequestDTO catalogRequestDTO = new CatalogRequestDTO();
catalogRequestDTO.setDeviceId(deviceId);
String sn = String.valueOf((int) (Math.random() * 9 + 1) * 100000);
catalogRequestDTO.setSn(sn);
String key = GenericSubscribe.Helper.getKey(CmdType.CATALOG, deviceId);
sipSubscribe.getCatalogSubscribe().addPublisher(key);
final ScheduledFuture<?>[] schedule = new ScheduledFuture<?>[1];
Flow.Subscriber<SIPRequest> subscriber = catalog(key, device, schedule, result);
// 60秒超时计时器
schedule[0] = scheduledExecutorService.schedule(subscriber::onComplete, 60 , TimeUnit.SECONDS);
// 添加订阅
sipSubscribe.getCatalogSubscribe().addSubscribe(key, subscriber);
sipSender.sendRequest((provider, ip, port)-> SipRequestBuilder.createMessageRequest(device,ip,port,SipRequestBuilder.getCSeq(), XmlUtils.toXml(catalogRequestDTO), SipUtil.generateViaTag(),
SipUtil.generateFromTag(), provider.getNewCallId()));
return result;
}
private Flow.Subscriber<SIPRequest> catalog(String key, WvpProxyDocking device, ScheduledFuture<?>[] schedule,CompletableFuture<CatalogResponseDTO> result){
List<CatalogItemDTO> deviceList = new ArrayList<>();
return new Flow.Subscriber<>() {
Flow.Subscription subscription;
CatalogResponseDTO dto;
final AtomicLong getNum = new AtomicLong(0);
@Override
public void onSubscribe(Flow.Subscription subscription) {
log.info("开始订阅 {}", key);
this.subscription = subscription;
subscription.request(1);
}
@Override
public void onNext(SIPRequest item) {
CatalogResponseDTO responseDTO = XmlUtils.parse(item.getRawContent(), CatalogResponseDTO.class, GB28181Constant.CHARSET);
dto = responseDTO;
Long sumNum = responseDTO.getSumNum();
log.info("{}",responseDTO);
getNum.getAndAdd(responseDTO.getDeviceList().getDeviceList().size());
deviceList.addAll(responseDTO.getDeviceList().getDeviceList());
if(getNum.get() < sumNum){
subscription.request(1);
} else{
onComplete();
}
}
@Override
public void onError(Throwable throwable) {
onComplete();
}
@Override
public void onComplete() {
sipSubscribe.getCatalogSubscribe().delPublisher(key);
schedule[0].cancel(true);
log.info("订阅结束 {}", key);
if(dto != null){
dto.getDeviceList().setDeviceList(deviceList);
scheduledExecutorService.execute(()->{
updateProxyDevice(dto);
});
}
result.complete(dto);
}
};
}
public void updateProxyDevice(CatalogResponseDTO dto){
String gbDeviceId = dto.getDeviceId();
List<WvpProxyDevice> deviceByGbDeviceId = deviceService.getDeviceByGbDeviceId(gbDeviceId);
List<String> existChannels = deviceByGbDeviceId.stream().map(WvpProxyDevice::getGbDeviceChannelId).toList();
List<CatalogItemDTO> deviceList = dto.getDeviceList().getDeviceList();
List<String> catalogChannels = deviceList.stream().map(CatalogItemDTO::getDeviceId).toList();
List<String> noexist = ListUtils.subtract(catalogChannels, existChannels);
noexist.forEach(channel->{
log.info("更新 设备 {}, 通道 {} 信息", gbDeviceId, channel);
deviceService.autoUpdateDeviceByGbDeviceIdAndChannel(gbDeviceId,channel);
});
}
}

View File

@ -0,0 +1,201 @@
package cn.skcks.docking.gb28181.wvp.service.device;
import cn.skcks.docking.gb28181.common.json.JsonException;
import cn.skcks.docking.gb28181.wvp.orm.mybatis.dynamic.mapper.WvpProxyDeviceDynamicSqlSupport;
import cn.skcks.docking.gb28181.wvp.orm.mybatis.dynamic.mapper.WvpProxyDeviceMapper;
import cn.skcks.docking.gb28181.wvp.orm.mybatis.dynamic.model.WvpProxyDevice;
import com.github.pagehelper.ISelect;
import com.github.pagehelper.Page;
import com.github.pagehelper.PageHelper;
import com.github.pagehelper.PageInfo;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.text.MessageFormat;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import static org.mybatis.dynamic.sql.SqlBuilder.isEqualTo;
@Slf4j
@RequiredArgsConstructor
@Service
public class DeviceService {
private final WvpProxyDeviceMapper deviceMapper;
public Optional<WvpProxyDevice> getDeviceById(Long id){
return deviceMapper.selectOne(s->
s.where(WvpProxyDeviceDynamicSqlSupport.id, isEqualTo(id)));
}
public Optional<WvpProxyDevice> getDeviceByDeviceCode(String deviceCode){
return deviceMapper.selectOne(s->s.where(WvpProxyDeviceDynamicSqlSupport.deviceCode,isEqualTo(deviceCode)).limit(1));
}
public List<WvpProxyDevice> getDeviceByGbDeviceId(String gbDeviceId){
return deviceMapper.select(s->
s.where(WvpProxyDeviceDynamicSqlSupport.gbDeviceId,isEqualTo(gbDeviceId)));
}
public Optional<WvpProxyDevice> getDeviceByGbDeviceIdAndChannel(String gbDeviceId,String channel){
return deviceMapper.selectOne(s->
s.where(WvpProxyDeviceDynamicSqlSupport.gbDeviceId,isEqualTo(gbDeviceId))
.and(WvpProxyDeviceDynamicSqlSupport.gbDeviceChannelId,isEqualTo(channel)).limit(1));
}
@Transactional
public boolean autoUpdateDeviceByGbDeviceIdAndChannel(String gbDeviceId, String channel){
List<WvpProxyDevice> deviceByGbDeviceId = getDeviceByGbDeviceId(gbDeviceId);
if(deviceByGbDeviceId.isEmpty()){
WvpProxyDevice device = new WvpProxyDevice();
device.setDeviceCode("");
device.setGbDeviceId(gbDeviceId);
device.setGbDeviceChannelId(channel);
return deviceMapper.insert(device) > 0;
}
Optional<WvpProxyDevice> deviceByGbDeviceIdAndChannel = getDeviceByGbDeviceIdAndChannel(gbDeviceId,channel);
if(deviceByGbDeviceIdAndChannel.isPresent()) {
return deviceMapper.update(u->
u.set(WvpProxyDeviceDynamicSqlSupport.gbDeviceChannelId).equalTo(channel)
.where(WvpProxyDeviceDynamicSqlSupport.gbDeviceChannelId, isEqualTo(gbDeviceId))
.and(WvpProxyDeviceDynamicSqlSupport.gbDeviceChannelId, isEqualTo(channel))) > 0;
} else {
WvpProxyDevice device = new WvpProxyDevice();
device.setDeviceCode("");
device.setGbDeviceId(gbDeviceId);
device.setGbDeviceChannelId(channel);
return deviceMapper.insert(device) > 0;
}
}
/**
* 添加设备
* @param device 设备
* @return 是否成功
*/
@SneakyThrows
public boolean addDevice(WvpProxyDevice device) {
if(device == null){
return false;
}
String deviceCode = device.getDeviceCode();
if(StringUtils.isBlank(deviceCode)){
throw new JsonException("设备编码不能为空");
}
if(getDeviceByDeviceCode(deviceCode).isPresent()){
throw new JsonException(MessageFormat.format("设备编码 {0} 已存在" ,deviceCode));
}
String gbDeviceId = device.getGbDeviceId();
String channel = device.getGbDeviceChannelId();
if(StringUtils.isBlank(gbDeviceId)){
throw new JsonException("国标编码不能为空");
}
if(StringUtils.isBlank(channel)){
throw new JsonException("国标通道不能为空");
}
if(getDeviceByGbDeviceIdAndChannel(gbDeviceId,channel).isPresent()){
throw new JsonException(MessageFormat.format("国标编码 {0}, 通道 {1} 已存在" ,gbDeviceId, channel));
}
return deviceMapper.insert(device) > 0;
}
public boolean deleteDeviceByGbDeviceIdAndChannel(String gbDeviceId, String channel){
return deviceMapper.delete(d->
d.where(WvpProxyDeviceDynamicSqlSupport.gbDeviceChannelId, isEqualTo(gbDeviceId))
.and(WvpProxyDeviceDynamicSqlSupport.gbDeviceChannelId, isEqualTo(channel))) > 0;
}
/**
* 依据 id deviceCode gbDeviceId 删除设备信息
* @param device 设备
* @return 是否成功
*/
public boolean deleteDevice(WvpProxyDevice device){
if(device == null){
return false;
}
Long id = device.getId();
String deviceCode = device.getDeviceCode();
String gbDeviceId = device.getGbDeviceId();
if(id != null){
return deviceMapper.deleteByPrimaryKey(id) > 0;
} else if(StringUtils.isNotBlank(deviceCode)){
return deviceMapper.delete(d->d.where(WvpProxyDeviceDynamicSqlSupport.deviceCode,isEqualTo(deviceCode))) > 0;
} else if(StringUtils.isNotBlank(gbDeviceId)){
return deviceMapper.delete(d->d.where(WvpProxyDeviceDynamicSqlSupport.gbDeviceId,isEqualTo(gbDeviceId))) > 0;
} else {
return false;
}
}
/**
* 分页查询设备
* @param page 页数
* @param size 数量
* @return 分页设备
*/
public PageInfo<WvpProxyDevice> getDevicesWithPage(int page, int size){
ISelect select = () -> deviceMapper.select(u -> u.orderBy(WvpProxyDeviceDynamicSqlSupport.id.descending()));
return getDevicesWithPage(page,size, select);
}
/**
* 分页查询设备
* @param page 页数
* @param size 数量
* @param select 查询语句
* @return 分页设备
*/
public PageInfo<WvpProxyDevice> getDevicesWithPage(int page, int size, ISelect select){
PageInfo<WvpProxyDevice> pageInfo;
try (Page<WvpProxyDevice> startPage = PageHelper.startPage(page, size)) {
pageInfo = startPage.doSelectPageInfo(select);
}
return pageInfo;
}
@SneakyThrows
public boolean modifyDevice(WvpProxyDevice device){
if(device == null){
return false;
}
Long id = device.getId();
if(id == null){
throw new JsonException("id 不能为空");
}
String deviceCode = device.getDeviceCode();
if(StringUtils.isNotBlank(deviceCode)){
WvpProxyDevice existDevice = getDeviceByDeviceCode(deviceCode).orElse(null);
if(existDevice != null && !Objects.equals(existDevice.getId(), device.getId())){
throw new JsonException(MessageFormat.format("设备编码 {0} 已存在", deviceCode));
}
}
String gbDeviceId = device.getGbDeviceId();
String gbDeviceChannelId = device.getGbDeviceChannelId();
if(gbDeviceId != null && StringUtils.isBlank(gbDeviceId)){
throw new JsonException("国标id 不能为空");
}
if(gbDeviceChannelId != null && StringUtils.isBlank(gbDeviceChannelId)){
throw new JsonException("通道 不能为空");
}
if(StringUtils.isNotBlank(gbDeviceId) && StringUtils.isNotBlank(gbDeviceChannelId)){
WvpProxyDevice existDevice = getDeviceByGbDeviceIdAndChannel(gbDeviceId, gbDeviceChannelId).orElse(null);
if(existDevice != null && !Objects.equals(existDevice.getId(), device.getId())){
throw new JsonException(MessageFormat.format("国标id {0} ,通道 {1} 已存在", gbDeviceId, gbDeviceChannelId));
}
}
return deviceMapper.updateByPrimaryKeySelective(device) > 0;
}
}

View File

@ -0,0 +1,66 @@
package cn.skcks.docking.gb28181.wvp.service.device.control;
import cn.skcks.docking.gb28181.common.json.JsonException;
import cn.skcks.docking.gb28181.common.xml.XmlUtils;
import cn.skcks.docking.gb28181.core.sip.utils.SipUtil;
import cn.skcks.docking.gb28181.wvp.orm.mybatis.dynamic.model.WvpProxyDevice;
import cn.skcks.docking.gb28181.wvp.orm.mybatis.dynamic.model.WvpProxyDocking;
import cn.skcks.docking.gb28181.wvp.service.device.DeviceService;
import cn.skcks.docking.gb28181.wvp.service.docking.DockingService;
import cn.skcks.docking.gb28181.wvp.sip.message.message.device.control.DeviceControlDTO;
import cn.skcks.docking.gb28181.wvp.sip.request.SipRequestBuilder;
import cn.skcks.docking.gb28181.wvp.sip.sender.SipSender;
import cn.skcks.docking.gb28181.wvp.sip.subscribe.SipSubscribe;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.text.MessageFormat;
import java.util.Optional;
@Slf4j
@Service
@RequiredArgsConstructor
public class DeviceControlService {
private final SipSender sipSender;
private final SipSubscribe sipSubscribe;
private final DockingService dockingService;
private final DeviceService deviceService;
@SneakyThrows
public void sendRecordControl(String deviceCode, String recordCmd){
Optional<WvpProxyDevice> deviceByDeviceCode = deviceService.getDeviceByDeviceCode(deviceCode);
if (deviceByDeviceCode.isEmpty()) {
String reason = MessageFormat.format("未能找到 设备编码 为 {0} 的设备", deviceCode);
log.error("{}",reason);
throw new JsonException(reason);
} else {
WvpProxyDevice device = deviceByDeviceCode.get();
sendRecordControl(device.getGbDeviceId(), device.getGbDeviceChannelId(), recordCmd);
}
}
public void sendRecordControl(String gbDeviceId, String channel, String recordCmd){
Optional<WvpProxyDocking> deviceByGbDeviceId = dockingService.getDeviceByGbDeviceId(gbDeviceId);
if(deviceByGbDeviceId.isEmpty()){
log.info("未能找到 国标编码 {} 的注册信息", gbDeviceId);
return;
}
Optional<WvpProxyDevice> deviceByGbDeviceIdAndChannel = deviceService.getDeviceByGbDeviceIdAndChannel(gbDeviceId, channel);
if (deviceByGbDeviceIdAndChannel.isEmpty()) {
log.info("未能找到 编码 {}, 通道 {} 的设备", gbDeviceId, channel);
return;
}
WvpProxyDocking device = deviceByGbDeviceId.get();
String sn = String.valueOf((int) (Math.random() * 9 + 1) * 100000);
DeviceControlDTO deviceControlDTO = DeviceControlDTO.builder()
.sn(sn)
.deviceId(channel)
.recordCmd(recordCmd)
.build();
sipSender.sendRequest((provider, ip, port)-> SipRequestBuilder.createMessageRequest(device,ip,port,SipRequestBuilder.getCSeq(), XmlUtils.toXml(deviceControlDTO), SipUtil.generateViaTag(),
SipUtil.generateFromTag(), provider.getNewCallId()));
}
}

View File

@ -0,0 +1,99 @@
package cn.skcks.docking.gb28181.wvp.service.docking;
import cn.skcks.docking.gb28181.common.json.JsonException;
import cn.skcks.docking.gb28181.wvp.orm.mybatis.dynamic.mapper.WvpProxyDockingDynamicSqlSupport;
import cn.skcks.docking.gb28181.wvp.orm.mybatis.dynamic.mapper.WvpProxyDockingMapper;
import cn.skcks.docking.gb28181.wvp.orm.mybatis.dynamic.model.WvpProxyDocking;
import com.github.pagehelper.ISelect;
import com.github.pagehelper.Page;
import com.github.pagehelper.PageHelper;
import com.github.pagehelper.PageInfo;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.Optional;
import static org.mybatis.dynamic.sql.SqlBuilder.isEqualTo;
@Slf4j
@Service
@RequiredArgsConstructor
public class DockingService {
private final WvpProxyDockingMapper wvpProxyDockingMapper;
public Optional<WvpProxyDocking> getDeviceById(Long id){
return wvpProxyDockingMapper.selectOne(s->
s.where(WvpProxyDockingDynamicSqlSupport.id, isEqualTo(id)));
}
public Optional<WvpProxyDocking> getDeviceByDeviceCode(String deviceCode){
return wvpProxyDockingMapper.selectOne(s->
s.where(WvpProxyDockingDynamicSqlSupport.gbDeviceId,isEqualTo(deviceCode)));
}
public Boolean hasDeviceByDeviceCode(String deviceCode){
return getDeviceByDeviceCode(deviceCode).orElse(null) != null;
}
public Optional<WvpProxyDocking> getDeviceByGbDeviceId(String gbDeviceId){
return wvpProxyDockingMapper.selectOne(s->
s.where(WvpProxyDockingDynamicSqlSupport.gbDeviceId, isEqualTo(gbDeviceId)));
}
public Boolean hasDeviceByGbDeviceId(String deviceCode){
return getDeviceByGbDeviceId(deviceCode).orElse(null) != null;
}
/**
* 添加设备
* @param device 设备
* @return 是否成功
*/
@SneakyThrows
@Transactional
public boolean addDevice(WvpProxyDocking device) {
if(device == null){
return false;
}
String deviceCode = device.getGbDeviceId();
if(StringUtils.isBlank(deviceCode)){
throw new JsonException("设备编码不能为空");
}
if(getDeviceByDeviceCode(deviceCode).isPresent()){
wvpProxyDockingMapper.delete(d->d.where(WvpProxyDockingDynamicSqlSupport.gbDeviceId,isEqualTo(deviceCode)));
}
return wvpProxyDockingMapper.insert(device) > 0;
}
/**
* 分页查询对接设备/平台
* @param page 页数
* @param size 数量
* @return 分页设备/平台
*/
public PageInfo<WvpProxyDocking> getDockingWithPage(int page, int size){
ISelect select = () -> wvpProxyDockingMapper.select(s -> s.orderBy(WvpProxyDockingDynamicSqlSupport.id.descending()));
return getDockingWithPage(page,size, select);
}
/**
* 分页查询对接设备/平台
* @param page 页数
* @param size 数量
* @param select 查询语句
* @return 分页设备/平台
*/
public PageInfo<WvpProxyDocking> getDockingWithPage(int page, int size, ISelect select){
PageInfo<WvpProxyDocking> pageInfo;
try (Page<WvpProxyDocking> startPage = PageHelper.startPage(page, size)) {
pageInfo = startPage.doSelectPageInfo(select);
}
return pageInfo;
}
}

View File

@ -0,0 +1,97 @@
package cn.skcks.docking.gb28181.wvp.service.download;
import cn.hutool.core.io.IoUtil;
import cn.skcks.docking.gb28181.common.json.JsonResponse;
import jakarta.servlet.AsyncContext;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.apache.hc.client5.http.classic.methods.HttpGet;
import org.apache.hc.client5.http.classic.methods.HttpHead;
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
import org.apache.hc.client5.http.impl.classic.HttpClients;
import org.apache.hc.core5.http.ClassicHttpResponse;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Service;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
import java.util.Optional;
@Slf4j
@Service
public class DownloadService {
@SneakyThrows
public void header(HttpServletRequest request, HttpServletResponse response, String url) {
AsyncContext asyncContext = request.startAsync();
asyncContext.setTimeout(0);
asyncContext.start(() -> {
try {
response.setHeader("Accept-Ranges", "none");
response.setHeader("Connection", "close");
header((HttpServletResponse) asyncContext.getResponse(), url);
} finally {
log.info("record 结束");
asyncContext.complete();
}
});
}
@SneakyThrows
private void header(HttpServletResponse response, String url) {
try (CloseableHttpClient client = HttpClients.custom().build()) {
HttpHead httpHead = new HttpHead(url);
client.execute(httpHead, resp -> {
setHeaderFromProxy(resp,response);
return null;
});
}
}
@SneakyThrows
private void setHeaderFromProxy(ClassicHttpResponse resp, HttpServletResponse response){
response.setContentType(resp.getEntity().getContentType());
Optional.ofNullable(resp.getHeader("Content-Disposition")).ifPresent((header)->{
response.setHeader(header.getName(), header.getValue());
});
}
@SneakyThrows
public void download(AsyncContext asyncContext , HttpServletResponse response, String url) {
asyncContext.start(() -> {
try {
response.setHeader("Accept-Ranges", "none");
response.setHeader("Connection", "close");
download((HttpServletResponse) asyncContext.getResponse(), url);
} finally {
log.info("record 结束");
asyncContext.complete();
}
});
}
@SneakyThrows
public void download(HttpServletRequest request, HttpServletResponse response, String url) {
AsyncContext asyncContext = request.startAsync();
asyncContext.setTimeout(0);
download(asyncContext, response, url);
}
@SneakyThrows
public void download(HttpServletResponse response, String url) {
OutputStream outputStream = response.getOutputStream();
try (CloseableHttpClient client = HttpClients.custom().build()) {
HttpGet httpGet = new HttpGet(url);
client.execute(httpGet, resp -> {
setHeaderFromProxy(resp,response);
InputStream stream = resp.getEntity().getContent();
IoUtil.copy(stream, outputStream);
return stream;
});
}
}
}

View File

@ -0,0 +1,75 @@
package cn.skcks.docking.gb28181.wvp.service.ffmpeg;
import cn.skcks.docking.gb28181.wvp.config.FfmpegConfig;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.exec.*;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service;
import java.util.concurrent.TimeUnit;
@Slf4j
@Service
@RequiredArgsConstructor
public class FfmpegSupportService {
private final FfmpegConfig ffmpegConfig;
@SneakyThrows
public Executor downloadToStream(String input, String out, long time, TimeUnit unit, ExecuteStreamHandler streamHandler, ExecuteResultHandler executeResultHandler) {
FfmpegConfig.Rtp rtp = ffmpegConfig.getRtp();
FfmpegConfig.Debug debug = ffmpegConfig.getDebug();
String inputParam = debug.getDownload() ? rtp.getDownload() : StringUtils.joinWith(" ", "-y", rtp.getDownload(), input);
log.info("视频输入参数 {}", inputParam);
String outputParam = debug.getOutput() ? rtp.getOutput() : StringUtils.joinWith(" ", "-t", unit.toSeconds(time), rtp.getOutput(), out);
log.info("视频输出参数 {}", outputParam);
return ffmpegExecutor(inputParam, outputParam, unit.toSeconds(time) + 60, TimeUnit.SECONDS, streamHandler, executeResultHandler);
}
@SneakyThrows
public Executor downloadToStream(String input, long time, TimeUnit unit, ExecuteStreamHandler streamHandler, ExecuteResultHandler executeResultHandler) {
return downloadToStream(input, "-", time, unit, streamHandler, executeResultHandler);
}
@SneakyThrows
public Executor playbackToStream(String input, String out, long time, TimeUnit unit, ExecuteStreamHandler streamHandler, ExecuteResultHandler executeResultHandler){
FfmpegConfig.Rtp rtp = ffmpegConfig.getRtp();
FfmpegConfig.Debug debug = ffmpegConfig.getDebug();
String inputParam = debug.getInput() ? rtp.getInput() : StringUtils.joinWith(" ", "-y", rtp.getInput(), input);
log.info("视频输入参数 {}", inputParam);
String outputParam = debug.getOutput() ? rtp.getOutput() : StringUtils.joinWith(" ", rtp.getOutput(), out);
log.info("视频输出参数 {}", outputParam);
return ffmpegExecutor(inputParam, outputParam, unit.toSeconds(time) + 60, TimeUnit.SECONDS, streamHandler, executeResultHandler);
}
@SneakyThrows
public Executor playbackToStream(String input, long time, TimeUnit unit, ExecuteStreamHandler streamHandler, ExecuteResultHandler executeResultHandler) {
return playbackToStream(input, "-", time, unit, streamHandler, executeResultHandler);
}
@SneakyThrows
public Executor ffmpegExecutor(String inputParam,String outputParam, long time, TimeUnit unit,ExecuteStreamHandler streamHandler,ExecuteResultHandler executeResultHandler){
FfmpegConfig.Rtp rtp = ffmpegConfig.getRtp();
String logLevelParam = StringUtils.joinWith(" ","-loglevel", rtp.getLogLevel());
String command = StringUtils.joinWith(" ", ffmpegConfig.getFfmpeg(), logLevelParam, inputParam, outputParam);
CommandLine commandLine = CommandLine.parse(command);
log.info("{}", commandLine);
Executor executor = new DefaultExecutor();
ExecuteWatchdog watchdog = new ExecuteWatchdog(unit.toMillis(time));
executor.setStreamHandler(streamHandler);
executor.setExitValues(null);
executor.setWatchdog(watchdog);
if(executeResultHandler == null){
executor.execute(commandLine);
} else {
executor.execute(commandLine, executeResultHandler);
}
return executor;
}
}

View File

@ -0,0 +1,717 @@
package cn.skcks.docking.gb28181.wvp.service.gb28181;
import cn.hutool.core.date.*;
import cn.hutool.core.io.IoUtil;
import cn.hutool.core.util.IdUtil;
import cn.skcks.docking.gb28181.common.json.JsonException;
import cn.skcks.docking.gb28181.common.json.JsonResponse;
import cn.skcks.docking.gb28181.common.json.JsonUtils;
import cn.skcks.docking.gb28181.common.redis.RedisUtil;
import cn.skcks.docking.gb28181.core.sip.gb28181.cache.CacheUtil;
import cn.skcks.docking.gb28181.core.sip.gb28181.constant.GB28181Constant;
import cn.skcks.docking.gb28181.core.sip.message.processor.MessageProcessor;
import cn.skcks.docking.gb28181.core.sip.message.subscribe.GenericSubscribe;
import cn.skcks.docking.gb28181.core.sip.utils.SipUtil;
import cn.skcks.docking.gb28181.media.config.ZlmMediaConfig;
import cn.skcks.docking.gb28181.media.dto.rtp.CloseRtpServer;
import cn.skcks.docking.gb28181.media.dto.rtp.GetRtpInfoResp;
import cn.skcks.docking.gb28181.media.dto.rtp.OpenRtpServer;
import cn.skcks.docking.gb28181.media.dto.rtp.OpenRtpServerResp;
import cn.skcks.docking.gb28181.media.dto.status.ResponseStatus;
import cn.skcks.docking.gb28181.media.proxy.ZlmMediaService;
import cn.skcks.docking.gb28181.sdp.GB28181Description;
import cn.skcks.docking.gb28181.sdp.GB28181SDPBuilder;
import cn.skcks.docking.gb28181.sdp.media.MediaStreamMode;
import cn.skcks.docking.gb28181.service.record.vo.RecordInfoItemVO;
import cn.skcks.docking.gb28181.service.ssrc.SsrcService;
import cn.skcks.docking.gb28181.wvp.config.MediaRtmpConfig;
import cn.skcks.docking.gb28181.wvp.config.ProxySipConfig;
import cn.skcks.docking.gb28181.wvp.config.WvpProxyConfig;
import cn.skcks.docking.gb28181.wvp.executor.DefaultVideoExecutor;
import cn.skcks.docking.gb28181.wvp.orm.mybatis.dynamic.model.WvpProxyDevice;
import cn.skcks.docking.gb28181.wvp.orm.mybatis.dynamic.model.WvpProxyDocking;
import cn.skcks.docking.gb28181.wvp.service.device.DeviceService;
import cn.skcks.docking.gb28181.wvp.service.docking.DockingService;
import cn.skcks.docking.gb28181.wvp.service.record.RecordInfoService;
import cn.skcks.docking.gb28181.wvp.service.record.dto.RecordInfoDTO;
import cn.skcks.docking.gb28181.wvp.service.video.VideoService;
import cn.skcks.docking.gb28181.wvp.sip.request.SipRequestBuilder;
import cn.skcks.docking.gb28181.wvp.sip.response.SipResponseBuilder;
import cn.skcks.docking.gb28181.wvp.sip.sender.SipSender;
import cn.skcks.docking.gb28181.wvp.sip.subscribe.SipSubscribe;
import cn.skcks.docking.gb28181.wvp.utils.RetryUtil;
import com.github.rholder.retry.Retryer;
import com.github.rholder.retry.RetryerBuilder;
import com.github.rholder.retry.StopStrategies;
import com.github.rholder.retry.WaitStrategies;
import gov.nist.javax.sdp.MediaDescriptionImpl;
import gov.nist.javax.sdp.fields.TimeField;
import gov.nist.javax.sdp.fields.URIField;
import gov.nist.javax.sip.message.SIPRequest;
import gov.nist.javax.sip.message.SIPResponse;
import jakarta.servlet.AsyncContext;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.*;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Service;
import org.springframework.web.context.request.async.DeferredResult;
import javax.sdp.Connection;
import javax.sdp.SdpFactory;
import javax.sdp.TimeDescription;
import javax.sip.header.CallIdHeader;
import javax.sip.message.Request;
import javax.sip.message.Response;
import java.nio.charset.StandardCharsets;
import java.text.MessageFormat;
import java.time.Duration;
import java.time.ZoneId;
import java.util.Date;
import java.util.List;
import java.util.Optional;
import java.util.Vector;
import java.util.concurrent.*;
@Slf4j
@Service
@RequiredArgsConstructor
public class Gb28181DownloadService {
private final ZlmMediaService zlmMediaService;
private final ZlmMediaConfig zlmMediaConfig;
private final SsrcService ssrcService;
private final DeviceService deviceService;
private final DockingService dockingService;
private final ProxySipConfig proxySipConfig;
private final SipSender sender;
private final SipSubscribe subscribe;
private final VideoService videoService;
private final WvpProxyConfig wvpProxyConfig;
private final RecordInfoService recordInfoService;
private final MediaRtmpConfig mediaRtmpConfig;
@Qualifier(DefaultVideoExecutor.EXECUTOR_BEAN_NAME)
private final Executor executor;
private final ScheduledExecutorService scheduledExecutorService = new ScheduledThreadPoolExecutor(64);
private final ConcurrentMap<String, DeferredResult<JsonResponse<String>>> requestMap = new ConcurrentHashMap<>();
private final RealtimeManager realtimeManager;
@Getter
private final RealTime realtime = new RealTime();
@NoArgsConstructor
@AllArgsConstructor
@Data
public static class VideoInfo {
private String streamId;
private String url;
private String callId;
private WvpProxyDevice device;
}
public void header(HttpServletResponse response) {
response.setContentType("video/mp4");
response.setHeader("Accept-Ranges", "none");
response.setHeader("Connection", "close");
}
public void streamHeader(HttpServletResponse response) {
response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE);
response.setHeader("Accept-Ranges", "none");
response.setHeader("Connection", "close");
}
public void header(HttpServletResponse response, String fileName) {
header(response);
response.setHeader("Content-Disposition",
MessageFormat.format("attachment; filename=\"{0}.mp4\"",fileName));
}
private String videoRtmpUrl(String streamId) {
String rtmpSchema = "rtmp://" + zlmMediaConfig.getIp() + ":" + mediaRtmpConfig.getRtmpPort();
return StringUtils.joinWith("/", rtmpSchema, "rtp", streamId);
}
private String videoWsUrl(String rtmpUrl){
String rtmpSchema = "rtmp://" + zlmMediaConfig.getIp() + ":" + mediaRtmpConfig.getRtmpPort();
if(StringUtils.isNotBlank(proxySipConfig.getProxyMediaUrl())){
return StringUtils.replace(rtmpUrl + ".live.flv", rtmpSchema, proxySipConfig.getProxyMediaUrl());
} else {
String wsSchema = StringUtils.replace(zlmMediaConfig.getUrl(), "http://", "ws://");
return StringUtils.replace(rtmpUrl + ".live.flv", rtmpSchema, wsSchema);
}
}
@SneakyThrows
private void writeErrorToResponse(HttpServletResponse response, JsonResponse<?> json) {
response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
response.setCharacterEncoding(StandardCharsets.UTF_8.name());
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
IoUtil.writeUtf8(response.getOutputStream(), false, json);
}
private int openRtpServer(String streamId, int streamMode) {
GetRtpInfoResp rtpInfo = zlmMediaService.getRtpInfo(streamId);
if (rtpInfo.getExist()) {
log.warn("流 {} 已存在", streamId);
return 0;
}
OpenRtpServer openRtpServer = new OpenRtpServer();
openRtpServer.setPort(0);
openRtpServer.setStreamId(streamId);
openRtpServer.setTcpMode(streamMode);
OpenRtpServerResp openRtpServerResp = zlmMediaService.openRtpServer(openRtpServer);
log.info("openRtpServerResp => {} => {}", streamId, openRtpServerResp);
if (!openRtpServerResp.getCode().equals(ResponseStatus.Success)) {
log.error("{}", openRtpServerResp.getCode().getMsg());
return -1;
}
return openRtpServerResp.getPort();
}
@SneakyThrows
@SuppressWarnings({"UnstableApiUsage", "unchecked"})
public void video(HttpServletRequest request, HttpServletResponse response, String deviceCode, Date startTime, Date endTime, Boolean fileHeader, Boolean useDownload) {
AsyncContext asyncContext = request.startAsync();
asyncContext.setTimeout(DateUtil.between(startTime, DateUtil.offsetSecond(endTime, 60), DateUnit.MS));
asyncContext.start(()->{
DateTime start = DateUtil.date();
HttpServletResponse asyncResponse = (HttpServletResponse)asyncContext.getResponse();
try{
if(proxySipConfig.isUseRecordInfoQueryBeforeDownload()){
String name = MessageFormat.format("{0} {1}-{2}", deviceCode, startTime, endTime);
Retryer<JsonResponse<List<RecordInfoItemVO>>> retryer = RetryerBuilder.<JsonResponse<List<RecordInfoItemVO>>>newBuilder()
// 异常就重试
.retryIfException()
.retryIfRuntimeException()
// 重试间隔
.withWaitStrategy(WaitStrategies.fixedWait(proxySipConfig.getRetryRecordInfoQueryBeforeDownloadInterval(), proxySipConfig.getRetryRecordInfoQueryBeforeDownloadIntervalUnit()))
// 重试次数
.withStopStrategy(StopStrategies.stopAfterAttempt(proxySipConfig.getRetryRecordInfoQueryBeforeDownloadTimes()))
.retryIfResult((result) -> {
log.info("{}", result);
return result == null ||
result.getCode() != Response.OK ||
result.getData() == null ||
result.getData().isEmpty();
})
.withRetryListener(RetryUtil.defaultRetryListener(name)).build();
retryer.call(()->{
CompletableFuture<JsonResponse<List<RecordInfoItemVO>>> future = new CompletableFuture<>();
future.completeOnTimeout(JsonResponse.error("录像查询超时"),1,TimeUnit.MINUTES);
// 发起设备录像查询
DeferredResult<JsonResponse<List<RecordInfoItemVO>>> requestedRecordInfo =
recordInfoService.requestRecordInfo(new RecordInfoDTO(deviceCode, startTime, endTime, "", 0, "all"));
requestedRecordInfo.setResultHandler(result -> {
future.complete((JsonResponse<List<RecordInfoItemVO>>) result);
});
requestedRecordInfo.onError((throwable)->{
future.complete(JsonResponse.error(throwable.getMessage()));
});
return future.get();
});
}
download(deviceCode, startTime, endTime, useDownload, true).whenComplete((videoInfo, e) -> {
writeFileHeader(response, deviceCode, startTime, endTime, fileHeader);
log.info("videoInfo {}", videoInfo);
if (e != null) {
writeErrorToResponse(asyncResponse, JsonResponse.error(e.getMessage()));
} else if (videoInfo == null) {
writeErrorToResponse(asyncResponse, JsonResponse.error("下载失败"));
} else if (wvpProxyConfig.getUseFfmpeg()) {
log.info("开始 ffmpeg 录制, deviceCode {}, startTime {}, endTime {}", deviceCode, DateUtil.formatDateTime(startTime), DateUtil.formatDateTime(endTime));
videoService.ffmpegRecord(request, asyncResponse, videoInfo.getUrl(), startTime, endTime, DateUtil.between(startTime, endTime, DateUnit.SECOND), videoInfo.getDevice(), videoInfo.getCallId());
DateTime end = DateUtil.date();
asyncContext.complete();
log.info("下载总耗时: {}, deviceCode {}, startTime {}, endTime {}", DateUtil.between(start, end, DateUnit.SECOND), deviceCode, DateUtil.formatDateTime(startTime), DateUtil.formatDateTime(endTime));
} else {
videoService.javaCVrecord(asyncResponse, videoInfo.getUrl(), DateUtil.between(startTime, endTime, DateUnit.SECOND) + 15);
}
});
} catch(Exception e) {
writeErrorToResponse(asyncResponse, JsonResponse.error(e.getMessage()));
} finally {
if(!wvpProxyConfig.getUseFfmpeg()){
DateTime end = DateUtil.date();
asyncContext.complete();
log.info("下载总耗时: {}, deviceCode {}, startTime {}, endTime {}", DateUtil.between(start,end, DateUnit.SECOND), deviceCode, DateUtil.formatDateTime(startTime), DateUtil.formatDateTime(endTime));
}
}
});
}
@SneakyThrows
public DeferredResult<JsonResponse<String>> videoUrl(String deviceCode, Date startTime, Date endTime) {
long time = DateUtil.between(startTime, endTime, DateUnit.MS);
DeferredResult<JsonResponse<String>> result = new DeferredResult<>(time);
result.onTimeout(()->{
result.setResult(JsonResponse.error("请求超时"));
});
WvpProxyDevice device = deviceService.getDeviceByDeviceCode(deviceCode).orElse(null);
if(device == null){
result.setResult(JsonResponse.error("设备不存在"));
return result;
}
WvpProxyDocking docking = dockingService.getDeviceByDeviceCode(device.getGbDeviceId()).orElse(null);
if(docking == null){
result.setResult(JsonResponse.error("设备(通道)不存在"));
return result;
}
closeExistRequest(deviceCode);
requestMap.put(deviceCode, result);
// 间隔一定时间(200ms) 给设备足够的时间结束前次请求
scheduledExecutorService.schedule(()->{
download(deviceCode, startTime, endTime, false).whenComplete((videoInfo, e)->{
log.info("获取媒体信息 {}", videoInfo);
String cacheKey = CacheUtil.getKey(docking.getGbDeviceId(), device.getGbDeviceChannelId());
String existCallId = RedisUtil.StringOps.get(cacheKey);
// 到达时间后 延迟 10秒 主动结束, 防止某些设备不会主动结束
scheduledExecutorService.schedule(()->{
log.info("到达结束时间 发送 bye 关闭 {} {}", videoInfo.getDevice().getGbDeviceChannelId(), videoInfo.getCallId());
String deviceIp = docking.getIp();
int devicePort = Integer.parseInt(docking.getPort());
if(StringUtils.isNotBlank(existCallId)) {
sender.sendRequest((provider, localIp, localPort) ->
SipRequestBuilder.createByeRequest(deviceIp, devicePort, device.getGbDeviceChannelId(), SipUtil.generateFromTag(), null, existCallId));
}
RedisUtil.KeyOps.delete(cacheKey);
zlmMediaService.closeRtpServer(CloseRtpServer.builder()
.streamId(videoInfo.streamId)
.build());
}, time + Duration.ofSeconds(10).toMillis(), TimeUnit.MILLISECONDS);
String url = videoWsUrl(videoInfo.getUrl());
url = StringUtils.replaceOnce(url, ".live.flv", ".live.mp4");
result.setResult(JsonResponse.success(url));
});
}, 200, TimeUnit.MILLISECONDS);
return result;
}
@SneakyThrows
public synchronized DeferredResult<JsonResponse<String>> realtimeVideoUrl(String deviceCode){
DeferredResult<JsonResponse<String>> result = new DeferredResult<>(TimeUnit.SECONDS.toMillis(60));
result.onTimeout(()->{
result.setResult(JsonResponse.error("请求超时"));
});
WvpProxyDevice device = deviceService.getDeviceByDeviceCode(deviceCode).orElse(null);
if(device == null){
result.setResult(JsonResponse.error("设备不存在"));
return result;
}
WvpProxyDocking docking = dockingService.getDeviceByDeviceCode(device.getGbDeviceId()).orElse(null);
if(docking == null){
result.setResult(JsonResponse.error("设备(通道)不存在"));
return result;
}
String existUrl = RedisUtil.StringOps.get(CacheUtil.getKey(GB28181SDPBuilder.Action.PLAY.getAction(), deviceCode));
if(Optional.ofNullable(existUrl).isPresent()){
result.setResult(JsonResponse.success(existUrl));
return result;
}
// 间隔一定时间(200ms) 给设备足够的时间结束前次请求
scheduledExecutorService.schedule(()->{
realtime.realtime(deviceCode).whenComplete((videoInfo, e)->{
if(videoInfo == null){
result.setResult(JsonResponse.error("媒体信息获取失败"));
return;
}
log.info("获取媒体信息 {}", videoInfo);
// 原始链接转换为前端可用的链接
RedisUtil.StringOps.set(CacheUtil.getKey(GB28181SDPBuilder.Action.PLAY.getAction(), videoInfo.getCallId()), JsonUtils.toJson(videoInfo));
String url = videoWsUrl(videoInfo.getUrl());
videoInfo.setUrl(url);
realtimeManager.addPlaying(deviceCode, videoInfo);
result.setResult(JsonResponse.success(url));
});
}, 200, TimeUnit.MILLISECONDS);
return result;
}
private void closeExistRequest(String deviceCode) {
requestMap.computeIfPresent(deviceCode,(key,requestResult)->{
log.info("关闭已存在的视频请求 {}", deviceCode);
if(!requestResult.hasResult()){
requestResult.setResult(JsonResponse.error("同一设备重复请求, 本次请求结束"));
realtimeManager.removePlaying(deviceCode);
}
return null;
});
}
public void closeRealtimeVideoNow(String deviceCode){
realtimeManager.removePlaying(deviceCode);
}
@SneakyThrows
public void videoStream(HttpServletRequest request, HttpServletResponse response, String deviceCode, Date startTime, Date endTime) {
AsyncContext asyncContext = request.startAsync();
asyncContext.setTimeout(0);
asyncContext.start(()->{
DateTime start = DateUtil.date();
HttpServletResponse asyncResponse = (HttpServletResponse)asyncContext.getResponse();
try{
download(deviceCode, startTime,endTime, true).whenComplete((videoInfo, e)->{
streamHeader(asyncResponse);
if(e != null){
writeErrorToResponse(asyncResponse, JsonResponse.error(e.getMessage()));
} else if(videoInfo == null){
writeErrorToResponse(asyncResponse, JsonResponse.error("下载失败"));
} else if(wvpProxyConfig.getUseFfmpeg()){
log.info("开始 ffmpeg 录制, deviceCode {}, startTime {}, endTime {}", deviceCode, DateUtil.formatDateTime(startTime), DateUtil.formatDateTime(endTime));
videoService.ffmpegRecord(request, asyncResponse, videoInfo.getUrl(), startTime, endTime, DateUtil.between(startTime, endTime, DateUnit.SECOND), videoInfo.getDevice(), videoInfo.getCallId());
DateTime end = DateUtil.date();
asyncContext.complete();
log.info("下载总耗时: {}, deviceCode {}, startTime {}, endTime {}", DateUtil.between(start, end, DateUnit.SECOND), deviceCode, DateUtil.formatDateTime(startTime), DateUtil.formatDateTime(endTime));
} else {
videoService.javaCVrecord(asyncResponse, videoInfo.getUrl(), DateUtil.between(startTime,endTime,DateUnit.SECOND) + 15);
}
});
} catch(Exception e) {
writeErrorToResponse(asyncResponse, JsonResponse.error(e.getMessage()));
} finally {
if(!wvpProxyConfig.getUseFfmpeg()){
DateTime end = DateUtil.date();
asyncContext.complete();
log.info("下载总耗时: {}, deviceCode {}, startTime {}, endTime {}", DateUtil.between(start,end, DateUnit.SECOND), deviceCode, DateUtil.formatDateTime(startTime), DateUtil.formatDateTime(endTime));
}
}
});
}
private void writeFileHeader(HttpServletResponse response, String deviceCode, Date startTime, Date endTime, Boolean fileHeader){
if(fileHeader){
header(response, StringUtils.joinWith("_",
deviceCode,
DateUtil.format(startTime, DatePattern.PURE_DATETIME_FORMAT),
DateUtil.format(endTime, DatePattern.PURE_DATETIME_FORMAT)));
} else {
header(response);
}
}
private class RealTime {
@SneakyThrows
public CompletableFuture<VideoInfo> realtime(String deviceCode) {
Optional<WvpProxyDevice> deviceByDeviceCode = deviceService.getDeviceByDeviceCode(deviceCode);
if (deviceByDeviceCode.isEmpty()) {
String reason = MessageFormat.format("未能找到 设备编码 为 {0} 的设备", deviceCode);
log.error("{}",reason);
throw new JsonException(reason);
} else {
WvpProxyDevice device = deviceByDeviceCode.get();
return realtime(device.getGbDeviceId(), device.getGbDeviceChannelId());
}
}
@SneakyThrows
public CompletableFuture<VideoInfo> realtime(String gbDeviceId, String channel){
CompletableFuture<VideoInfo> result = new CompletableFuture<>();
Optional<WvpProxyDocking> deviceByGbDeviceId = dockingService.getDeviceByGbDeviceId(gbDeviceId);
if(deviceByGbDeviceId.isEmpty()){
log.info("未能找到 国标编码 {} 的注册信息", gbDeviceId);
result.complete(null);
return result;
}
Optional<WvpProxyDevice> deviceByGbDeviceIdAndChannel = deviceService.getDeviceByGbDeviceIdAndChannel(gbDeviceId, channel);
if (deviceByGbDeviceIdAndChannel.isEmpty()) {
log.info("未能找到 编码 {}, 通道 {} 的设备", gbDeviceId, channel);
result.complete(null);
return result;
}
WvpProxyDevice device = deviceByGbDeviceIdAndChannel.get();
WvpProxyDocking docking = deviceByGbDeviceId.get();
String streamId = GB28181SDPBuilder.getStreamId(gbDeviceId, channel, IdUtil.getSnowflakeNextIdStr());
int isTcp = proxySipConfig.getStreamMode() == MediaStreamMode.UDP ? 0 : 1;
MediaStreamMode streamMode = proxySipConfig.getStreamMode();
String ip = zlmMediaConfig.getIp();
int port = openRtpServer(streamId, isTcp);
if(port <= 0){
log.error("zlm 暂无可用端口");
result.complete(null);
return result;
}
String ssrc = ssrcService.getPlaySsrc();
GB28181Description gb28181Description = GB28181SDPBuilder.Receiver.play(gbDeviceId, channel, Connection.IP4, ip, port, ssrc, streamMode);
sender.sendRequest(inviteRequest(docking, device, gb28181Description, ssrc, streamId, result, false));
return result;
}
@SneakyThrows
public SipSender.SendRequest inviteRequest(WvpProxyDocking docking, WvpProxyDevice device, GB28181Description description, String ssrc, String streamId, CompletableFuture<VideoInfo> result, Boolean prefetch) {
String cacheKey = CacheUtil.getKey(docking.getGbDeviceId(), device.getGbDeviceChannelId());
String existCallId = RedisUtil.StringOps.get(cacheKey);
// 限制单个设备/通道 只能 点播 一路
if(StringUtils.isNotBlank(existCallId)){
String deviceIp = docking.getIp();
int devicePort = Integer.parseInt(docking.getPort());
sender.sendRequest((provider,localIp,localPort)->
SipRequestBuilder.createByeRequest(deviceIp, devicePort, device.getGbDeviceChannelId(), SipUtil.generateFromTag(), null, existCallId));
RedisUtil.KeyOps.delete(cacheKey);
log.info("关闭已存在的 点播请求 {} {}", cacheKey, existCallId);
Thread.sleep(500);
}
return (provider, ip, port) -> {
CallIdHeader callId = provider.getNewCallId();
String subscribeKey = GenericSubscribe.Helper.getKey(Request.INVITE, callId.getCallId());
subscribe.getInviteSubscribe().addPublisher(subscribeKey);
Flow.Subscriber<SIPResponse> subscriber = inviteSubscriber(docking,device,subscribeKey,cacheKey, ssrc, streamId, result, 0, TimeUnit.SECONDS, prefetch);
subscribe.getInviteSubscribe().addSubscribe(subscribeKey, subscriber);
RedisUtil.StringOps.set(cacheKey, callId.getCallId());
return SipRequestBuilder.createInviteRequest(ip, port, docking, device.getGbDeviceChannelId(), description.toString(), SipUtil.generateViaTag(), SipUtil.generateFromTag(), null, ssrc, callId);
};
}
}
public CompletableFuture<VideoInfo> download(String deviceCode, Date startTime, Date endTime, Boolean prefetch) {
return download(deviceCode,startTime,endTime, proxySipConfig.isUsePlaybackToDownload(), prefetch);
}
@SneakyThrows
public CompletableFuture<VideoInfo> download(String deviceCode, Date startTime, Date endTime, Boolean useDownload, Boolean prefetch) {
Optional<WvpProxyDevice> deviceByDeviceCode = deviceService.getDeviceByDeviceCode(deviceCode);
if (deviceByDeviceCode.isEmpty()) {
String reason = MessageFormat.format("未能找到 设备编码 为 {0} 的设备", deviceCode);
log.error("{}",reason);
throw new JsonException(reason);
} else {
WvpProxyDevice device = deviceByDeviceCode.get();
return download(device.getGbDeviceId(), device.getGbDeviceChannelId(), startTime, endTime, useDownload, prefetch);
}
}
@SneakyThrows
public CompletableFuture<VideoInfo> download(String gbDeviceId, String channel, Date startTime, Date endTime, Boolean useDownload, Boolean prefetch){
CompletableFuture<VideoInfo> result = new CompletableFuture<>();
Optional<WvpProxyDocking> deviceByGbDeviceId = dockingService.getDeviceByGbDeviceId(gbDeviceId);
long time = DateUtil.between(startTime, endTime, DateUnit.SECOND);
if(deviceByGbDeviceId.isEmpty()){
log.info("未能找到 国标编码 {} 的注册信息", gbDeviceId);
result.complete(null);
return result;
}
Optional<WvpProxyDevice> deviceByGbDeviceIdAndChannel = deviceService.getDeviceByGbDeviceIdAndChannel(gbDeviceId, channel);
if (deviceByGbDeviceIdAndChannel.isEmpty()) {
log.info("未能找到 编码 {}, 通道 {} 的设备", gbDeviceId, channel);
result.complete(null);
return result;
}
WvpProxyDevice device = deviceByGbDeviceIdAndChannel.get();
WvpProxyDocking docking = deviceByGbDeviceId.get();
ZoneId zoneId = ZoneId.of(GB28181Constant.TIME_ZONE);
long start = LocalDateTimeUtil.of(startTime.toInstant(), zoneId).atZone(zoneId).toEpochSecond();
long end = LocalDateTimeUtil.of(endTime.toInstant(), zoneId).atZone(zoneId).toEpochSecond();
String streamId = GB28181SDPBuilder.getStreamId(gbDeviceId, channel, String.valueOf(start), String.valueOf(end), IdUtil.getSnowflakeNextIdStr());
int isTcp = proxySipConfig.getStreamMode() == MediaStreamMode.UDP ? 0 : 1;
MediaStreamMode streamMode = proxySipConfig.getStreamMode();
String ip = zlmMediaConfig.getIp();
int port = openRtpServer(streamId, isTcp);
if(port <= 0){
log.error("zlm 暂无可用端口");
result.complete(null);
return result;
}
String ssrc = ssrcService.getPlaySsrc();
TimeField timeField = new TimeField();
timeField.setStartTime(start);
timeField.setStopTime(end);
TimeDescription timeDescription = SdpFactory.getInstance().createTimeDescription(timeField);
GB28181SDPBuilder.Action action = GB28181SDPBuilder.Action.DOWNLOAD;
if(useDownload == null ? proxySipConfig.isUsePlaybackToDownload(): !useDownload){
action = GB28181SDPBuilder.Action.PLAY_BACK;
}
GB28181Description gb28181Description = GB28181SDPBuilder.Receiver.build(action, gbDeviceId, channel, Connection.IP4, ip, port, ssrc, streamMode, timeDescription);
gb28181Description.setSessionName(SdpFactory.getInstance().createSessionName(action.getAction()));
gb28181Description.setTimeDescriptions(new Vector<>(){{add(timeDescription);}});
MediaDescriptionImpl media = (MediaDescriptionImpl) gb28181Description.getMediaDescriptions(true).get(0);
if(proxySipConfig.getStreamMode() != MediaStreamMode.UDP){
media.getMedia().setProtocol("RTP/AVP/TCP");
}
if(useDownload == null ? !proxySipConfig.isUsePlaybackToDownload(): useDownload){
media.setAttribute("downloadspeed", String.valueOf(4));
}
URIField uriField = new URIField();
uriField.setURI(StringUtils.joinWith(":", channel, "0"));
gb28181Description.setURI(uriField);
sender.sendRequest(inviteRequest(docking, device, gb28181Description, action, ssrc, streamId, result, time, prefetch));
return result;
}
@SneakyThrows
public SipSender.SendRequest inviteRequest(WvpProxyDocking docking, WvpProxyDevice device, GB28181Description description, GB28181SDPBuilder.Action action, String ssrc, String streamId, CompletableFuture<VideoInfo> result, long time, Boolean prefetch) {
String cacheKey = CacheUtil.getKey(docking.getGbDeviceId(), device.getGbDeviceChannelId());
String existCallId = RedisUtil.StringOps.get(cacheKey);
// 限制单个设备/通道 只能 点播 一路
if(StringUtils.isNotBlank(existCallId)){
String deviceIp = docking.getIp();
int devicePort = Integer.parseInt(docking.getPort());
sender.sendRequest((provider,localIp,localPort)->
SipRequestBuilder.createByeRequest(deviceIp, devicePort, device.getGbDeviceChannelId(), SipUtil.generateFromTag(), null, existCallId));
RedisUtil.KeyOps.delete(cacheKey);
log.info("关闭已存在的 点播请求 {} {}", cacheKey, existCallId);
Thread.sleep(500);
}
return (provider, ip, port) -> {
CallIdHeader callId = provider.getNewCallId();
String subscribeKey = GenericSubscribe.Helper.getKey(Request.INVITE, callId.getCallId());
subscribe.getInviteSubscribe().addPublisher(subscribeKey);
Flow.Subscriber<SIPResponse> subscriber = inviteSubscriber(docking,device,subscribeKey,cacheKey, ssrc, streamId, result, time + 60, TimeUnit.SECONDS, prefetch);
subscribe.getInviteSubscribe().addSubscribe(subscribeKey, subscriber);
RedisUtil.StringOps.set(cacheKey, callId.getCallId());
if(prefetch){
// 用以 提前 启动 ffmpeg 预备录制, 需要配置 ffmpeg rw_timeout 时长 避免收不到流
result.complete(new VideoInfo(streamId, videoRtmpUrl(streamId), callId.getCallId(), device));
}
return SipRequestBuilder.createInviteRequest(ip, port, docking, device.getGbDeviceChannelId(), description.toString(), SipUtil.generateViaTag(), SipUtil.generateFromTag(), null, ssrc, callId);
};
}
public Flow.Subscriber<SIPResponse> inviteSubscriber(WvpProxyDocking docking, WvpProxyDevice device, String subscribeKey, String cacheKey, String ssrc,String streamId,CompletableFuture<VideoInfo> result, long time, TimeUnit unit, Boolean prefetch){
ScheduledFuture<?>[] schedule = new ScheduledFuture<?>[1];
Flow.Subscriber<SIPResponse> subscriber = new Flow.Subscriber<>() {
private Flow.Subscription subscription;
@Override
public void onSubscribe(Flow.Subscription subscription) {
this.subscription = subscription;
log.info("订阅 {} {}", MessageProcessor.Method.INVITE, subscribeKey);
subscription.request(1);
}
@Override
public void onNext(SIPResponse item) {
int statusCode = item.getStatusCode();
log.debug("{} 收到订阅消息 {}", subscribeKey, item);
if (statusCode == Response.TRYING) {
log.info("订阅 {} {} 尝试连接流媒体服务", MessageProcessor.Method.INVITE, subscribeKey);
subscription.request(1);
} else if (statusCode >= Response.OK && statusCode < Response.MULTIPLE_CHOICES) {
log.info("订阅 {} {} 流媒体服务连接成功, 开始推送视频流", MessageProcessor.Method.INVITE, subscribeKey);
log.info("收到响应状态 {}", statusCode);
String callId = item.getCallId().getCallId();
if(!prefetch){
// 相应 200OK 后再返回, 用于对延迟不敏感的实时请求
result.complete(new VideoInfo(streamId, videoRtmpUrl(streamId), item.getCallId().getCallId(), device));
}
scheduledExecutorService.schedule(()->{
sender.sendRequest(((provider, ip, port) -> {
String fromTag = item.getFromTag();
String toTag = item.getToTag();
String key = GenericSubscribe.Helper.getKey(Request.BYE, callId);
subscribe.getByeSubscribe().addPublisher(key);
subscribe.getByeSubscribe().addSubscribe(key, byeSubscriber(key, device, cacheKey, streamId, time, unit));
return SipRequestBuilder.createAckRequest(Response.OK, ip, port, docking, device.getGbDeviceChannelId(), fromTag, toTag, callId);
}));
},200,TimeUnit.MILLISECONDS);
} else {
log.info("订阅 {} {} 连接流媒体服务时出现异常, 终止订阅", MessageProcessor.Method.INVITE, subscribeKey);
zlmMediaService.closeRtpServer(new CloseRtpServer(streamId));
result.complete(null);
ssrcService.releaseSsrc(zlmMediaConfig.getId(), ssrc);
onComplete();
}
}
@Override
public void onError(Throwable throwable) {
onComplete();
}
@Override
public void onComplete() {
subscribe.getInviteSubscribe().delPublisher(subscribeKey);
schedule[0].cancel(true);
}
};
if(time == 0){
schedule[0] = scheduledExecutorService.schedule(subscriber::onComplete, 60, unit);
} else {
schedule[0] = scheduledExecutorService.schedule(subscriber::onComplete, time, unit);
}
return subscriber;
}
public Flow.Subscriber<SIPRequest> byeSubscriber(String key,WvpProxyDevice device, String cacheKey,String streamId, long time, TimeUnit unit){
ScheduledFuture<?>[] schedule = new ScheduledFuture<?>[1];
Flow.Subscriber<SIPRequest> subscriber = new Flow.Subscriber<>() {
@Override
public void onSubscribe(Flow.Subscription subscription) {
log.info("创建订阅 {}", key);
subscription.request(1);
}
@SneakyThrows
@Override
public void onNext(SIPRequest request) {
RedisUtil.KeyOps.delete(cacheKey);
String transport = request.getTopmostViaHeader().getTransport();
String ip = request.getLocalAddress().getHostAddress();
if(time <= 0) {
String callId = request.getCallId().getCallId();
String infoKey = CacheUtil.getKey(GB28181SDPBuilder.Action.PLAY.getAction(), callId);
VideoInfo parse = JsonUtils.parse(RedisUtil.StringOps.get(infoKey), VideoInfo.class);
Optional.ofNullable(parse).ifPresent((info)->{
if(info.getCallId().equalsIgnoreCase(callId)){
realtimeManager.removePlaying(device.getDeviceCode());
}
});
RedisUtil.KeyOps.delete(infoKey);
}
sender.getProvider(transport,ip)
.sendResponse(SipResponseBuilder.response(request, Response.OK, "OK"));
onComplete();
}
@Override
public void onError(Throwable throwable) {
throwable.printStackTrace();
onComplete();
}
@Override
public void onComplete() {
subscribe.getByeSubscribe().delPublisher(key);
if(time > 0){
schedule[0].cancel(true);
}
zlmMediaService.closeRtpServer(new CloseRtpServer(streamId));
}
};
if(time > 0){
schedule[0] = scheduledExecutorService.schedule(subscriber::onComplete, time, unit);
}
return subscriber;
}
}

View File

@ -0,0 +1,113 @@
package cn.skcks.docking.gb28181.wvp.service.gb28181;
import cn.skcks.docking.gb28181.common.json.JsonUtils;
import cn.skcks.docking.gb28181.common.redis.RedisUtil;
import cn.skcks.docking.gb28181.core.sip.gb28181.cache.CacheUtil;
import cn.skcks.docking.gb28181.core.sip.utils.SipUtil;
import cn.skcks.docking.gb28181.media.dto.rtp.CloseRtpServer;
import cn.skcks.docking.gb28181.media.proxy.ZlmMediaService;
import cn.skcks.docking.gb28181.sdp.GB28181SDPBuilder;
import cn.skcks.docking.gb28181.wvp.orm.mybatis.dynamic.model.WvpProxyDevice;
import cn.skcks.docking.gb28181.wvp.orm.mybatis.dynamic.model.WvpProxyDocking;
import cn.skcks.docking.gb28181.wvp.service.device.DeviceService;
import cn.skcks.docking.gb28181.wvp.service.docking.DockingService;
import cn.skcks.docking.gb28181.wvp.sip.request.SipRequestBuilder;
import cn.skcks.docking.gb28181.wvp.sip.sender.SipSender;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.util.concurrent.*;
@Slf4j
@Service
@RequiredArgsConstructor
public class RealtimeManager {
public final static String REALTIME_KEY = "realtime:";
private final ZlmMediaService zlmMediaService;
private final DeviceService deviceService;
private final DockingService dockingService;
private final SipSender sender;
private final ScheduledExecutorService scheduledExecutorService = Executors.newSingleThreadScheduledExecutor();
private final ConcurrentMap<String, ScheduledFuture<?>> playing = new ConcurrentHashMap<>();
private String getRealtimeKey(String deviceCode){
return REALTIME_KEY + deviceCode;
}
public void addPlaying(String deviceCode, Gb28181DownloadService.VideoInfo videoInfo) {
RedisUtil.StringOps.set(CacheUtil.getKey(GB28181SDPBuilder.Action.PLAY.getAction(), deviceCode), videoInfo.getUrl());
ScheduledFuture<?> schedule = playing.get(deviceCode);
if(schedule != null){
schedule.cancel(true);
}
// 定时任务
schedule = scheduledExecutorService.schedule(()->{
log.info("[定时任务] 关闭设备(deviceCode => {}) 视频", deviceCode);
playing.remove(deviceCode);
Gb28181DownloadService.VideoInfo _videoInfo = JsonUtils.parse(RedisUtil.StringOps.get(getRealtimeKey(deviceCode)), Gb28181DownloadService.VideoInfo.class);
RedisUtil.KeyOps.delete(getRealtimeKey(deviceCode));
close(deviceCode, _videoInfo);
}, 3, TimeUnit.MINUTES);
// 缓存
RedisUtil.StringOps.set(getRealtimeKey(deviceCode), JsonUtils.toJson(videoInfo));
playing.put(deviceCode, schedule);
}
public void removePlaying(String deviceCode) {
ScheduledFuture<?> schedule = playing.get(deviceCode);
if(schedule != null){
schedule.cancel(true);
playing.remove(deviceCode);
}
Gb28181DownloadService.VideoInfo videoInfo = JsonUtils.parse(RedisUtil.StringOps.get(getRealtimeKey(deviceCode)), Gb28181DownloadService.VideoInfo.class);
if(videoInfo == null) {
log.warn("未找到 设备(deviceCode => {}) 视频信息", deviceCode);
return;
}
close(deviceCode, videoInfo);
}
/**
* 关闭视频连接
* @param deviceCode 设备编码
* @param videoInfo 视频信息
*/
private void close(String deviceCode, Gb28181DownloadService.VideoInfo videoInfo){
RedisUtil.KeyOps.delete(getRealtimeKey(deviceCode));
RedisUtil.KeyOps.delete(CacheUtil.getKey(GB28181SDPBuilder.Action.PLAY.getAction(), deviceCode));
WvpProxyDevice device = deviceService.getDeviceByDeviceCode(deviceCode).orElse(null);
if(device == null){
return;
}
WvpProxyDocking docking = dockingService.getDeviceByDeviceCode(device.getGbDeviceId()).orElse(null);
if(docking == null){
return;
}
String cacheKey = CacheUtil.getKey(docking.getGbDeviceId(), device.getGbDeviceChannelId());
String existCallId = RedisUtil.StringOps.get(cacheKey);
String deviceIp = docking.getIp();
int devicePort = Integer.parseInt(docking.getPort());
// 判断缓存的 视频信息 缓存的 callId 是否相同 避免 下级点播出现信息不一致
if(videoInfo.getCallId().equalsIgnoreCase(existCallId)){
log.info("关闭视频连接, {} => {}",deviceCode, existCallId);
sender.sendRequest((provider, localIp, localPort) ->
SipRequestBuilder.createByeRequest(deviceIp, devicePort, device.getGbDeviceChannelId(), SipUtil.generateFromTag(), null, existCallId));
}
// 无论是否存在都调用一次 zlm 关闭流, 避免异常情况
zlmMediaService.closeRtpServer(CloseRtpServer.builder()
.streamId(videoInfo.getStreamId())
.build());
}
}

View File

@ -0,0 +1,87 @@
package cn.skcks.docking.gb28181.wvp.service.record;
import cn.skcks.docking.gb28181.common.json.JsonException;
import cn.skcks.docking.gb28181.common.json.JsonResponse;
import cn.skcks.docking.gb28181.common.xml.XmlUtils;
import cn.skcks.docking.gb28181.core.sip.gb28181.constant.CmdType;
import cn.skcks.docking.gb28181.core.sip.message.subscribe.GenericSubscribe;
import cn.skcks.docking.gb28181.core.sip.utils.SipUtil;
import cn.skcks.docking.gb28181.service.record.vo.RecordInfoItemVO;
import cn.skcks.docking.gb28181.sip.manscdp.recordinfo.request.RecordInfoRequestDTO;
import cn.skcks.docking.gb28181.wvp.orm.mybatis.dynamic.model.WvpProxyDevice;
import cn.skcks.docking.gb28181.wvp.orm.mybatis.dynamic.model.WvpProxyDocking;
import cn.skcks.docking.gb28181.wvp.service.device.DeviceService;
import cn.skcks.docking.gb28181.wvp.service.docking.DockingService;
import cn.skcks.docking.gb28181.wvp.service.record.dto.RecordInfoDTO;
import cn.skcks.docking.gb28181.wvp.sip.request.SipRequestBuilder;
import cn.skcks.docking.gb28181.wvp.sip.sender.SipSender;
import cn.skcks.docking.gb28181.wvp.sip.subscribe.RecordSubscribe;
import cn.skcks.docking.gb28181.wvp.sip.subscribe.SipSubscribe;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.web.context.request.async.DeferredResult;
import java.text.MessageFormat;
import java.util.List;
import java.util.Optional;
@Slf4j
@Service
@RequiredArgsConstructor
public class RecordInfoService {
private final SipSender sipSender;
private final SipSubscribe sipSubscribe;
private final DockingService dockingService;
private final DeviceService deviceService;
@SneakyThrows
public DeferredResult<JsonResponse<List<RecordInfoItemVO>>> requestRecordInfo(RecordInfoDTO dto){
String deviceCode = dto.getDeviceCode();
Optional<WvpProxyDevice> deviceByDeviceCode = deviceService.getDeviceByDeviceCode(deviceCode);
if (deviceByDeviceCode.isEmpty()) {
String reason = MessageFormat.format("未能找到 设备编码 为 {0} 的设备", deviceCode);
log.error("{}",reason);
throw new JsonException(reason);
} else {
WvpProxyDevice device = deviceByDeviceCode.get();
return requestRecordInfo(device.getGbDeviceId(), device.getGbDeviceChannelId(), dto);
}
}
public DeferredResult<JsonResponse<List<RecordInfoItemVO>>> requestRecordInfo(String gbDeviceId, String channel, RecordInfoDTO dto){
DeferredResult<JsonResponse<List<RecordInfoItemVO>>> result = new DeferredResult<>();
Optional<WvpProxyDocking> deviceByGbDeviceId = dockingService.getDeviceByGbDeviceId(gbDeviceId);
if(deviceByGbDeviceId.isEmpty()){
log.info("未能找到 国标编码 {} 的注册信息", gbDeviceId);
result.setResult(JsonResponse.error(MessageFormat.format("未能找到 设备编码 为 {0} 的设备", gbDeviceId)));
return result;
}
Optional<WvpProxyDevice> deviceByGbDeviceIdAndChannel = deviceService.getDeviceByGbDeviceIdAndChannel(gbDeviceId, channel);
if (deviceByGbDeviceIdAndChannel.isEmpty()) {
log.info("未能找到 编码 {}, 通道 {} 的设备", gbDeviceId, channel);
result.setResult(JsonResponse.error(MessageFormat.format("未能找到 编码 {0}, 通道 {1} 的设备", gbDeviceId, channel)));
return result;
}
WvpProxyDocking device = deviceByGbDeviceId.get();
String sn = String.valueOf((int) (Math.random() * 9 + 1) * 100000);
RecordInfoRequestDTO recordInfoRequestDTO = RecordInfoRequestDTO.builder()
.deviceId(channel)
.sn(sn)
.startTime(dto.getStartTime())
.endTime(dto.getEndTime())
.type(dto.getType())
.secrecy(dto.getSecrecy())
.filePath(dto.getFilePath())
.indistinctQuery(0)
.build();
String key = GenericSubscribe.Helper.getKey(CmdType.RECORD_INFO, channel, sn);
sipSubscribe.getMessageSubscribe().addPublisher(key);
sipSubscribe.getMessageSubscribe().addSubscribe(key, new RecordSubscribe(sipSubscribe, key, result, gbDeviceId));
sipSender.sendRequest((provider, ip, port)-> SipRequestBuilder.createMessageRequest(device,ip,port,SipRequestBuilder.getCSeq(), XmlUtils.toXml(recordInfoRequestDTO), SipUtil.generateViaTag(),
SipUtil.generateFromTag(), provider.getNewCallId()));
return result;
}
}

View File

@ -0,0 +1,35 @@
package cn.skcks.docking.gb28181.wvp.service.record.dto;
import cn.hutool.core.date.DatePattern;
import cn.skcks.docking.gb28181.constant.GB28181Constant;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.format.annotation.DateTimeFormat;
import java.util.Date;
@AllArgsConstructor
@NoArgsConstructor
@Data
public class RecordInfoDTO {
/**
* 目标设备的设备编码(必选)
*/
private String deviceCode;
@DateTimeFormat(pattern= DatePattern.NORM_DATETIME_PATTERN)
@JsonFormat(pattern = DatePattern.NORM_DATETIME_PATTERN, timezone = GB28181Constant.TIME_ZONE)
private Date startTime;
@DateTimeFormat(pattern= DatePattern.NORM_DATETIME_PATTERN)
@JsonFormat(pattern = DatePattern.NORM_DATETIME_PATTERN, timezone = GB28181Constant.TIME_ZONE)
private Date endTime;
private String filePath;
private Integer Secrecy = 0;
private String type = "all";
}

View File

@ -0,0 +1,20 @@
package cn.skcks.docking.gb28181.wvp.service.report;
import cn.skcks.docking.gb28181.common.json.JsonResponse;
import cn.skcks.docking.gb28181.media.feign.IgnoreSSLFeignClientConfig;
import cn.skcks.docking.gb28181.wvp.dto.report.ReportReq;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.util.MultiValueMap;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
@FeignClient(
name = "ReportServiceProxy",
url = "${report.url}",
configuration = {IgnoreSSLFeignClientConfig.class}
)
public interface ReportApi {
@PostMapping
JsonResponse<?> report(@RequestHeader MultiValueMap<String, String> headers, @RequestBody ReportReq body);
}

View File

@ -0,0 +1,52 @@
package cn.skcks.docking.gb28181.wvp.service.report;
import cn.hutool.core.date.BetweenFormatter;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.io.unit.DataSizeUtil;
import cn.hutool.core.util.IdUtil;
import cn.skcks.docking.gb28181.wvp.config.ReportConfig;
import cn.skcks.docking.gb28181.wvp.dto.report.ReportReq;
import cn.skcks.docking.gb28181.wvp.orm.mybatis.dynamic.model.WvpProxyDevice;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.util.LinkedMultiValueMap;
import java.util.Date;
@Slf4j
@Service
@RequiredArgsConstructor
public class ReportService {
private final ReportApi reportApi;
private final ReportConfig reportConfig;
public void report(HttpServletRequest request, WvpProxyDevice device, Date startTime, Date endTime, long fileSize) {
if(!reportConfig.getEnabled()){
return;
}
ReportReq reportReq = new ReportReq(IdUtil.fastUUID(),
device.getDeviceCode(),
device.getGbDeviceChannelId(),
DateUtil.formatBetween(startTime, endTime, BetweenFormatter.Level.SECOND),
new ReportReq.TimeRange(DateUtil.formatDateTime(startTime), DateUtil.formatDateTime(endTime)),
DateUtil.now(),
DataSizeUtil.format(fileSize));
LinkedMultiValueMap<String, String> headers = new LinkedMultiValueMap<>();
reportConfig.getCustomHeaders().forEach(headers::add);
request.getHeaderNames().asIterator().forEachRemaining(headerKey -> {
String header = request.getHeader(headerKey);
headers.add(headerKey, header);
});
headers.add("X-Client-Ip", request.getRemoteAddr());
log.info("上报调用信息 {}", reportReq);
try{
reportApi.report(headers, reportReq);
} catch (Exception e){
log.error("上报调用信息失败", e);
}
}
}

View File

@ -1,81 +0,0 @@
package cn.skcks.docking.gb28181.wvp.service.video;
import cn.hutool.core.io.IoUtil;
import cn.hutool.core.util.IdUtil;
import jakarta.servlet.http.HttpServletResponse;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.bytedeco.ffmpeg.global.avcodec;
import org.bytedeco.ffmpeg.global.avutil;
import org.bytedeco.javacv.*;
import org.springframework.stereotype.Service;
import java.io.*;
import java.nio.file.Path;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
@Slf4j
@Service
public class RecordService {
public void header(HttpServletResponse response){
response.setContentType("video/mp4");
response.setHeader("Accept-Ranges","none");
response.setHeader("Connection","close");
}
@SneakyThrows
public void record(HttpServletResponse response, String url, long timeout){
response.reset();
// response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE);
header(response);
Path tmp = Path.of(System.getProperty("java.io.tmpdir"), IdUtil.getSnowflakeNextIdStr()).toAbsolutePath();
File file = new File(tmp + ".mp4");
log.info("创建文件 {}, {}", file, file.createNewFile());
log.info("url {}", url);
try (FFmpegFrameGrabber grabber = new FFmpegFrameGrabber(url)) {
grabber.start();
try(FFmpegFrameRecorder recorder = new FFmpegFrameRecorder(file, grabber.getImageWidth(), grabber.getImageHeight(),grabber.getAudioChannels())){
recorder.start();
log.info("开始录像");
log.info("{}", file);
recorder.setVideoCodec(avcodec.AV_CODEC_ID_H264);
recorder.setPixelFormat(avutil.AV_PIX_FMT_YUV420P); //视频源数据yuv
recorder.setAudioCodec(avcodec.AV_CODEC_ID_AAC); //设置音频压缩方式
recorder.setFormat("mp4");
recorder.setVideoOption("threads", String.valueOf(Runtime.getRuntime().availableProcessors())); //解码线程数
ScheduledExecutorService scheduledExecutorService = Executors.newSingleThreadScheduledExecutor();
AtomicBoolean record = new AtomicBoolean(true);
scheduledExecutorService.schedule(()->{
log.info("到达超时时间, 结束录制");
record.set(false);
}, timeout, TimeUnit.SECONDS);
try {
Frame frame;
while (record.get() && (frame = grabber.grab()) != null) {
recorder.record(frame);
}
grabber.stop();
recorder.stop();
} catch (FFmpegFrameRecorder.Exception | FrameGrabber.Exception e) {
throw new RuntimeException(e);
}
}
} finally {
log.info("结束录制");
InputStream inputStream = new BufferedInputStream(new FileInputStream(file));
OutputStream outputStream = new BufferedOutputStream(response.getOutputStream());
try{
IoUtil.copy(inputStream, outputStream);
} catch (Exception ignore){}
log.info("临时文件 {} 写入 响应 完成", file);
log.info("删除临时文件 {} {}", file, file.delete());
}
}
}

View File

@ -0,0 +1,325 @@
package cn.skcks.docking.gb28181.wvp.service.video;
import cn.hutool.core.date.DateTime;
import cn.hutool.core.date.DateUnit;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.io.IoUtil;
import cn.skcks.docking.gb28181.core.sip.utils.SipUtil;
import cn.skcks.docking.gb28181.wvp.config.FfmpegConfig;
import cn.skcks.docking.gb28181.wvp.config.ProxySipConfig;
import cn.skcks.docking.gb28181.wvp.config.WvpProxyConfig;
import cn.skcks.docking.gb28181.wvp.orm.mybatis.dynamic.model.WvpProxyDevice;
import cn.skcks.docking.gb28181.wvp.orm.mybatis.dynamic.model.WvpProxyDocking;
import cn.skcks.docking.gb28181.wvp.service.docking.DockingService;
import cn.skcks.docking.gb28181.wvp.service.ffmpeg.FfmpegSupportService;
import cn.skcks.docking.gb28181.wvp.service.report.ReportService;
import cn.skcks.docking.gb28181.wvp.sip.request.SipRequestBuilder;
import cn.skcks.docking.gb28181.wvp.sip.sender.SipSender;
import jakarta.servlet.AsyncContext;
import jakarta.servlet.ServletOutputStream;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.AccessLevel;
import lombok.RequiredArgsConstructor;
import lombok.Setter;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.exec.*;
import org.bytedeco.ffmpeg.avcodec.AVPacket;
import org.bytedeco.ffmpeg.global.avcodec;
import org.bytedeco.ffmpeg.global.avutil;
import org.bytedeco.javacv.FFmpegFrameGrabber;
import org.bytedeco.javacv.FFmpegFrameRecorder;
import org.bytedeco.javacv.FrameGrabber;
import org.springframework.stereotype.Service;
import java.io.*;
import java.util.Date;
import java.util.Optional;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
@Slf4j
@Service
@RequiredArgsConstructor
public class VideoService {
private final FfmpegSupportService ffmpegSupportService;
private final WvpProxyConfig wvpProxyConfig;
private final ProxySipConfig proxySipConfig;
private final DockingService dockingService;
private final SipSender sender;
private final FfmpegConfig ffmpegConfig;
private final ReportService reportService;
private final ScheduledExecutorService scheduledExecutorService = Executors.newSingleThreadScheduledExecutor();
/**
* 写入 flv 响应头信息
* @param response HttpServletResponse 响应
*/
public void header(HttpServletResponse response) {
response.setContentType("video/mp4");
response.setHeader("Accept-Ranges", "none");
response.setHeader("Connection", "close");
response.setHeader("Content-Disposition","attachment; filename=\"record.mp4\"");
}
/**
* 录制视频 并写入 响应
* @param request HttpServletRequest 请求
* @param response HttpServletResponse 同步响应
* @param url 要录制的视频地址
* @param time 录制时长 (单位: )
*/
@SneakyThrows
public void record(HttpServletRequest request, HttpServletResponse response, String url, long time) {
AsyncContext asyncContext = request.startAsync();
asyncContext.setTimeout(0);
asyncContext.start(() -> {
header(response);
try{
record(asyncContext.getResponse(), url, time);
} finally {
log.info("record 结束");
asyncContext.complete();
}
});
}
/**
* 录制视频 并写入 异步响应
* @param response AsyncContext.getResponse 异步响应
* @param url 要录制的视频地址
* @param time 录制时长 (单位: )
*/
@SneakyThrows
public void record(ServletResponse response, String url, long time) {
if (wvpProxyConfig.getUseFfmpeg()) {
ffmpegRecord(response, url, time);
} else {
javaCVrecord(response, url, time);
}
}
@RequiredArgsConstructor
public class FfmpegExecuteResultHandler extends DefaultExecuteResultHandler implements ExecuteResultHandler {
private final static long SLEEP_TIME_MS = 50;
@Setter(AccessLevel.PRIVATE)
private boolean hasResult = false;
private final WvpProxyDevice device;
private final String callId;
private final SipSender sender;
private void mediaStatus(){
String deviceId = device.getGbDeviceId();
Optional<WvpProxyDocking> deviceByGbDeviceId = dockingService.getDeviceByGbDeviceId(deviceId);
if(deviceByGbDeviceId.isEmpty()){
return;
}
WvpProxyDocking wvpProxyDocking = deviceByGbDeviceId.get();
String ip = wvpProxyDocking.getIp();
int port = Integer.parseInt(wvpProxyDocking.getPort());
try{
sender.sendRequest((provider,localIp,localPort)->
SipRequestBuilder.createByeRequest(ip, port, device.getGbDeviceChannelId(), SipUtil.generateFromTag(), null, callId));
}catch (Exception e){
log.error("bye 请求发送失败 {}",e.getMessage());
}
}
public boolean hasResult() {
return hasResult;
}
@SneakyThrows
public void waitFor() {
while (!hasResult()) {
Thread.sleep(SLEEP_TIME_MS);
}
}
@Override
public void onProcessComplete(int exitValue) {
hasResult = true;
mediaStatus();
}
@Override
public void onProcessFailed(ExecuteException e) {
hasResult = true;
mediaStatus();
}
}
public FfmpegExecuteResultHandler mediaStatus(WvpProxyDevice device, String key){
return new FfmpegExecuteResultHandler(device,key,sender);
}
/**
* 录制视频 并写入 异步响应
* @param response AsyncContext.getResponse 异步响应
* @param url 要录制的视频地址
* @param time 录制时长 (单位: )
*/
@SneakyThrows
public void javaCVrecord(ServletResponse response, String url, long time) {
ByteArrayOutputStream stream = new ByteArrayOutputStream();
OutputStream outputStream = response.getOutputStream();
log.info("准备录制 url {}, time: {}", url, time);
// FFmpeg 调试日志
// FFmpegLogCallback.set();
FFmpegFrameGrabber grabber = new FFmpegFrameGrabber(url);
grabber.start();
FFmpegFrameRecorder recorder = new FFmpegFrameRecorder(stream, grabber.getImageWidth(), grabber.getImageHeight(), grabber.getAudioChannels());
recorder.setInterleaved(true);
recorder.setVideoOption("preset", "ultrafast");
recorder.setVideoOption("tune", "zerolatency");
recorder.setVideoOption("crf", "25");
recorder.setFrameRate(grabber.getFrameRate());
recorder.setSampleRate(grabber.getSampleRate());
recorder.setOption("flvflags", "no_duration_filesize");
recorder.setOption("movflags","frag_keyframe+empty_moov");
if (grabber.getAudioChannels() > 0) {
recorder.setAudioChannels(grabber.getAudioChannels());
recorder.setAudioBitrate(grabber.getAudioBitrate());
recorder.setAudioCodec(grabber.getAudioCodec());
}
recorder.setVideoBitrate(grabber.getVideoBitrate());
// recorder.setVideoCodec(grabber.getVideoCodec());
recorder.setVideoCodec(avcodec.AV_CODEC_ID_H264);
recorder.setPixelFormat(avutil.AV_PIX_FMT_YUV420P); // 视频源数据yuv
recorder.setAudioCodec(avcodec.AV_CODEC_ID_AAC); // 设置音频压缩方式
recorder.setFormat("mp4");
recorder.setVideoOption("threads", String.valueOf(Runtime.getRuntime().availableProcessors())); // 解码线程数
recorder.start(grabber.getFormatContext());
log.info("开始录制 {}", url);
ScheduledExecutorService scheduledExecutorService = Executors.newSingleThreadScheduledExecutor();
AtomicBoolean record = new AtomicBoolean(true);
scheduledExecutorService.schedule(() -> {
log.info("到达结束时间, 结束录制 {}", url);
record.set(false);
}, time, TimeUnit.SECONDS);
try {
AVPacket k;
while (record.get() && (k = grabber.grabPacket()) != null) {
recorder.recordPacket(k);
if(stream.size() > 0){
outputStream.write(stream.toByteArray());
outputStream.flush();
stream.reset();
}
avcodec.av_packet_unref(k);
}
grabber.close();
recorder.close();
} catch (FFmpegFrameRecorder.Exception | FrameGrabber.Exception e) {
throw new RuntimeException(e);
} catch (IOException ignore){}
finally {
log.info("结束录制 {}", url);
stream.close();
outputStream.close();
}
}
/**
* 录制视频 并写入 异步响应
* @param response AsyncContext.getResponse 异步响应
* @param url 要录制的视频地址
* @param time 录制时长 (单位: )
*/
@SneakyThrows
public void ffmpegRecord(HttpServletRequest request, ServletResponse response, String url, Date startTime, Date endTime, long time, WvpProxyDevice device, String callId){
String tmpDir = ffmpegConfig.getTmpDir();
String fileName = callId + ".mp4";
File file = new File(tmpDir, fileName);
Executor executor;
DefaultExecuteResultHandler executeResultHandler = mediaStatus(device,callId);
if(ffmpegConfig.getUseTmpFile()) {
OutputStream outputStream = new PipedOutputStream();
String filePath = file.getAbsolutePath();
PumpStreamHandler streamHandler = new PumpStreamHandler(outputStream, System.err);
if(proxySipConfig.isUsePlaybackToDownload()){
executor = ffmpegSupportService.playbackToStream(url, filePath, time, TimeUnit.SECONDS,streamHandler,executeResultHandler);
} else {
executor = ffmpegSupportService.downloadToStream(url, filePath, time, TimeUnit.SECONDS,streamHandler,executeResultHandler);
}
} else {
OutputStream outputStream = response.getOutputStream();
PumpStreamHandler streamHandler = new PumpStreamHandler(outputStream, System.err);
if(proxySipConfig.isUsePlaybackToDownload()){
executor = ffmpegSupportService.playbackToStream(url, time, TimeUnit.SECONDS,streamHandler,executeResultHandler);
} else {
executor = ffmpegSupportService.downloadToStream(url, time, TimeUnit.SECONDS,streamHandler,executeResultHandler);
}
}
DateTime start = DateUtil.date();
log.info("开始录制 {}", url);
ScheduledFuture<?> schedule = scheduledExecutorService.schedule(() -> {
log.info("到达结束时间, 结束录制 {}", url);
executor.getWatchdog().destroyProcess();
log.info("结束录制 {}", url);
}, time + 60, TimeUnit.SECONDS);
executeResultHandler.waitFor();
schedule.cancel(true);
DateTime end = DateUtil.date();
log.info("录制进程结束 {}, 录制耗时: {}", url, DateUtil.between(start,end, DateUnit.SECOND));
if(ffmpegConfig.getUseTmpFile()) {
ServletOutputStream servletOutputStream = response.getOutputStream();
try{
log.info("临时文件 {}(大小 {})", file.getAbsolutePath(), file.length());
IoUtil.copy(new FileInputStream(file), servletOutputStream);
response.flushBuffer();
reportService.report(request, device, startTime, endTime, file.length());
} catch (Exception e){
reportService.report(request, device, startTime, endTime, -1);
log.error("写入 http 响应异常: {}", e.getMessage());
} finally {
System.gc();
boolean delete = file.delete();
log.info("删除临时文件 {} => {}", file, delete);
}
}
}
/**
* 录制视频 并写入 异步响应
* @param response AsyncContext.getResponse 异步响应
* @param url 要录制的视频地址
* @param time 录制时长 (单位: )
*/
@SneakyThrows
public void ffmpegRecord(ServletResponse response, String url, long time){
ServletOutputStream outputStream = response.getOutputStream();
ByteArrayOutputStream errorStream = new ByteArrayOutputStream();
PumpStreamHandler streamHandler = new PumpStreamHandler(outputStream, errorStream);
DefaultExecuteResultHandler defaultExecuteResultHandler = new DefaultExecuteResultHandler();
DateTime startTime = DateUtil.date();
Executor executor;
if(proxySipConfig.isUsePlaybackToDownload()){
executor = ffmpegSupportService.playbackToStream(url, time, TimeUnit.SECONDS,streamHandler,defaultExecuteResultHandler);
} else {
executor = ffmpegSupportService.downloadToStream(url, time, TimeUnit.SECONDS,streamHandler,defaultExecuteResultHandler);
}
log.info("开始录制 {}", url);
ScheduledExecutorService scheduledExecutorService = Executors.newSingleThreadScheduledExecutor();
ScheduledFuture<?> schedule = scheduledExecutorService.schedule(() -> {
log.info("到达结束时间, 结束录制 {}", url);
executor.getWatchdog().destroyProcess();
log.info("结束录制 {}", url);
}, time, TimeUnit.SECONDS);
defaultExecuteResultHandler.waitFor();
schedule.cancel(true);
DateTime endTime = DateUtil.date();
log.info("录制进程结束 {}, 录制耗时: {}", url, DateUtil.between(startTime,endTime, DateUnit.SECOND));
}
}

View File

@ -0,0 +1,444 @@
package cn.skcks.docking.gb28181.wvp.service.wvp;
import cn.hutool.cache.CacheUtil;
import cn.hutool.cache.impl.TimedCache;
import cn.hutool.core.date.DateUnit;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.io.IoUtil;
import cn.hutool.crypto.digest.MD5;
import cn.skcks.docking.gb28181.common.json.JsonException;
import cn.skcks.docking.gb28181.common.json.JsonResponse;
import cn.skcks.docking.gb28181.wvp.config.WvpProxyConfig;
import cn.skcks.docking.gb28181.wvp.dto.common.GeneralTimeReq;
import cn.skcks.docking.gb28181.wvp.dto.device.DeviceChannel;
import cn.skcks.docking.gb28181.wvp.dto.device.GetDeviceChannelsReq;
import cn.skcks.docking.gb28181.wvp.dto.device.GetDeviceChannelsResp;
import cn.skcks.docking.gb28181.wvp.dto.download.DownloadStartReq;
import cn.skcks.docking.gb28181.wvp.dto.login.WvpLoginReq;
import cn.skcks.docking.gb28181.wvp.dto.login.WvpLoginResp;
import cn.skcks.docking.gb28181.wvp.dto.media.proxy.AddDownloadTaskReq;
import cn.skcks.docking.gb28181.wvp.dto.media.proxy.GetDownloadTaskReq;
import cn.skcks.docking.gb28181.wvp.dto.media.proxy.GetDownloadTaskResp;
import cn.skcks.docking.gb28181.wvp.dto.record.QueryRecordReq;
import cn.skcks.docking.gb28181.wvp.dto.record.QueryRecordResp;
import cn.skcks.docking.gb28181.wvp.dto.stream.StreamContent;
import cn.skcks.docking.gb28181.wvp.orm.mybatis.dynamic.model.WvpProxyDevice;
import cn.skcks.docking.gb28181.wvp.orm.mybatis.dynamic.model.WvpProxyDocking;
import cn.skcks.docking.gb28181.wvp.proxy.WvpProxyClient;
import cn.skcks.docking.gb28181.wvp.service.device.DeviceService;
import cn.skcks.docking.gb28181.wvp.service.docking.DockingService;
import cn.skcks.docking.gb28181.wvp.service.download.DownloadService;
import cn.skcks.docking.gb28181.wvp.service.report.ReportService;
import cn.skcks.docking.gb28181.wvp.service.video.VideoService;
import cn.skcks.docking.gb28181.wvp.utils.RetryUtil;
import com.github.rholder.retry.*;
import jakarta.servlet.AsyncContext;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import org.springframework.web.context.request.async.DeferredResult;
import java.nio.charset.StandardCharsets;
import java.text.MessageFormat;
import java.util.Date;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicLong;
@Slf4j
@Service
@RequiredArgsConstructor
public class WvpService {
private final WvpProxyClient wvpProxyClient;
private final WvpProxyConfig wvpProxyConfig;
private final DeviceService deviceService;
private final DownloadService downloadService;
private final VideoService videoService;
private final DockingService dockingService;
private final TimedCache<String, String> cache = CacheUtil.newTimedCache(TimeUnit.HOURS.toMillis(1));
private final ScheduledExecutorService scheduledExecutorService = Executors.newSingleThreadScheduledExecutor();
private final ConcurrentMap<String, ScheduledFuture<?>> playing = new ConcurrentHashMap<>();
private final ReportService reportService;
public void header(HttpServletResponse response) {
response.setContentType("video/mp4");
response.setHeader("Accept-Ranges", "none");
response.setHeader("Connection", "close");
// response.setHeader("Content-Disposition","attachment; filename=\"record.mp4\"");
}
@SneakyThrows
private void writeErrorToResponse(HttpServletResponse response, JsonResponse<?> json) {
response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
response.setCharacterEncoding(StandardCharsets.UTF_8.name());
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
IoUtil.writeUtf8(response.getOutputStream(), false, json);
}
@SneakyThrows
public void video(HttpServletRequest request, HttpServletResponse response, String deviceCode, Date startTime, Date endTime) {
Optional<WvpProxyDevice> wvpProxyDeviceList = deviceService.getDeviceByDeviceCode(deviceCode);
if (wvpProxyDeviceList.isEmpty()) {
writeErrorToResponse(response, JsonResponse.error("设备不存在"));
return;
}
WvpProxyDevice wvpProxyDevice = wvpProxyDeviceList.get();
String deviceId = wvpProxyDevice.getGbDeviceId();
String channelId = wvpProxyDevice.getGbDeviceChannelId();
log.info("设备编码 (deviceCode=>{}) 查询到的设备信息 国标id(gbDeviceId => {}), 通道(channelId => {})", deviceCode, deviceId, channelId);
request.startAsync();
AsyncContext asyncContext = request.getAsyncContext();
asyncContext.setTimeout(0);
asyncContext.start(() -> {
HttpServletResponse asyncResponse = (HttpServletResponse) asyncContext.getResponse();
try {
Retryer<JsonResponse<?>> genericRetryer = RetryUtil.getDefaultGenericRetryer("调用 wvp api 查询设备历史");
genericRetryer.call(() -> video(asyncResponse, deviceCode, deviceId, channelId, startTime, endTime));
} catch (RetryException e) {
Attempt<?> failedAttempt = e.getLastFailedAttempt();
String reason;
if (failedAttempt.hasException()) {
reason = MessageFormat.format("调用 wvp api 查询设备: {0} 历史失败, 已重试 {1} 次, 异常: {2}", deviceCode, e.getNumberOfFailedAttempts(), failedAttempt.getExceptionCause().getMessage());
} else {
reason = MessageFormat.format("调用 wvp api 查询设备: {0} 历史失败, 已重试 {1} 次", deviceCode, e.getNumberOfFailedAttempts());
}
log.error(reason);
writeErrorToResponse(asyncResponse, JsonResponse.error(reason));
} catch (Exception e) {
String reason = MessageFormat.format("调用 wvp api 查询设备({0})历史失败, 异常: {1}", deviceCode, e.getMessage());
writeErrorToResponse(asyncResponse, JsonResponse.error(reason));
} finally {
reportService.report(request,wvpProxyDevice, startTime, endTime,-1);
log.info("asyncContext 结束");
asyncContext.complete();
}
});
}
/**
* 执行 视频查询
* <p>返回一个 JsonResponse 类型的执行结果 <p>
* <p>如果返回值为 null code 不为 0 200 则视为执行失败</p>
* <p>如果排除异常 也视为执行失败</p>
*
* @param response 异步响应
* @param deviceCode 设备编码 21位
* @param deviceId 国标设备编码 20位
* @param channelId 通道id
* @param startDateTime 开始时间
* @param endDateTime 结束时间
* @return JsonResponse 类型的执行结果 如果 null code 不为 0 200 则视为执行失败
*/
@SneakyThrows
public JsonResponse<?> video(HttpServletResponse response, String deviceCode, String deviceId, String channelId, Date startDateTime, Date endDateTime) {
String token = login();
log.debug("通过 wvp 查询设备 国标id(gbDeviceId => {}) 通道信息", deviceId);
JsonResponse<GetDeviceChannelsResp> deviceChannels = wvpProxyClient.getDeviceChannels(
token,
deviceId,
GetDeviceChannelsReq.builder().query(channelId).build());
if (deviceChannels.getData() == null || deviceChannels.getData().getTotal() == 0) {
writeErrorToResponse(response, JsonResponse.error(MessageFormat.format("未能获取 设备: {0}, 国标id: {1}, 的通道信息", deviceCode, deviceId)));
return JsonResponse.success(null);
}
List<DeviceChannel> list = deviceChannels.getData().getList();
log.info("通过 wvp 获取到 查询设备 国标id(gbDeviceId => {}), 通道数量 => {}", deviceId, list.size());
DeviceChannel deviceChannel = list.parallelStream().filter(item -> item.getChannelId().equalsIgnoreCase(channelId)).findFirst().orElse(null);
if (deviceChannel == null) {
writeErrorToResponse(response, JsonResponse.error(MessageFormat.format("未查询到 设备: {0}, 国标id: {1}, 通道: {2} 信息", deviceCode, deviceId, channelId)));
return JsonResponse.success(null);
}
String startTime = DateUtil.formatDateTime(startDateTime);
String endTime = DateUtil.formatDateTime(endDateTime);
Retryer<JsonResponse<List<QueryRecordResp.RecordListDTO>>> queryRecordRetryer = RetryUtil.getDefaultRetryer("调用 wvp 设备历史查询 api");
JsonResponse<List<QueryRecordResp.RecordListDTO>> recordListResponse = queryRecordRetryer.call(() -> {
JsonResponse<QueryRecordResp> queryRecord = wvpProxyClient.queryRecord(token, deviceId, channelId, new QueryRecordReq(startTime, endTime));
QueryRecordResp queryRecordData = queryRecord.getData();
if (queryRecordData == null) {
String reason = MessageFormat.format("通过 wvp 查询历史录像 失败 设备: {0}, 国标id: {1}, 通道: {2}, 错误信息: {3}", deviceCode, deviceId, channelId, queryRecord.getMsg());
log.error(reason);
log.error("查询历史录像 返回结果 => {}", queryRecord);
throw new JsonException(reason);
}
List<QueryRecordResp.RecordListDTO> recordList = queryRecordData.getRecordList();
if (CollectionUtils.isEmpty(recordList)) {
String reason = MessageFormat.format("通过 wvp 查询历史录像 失败 设备: {0}, 国标id: {1}, 通道: {2}, 查询时间范围 开始时间: {3}, 结束时间: {4}, 录像数量为 0", deviceCode, deviceId, channelId, startTime, endTime);
throw new JsonException(reason);
}
log.info("通过 wvp 查询到 {} 条历史录像 设备: {}, 国标id: {}, 通道: {}, 开始时间: {}, 结束时间: {}", recordList.size(), deviceCode, deviceId, channelId, startTime, endTime);
return JsonResponse.success(recordList);
});
List<QueryRecordResp.RecordListDTO> recordList = recordListResponse.getData();
recordList.forEach(record -> {
log.debug("{}", record);
});
Boolean useWvpAssist = wvpProxyConfig.getUseWvpAssist();
log.info("准备下载 deviceCode: {}, deviceId: {}, channelId:{}, ({}~{}) 历史视频, 通过 wvp-assist: {}", deviceCode, deviceId, channelId, startTime, endTime, useWvpAssist);
if (useWvpAssist) {
try {
downloadFromWvpAssist(response, token, deviceCode, deviceId, channelId, startTime, endTime);
} catch (Exception e) {
log.warn("尝试通过 wvp-assist 下载视频失败, 尝试通过 视频回放 拉取视频");
downloadFromPlayback(response, token, deviceId, channelId, startTime, endTime);
}
} else {
downloadFromPlayback(response, token, deviceId, channelId, startTime, endTime);
}
return JsonResponse.success(null);
}
private void downloadFromWvpAssist(HttpServletResponse response, String token, String deviceCode, String deviceId, String channelId, String startTime, String endTime) {
JsonResponse<String> videoPathResponse = downloadFromWvpAssist(deviceCode, deviceId, channelId, startTime, endTime, token);
String videoUrl = videoPathResponse.getData();
log.info("设备(deviceCode {}) (deviceId {}, channel{}) ({} ~ {}) 视频下载地址 {}", deviceCode, deviceId, channelId, startTime, endTime, videoUrl);
downloadService.download(response, videoUrl);
}
@SneakyThrows
private void downloadFromPlayback(HttpServletResponse response, String token, String deviceId, String channelId, String startTime, String endTime) {
Retryer<JsonResponse<StreamContent>> playBackRetryer = RetryUtil
.<StreamContent>getDefaultRetryerBuilder("通过回放获取实时视频流下载", 100, TimeUnit.MILLISECONDS, 5)
.build();
JsonResponse<StreamContent> videoStreamResponse = playBackRetryer.call(() -> {
JsonResponse<StreamContent> streamContentJsonResponse = wvpProxyClient.playbackStart(token, deviceId, channelId, new GeneralTimeReq(startTime, endTime));
log.info("实时视频流下载 响应:{} ", streamContentJsonResponse);
return streamContentJsonResponse;
});
StreamContent streamContent = videoStreamResponse.getData();
String stream = streamContent.getStream();
String streamUrl = streamContent.getFlv();
try {
header(response);
videoService.record(response, streamUrl, DateUtil.between(DateUtil.parseDateTime(startTime), DateUtil.parseDateTime(endTime), DateUnit.SECOND));
} finally {
wvpProxyClient.playbackStop(token, deviceId, channelId, stream);
}
}
@SuppressWarnings("UnstableApiUsage")
@SneakyThrows
private JsonResponse<String> downloadFromWvpAssist(String deviceCode, String deviceId, String channelId, String startTime, String endTime, String token) {
Retryer<JsonResponse<String>> downloadRetryer = RetryUtil.getDefaultRetryer("调用 wvp 设备历史视频下载 api");
return downloadRetryer.call(() -> {
JsonResponse<StreamContent> downloadStart = wvpProxyClient.downloadStart(token, deviceId, channelId, DownloadStartReq.builder()
.startTime(startTime)
.endTime(endTime)
.build());
if (downloadStart.getData() == null) {
throw new JsonException(downloadStart.getMsg());
}
StreamContent downloadStartData = downloadStart.getData();
String mediaServerId = downloadStartData.getMediaServerId();
String app = downloadStartData.getApp();
String stream = downloadStartData.getStream();
log.info("开始下载 mediaServerId: {}, app: {}, stream: {}", mediaServerId, app, stream);
AtomicLong keepTime = new AtomicLong();
Double downloadProgress = 0D;
Retryer<JsonResponse<StreamContent>> downloadProgressRetryer = RetryUtil.<StreamContent>getDefaultRetryerBuilder(MessageFormat.format("查询设备(deviceCode {0}) (deviceId {1}, channel{2}) ({3} ~ {4}) 历史视频下载进度", deviceCode, deviceId, channelId, startTime, endTime))
// 重试间隔
.withWaitStrategy(WaitStrategies.fixedWait(1, TimeUnit.SECONDS))
.retryIfResult(result -> {
keepTime.getAndIncrement();
if (keepTime.get() > 60) {
return false;
}
if (result == null || result.getData() == null) {
return true;
}
if (!result.getData().getProgress().equals(downloadProgress)) {
keepTime.set(0);
return true;
}
return result.getData().getProgress().equals(downloadProgress) && keepTime.get() <= 60;
})
.withRetryListener(new RetryListener() {
@Override
public <V> void onRetry(Attempt<V> attempt) {
if (attempt.hasResult()) {
log.debug("第 {} 次 获取 设备(deviceCode {}) (deviceId {}, channel{}) ({} ~ {}) 历史视频下载进度 mediaServerId: {}, app: {}, stream: {}", attempt.getAttemptNumber(), deviceCode, deviceId, channelId, startTime, endTime, mediaServerId, app, stream);
} else {
log.debug("第 {} 次 获取 设备(deviceCode {}) (deviceId {}, channel{}) ({} ~ {}) 历史视频下载进度失败 mediaServerId: {}, app: {}, stream: {}", attempt.getAttemptNumber(), deviceCode, deviceId, channelId, startTime, endTime, mediaServerId, app, stream);
}
}
})
.withStopStrategy(StopStrategies.neverStop())
.build();
downloadProgressRetryer.call(() -> {
JsonResponse<StreamContent> downloadProgressResp = wvpProxyClient.downloadProgress(token, deviceId, channelId, stream);
StreamContent data = downloadProgressResp.getData();
log.debug("设备(deviceCode {}) (deviceId {}, channel{}) ({} ~ {}) 历史视频下载进度 {}% mediaServerId: {}, app: {}, stream: {}", deviceCode, deviceId, channelId, startTime, endTime, data.getProgress(), mediaServerId, app, stream);
return downloadProgressResp;
});
wvpProxyClient.downloadStop(token, deviceId, channelId, stream);
JsonResponse<String> addDownloadTask2MediaServer = wvpProxyClient.addDownloadTask2MediaServer(token, mediaServerId, new AddDownloadTaskReq(app, stream));
String taskId = addDownloadTask2MediaServer.getData();
Retryer<JsonResponse<String>> mediaServerRetryer = RetryUtil.<String>getDefaultRetryerBuilder("从 mediaServer 获取视频")
// 重试间隔
.withWaitStrategy(WaitStrategies.fixedWait(RetryUtil.DEFAULT_RETRY_INTERVAL, TimeUnit.SECONDS))
// 重试次数
.withStopStrategy(StopStrategies.stopAfterAttempt(RetryUtil.DEFAULT_RETRY_TIME))
.retryIfResult(RetryUtil.defaultRetryIf())
.withRetryListener(RetryUtil.defaultRetryListener("从 mediaServer 获取视频"))
.withStopStrategy(StopStrategies.stopAfterAttempt(10))
.build();
return mediaServerRetryer.call(() -> {
JsonResponse<List<GetDownloadTaskResp>> downloadTask4MediaServer = wvpProxyClient.getDownloadTask4MediaServer(token, mediaServerId, new GetDownloadTaskReq(app, stream, taskId, true));
if (downloadTask4MediaServer.getData().size() > 0) {
return JsonResponse.success(downloadTask4MediaServer.getData().get(0).getPlayFile());
} else {
return JsonResponse.error(null);
}
});
});
}
@SneakyThrows
public Optional<DeviceChannel> getDeviceChannelByDeviceCode(String token, String deviceCode) {
WvpProxyDevice device = deviceService.getDeviceByDeviceCode(deviceCode).orElse(null);
if (device == null) {
throw new JsonException("设备不存在");
}
String channelId = device.getGbDeviceChannelId();
WvpProxyDocking docking = dockingService.getDeviceByDeviceCode(device.getGbDeviceId()).orElse(null);
if (docking == null) {
throw new JsonException("设备(通道)不存在");
}
Optional<DeviceChannel> deviceChannel = Optional.empty();
for (String deviceId : wvpProxyConfig.getParents()) {
log.debug("通过 wvp 查询设备 国标id(gbDeviceId => {}) 通道信息", deviceId);
JsonResponse<GetDeviceChannelsResp> deviceChannels = wvpProxyClient.getDeviceChannels(
token,
deviceId,
GetDeviceChannelsReq.builder()
.query(channelId)
.count(Integer.MAX_VALUE)
.build());
if (deviceChannels.getData() != null && deviceChannels.getData().getTotal() > 0) {
deviceChannel = deviceChannels.getData()
.getList()
.parallelStream()
.filter(item -> item.getChannelId().equalsIgnoreCase(channelId))
.findFirst();
if (deviceChannel.isPresent()) {
break;
}
}
}
return deviceChannel;
}
@SneakyThrows
@SuppressWarnings("UnstableApiUsage")
public DeferredResult<JsonResponse<String>> realtimeVideoUrl(String deviceCode) {
DeferredResult<JsonResponse<String>> result = new DeferredResult<>(TimeUnit.SECONDS.toMillis(60));
result.onTimeout(() -> {
log.error("timeout");
result.setResult(JsonResponse.error("请求超时"));
});
ScheduledFuture<?> schedule = playing.get(deviceCode);
if (schedule != null) {
schedule.cancel(true);
}
final String[] token = {login()};
Retryer<Optional<DeviceChannel>> defaultGenericRetryer = RetryerBuilder.<Optional<DeviceChannel>>newBuilder()
// 异常就重试
.retryIfException()
.retryIfRuntimeException()
// 重试间隔
.withWaitStrategy(WaitStrategies.fixedWait(3, TimeUnit.SECONDS))
// 重试次数
.withStopStrategy(StopStrategies.stopAfterAttempt(3))
.withRetryListener(new RetryListener() {
@Override
public <V> void onRetry(Attempt<V> attempt) {
if (attempt.hasException()) {
log.info("异常 {}", attempt.getExceptionCause().getMessage());
cache.remove("token");
token[0] = login();
}
}
}).build();
Optional<DeviceChannel> deviceChannel = defaultGenericRetryer.call(() -> getDeviceChannelByDeviceCode(token[0], deviceCode));
if (deviceChannel.isEmpty()) {
result.setResult(JsonResponse.error(MessageFormat.format("未能获取 设备: {0} 的通道信息", deviceCode)));
return result;
}
DeviceChannel dc = deviceChannel.get();
log.info("设备通道信息 => {}", dc);
try {
StreamContent data = wvpProxyClient.playStart(token[0], dc.getDeviceId(), dc.getChannelId(), false).getData();
log.info("实时流信息 {}", data);
String url = StringUtils.joinWith("/",
wvpProxyConfig.getProxyMediaUrl(), data.getApp(),
StringUtils.joinWith(".", data.getStream(), "live", "flv"));
// 定时任务
schedule = scheduledExecutorService.schedule(() -> {
log.info("[定时任务] 关闭设备(deviceCode => {}) 视频", deviceCode);
playing.remove(deviceCode);
closeRealtimeVideo(deviceCode);
}, wvpProxyConfig.getRealtimeVideoDuration().getSeconds(), TimeUnit.SECONDS);
playing.put(deviceCode, schedule);
result.setResult(JsonResponse.success(url));
} catch (Exception e) {
log.error("点播失败", e);
result.setResult(JsonResponse.error("点播失败"));
return result;
}
return result;
}
public void closeRealtimeVideo(String deviceCode) {
String token = login();
Optional<DeviceChannel> deviceChannel = getDeviceChannelByDeviceCode(token, deviceCode);
if (deviceChannel.isEmpty()) {
return;
}
DeviceChannel dc = deviceChannel.get();
log.info("设备通道信息 => {}", dc);
wvpProxyClient.playStop(token, dc.getDeviceId(), dc.getChannelId(), false);
}
private String login() {
return cache.get("token", false, () -> {
String passwdMd5 = MD5.create().digestHex(wvpProxyConfig.getPasswd());
WvpLoginReq loginReq = WvpLoginReq.builder()
.username(wvpProxyConfig.getUser())
.password(passwdMd5)
.build();
JsonResponse<WvpLoginResp> login = wvpProxyClient.login(loginReq);
String token = login.getData().getAccessToken();
log.info("wvp 登录成功 token => {}", token);
return token;
});
}
}

View File

@ -0,0 +1,55 @@
package cn.skcks.docking.gb28181.wvp.sip;
import cn.skcks.docking.gb28181.common.json.JsonUtils;
import cn.skcks.docking.gb28181.core.sip.service.SipService;
import cn.skcks.docking.gb28181.wvp.config.ProxySipConfig;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.SmartLifecycle;
import org.springframework.context.annotation.DependsOn;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
@Order(0)
@Slf4j
@RequiredArgsConstructor
@Component
@DependsOn("wvpProxyOrmInitService")
public class SipStarter implements SmartLifecycle {
private final ProxySipConfig proxySipConfig;
private final SipService sipService;
private boolean isRunning;
@Override
public void start() {
if(checkConfig()){
isRunning = true;
log.debug("sip 服务 启动");
sipService.run();
}
}
@Override
public void stop() {
log.debug("sip 服务 关闭");
sipService.stop();
isRunning = false;
}
@Override
public boolean isRunning() {
return isRunning;
}
public boolean checkConfig(){
log.debug("sip 配置信息 => \n{}", JsonUtils.toJson(proxySipConfig));
if(CollectionUtils.isEmpty(proxySipConfig.getIp())){
log.error("sip ip 配置错误, 请检查配置是否正确");
return false;
}
return true;
}
}

View File

@ -0,0 +1,106 @@
package cn.skcks.docking.gb28181.wvp.sip.listener;
import cn.skcks.docking.gb28181.core.sip.executor.DefaultSipExecutor;
import cn.skcks.docking.gb28181.core.sip.listener.SipListener;
import cn.skcks.docking.gb28181.core.sip.message.processor.MessageProcessor;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
import javax.sip.*;
import javax.sip.header.CSeqHeader;
import javax.sip.header.CallIdHeader;
import javax.sip.message.Request;
import javax.sip.message.Response;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
@RequiredArgsConstructor
@Component
@Slf4j
public class SipListenerImpl implements SipListener {
private final ConcurrentMap<String, MessageProcessor> requestProcessor = new ConcurrentHashMap<>();
private final ConcurrentMap<String, MessageProcessor> responseProcessor = new ConcurrentHashMap<>();
public void addRequestProcessor(String method, MessageProcessor messageProcessor) {
log.debug("[SipListener] 注册 {} 请求处理器", method);
requestProcessor.put(method, messageProcessor);
}
public void addResponseProcessor(String method, MessageProcessor messageProcessor) {
log.debug("[SipListener] 注册 {} 响应处理器", method);
responseProcessor.put(method, messageProcessor);
}
@Override
@Async(DefaultSipExecutor.EXECUTOR_BEAN_NAME)
public void processRequest(RequestEvent requestEvent) {
String method = requestEvent.getRequest().getMethod();
log.debug("传入请求 method => {}", method);
Optional.ofNullable(requestProcessor.get(method)).ifPresent(processor -> {
processor.process(requestEvent);
});
}
@Override
@Async(DefaultSipExecutor.EXECUTOR_BEAN_NAME)
public void processResponse(ResponseEvent responseEvent) {
Response response = responseEvent.getResponse();
int status = response.getStatusCode();
CSeqHeader cseqHeader = (CSeqHeader) response.getHeader(CSeqHeader.NAME);
String method = cseqHeader.getMethod();
log.debug("{} {}", method, response);
// Success
if (((status >= Response.OK) && (status < Response.MULTIPLE_CHOICES)) || status == Response.UNAUTHORIZED) {
log.debug("传入响应 method => {}", method);
Optional.ofNullable(responseProcessor.get(method)).ifPresent(processor -> {
processor.process(responseEvent);
});
} else if ((status >= Response.TRYING) && (status < Response.OK)) {
// 增加其它无需回复的响应如101180等
Optional.ofNullable(responseProcessor.get(method)).ifPresent(processor -> {
processor.process(responseEvent);
});
} else {
log.warn("接收到失败的response响应status" + status + ",message:" + response.getReasonPhrase());
if (responseEvent.getDialog() != null) {
responseEvent.getDialog().delete();
}
}
}
@Override
public void processTimeout(TimeoutEvent timeoutEvent) {
ClientTransaction clientTransaction = timeoutEvent.getClientTransaction();
if (clientTransaction != null) {
Request request = clientTransaction.getRequest();
if (request != null) {
CallIdHeader callIdHeader = (CallIdHeader) request.getHeader(CallIdHeader.NAME);
if (callIdHeader != null) {
log.debug("会话超时 callId => {}", callIdHeader.getCallId());
}
}
}
}
@Override
public void processIOException(IOExceptionEvent exceptionEvent) {
}
@Override
public void processTransactionTerminated(TransactionTerminatedEvent transactionTerminatedEvent) {
}
@Override
public void processDialogTerminated(DialogTerminatedEvent dialogTerminatedEvent) {
CallIdHeader callIdHeader = dialogTerminatedEvent.getDialog().getCallId();
log.debug("会话终止 callId => {}", callIdHeader.getCallId());
}
}

View File

@ -0,0 +1,58 @@
package cn.skcks.docking.gb28181.wvp.sip.message.bye.request.request;
import cn.skcks.docking.gb28181.common.redis.RedisUtil;
import cn.skcks.docking.gb28181.core.sip.gb28181.cache.CacheUtil;
import cn.skcks.docking.gb28181.core.sip.listener.SipListener;
import cn.skcks.docking.gb28181.core.sip.message.processor.MessageProcessor;
import cn.skcks.docking.gb28181.core.sip.message.subscribe.GenericSubscribe;
import cn.skcks.docking.gb28181.sdp.GB28181SDPBuilder;
import cn.skcks.docking.gb28181.wvp.sip.response.SipResponseBuilder;
import cn.skcks.docking.gb28181.wvp.sip.sender.SipSender;
import cn.skcks.docking.gb28181.wvp.sip.subscribe.SipSubscribe;
import gov.nist.javax.sip.message.SIPRequest;
import jakarta.annotation.PostConstruct;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import javax.sip.RequestEvent;
import javax.sip.message.Request;
import javax.sip.message.Response;
import java.util.EventObject;
import java.util.Optional;
@Slf4j
@RequiredArgsConstructor
@Component
public class ByeRequestProcessor implements MessageProcessor {
private final SipListener sipListener;
private final SipSubscribe subscribe;
private final SipSender sender;
@PostConstruct
@Override
public void init() {
sipListener.addRequestProcessor(Request.BYE, this);
}
@Override
public void process(EventObject eventObject) {
RequestEvent requestEvent = (RequestEvent) eventObject;
SIPRequest request = (SIPRequest) requestEvent.getRequest();
String callId = request.getCallId().getCallId();
String key = GenericSubscribe.Helper.getKey(Request.BYE, callId);
log.info("key {}", key);
String ip = request.getLocalAddress().getHostAddress();
String transport = request.getTopmostViaHeader().getTransport();
Optional.ofNullable(subscribe.getByeSubscribe().getPublisher(key))
.ifPresentOrElse(
publisher -> publisher.submit(request),
() -> {
RedisUtil.KeyOps.delete(CacheUtil.getKey(GB28181SDPBuilder.Action.PLAY.getAction(), callId));
sender.sendResponse(ip, transport, ((provider, ip1, port) ->
SipResponseBuilder.response(request, Response.OK, "OK")));
});
}
}

View File

@ -0,0 +1,41 @@
package cn.skcks.docking.gb28181.wvp.sip.message.invite.response;
import cn.skcks.docking.gb28181.core.sip.listener.SipListener;
import cn.skcks.docking.gb28181.core.sip.message.processor.MessageProcessor;
import cn.skcks.docking.gb28181.core.sip.message.subscribe.GenericSubscribe;
import cn.skcks.docking.gb28181.wvp.sip.subscribe.SipSubscribe;
import gov.nist.javax.sip.message.SIPResponse;
import jakarta.annotation.PostConstruct;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import javax.sip.ResponseEvent;
import javax.sip.message.Request;
import java.util.EventObject;
import java.util.Optional;
@Slf4j
@RequiredArgsConstructor
@Component
public class InviteResponseProcessor implements MessageProcessor {
private final SipListener sipListener;
private final SipSubscribe subscribe;
@PostConstruct
@Override
public void init() {
sipListener.addResponseProcessor(Request.INVITE, this);
}
@Override
public void process(EventObject eventObject) {
ResponseEvent requestEvent = (ResponseEvent) eventObject;
SIPResponse response = (SIPResponse) requestEvent.getResponse();
String callId = response.getCallId().getCallId();
String subscribeKey = GenericSubscribe.Helper.getKey(Request.INVITE, callId);
log.info("收到 INVITE 响应, key {}", subscribeKey);
Optional.ofNullable(subscribe.getInviteSubscribe().getPublisher(subscribeKey))
.ifPresent(publisher->publisher.submit(response));
}
}

View File

@ -0,0 +1,22 @@
package cn.skcks.docking.gb28181.wvp.sip.message.message.catalog.dto;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
@JacksonXmlRootElement(localName = "DeviceList")
@AllArgsConstructor
@NoArgsConstructor
@Data
public class CatalogDeviceListDTO {
@JacksonXmlProperty(isAttribute = true)
private Integer num;
@JacksonXmlProperty(localName = "Item")
@JacksonXmlElementWrapper(useWrapping = false)
private List<CatalogItemDTO> deviceList;
}

View File

@ -0,0 +1,151 @@
package cn.skcks.docking.gb28181.wvp.sip.message.message.catalog.dto;
import cn.hutool.core.date.DatePattern;
import cn.skcks.docking.gb28181.core.sip.gb28181.constant.GB28181Constant;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.Date;
@JacksonXmlRootElement(localName = "Item")
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Data
public class CatalogItemDTO {
/**
* 设备/区域/系统编码(必选)
*/
@JacksonXmlProperty(localName = "DeviceID")
private String deviceId;
/**
* 设备/区域/系统名称(必选)
*/
private String name;
/**
* 当为设备时,设备厂商(必选)
*/
private String manufacturer;
/**
* 当为设备时,设备型号(必选)
*/
private String model;
/**
* 当为设备时,设备归属(必选)
*/
private String owner;
/**
* 行政区域(必选)
*/
@JacksonXmlProperty(localName = "CivilCode")
private String civilCode;
/**
* 警区(可选)
*/
private String block;
/**
* 当为设备时,安装地址(必选)
*/
private String address;
/**
* 当为设备时,是否有子设备(必选)1有, 0没有
*/
@Builder.Default
private Integer parental = 0;
/**
* 父设备/区域/系统ID(必选)
*/
@JacksonXmlProperty(localName = "ParentID")
private String parentId;
/**
* 信令安全模式(可选)缺省为0; 0:不采用;2:S/MIME 签名方式;3:S/ MIME加密签名同时采用方式;4:数字摘要方式
*/
@Builder.Default
private Integer safetyWay = 0;
/**
* 注册方式(必选)缺省为1;1:符合IETF RFC3261标准的认证注册模 ;2:基于口令的双向认证注册模式;3:基于数字证书的双向认证注册模式
*/
@Builder.Default
private Integer registerWay = 1;
/**
* 证书序列号(有证书的设备必选)
*/
private String certNum;
/**
* 证书有效标识(有证书的设备必选)缺省为0;证书有效标识:0:无效 1: 有效
*/
@Builder.Default
private Integer certifiable = 0;
/**
* 无效原因码(有证书且证书无效的设备必选)
*/
@Builder.Default
private Integer errCode = 0;
/**
* 证书终止有效期(有证书的设备必选)
*/
@JsonFormat(pattern = DatePattern.UTC_SIMPLE_PATTERN, timezone = GB28181Constant.TIME_ZONE)
private Date endTime;
/**
* 保密属性(必选)缺省为0;0:不涉密,1:涉密
*/
@Builder.Default
private Integer secrecy = 0;
/**
* 设备/区域/系统IP地址(可选)
*/
@JacksonXmlProperty(localName = "IPAddress")
private String ipAddress;
/**
* 设备/区域/系统端口(可选)
*/
private Integer port;
/**
* 设备口令(可选)
*/
private String password;
/**
* 设备状态(必选)
*/
@Builder.Default
private String status = "ON";
/**
* 经度(可选)
*/
@Builder.Default
private String longitude = "0.0";
/**
* 纬度(可选)
*/
@Builder.Default
private String latitude = "0.0";
}

View File

@ -0,0 +1,27 @@
package cn.skcks.docking.gb28181.wvp.sip.message.message.catalog.dto;
import cn.skcks.docking.gb28181.core.sip.gb28181.constant.CmdType;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@JacksonXmlRootElement(localName = "Query")
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Data
public class CatalogRequestDTO {
@Builder.Default
private String cmdType = CmdType.CATALOG;
@JacksonXmlProperty(localName = "SN")
private String sn;
/**
* 目标设备的设备编码(必选)
*/
@JacksonXmlProperty(localName = "DeviceID")
private String deviceId;
}

View File

@ -0,0 +1,31 @@
package cn.skcks.docking.gb28181.wvp.sip.message.message.catalog.dto;
import cn.skcks.docking.gb28181.core.sip.gb28181.constant.CmdType;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@JacksonXmlRootElement(localName = "Response")
@AllArgsConstructor
@NoArgsConstructor
@Builder
@Data
public class CatalogResponseDTO {
@Builder.Default
private String cmdType = CmdType.CATALOG;
@JacksonXmlProperty(localName = "SN")
private String sn;
/**
* 目标设备的设备编码(必选)
*/
@JacksonXmlProperty(localName = "DeviceID")
private String deviceId;
private Long sumNum;
private CatalogDeviceListDTO deviceList;
}

View File

@ -0,0 +1,77 @@
package cn.skcks.docking.gb28181.wvp.sip.message.message.device.control;
import cn.skcks.docking.gb28181.core.sip.gb28181.constant.CmdType;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@JacksonXmlRootElement(localName = "Control")
@JsonInclude(JsonInclude.Include.NON_EMPTY)
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Data
public class DeviceControlDTO {
@Builder.Default
private String cmdType = CmdType.DEVICE_CONTROL;
@JacksonXmlProperty(localName = "SN")
private String sn;
/**
* 目标设备的设备编码(必选)
*/
@JacksonXmlProperty(localName = "DeviceID")
private String deviceId;
/**
* 录像控制命令
*/
private String recordCmd;
/**
* 云台控制命令
*/
@JacksonXmlProperty(localName = "PTZCmd")
private String ptzCmd;
/**
* 远程启动
*/
private String teleBoot;
/**
* 布防撤防
*/
private String guardCmd;
/**
* 告警控制
*/
private String alarmCmd;
/**
* 强制关键帧
*/
@JacksonXmlProperty(localName = "IFameCmd")
private String iFameCmd;
/**
* 拉框放大
*/
private String dragZoomIn;
/**
* 拉框缩小
*/
private String dragZoomOut;
/**
* 看守位
*/
private String homePosition;
}

View File

@ -0,0 +1,30 @@
package cn.skcks.docking.gb28181.wvp.sip.message.message.notify;
import cn.skcks.docking.gb28181.core.sip.gb28181.constant.CmdType;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@JacksonXmlRootElement(localName = "Notify")
@AllArgsConstructor
@NoArgsConstructor
@Builder
@Data
public class MediaStatusRequestDTO {
@Builder.Default
private String cmdType = CmdType.MEDIA_STATUS;
@JacksonXmlProperty(localName = "SN")
private String sn;
/**
* 目标设备的设备编码(必选)
*/
@JacksonXmlProperty(localName = "DeviceID")
private String deviceId;
@JacksonXmlProperty(localName = "NotifyType")
private String notifyType = "121";
}

View File

@ -0,0 +1,135 @@
package cn.skcks.docking.gb28181.wvp.sip.message.message.processor.request;
import cn.skcks.docking.gb28181.common.json.ResponseStatus;
import cn.skcks.docking.gb28181.common.xml.XmlUtils;
import cn.skcks.docking.gb28181.core.sip.gb28181.constant.CmdType;
import cn.skcks.docking.gb28181.core.sip.gb28181.constant.GB28181Constant;
import cn.skcks.docking.gb28181.core.sip.listener.SipListener;
import cn.skcks.docking.gb28181.core.sip.message.processor.MessageProcessor;
import cn.skcks.docking.gb28181.core.sip.message.processor.message.request.dto.MessageDTO;
import cn.skcks.docking.gb28181.core.sip.message.sender.SipMessageSender;
import cn.skcks.docking.gb28181.core.sip.message.subscribe.GenericSubscribe;
import cn.skcks.docking.gb28181.core.sip.utils.SipUtil;
import cn.skcks.docking.gb28181.sip.manscdp.recordinfo.response.RecordInfoResponseDTO;
import cn.skcks.docking.gb28181.wvp.service.docking.DockingService;
import cn.skcks.docking.gb28181.wvp.sip.message.message.catalog.dto.CatalogResponseDTO;
import cn.skcks.docking.gb28181.wvp.sip.message.message.notify.MediaStatusRequestDTO;
import cn.skcks.docking.gb28181.wvp.sip.request.SipRequestBuilder;
import cn.skcks.docking.gb28181.wvp.sip.response.SipResponseBuilder;
import cn.skcks.docking.gb28181.wvp.sip.sender.SipSender;
import cn.skcks.docking.gb28181.wvp.sip.subscribe.SipSubscribe;
import gov.nist.javax.sip.message.SIPRequest;
import gov.nist.javax.sip.message.SIPResponse;
import jakarta.annotation.PostConstruct;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Component;
import javax.sip.RequestEvent;
import javax.sip.address.SipURI;
import javax.sip.header.CallIdHeader;
import javax.sip.message.Request;
import javax.sip.message.Response;
import java.util.EventObject;
import java.util.Optional;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
@Slf4j
@RequiredArgsConstructor
@Component
public class MessageRequestProcessor implements MessageProcessor {
private final SipListener sipListener;
private final SipMessageSender sender;
private final SipSender sipSender;
private final SipSubscribe subscribe;
private final DockingService dockingService;
private final ScheduledExecutorService scheduledExecutorService = Executors.newSingleThreadScheduledExecutor();
@PostConstruct
@Override
public void init() {
sipListener.addRequestProcessor(Request.MESSAGE,this);
}
@Override
public void process(EventObject eventObject) {
RequestEvent requestEvent = (RequestEvent) eventObject;
SIPRequest request = (SIPRequest)requestEvent.getRequest();
String deviceId = SipUtil.getUserIdFromFromHeader(request);
CallIdHeader callIdHeader = request.getCallIdHeader();
byte[] content = request.getRawContent();
MessageDTO messageDto = XmlUtils.parse(content, MessageDTO.class, GB28181Constant.CHARSET);
log.debug("接收到的消息 => {}", messageDto);
String senderIp = request.getLocalAddress().getHostAddress();
if(!dockingService.hasDeviceByDeviceCode(deviceId)){
log.info("未找到相关设备信息 => {}", deviceId);
Response response = response(request,Response.NOT_FOUND,"设备未注册");
sender.send(senderIp,response);
return;
}
Response ok = response(request, Response.OK, "OK");
Response response;
if(StringUtils.equalsAnyIgnoreCase(messageDto.getCmdType(), CmdType.KEEPALIVE)){
response = ok;
// 更新设备在线状态
} else if(messageDto.getCmdType().equalsIgnoreCase(cn.skcks.docking.gb28181.constant.CmdType.RECORD_INFO)) {
response = ok;
RecordInfoResponseDTO dto = XmlUtils.parse(content, RecordInfoResponseDTO.class, GB28181Constant.CHARSET);
String key = GenericSubscribe.Helper.getKey(cn.skcks.docking.gb28181.constant.CmdType.RECORD_INFO, dto.getDeviceId(), dto.getSn());
Optional.ofNullable(subscribe.getMessageSubscribe().getPublisher(key))
.ifPresentOrElse(publisher -> publisher.submit(request),
() -> log.warn("对应订阅 {} 已结束, 异常数据 => {}", key, dto));
} else if(messageDto.getCmdType().equalsIgnoreCase(CmdType.CATALOG)){
response = ok;
CatalogResponseDTO dto = XmlUtils.parse(content, CatalogResponseDTO.class, GB28181Constant.CHARSET);
String key = GenericSubscribe.Helper.getKey(CmdType.CATALOG, dto.getDeviceId());
Optional.ofNullable(subscribe.getCatalogSubscribe().getPublisher(key))
.ifPresentOrElse(publisher-> publisher.submit(request),
()-> log.warn("对应订阅 {} 已结束, 异常数据 => {}",key, dto));
} else if(messageDto.getCmdType().equalsIgnoreCase(CmdType.MEDIA_STATUS)){
response = ok;
MediaStatusRequestDTO parse = XmlUtils.parse(content, MediaStatusRequestDTO.class, GB28181Constant.CHARSET);
if(StringUtils.equalsIgnoreCase(parse.getNotifyType(),"121")){
String ip = request.getLocalAddress().getHostAddress();
String transport = request.getTopmostViaHeader().getTransport();
scheduledExecutorService.schedule(()->{
sipSender.sendResponse(ip, transport, ((provider, ip1, port) ->
SipResponseBuilder.response(request, Response.OK, "OK")));
String targetIp = request.getRemoteAddress().getHostAddress();
int targetPort = request.getRemotePort();
String targetId = ((SipURI)request.getFromHeader().getAddress().getURI()).getUser();
String callId = request.getCallIdHeader().getCallId();
Request byeRequest = SipRequestBuilder.createByeRequest(targetIp, targetPort, targetId, SipUtil.generateFromTag(), null, callId);
sipSender.sendRequest(((provider, ip1, port) -> byeRequest));
},100, TimeUnit.MILLISECONDS);
}
}else {
response = response(request, Response.NOT_IMPLEMENTED, ResponseStatus.NOT_IMPLEMENTED.getMessage());
}
sender.send(senderIp, response);
}
@SneakyThrows
public Response response(SIPRequest request, int status, String message){
if (request.getToHeader().getTag() == null) {
request.getToHeader().setTag(SipUtil.generateTag());
}
SIPResponse response = (SIPResponse)getMessageFactory().createResponse(status, request);
if (message != null) {
response.setReasonPhrase(message);
}
return response;
}
}

View File

@ -0,0 +1,145 @@
package cn.skcks.docking.gb28181.wvp.sip.message.register.request;
import cn.skcks.docking.gb28181.config.sip.SipConfig;
import cn.skcks.docking.gb28181.core.sip.dto.RemoteInfo;
import cn.skcks.docking.gb28181.core.sip.gb28181.sip.GbSipDate;
import cn.skcks.docking.gb28181.core.sip.listener.SipListener;
import cn.skcks.docking.gb28181.core.sip.message.auth.DigestServerAuthenticationHelper;
import cn.skcks.docking.gb28181.core.sip.message.processor.MessageProcessor;
import cn.skcks.docking.gb28181.core.sip.message.sender.SipMessageSender;
import cn.skcks.docking.gb28181.core.sip.utils.SipUtil;
import cn.skcks.docking.gb28181.wvp.orm.mybatis.dynamic.model.WvpProxyDocking;
import cn.skcks.docking.gb28181.wvp.service.docking.DockingService;
import gov.nist.javax.sip.address.SipUri;
import gov.nist.javax.sip.header.Authorization;
import gov.nist.javax.sip.header.SIPDateHeader;
import gov.nist.javax.sip.message.SIPRequest;
import jakarta.annotation.PostConstruct;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Component;
import javax.sip.RequestEvent;
import javax.sip.address.Address;
import javax.sip.header.ExpiresHeader;
import javax.sip.header.FromHeader;
import javax.sip.header.ViaHeader;
import javax.sip.message.Request;
import javax.sip.message.Response;
import java.util.Calendar;
import java.util.EventObject;
import java.util.Locale;
@Slf4j
@RequiredArgsConstructor
@Component
public class RegisterRequestProcessor implements MessageProcessor {
private final SipListener sipListener;
private final SipMessageSender sender;
private final SipConfig sipConfig;
private final DockingService dockingService;
@PostConstruct
@Override
public void init(){
sipListener.addRequestProcessor(Method.REGISTER,this);
}
@SneakyThrows
@Override
public void process(EventObject eventObject) {
RequestEvent requestEvent = (RequestEvent) eventObject;
SIPRequest request = (SIPRequest)requestEvent.getRequest();
FromHeader fromHeader = request.getFrom();
Address address = fromHeader.getAddress();
log.debug("From {}",address);
SipUri uri = (SipUri)address.getURI();
String deviceId = uri.getUser();
log.debug("请求注册 设备id => {}", deviceId);
Boolean hasDevice = dockingService.hasDeviceByDeviceCode(deviceId);
String senderIp = request.getLocalAddress().getHostAddress();
RemoteInfo remoteInfo = SipUtil.getRemoteInfoFromRequest(request, false);
log.debug("远程连接信息 => {}", remoteInfo);
if(!hasDevice){
log.info("新注册的设备 deviceId => {}", deviceId);
}
String password = sipConfig.getPassword();
Authorization authorization = request.getAuthorization();
if(authorization == null && StringUtils.isNotBlank(password)){
Response response = getMessageFactory().createResponse(Response.UNAUTHORIZED, request);
DigestServerAuthenticationHelper.generateChallenge(getHeaderFactory(),response,sipConfig.getDomain());
sender.send(senderIp,response);
return;
}
log.debug("认证信息 => {}", authorization);
boolean authPass = StringUtils.isBlank(password) ||
DigestServerAuthenticationHelper.doAuthenticatePlainTextPassword(request,password);
if(!authPass){
Response response = getMessageFactory().createResponse(Response.FORBIDDEN, request);
response.setReasonPhrase("Forbidden");
log.info("设备注册信息认证失败 deviceId => {}", deviceId);
sender.send(senderIp,response);
return;
}
log.debug("设备 deviceId => {}, 认证通过", deviceId);
registerDevice(deviceId, request, senderIp, remoteInfo);
}
@SneakyThrows
private Response generateRegisterResponse(Request request){
SIPRequest sipRequest = (SIPRequest) request;
ExpiresHeader expires = sipRequest.getExpires();
if(expires == null){
return getMessageFactory().createResponse(Response.BAD_REQUEST, request);
}
Response response = getMessageFactory().createResponse(Response.OK, request);
// 添加date头
SIPDateHeader dateHeader = new SIPDateHeader();
// GB28181 日期
GbSipDate gbSipDate = new GbSipDate(Calendar.getInstance(Locale.ENGLISH).getTimeInMillis());
dateHeader.setDate(gbSipDate);
response.addHeader(dateHeader);
response.addHeader(sipRequest.getContactHeader());
response.addHeader(expires);
return response;
}
@SneakyThrows
private void registerDevice(String deviceId, SIPRequest request, String senderIp, RemoteInfo remoteInfo) {
WvpProxyDocking device = new WvpProxyDocking();
device.setGbDeviceId(deviceId);
device.setIp(remoteInfo.getIp());
device.setPort(String.valueOf(remoteInfo.getPort()));
dockingService.addDevice(device);
Response response = generateRegisterResponse(request);
sender.send(senderIp, response);
ViaHeader viaHeader = request.getTopmostViaHeader();
String transport = viaHeader.getTransport();
int expires = request.getExpires().getExpires();
// expires == 0 注销
if (expires == 0) {
log.info("设备注销 deviceId => {}", deviceId);
}
}
}

View File

@ -0,0 +1,329 @@
package cn.skcks.docking.gb28181.wvp.sip.request;
import cn.skcks.docking.gb28181.common.redis.RedisUtil;
import cn.skcks.docking.gb28181.core.sip.gb28181.cache.CacheUtil;
import cn.skcks.docking.gb28181.core.sip.gb28181.constant.GB28181Constant;
import cn.skcks.docking.gb28181.core.sip.message.MessageHelper;
import cn.skcks.docking.gb28181.core.sip.utils.SipUtil;
import cn.skcks.docking.gb28181.wvp.config.ProxySipConfig;
import cn.skcks.docking.gb28181.wvp.orm.mybatis.dynamic.model.WvpProxyDocking;
import gov.nist.javax.sip.message.MessageFactoryImpl;
import lombok.SneakyThrows;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.annotation.DependsOn;
import org.springframework.stereotype.Component;
import javax.sip.SipFactory;
import javax.sip.address.Address;
import javax.sip.address.SipURI;
import javax.sip.header.*;
import javax.sip.message.Request;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
@SuppressWarnings("Duplicates")
@DependsOn("proxySipConfig")
@Component
public class SipRequestBuilder implements ApplicationContextAware {
private static ProxySipConfig sipConfig;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
sipConfig = applicationContext.getBean(ProxySipConfig.class);
}
private static SipFactory getSipFactory(){
return SipFactory.getInstance();
}
@SneakyThrows
private static List<ViaHeader> getViaHeaders(String ip,int port, String transport, String viaTag){
ViaHeader viaHeader = getSipFactory().createHeaderFactory().createViaHeader(ip, port, transport, viaTag);
viaHeader.setRPort();
return Collections.singletonList(viaHeader);
}
@SneakyThrows
private static CSeqHeader getCSeqHeader(long cSeq, String method){
return getSipFactory().createHeaderFactory().createCSeqHeader(cSeq, method);
}
@SneakyThrows
public static Request createMessageRequest(WvpProxyDocking device, String ip, int port, long cSeq, String content, String fromTag, CallIdHeader callIdHeader) {
Request request;
String target = StringUtils.joinWith(":", device.getIp(), device.getPort());
// sip uri
SipURI requestURI = MessageHelper.createSipURI(device.getGbDeviceId(), target);
// via
List<ViaHeader> viaHeaders = getViaHeaders(ip, port, sipConfig.getTransport(), null );
String from = StringUtils.joinWith(":", ip, port);
// from
SipURI fromSipURI = MessageHelper.createSipURI(device.getGbDeviceId(), from);
Address fromAddress = MessageHelper.createAddress(fromSipURI);
FromHeader fromHeader = MessageHelper.createFromHeader(fromAddress, fromTag);
// to
SipURI toSipURI = MessageHelper.createSipURI(device.getGbDeviceId(), target);
Address toAddress = MessageHelper.createAddress(toSipURI);
ToHeader toHeader = MessageHelper.createToHeader(toAddress, null);
// Forwards
MaxForwardsHeader maxForwards = MessageHelper.createMaxForwardsHeader(70);
// ceq
CSeqHeader cSeqHeader = getSipFactory().createHeaderFactory().createCSeqHeader(cSeq, Request.MESSAGE);
// 使用 GB28181 默认编码 否则中文将会乱码
MessageFactoryImpl messageFactory = (MessageFactoryImpl) getSipFactory().createMessageFactory();
messageFactory.setDefaultContentEncodingCharset(GB28181Constant.CHARSET);
request = messageFactory.createRequest(requestURI, Request.MESSAGE, callIdHeader, cSeqHeader, fromHeader,
toHeader, viaHeaders, maxForwards);
request.addHeader(SipUtil.createUserAgentHeader());
ContentTypeHeader contentTypeHeader = getSipFactory().createHeaderFactory().createContentTypeHeader("Application", "MANSCDP+xml");
request.setContent(content, contentTypeHeader);
return request;
}
@SneakyThrows
public static Request createMessageRequest(WvpProxyDocking device, String ip, int port,long cSeq,String content, String viaTag, String fromTag, CallIdHeader callIdHeader) {
Request request;
String target = StringUtils.joinWith(":", device.getIp(), device.getPort());
// sip uri
SipURI requestURI = MessageHelper.createSipURI(device.getGbDeviceId(), target);
// via
List<ViaHeader> viaHeaders = getViaHeaders(ip, port, sipConfig.getTransport(), viaTag );
String from = StringUtils.joinWith(":", ip, port);
// from
SipURI fromSipURI = MessageHelper.createSipURI(sipConfig.getId(), from);
Address fromAddress = MessageHelper.createAddress(fromSipURI);
FromHeader fromHeader = MessageHelper.createFromHeader(fromAddress, fromTag);
// to
SipURI toSipURI = MessageHelper.createSipURI(device.getGbDeviceId(), target);
Address toAddress = MessageHelper.createAddress(toSipURI);
ToHeader toHeader = MessageHelper.createToHeader(toAddress, null);
// Forwards
MaxForwardsHeader maxForwards = MessageHelper.createMaxForwardsHeader(70);
// ceq
CSeqHeader cSeqHeader = getSipFactory().createHeaderFactory().createCSeqHeader(cSeq, Request.MESSAGE);
// 使用 GB28181 默认编码 否则中文将会乱码
MessageFactoryImpl messageFactory = (MessageFactoryImpl) getSipFactory().createMessageFactory();
messageFactory.setDefaultContentEncodingCharset(GB28181Constant.CHARSET);
request = messageFactory.createRequest(requestURI, Request.MESSAGE, callIdHeader, cSeqHeader, fromHeader,
toHeader, viaHeaders, maxForwards);
request.addHeader(SipUtil.createUserAgentHeader());
ContentTypeHeader contentTypeHeader = getSipFactory().createHeaderFactory().createContentTypeHeader("Application", "MANSCDP+xml");
request.setContent(content, contentTypeHeader);
return request;
}
@SneakyThrows
public static Request createInviteRequest(String ip, int port, WvpProxyDocking device, String channelId, String content, String viaTag, String fromTag, String toTag, String ssrc, CallIdHeader callIdHeader){
Request request;
String target = StringUtils.joinWith(":", device.getIp(), device.getPort());
SipURI requestLine = MessageHelper.createSipURI(channelId, target);
// via
List<ViaHeader> viaHeaders = getViaHeaders(ip, port, sipConfig.getTransport(), viaTag );
// from
SipURI fromSipURI = MessageHelper.createSipURI(sipConfig.getId(), sipConfig.getDomain());
Address fromAddress = MessageHelper.createAddress(fromSipURI);
FromHeader fromHeader = MessageHelper.createFromHeader(fromAddress, fromTag);
// to
SipURI toSipURI = MessageHelper.createSipURI(channelId, target);
Address toAddress = MessageHelper.createAddress(toSipURI);
ToHeader toHeader = MessageHelper.createToHeader(toAddress, null);
// Forwards
MaxForwardsHeader maxForwards = getSipFactory().createHeaderFactory().createMaxForwardsHeader(70);
// cSeq
CSeqHeader cSeqHeader = getSipFactory().createHeaderFactory().createCSeqHeader(getCSeq(), Request.INVITE);
request = getSipFactory().createMessageFactory().createRequest(requestLine, Request.INVITE, callIdHeader, cSeqHeader, fromHeader, toHeader, viaHeaders, maxForwards);
request.addHeader(SipUtil.createUserAgentHeader());
Address concatAddress = MessageHelper.createAddress(MessageHelper.createSipURI(sipConfig.getId(), ip + ":" + port));
request.addHeader(getSipFactory().createHeaderFactory().createContactHeader(concatAddress));
// Subject
SubjectHeader subjectHeader = getSipFactory().createHeaderFactory().createSubjectHeader(String.format("%s:%s,%s:%s", channelId, ssrc, sipConfig.getId(), 0));
request.addHeader(subjectHeader);
request.addHeader(SipUtil.createUserAgentHeader());
ContentTypeHeader contentTypeHeader = getSipFactory().createHeaderFactory().createContentTypeHeader("APPLICATION", "SDP");
request.setContent(content, contentTypeHeader);
return request;
}
@SneakyThrows
public static Request createAckRequest(int status,String ip, int port, WvpProxyDocking device, String channelId, String fromTag, String toTag, String callId) {
Request request;
// 请求行
String target = StringUtils.joinWith(":", device.getIp(), device.getPort());
SipURI requestLine = MessageHelper.createSipURI(channelId, target);
// via
ArrayList<ViaHeader> viaHeaders = new ArrayList<ViaHeader>();
ViaHeader viaHeader = getSipFactory().createHeaderFactory().createViaHeader(ip, port, sipConfig.getTransport(), SipUtil.generateViaTag());
viaHeaders.add(viaHeader);
// from
SipURI fromSipURI = MessageHelper.createSipURI(sipConfig.getId(), sipConfig.getDomain());
Address fromAddress = MessageHelper.createAddress(fromSipURI);
FromHeader fromHeader = MessageHelper.createFromHeader(fromAddress, fromTag);
// to
SipURI toSipURI = MessageHelper.createSipURI(channelId, target);
Address toAddress = MessageHelper.createAddress(toSipURI);
ToHeader toHeader = MessageHelper.createToHeader(toAddress, toTag);
// Forwards
MaxForwardsHeader maxForwards = getSipFactory().createHeaderFactory().createMaxForwardsHeader(70);
// ceq
CSeqHeader cSeqHeader = getSipFactory().createHeaderFactory().createCSeqHeader(getCSeq(), Request.ACK);
CallIdHeader callIdHeader = getSipFactory().createHeaderFactory().createCallIdHeader(callId);
request = getSipFactory().createMessageFactory().createRequest(requestLine, Request.ACK, callIdHeader, cSeqHeader, fromHeader, toHeader, viaHeaders, maxForwards);
request.addHeader(SipUtil.createUserAgentHeader());
Address concatAddress = MessageHelper.createAddress(MessageHelper.createSipURI(sipConfig.getId(), ip + ":" + port));
request.addHeader(getSipFactory().createHeaderFactory().createContactHeader(concatAddress));
request.addHeader(SipUtil.createUserAgentHeader());
return request;
}
@SneakyThrows
public static Request createByeRequest(String ip, int port, WvpProxyDocking device, String channelId, String fromTag, String toTag, String callId) {
Request request;
// 请求行
String target = StringUtils.joinWith(":", device.getIp(), device.getPort());
SipURI requestLine = MessageHelper.createSipURI(channelId, target);
// via
ArrayList<ViaHeader> viaHeaders = new ArrayList<ViaHeader>();
ViaHeader viaHeader = getSipFactory().createHeaderFactory().createViaHeader(ip, port, sipConfig.getTransport(), SipUtil.generateViaTag());
viaHeaders.add(viaHeader);
// from
SipURI fromSipURI = MessageHelper.createSipURI(sipConfig.getId(), sipConfig.getDomain());
Address fromAddress = MessageHelper.createAddress(fromSipURI);
FromHeader fromHeader = MessageHelper.createFromHeader(fromAddress, fromTag);
// to
SipURI toSipURI = MessageHelper.createSipURI(channelId, target);
Address toAddress = MessageHelper.createAddress(toSipURI);
ToHeader toHeader = MessageHelper.createToHeader(toAddress, toTag);
// Forwards
MaxForwardsHeader maxForwards = getSipFactory().createHeaderFactory().createMaxForwardsHeader(70);
// ceq
CSeqHeader cSeqHeader = getSipFactory().createHeaderFactory().createCSeqHeader(getCSeq(), Request.BYE);
CallIdHeader callIdHeader = getSipFactory().createHeaderFactory().createCallIdHeader(callId);
request = getSipFactory().createMessageFactory().createRequest(requestLine, Request.BYE, callIdHeader, cSeqHeader, fromHeader, toHeader, viaHeaders, maxForwards);
request.addHeader(SipUtil.createUserAgentHeader());
Address concatAddress = MessageHelper.createAddress(MessageHelper.createSipURI(sipConfig.getId(), ip + ":" + port));
request.addHeader(getSipFactory().createHeaderFactory().createContactHeader(concatAddress));
request.addHeader(SipUtil.createUserAgentHeader());
return request;
}
@SneakyThrows
public static Request createByeRequest(String ip, int port, long cSeq, String targetId, String fromTag, String toTag, String callId) {
Request request;
// 请求行
String target = StringUtils.joinWith(":", ip, port);
SipURI requestLine = MessageHelper.createSipURI(targetId, target);
// via
ArrayList<ViaHeader> viaHeaders = new ArrayList<ViaHeader>();
ViaHeader viaHeader = getSipFactory().createHeaderFactory().createViaHeader(ip, port, sipConfig.getTransport(), SipUtil.generateViaTag());
viaHeaders.add(viaHeader);
// from
SipURI fromSipURI = MessageHelper.createSipURI(sipConfig.getId(), sipConfig.getDomain());
Address fromAddress = MessageHelper.createAddress(fromSipURI);
FromHeader fromHeader = MessageHelper.createFromHeader(fromAddress, fromTag);
// to
SipURI toSipURI = MessageHelper.createSipURI(targetId, target);
Address toAddress = MessageHelper.createAddress(toSipURI);
ToHeader toHeader = MessageHelper.createToHeader(toAddress, toTag);
// Forwards
MaxForwardsHeader maxForwards = getSipFactory().createHeaderFactory().createMaxForwardsHeader(70);
// ceq
CSeqHeader cSeqHeader = getSipFactory().createHeaderFactory().createCSeqHeader(cSeq, Request.BYE);
CallIdHeader callIdHeader = getSipFactory().createHeaderFactory().createCallIdHeader(callId);
request = getSipFactory().createMessageFactory().createRequest(requestLine, Request.BYE, callIdHeader, cSeqHeader, fromHeader, toHeader, viaHeaders, maxForwards);
request.addHeader(SipUtil.createUserAgentHeader());
Address concatAddress = MessageHelper.createAddress(MessageHelper.createSipURI(sipConfig.getId(), ip + ":" + port));
request.addHeader(getSipFactory().createHeaderFactory().createContactHeader(concatAddress));
request.addHeader(SipUtil.createUserAgentHeader());
return request;
}
@SneakyThrows
public static Request createByeRequest(String ip, int port, String targetId, String fromTag, String toTag, String callId) {
Request request;
// 请求行
String target = StringUtils.joinWith(":", ip, port);
SipURI requestLine = MessageHelper.createSipURI(targetId, target);
// via
ArrayList<ViaHeader> viaHeaders = new ArrayList<ViaHeader>();
ViaHeader viaHeader = getSipFactory().createHeaderFactory().createViaHeader(ip, port, sipConfig.getTransport(), SipUtil.generateViaTag());
viaHeaders.add(viaHeader);
// from
SipURI fromSipURI = MessageHelper.createSipURI(sipConfig.getId(), sipConfig.getDomain());
Address fromAddress = MessageHelper.createAddress(fromSipURI);
FromHeader fromHeader = MessageHelper.createFromHeader(fromAddress, fromTag);
// to
SipURI toSipURI = MessageHelper.createSipURI(targetId, target);
Address toAddress = MessageHelper.createAddress(toSipURI);
ToHeader toHeader = MessageHelper.createToHeader(toAddress, toTag);
// Forwards
MaxForwardsHeader maxForwards = getSipFactory().createHeaderFactory().createMaxForwardsHeader(70);
// ceq
CSeqHeader cSeqHeader = getSipFactory().createHeaderFactory().createCSeqHeader(getCSeq(), Request.BYE);
CallIdHeader callIdHeader = getSipFactory().createHeaderFactory().createCallIdHeader(callId);
request = getSipFactory().createMessageFactory().createRequest(requestLine, Request.BYE, callIdHeader, cSeqHeader, fromHeader, toHeader, viaHeaders, maxForwards);
request.addHeader(SipUtil.createUserAgentHeader());
Address concatAddress = MessageHelper.createAddress(MessageHelper.createSipURI(sipConfig.getId(), ip + ":" + port));
request.addHeader(getSipFactory().createHeaderFactory().createContactHeader(concatAddress));
request.addHeader(SipUtil.createUserAgentHeader());
return request;
}
public static long getCSeq() {
String key = CacheUtil.getKey(CacheUtil.SIP_C_SEQ_PREFIX,sipConfig.getId());
long result = 1L;
if(RedisUtil.KeyOps.hasKey(key)){
try {
result = RedisUtil.StringOps.incrBy(key,1L);
} finally {
if (result > Integer.MAX_VALUE) {
RedisUtil.StringOps.set(key, String.valueOf(1L));
result = 1L;
}
}
} else {
RedisUtil.StringOps.set(key, String.valueOf(result));
}
return result;
}
}

View File

@ -0,0 +1,56 @@
package cn.skcks.docking.gb28181.wvp.sip.response;
import cn.skcks.docking.gb28181.core.sip.gb28181.constant.GB28181Constant;
import cn.skcks.docking.gb28181.core.sip.message.MessageHelper;
import cn.skcks.docking.gb28181.core.sip.utils.SipUtil;
import cn.skcks.docking.gb28181.sdp.GB28181Description;
import gov.nist.javax.sip.message.MessageFactoryImpl;
import gov.nist.javax.sip.message.SIPRequest;
import gov.nist.javax.sip.message.SIPResponse;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import javax.sip.SipFactory;
import javax.sip.address.Address;
import javax.sip.address.SipURI;
import javax.sip.header.ContentTypeHeader;
import javax.sip.header.MaxForwardsHeader;
import javax.sip.message.Response;
@Slf4j
public class SipResponseBuilder {
@SneakyThrows
public static Response response(SIPRequest request, int status, String message){
if (request.getToHeader().getTag() == null) {
request.getToHeader().setTag(SipUtil.generateTag());
}
MessageFactoryImpl messageFactory = (MessageFactoryImpl)MessageHelper.getSipFactory().createMessageFactory();
// 使用 GB28181 默认编码 否则中文将会乱码
messageFactory.setDefaultContentEncodingCharset(GB28181Constant.CHARSET);
SIPResponse response = (SIPResponse)messageFactory.createResponse(status, request);
if (message != null) {
response.setReasonPhrase(message);
}
return response;
}
@SneakyThrows
public static Response responseSdp(SIPRequest request, GB28181Description sdp) {
MessageFactoryImpl messageFactory = (MessageFactoryImpl)MessageHelper.getSipFactory().createMessageFactory();
// 使用 GB28181 默认编码 否则中文将会乱码
messageFactory.setDefaultContentEncodingCharset(GB28181Constant.CHARSET);
SIPResponse response = (SIPResponse)messageFactory.createResponse(Response.OK, request);
SipFactory sipFactory = SipFactory.getInstance();
ContentTypeHeader contentTypeHeader = sipFactory.createHeaderFactory().createContentTypeHeader("application", "sdp");
response.setContent(sdp.toString(), contentTypeHeader);
SipURI sipURI = (SipURI) request.getRequestURI();
SipURI uri = MessageHelper.createSipURI(sipURI.getUser(), StringUtils.joinWith(":", sipURI.getHost() + ":" + sipURI.getPort()));
Address concatAddress = sipFactory.createAddressFactory().createAddress(uri);
MaxForwardsHeader maxForwardsHeader = MessageHelper.createMaxForwardsHeader(70);
response.setMaxForwards(maxForwardsHeader);
response.addHeader(sipFactory.createHeaderFactory().createContactHeader(concatAddress));
return response;
}
}

View File

@ -0,0 +1,83 @@
package cn.skcks.docking.gb28181.wvp.sip.sender;
import cn.skcks.docking.gb28181.core.sip.service.SipService;
import cn.skcks.docking.gb28181.wvp.config.ProxySipConfig;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Component;
import javax.sip.ListeningPoint;
import javax.sip.SipException;
import javax.sip.SipProvider;
import javax.sip.message.Request;
import javax.sip.message.Response;
import java.util.List;
import java.util.Objects;
@Slf4j
@RequiredArgsConstructor(onConstructor_ = {@Lazy})
@Component
public class SipSender {
private final SipService sipService;
private final ProxySipConfig sipConfig;
public SipProvider getProvider(String transport, String ip) {
return sipService.getProvider(transport, ip);
}
public List<SipProvider> getProviders() {
return sipConfig.getIp().stream().map(item -> getProvider(sipConfig.getTransport(), item))
.filter(Objects::nonNull)
.toList();
}
public void sendResponse(SipProvider sipProvider, SendResponse response) {
log.info("{}", sipProvider);
ListeningPoint[] listeningPoints = sipProvider.getListeningPoints();
if (listeningPoints == null || listeningPoints.length == 0) {
log.error("发送响应失败, 未找到有效的监听地址");
return;
}
ListeningPoint listeningPoint = listeningPoints[0];
String ip = listeningPoint.getIPAddress();
int port = listeningPoint.getPort();
try {
sipProvider.sendResponse(response.build(sipProvider, ip, port));
} catch (SipException e) {
log.error("向{} {}:{} 发送响应失败, 异常: {}", ip, listeningPoint.getPort(), listeningPoint.getTransport(), e.getMessage());
}
}
public void sendResponse(String senderIp,String transport, SendResponse response) {
SipProvider sipProvider = getProvider(transport, senderIp);
sendResponse(sipProvider, response);
}
public void sendRequest(SendRequest request) {
getProviders().parallelStream().forEach(sipProvider -> {
log.info("{}", sipProvider);
ListeningPoint[] listeningPoints = sipProvider.getListeningPoints();
if (listeningPoints == null || listeningPoints.length == 0) {
log.error("发送请求失败, 未找到有效的监听地址");
return;
}
ListeningPoint listeningPoint = listeningPoints[0];
String ip = listeningPoint.getIPAddress();
int port = listeningPoint.getPort();
try {
sipProvider.sendRequest(request.build(sipProvider, ip, port));
} catch (SipException e) {
log.error("向{} {}:{} 发送请求失败, 异常: {}", ip, listeningPoint.getPort(), listeningPoint.getTransport(), e.getMessage());
}
});
}
public interface SendRequest {
Request build(SipProvider provider, String ip, int port);
}
public interface SendResponse {
Response build(SipProvider provider, String ip, int port);
}
}

View File

@ -0,0 +1,44 @@
package cn.skcks.docking.gb28181.wvp.sip.subscribe;
import cn.skcks.docking.gb28181.core.sip.message.subscribe.GenericSubscribe;
import gov.nist.javax.sip.message.SIPRequest;
import lombok.RequiredArgsConstructor;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executor;
import java.util.concurrent.Flow;
import java.util.concurrent.SubmissionPublisher;
@RequiredArgsConstructor
public class ByeSubscribe implements GenericSubscribe<SIPRequest> {
private final Executor executor;
private static final Map<String, SubmissionPublisher<SIPRequest>> publishers = new ConcurrentHashMap<>();
public void close() {
Helper.close(publishers);
}
public void addPublisher(String key) {
Helper.addPublisher(executor, publishers, key);
}
public SubmissionPublisher<SIPRequest> getPublisher(String key) {
return Helper.getPublisher(publishers, key);
}
public void addSubscribe(String key, Flow.Subscriber<SIPRequest> subscribe) {
Helper.addSubscribe(publishers, key, subscribe);
}
@Override
public void delPublisher(String key) {
Helper.delPublisher(publishers, key);
}
@Override
public void complete(String key) {
delPublisher(key);
}
}

View File

@ -0,0 +1,44 @@
package cn.skcks.docking.gb28181.wvp.sip.subscribe;
import cn.skcks.docking.gb28181.core.sip.message.subscribe.GenericSubscribe;
import gov.nist.javax.sip.message.SIPRequest;
import lombok.RequiredArgsConstructor;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executor;
import java.util.concurrent.Flow;
import java.util.concurrent.SubmissionPublisher;
@RequiredArgsConstructor
public class CatalogSubscribe implements GenericSubscribe<SIPRequest> {
private final Executor executor;
private static final Map<String, SubmissionPublisher<SIPRequest>> publishers = new ConcurrentHashMap<>();
public void close() {
Helper.close(publishers);
}
public void addPublisher(String key) {
Helper.addPublisher(executor, publishers, key);
}
public SubmissionPublisher<SIPRequest> getPublisher(String key) {
return Helper.getPublisher(publishers, key);
}
public void addSubscribe(String key, Flow.Subscriber<SIPRequest> subscribe) {
Helper.addSubscribe(publishers, key, subscribe);
}
@Override
public void delPublisher(String key) {
Helper.delPublisher(publishers, key);
}
@Override
public void complete(String key) {
delPublisher(key);
}
}

View File

@ -0,0 +1,81 @@
package cn.skcks.docking.gb28181.wvp.sip.subscribe;
import cn.hutool.core.date.DateUtil;
import cn.skcks.docking.gb28181.common.json.JsonResponse;
import cn.skcks.docking.gb28181.service.record.convertor.RecordConvertor;
import cn.skcks.docking.gb28181.service.record.vo.RecordInfoItemVO;
import cn.skcks.docking.gb28181.sip.manscdp.recordinfo.response.RecordInfoItemDTO;
import cn.skcks.docking.gb28181.sip.manscdp.recordinfo.response.RecordInfoResponseDTO;
import cn.skcks.docking.gb28181.sip.utils.MANSCDPUtils;
import gov.nist.javax.sip.message.SIPRequest;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.context.request.async.DeferredResult;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Flow;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
import java.util.stream.Collectors;
@Slf4j
@RequiredArgsConstructor
public class RecordSubscribe implements Flow.Subscriber<SIPRequest>{
private final SipSubscribe subscribe;
private final String key;
private final DeferredResult<JsonResponse<List<RecordInfoItemVO>>> result;
private final String deviceId;
private final List<RecordInfoItemDTO> list = new ArrayList<>();
private final AtomicLong atomicSum = new AtomicLong(0);
private final AtomicLong atomicNum = new AtomicLong(0);
private Flow.Subscription subscription;
@Override
public void onSubscribe(Flow.Subscription subscription) {
this.subscription = subscription;
log.debug("建立订阅 => {}", key);
subscription.request(1);
}
@Override
public void onNext(SIPRequest item) {
RecordInfoResponseDTO data = MANSCDPUtils.parse(item.getRawContent(), RecordInfoResponseDTO.class);
atomicSum.set(Math.max(data.getSumNum(), atomicNum.get()));
atomicNum.addAndGet(data.getRecordList().getNum());
list.addAll(data.getRecordList().getRecordList());
long num = atomicNum.get();
long sum = atomicSum.get();
if(num > sum){
log.warn("检测到 设备 => {}, 未按规范实现, 订阅 => {}, 期望总数为 => {}, 已接收数量 => {}", deviceId, key, atomicSum.get(), atomicNum.get());
} else {
log.info("获取订阅 => {}, {}/{}", key, atomicNum.get(), atomicSum.get());
}
if (num >= sum) {
// 针对某些不按规范的设备
// 如果已获取数量 >= 约定的总数
// 就执行定时任务, 500ms 内未收到新的数据视为已结束
subscribe.getMessageSubscribe().refreshPublisher(key,500, TimeUnit.MILLISECONDS);
}
subscription.request(1);
}
@Override
public void onError(Throwable throwable) {
}
@Override
public void onComplete() {
result.setResult(JsonResponse.success(RecordConvertor.INSTANCE.dto2Vo(sortedRecordList(list))));
log.debug("订阅结束 => {}", key);
subscribe.getMessageSubscribe().delPublisher(key);
}
private List<RecordInfoItemDTO> sortedRecordList(List<RecordInfoItemDTO> list){
return list.stream().sorted((a,b)-> DateUtil.compare(a.getStartTime(),b.getStartTime())).collect(Collectors.toList());
}
}

View File

@ -0,0 +1,50 @@
package cn.skcks.docking.gb28181.wvp.sip.subscribe;
import cn.skcks.docking.gb28181.core.sip.executor.DefaultSipExecutor;
import cn.skcks.docking.gb28181.core.sip.message.subscribe.GenericSubscribe;
import cn.skcks.docking.gb28181.core.sip.message.subscribe.GenericTimeoutSubscribe;
import cn.skcks.docking.gb28181.core.sip.message.subscribe.InviteSubscribe;
import cn.skcks.docking.gb28181.core.sip.message.subscribe.SipRequestSubscribe;
import gov.nist.javax.sip.message.SIPRequest;
import gov.nist.javax.sip.message.SIPResponse;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import lombok.Data;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Service;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
@Slf4j
@Data
@RequiredArgsConstructor
@Service
public class SipSubscribe {
@Qualifier(DefaultSipExecutor.EXECUTOR_BEAN_NAME)
private final Executor executor;
private final ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(Runtime.getRuntime().availableProcessors());
private GenericSubscribe<SIPRequest> catalogSubscribe;
private GenericSubscribe<SIPResponse> inviteSubscribe;
private GenericSubscribe<SIPRequest> byeSubscribe;
private GenericTimeoutSubscribe<SIPRequest> messageSubscribe;
@PostConstruct
private void init() {
catalogSubscribe = new CatalogSubscribe(executor);
inviteSubscribe = new InviteSubscribe(executor);
byeSubscribe = new ByeSubscribe(executor);
messageSubscribe = new SipRequestSubscribe(executor, scheduledExecutorService);
}
@PreDestroy
private void destroy() {
catalogSubscribe.close();
inviteSubscribe.close();
byeSubscribe.close();
messageSubscribe.close();
}
}

View File

@ -0,0 +1,108 @@
package cn.skcks.docking.gb28181.wvp.utils;
import cn.skcks.docking.gb28181.common.json.JsonResponse;
import com.github.rholder.retry.*;
import com.google.common.base.Predicate;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.TimeUnit;
@Slf4j
@SuppressWarnings("UnstableApiUsage")
public class RetryUtil {
/**
* 默认重试次数
*/
public final static int DEFAULT_RETRY_TIME = 3;
/**
* 默认每次重试等待时间
*/
public final static long DEFAULT_RETRY_INTERVAL = 3;
public final static TimeUnit DEFAULT_RETRY_INTERVAL_UNIT = TimeUnit.SECONDS;
public static RetryListener defaultRetryListener(String name) {
return new RetryListener() {
@Override
public <V> void onRetry(Attempt<V> attempt) {
log.info("第 {} 次 执行 {} 结束", attempt.getAttemptNumber(), name);
if (attempt.hasException()) {
log.info("执行 {} 异常 {}", name, attempt.getExceptionCause().getMessage());
}
}
};
}
public static <T> Predicate<JsonResponse<T>> defaultRetryIf(){
return result -> result == null || (result.getCode() != 0 && result.getCode() != 200);
}
public static Predicate<JsonResponse<?>> defaultGenericRetryIf(){
return result -> result == null || (result.getCode() != 0 && result.getCode() != 200);
}
public static <T> RetryerBuilder<JsonResponse<T>> getDefaultRetryerBuilder(String name, int waitTime, TimeUnit waitUnit, int retryTime) {
return RetryerBuilder.<JsonResponse<T>>newBuilder()
// 异常就重试
.retryIfException()
.retryIfRuntimeException()
// 重试间隔
.withWaitStrategy(WaitStrategies.fixedWait(waitTime, waitUnit))
// 重试次数
.withStopStrategy(StopStrategies.stopAfterAttempt(retryTime))
.retryIfResult(defaultRetryIf())
.withRetryListener(defaultRetryListener(name));
}
public static <T> RetryerBuilder<JsonResponse<T>> getDefaultRetryerBuilder(String name) {
return RetryerBuilder.<JsonResponse<T>>newBuilder()
// 异常就重试
.retryIfException()
.retryIfRuntimeException();
}
public static <T> Retryer<JsonResponse<T>> getDefaultRetryer(String name) {
return RetryUtil.<T>getDefaultRetryerBuilder(name)
// 重试间隔
.withWaitStrategy(WaitStrategies.fixedWait(DEFAULT_RETRY_INTERVAL, TimeUnit.SECONDS))
// 重试次数
.withStopStrategy(StopStrategies.stopAfterAttempt(DEFAULT_RETRY_TIME))
.retryIfResult(defaultRetryIf())
.withRetryListener(defaultRetryListener(name))
.build();
}
public static RetryerBuilder<JsonResponse<?>> getDefaultGenericRetryerBuilder(String name, int retryTime, long retryInterval, TimeUnit retryTimeUnit,RetryListener retryListener) {
return RetryerBuilder.<JsonResponse<?>>newBuilder()
// 异常就重试
.retryIfException()
.retryIfRuntimeException()
// 重试间隔
.withWaitStrategy(WaitStrategies.fixedWait(retryInterval, retryTimeUnit))
// 重试次数
.withStopStrategy(StopStrategies.stopAfterAttempt(retryTime))
.retryIfResult(defaultGenericRetryIf())
.withRetryListener(retryListener);
}
public static RetryerBuilder<JsonResponse<?>> getDefaultGenericRetryerBuilder(String name) {
return getDefaultGenericRetryerBuilder(name, DEFAULT_RETRY_TIME, DEFAULT_RETRY_INTERVAL, DEFAULT_RETRY_INTERVAL_UNIT, defaultRetryListener(name));
}
public static Retryer<JsonResponse<?>> getDefaultGenericRetryer(String name, int retryTime, long retryInterval, TimeUnit retryTimeUnit, RetryListener retryListener) {
return getDefaultGenericRetryerBuilder(name, retryTime, retryInterval, retryTimeUnit, retryListener).build();
}
public static Retryer<JsonResponse<?>> getDefaultGenericRetryer(String name, int retryTime, long retryInterval, TimeUnit retryTimeUnit) {
return getDefaultGenericRetryer(name, retryTime, retryInterval, retryTimeUnit, defaultRetryListener(name));
}
public static Retryer<JsonResponse<?>> getDefaultGenericRetryer(String name, int retryTime, long retryInterval) {
return getDefaultGenericRetryer(name, retryTime, retryInterval, DEFAULT_RETRY_INTERVAL_UNIT);
}
public static Retryer<JsonResponse<?>> getDefaultGenericRetryer(String name) {
return getDefaultGenericRetryer(name, DEFAULT_RETRY_TIME, DEFAULT_RETRY_INTERVAL);
}
}

View File

@ -1,5 +0,0 @@
proxy:
wvp:
url: http://192.168.3.13:18978
user: admin
passwd: admin

View File

@ -0,0 +1,43 @@
server:
port: 18183
spring:
data:
redis:
# [必须修改] Redis服务器IP, REDIS安装在本机的,使用127.0.0.1
# host: 192.168.1.241
host: 10.10.10.200
# [必须修改] 端口号
port: 16379
# [可选] 数据库 DB
database: 15
# [可选] 访问密码,若你的redis服务器没有设置密码就不需要用密码去连接
password: 12341234
# [可选] 超时时间
timeout: 10000
datasource:
type: com.zaxxer.hikari.HikariDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
username: root
password: 12341234
url: jdbc:mysql://10.10.10.200:3306/gb28181_docking_platform?createDatabaseIfNotExist=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
cloud:
openfeign:
httpclient:
connection-timeout: 0
ok-http:
read-timeout: 0
media:
ip: 10.10.10.200
url: 'http://10.10.10.200:5080'
# url: 'http://10.10.10.200:12580/anything/'
id: amrWMKmbKqoBjRQ9
# secret: 035c73f7-bb6b-4889-a715-d9eb2d1925cc
secret: 4155cca6-2f9f-11ee-85e6-8de4ce2e7333
proxy:
wvp:
url: http://192.168.3.13:18978
user: admin
passwd: admin

View File

@ -1,7 +1,9 @@
package cn.skcks.docking.gb28181.wvp.test;
import cn.hutool.core.io.IoUtil;
import cn.hutool.crypto.digest.MD5;
import cn.skcks.docking.gb28181.common.json.JsonResponse;
import cn.skcks.docking.gb28181.wvp.WvpProxyTestApplication;
import cn.skcks.docking.gb28181.wvp.config.WvpProxyConfig;
import cn.skcks.docking.gb28181.wvp.dto.device.GetDeviceChannelsReq;
import cn.skcks.docking.gb28181.wvp.dto.device.GetDeviceChannelsResp;
@ -11,17 +13,24 @@ import cn.skcks.docking.gb28181.wvp.dto.login.WvpLoginReq;
import cn.skcks.docking.gb28181.wvp.dto.login.WvpLoginResp;
import cn.skcks.docking.gb28181.wvp.proxy.WvpProxyClient;
import lombok.extern.slf4j.Slf4j;
import org.apache.hc.client5.http.classic.methods.HttpGet;
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
import org.apache.hc.client5.http.impl.classic.HttpClients;
import org.apache.hc.core5.io.CloseMode;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Paths;
@Slf4j
@SpringBootTest
@EnableAutoConfiguration(exclude={DataSourceAutoConfiguration.class})
@SpringBootTest(classes = WvpProxyTestApplication.class)
@ExtendWith(SpringExtension.class)
public class WvpProxyTest {
@Autowired
@ -55,4 +64,24 @@ public class WvpProxyTest {
log.info("{}", item);
});
}
@Test
void downloadTest() throws IOException {
final CloseableHttpClient client = HttpClients.custom()
.build();
String tmpDir = new File(System.getProperty("java.io.tmpdir")).getAbsolutePath();
File file = new File(Paths.get(tmpDir,"test.mp4").toUri());
log.info("临时文件 路径 => {}",file.getAbsolutePath());
FileOutputStream outputStream = new FileOutputStream(file);
String url = "http://192.168.1.241:18979/download/recordTemp/0490d767d94ce20aedce57c862b6bfe9/rtp/59777645.mp4";
HttpGet httpGet = new HttpGet(url);
client.execute(httpGet, response -> {
InputStream stream = response.getEntity().getContent();
IoUtil.copy(stream,outputStream);
return stream;
});
client.close(CloseMode.GRACEFUL);
}
}

View File

@ -0,0 +1,19 @@
package cn.skcks.docking.gb28181.wvp.test;
import cn.skcks.docking.gb28181.common.xml.XmlUtils;
import cn.skcks.docking.gb28181.wvp.sip.message.message.device.control.DeviceControlDTO;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
@Slf4j
public class XmlTest {
@Test
public void test(){
DeviceControlDTO deviceControlDTO = DeviceControlDTO.builder()
.sn("100")
.deviceId("123456")
.recordCmd("PreRecord")
.build();
log.info("\n{}", XmlUtils.toXml(deviceControlDTO));
}
}

View File

@ -3,11 +3,17 @@ FROM eclipse-temurin:17-jre-focal
MAINTAINER Shikong <919411476@qq.com>
ENV HOME_PATH /opt/gb28181-docking-platform-wvp-proxy/
ENV TZ Asia/Shanghai
RUN mkdir -p $HOME_PATH
WORKDIR $HOME_PATH
RUN mkdir -p /usr/bin/ffmpeg
ADD ./ffmpeg/ffmpeg /usr/bin/ffmpeg/ffmpeg
ADD ./ffmpeg/ffprobe /usr/bin/ffmpeg/ffprobe
ADD ./ffmpeg/model /usr/bin/ffmpeg/model
ADD target/gb28181-wvp-proxy-starter.jar /opt/gb28181-docking-platform-wvp-proxy/starter.jar
EXPOSE 18182
EXPOSE 18183
ENTRYPOINT ["java", "-jar","/opt/gb28181-docking-platform-wvp-proxy/starter.jar"]

View File

@ -4,11 +4,13 @@ import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.FilterType;
import org.springframework.scheduling.annotation.EnableAsync;
@EnableFeignClients(basePackages = {
"cn.skcks.docking.gb28181.media",
"cn.skcks.docking.gb28181.wvp.proxy"
"cn.skcks.docking.gb28181.wvp.proxy",
"cn.skcks.docking.gb28181.wvp.service.report"
})
@SpringBootApplication
@ComponentScan(basePackages = {
@ -16,6 +18,25 @@ import org.springframework.scheduling.annotation.EnableAsync;
"cn.skcks.docking.gb28181.common",
"cn.skcks.docking.gb28181.wvp",
"cn.skcks.docking.gb28181.media",
"cn.skcks.docking.gb28181"
}, includeFilters = {
@ComponentScan.Filter(type = FilterType.REGEX, pattern = {
"cn.skcks.docking.gb28181.service.ssrc.*",
})
},
excludeFilters = {
@ComponentScan.Filter(type = FilterType.REGEX, pattern = {
"cn.skcks.docking.gb28181.starter.*",
"cn.skcks.docking.gb28181.config.sip.SipConfig",
"cn.skcks.docking.gb28181.core.sip.listener.*",
"cn.skcks.docking.gb28181.core.sip.message.processor.*",
"cn.skcks.docking.gb28181.core.sip.message.subscribe.*",
"cn.skcks.docking.gb28181.service.play.*",
"cn.skcks.docking.gb28181.service.record.*",
"cn.skcks.docking.gb28181.core.sip.message.request.*",
"cn.skcks.docking.gb28181.service.catalog.*",
"cn.skcks.docking.gb28181.service.notify.*"
})
})
@EnableAsync
public class Gb28181WvpProxyStarter {

View File

@ -1,5 +1,5 @@
server:
port: 18186
port: 18183
project:
version: @project.version@
@ -32,16 +32,88 @@ spring:
connection-timeout: 0
ok-http:
read-timeout: 0
media:
ip: 10.10.10.200
url: 'http://10.10.10.200:5080'
url: 'https://10.10.10.200:5444'
# url: 'http://10.10.10.200:12580/anything/'
id: amrWMKmbKqoBjRQ9
# secret: 035c73f7-bb6b-4889-a715-d9eb2d1925cc
secret: 4155cca6-2f9f-11ee-85e6-8de4ce2e7333
rtmp-port: 1936
proxy:
wvp:
url: http://192.168.3.13:18978
url: http://127.0.0.1:18978
# url: http://192.168.3.12:18978
user: admin
passwd: admin
use-ffmpeg: true
# 是否使用 wvp 的 api(wvp 的 并发有问题,仅保留用于兼容), 否则使用sip 信令直接操作设备
enable: false
# 是否使用 ffmpeg 编/解码, 否则使用内置 javacv
parents:
- 44050100002000000002
- 44050100002000000003
- 44050100001180000001
- 44050100001320000001
- 44050100001110000010
# 用于生成 代理 wvp 的 视频流 ws-flv 地址
#proxy-media-url: 'wss://192.168.1.241:9022/mf-config/media'
proxy-media-url: 'ws://10.10.10.200:5080'
# 实时视频单次点播持续时间 (默认: 15分钟)
realtime-video-duration: 15m
gb28181:
sip:
id: 44050100002000000003
# id: 44050100002000000005
domain: 4405010000
password: 123456
port: 5063
ip:
- 10.10.10.20
# - 192.168.0.195
stream-mode: udp
use-playback-to-download: false
# proxy-media-url: 'https://10.10.10.200:18181/media'
proxy-media-url: 'https://10.10.10.200:5444'
use-record-info-query-before-download: true
retry-record-info-query-before-download-interval: 3
retry-record-info-query-before-download-times: 20
retry-record-info-query-before-download-interval-unit: seconds
# - 192.168.1.241
device-api:
offset:
forward: 0s
back: 0s
ffmpeg-support:
ffmpeg: D:\Soft\Captura\ffmpeg\ffmpeg.exe
ffprobe: D:\Soft\Captura\ffmpeg\ffprobe.exe
rtp:
# input: -i http://10.10.10.200:5080/live/test.live.flv
input: -re -i
# output: -preset ultrafast -vcodec libx264 -acodec aac -movflags empty_moov+frag_keyframe+default_base_moof -vsync 2 -copyts -f flv # -rtsp_transport tcp
# output: -enc_time_base -1 -preset ultrafast -tune zerolatency -vcodec libx264 -an -movflags faststart -f flv # -rtsp_transport tcp
#output: -c:v libx264 -an -f flv # -rtsp_transport tcp
output: -c:v copy -an -f flv
#download: -rw_timeout 30000000 -rtmp_live recorded -tcp_nodelay 1 -thread_queue_size 1 -i
download: -rw_timeout 30000000 -rtmp_live recorded -tcp_nodelay 1 -thread_queue_size 1 -i
log-level: error
# download: -rtmp_live recorded -tcp_nodelay 1 -thread_queue_size 128 -i
debug:
download: false
input: false
output: false
tmp-dir: G:\Temp\record\download-proxy
use-tmp-file: true
# [可选] 日志配置, 一般不需要改
logging:
config: classpath:logback.xml
report:
enabled: false
url: http://127.0.0.1:8080/api/report
custom-headers:
agent: gb28181-proxy

View File

@ -1,10 +1,9 @@
server:
port: 18186
port: 18183
project:
version: @project.version@
spring:
data:
redis:
@ -24,7 +23,7 @@ spring:
driver-class-name: com.mysql.cj.jdbc.Driver
username: root
password: 123456a
url: jdbc:mysql://192.168.1.241:3306/gb28181_docking_platform?createDatabaseIfNotExist=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
url: jdbc:mysql://192.168.1.241:3306/gb28181_docking_platform_dev?createDatabaseIfNotExist=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
profiles:
active: local
cloud:
@ -33,6 +32,9 @@ spring:
connection-timeout: 0
ok-http:
read-timeout: 0
hc5:
connection-request-timeout: 60
connection-request-timeout-unit: seconds
media:
ip: 192.168.1.241
@ -44,6 +46,63 @@ media:
proxy:
wvp:
url: http://192.168.3.13:18978
url: http://192.168.3.12:18978
user: admin
passwd: admin
use-wvp-assist: false
# 是否使用 wvp 的 api(wvp 的 并发有问题,仅保留用于兼容), 否则使用sip 信令直接操作设备
enable: false
# 是否使用 ffmpeg 编/解码, 否则使用内置 javacv
use-ffmpeg: false
parents:
- 44050100002000000003
- 44050100001180000001
# 用于生成 代理 wvp 的 视频流 ws-flv 地址
proxy-media-url: 'wss://192.168.1.241:9022/mf-config/media'
# 实时视频单次点播持续时间 (默认: 15分钟)
realtime-video-duration: 15m
gb28181:
sip:
id: 44050100002000000005
domain: 4405010000
password: 123456
port: 5063
ip:
- 192.168.0.195
# - 192.168.3.10
# - 192.168.1.241
stream-mode: tcp_passive
use-playback-to-download: false
# 用于替换 返回的 url 值, 可用 nginx 或 caddy 代理 zlm
# proxy-media-url: 'http://10.10.10.200/media'
proxy-media-url: 'https://192.168.1.241:9022/mf-config/media'
device-api:
offset:
forward: 0s
back: 0s
ffmpeg-support:
ffmpeg: /usr/bin/ffmpeg/ffmpeg
# ffmpeg: C:\ffmpeg\bin\ffmpeg.exe
ffprobe: /usr/bin/ffmpeg/ffprobe
rtp:
input: -re -i
#output: -preset ultrafast -tune zerolatency -vcodec libx264 -acodec aac -movflags empty_moov+frag_keyframe+default_base_moof -f mp4 # -rtsp_transport tcp
# output: -enc_time_base -1 -preset ultrafast -tune zerolatency -vcodec libx264 -an -movflags empty_moov+frag_keyframe+default_base_moof -f mp4 # -rtsp_transport tcp
output: -enc_time_base -1 -preset ultrafast -tune zerolatency -vcodec libx264 -an -movflags faststart -f mp4 # -rtsp_transport tcp
download: -thread_queue_size 128 -fflags +genpts -enc_time_base -1 -i
debug:
download: false
input: false
output: false
# [可选] 日志配置, 一般不需要改
logging:
config: classpath:logback.xml
report:
enabled: false
url: http://127.0.0.1:8080/api/report
custom-headers:
agent: gb28181-proxy

View File

@ -0,0 +1,54 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration scan="true" scanPeriod="60 seconds" debug="false">
<contextName>logback</contextName>
<!--定义日志文件的存储地址 勿在 LogBack 的配置中使用相对路径-->
<!--<property name="log.path" value="./log/business_Log" />-->
<!--输出到控制台-->
<appender name="console" class="ch.qos.logback.core.ConsoleAppender">
<!-- <filter class="ch.qos.logback.classic.filter.ThresholdFilter">-->
<!-- <level>INFO</level>-->
<!-- </filter>-->
<!-- <withJansi>true</withJansi>-->
<encoder>
<!--<pattern>%d %p (%file:%line\)- %m%n</pattern>-->
<!--格式化输出:%d:表示日期 %thread:表示线程名 %-5level:级别从左显示5个字符宽度 %msg:日志消息 %n:是换行符-->
<pattern>%red(%d{yyyy-MM-dd HH:mm:ss.SSS}) %green([%thread]) %highlight(%-5level) %yellow(at %class.%method) (%file:%line\) - %cyan(%msg%n)</pattern>
<!--<pattern>%d{yyyy-MM-dd HH:mm:ss} [%thread] %magenta(%-5level) %green([%-50.50class]) >>> %cyan(%msg) %n</pattern>-->
<charset>UTF-8</charset>
</encoder>
</appender>
<!--&lt;!&ndash;输出到文件&ndash;&gt;-->
<!--<appender name="file" class="ch.qos.logback.core.rolling.RollingFileAppender">-->
<!-- <filter class="ch.qos.logback.classic.filter.ThresholdFilter">-->
<!-- <level>INFO</level>-->
<!-- </filter>-->
<!-- <file>${log.path}/logback.log</file>-->
<!-- <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">-->
<!-- <fileNamePattern>${log.path}/logback-%d{yyyy-MM-dd-HH-mm}.log</fileNamePattern>-->
<!-- <maxHistory>365</maxHistory>-->
<!-- &lt;!&ndash; <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">&ndash;&gt;-->
<!-- &lt;!&ndash; <maxFileSize>100kB</maxFileSize>&ndash;&gt;-->
<!-- &lt;!&ndash; </timeBasedFileNamingAndTriggeringPolicy>&ndash;&gt;-->
<!-- </rollingPolicy>-->
<!-- <encoder>-->
<!-- &lt;!&ndash;格式化输出:%d:表示日期 %thread:表示线程名 %-5level:级别从左显示5个字符宽度 %msg:日志消息 %n:是换行符&ndash;&gt;-->
<!-- <pattern>%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n</pattern>-->
<!-- <charset>UTF-8</charset>-->
<!-- </encoder>-->
<!--</appender>-->
<!-- 如果appender里没有限定日志级别那么root可以统一设置如果没有配置那么控制台和文件不会输出任何日志这里root的level不做限制-->
<root level="INFO">
<!-- 允许控制台输出-->
<appender-ref ref="console" />
<!--&lt;!&ndash; 允许文件输出&ndash;&gt;-->
<!--<appender-ref ref="file" />-->
</root>
<logger name="cn.skcks.docking.gb28181.core.sip.logger" level="INFO" />
<logger name="cn.skcks.docking.gb28181" level="DEBUG" />
</configuration>

View File

@ -2,9 +2,22 @@
# 用于缓存打包过程下载的依赖
mkdir repository
curDir=`pwd`
repository=${curDir}/repository
if ! test -e ${curDir}/gb28181-wvp-proxy-starter/ffmpeg;then
xz -d ${curDir}/ffmpeg/ffmpeg-git-amd64-static.tar.xz
tar -xvf ${curDir}/ffmpeg/ffmpeg-git-amd64-static.tar -C ${curDir}/ffmpeg/
mv ${curDir}/ffmpeg/ffmpeg-git*-static/* ${curDir}/ffmpeg
rm -rf ${curDir}/ffmpeg/ffmpeg-git*-static
mkdir -p ${curDir}/gb28181-wvp-proxy-starter/ffmpeg
cp ${curDir}/ffmpeg/ffmpeg ${curDir}/gb28181-wvp-proxy-starter/ffmpeg
cp ${curDir}/ffmpeg/ffprobe ${curDir}/gb28181-wvp-proxy-starter/ffmpeg
cp -r ${curDir}/ffmpeg/model ${curDir}/gb28181-wvp-proxy-starter/ffmpeg
fi
docker run --name maven --rm \
-v ${curDir}:/usr/src/mymaven \
-v ${curDir}/repository:/root/.m2/repository \
-v ${repository}:/root/.m2/repository \
-v ${curDir}/settings.xml:/usr/share/maven/ref/settings.xml \
-v /etc/docker/daemon.json:/etc/docker/daemon.json -v /var/run/docker.sock:/var/run/docker.sock -v /usr/bin/docker:/usr/bin/docker \
-w /usr/src/mymaven \

View File

@ -57,7 +57,7 @@
<!-- <docker.registry.password>XXX</docker.registry.password>-->
<docker.maven.plugin.version>1.4.13</docker.maven.plugin.version>
<gb28181.docking.version>0.0.1-SNAPSHOT</gb28181.docking.version>
<gb28181.docking.version>0.1.0</gb28181.docking.version>
</properties>
<profiles>
@ -126,6 +126,12 @@
<version>${gb28181.docking.version}</version>
</dependency>
<dependency>
<groupId>cn.skcks.docking.gb28181</groupId>
<artifactId>gb28181-service</artifactId>
<version>${gb28181.docking.version}</version>
</dependency>
<!--MapStruct-->
<dependency>
<groupId>org.mapstruct</groupId>

97
test/main.py Normal file
View File

@ -0,0 +1,97 @@
from concurrent.futures import ThreadPoolExecutor
import os
import os.path as path
import sys
import datetime
import urllib
import urllib.parse
import urllib.request
work_path = sys.path[0]
# 设备列表
devices_file = "devices.txt"
devices_file_path = path.join(work_path, devices_file)
# 下载目录
tmp_dir = "download"
tmp_path = path.join(work_path, tmp_dir)
server = "http://127.0.0.1:18183/video"
# server = "http://httpbin.org/anything/video"
def check_or_mk_tmp_dir():
if not path.exists(tmp_path):
os.mkdir(tmp_path)
def check_or_create_devices_file():
if not path.exists(devices_file_path):
with open(devices_file_path, mode="w", encoding="utf8") as f:
f.write("# 设备编码 一行一个\n")
def read_devices_file():
check_or_create_devices_file()
devices = []
with open(devices_file_path, mode="r", encoding="utf8") as f:
while True:
line = f.readline()
if not line:
break
line = line.strip()
if line.startswith("#") or len(line) == 0:
continue
else:
devices.append(line)
print(f"读取设备数量: {len(devices)}")
return devices
def tasks(device: str, start_time: str, end_time: str):
params = {
"start_time": start_time,
"end_time": end_time,
"device_id": device,
"useDownload": True,
}
url_params = urllib.parse.urlencode(params)
url = urllib.parse.urljoin(server, f"?{url_params}")
start = datetime.datetime.now()
start_str = start.strftime("%Y-%m-%d %H:%M:%S.%f")
print(f"{start_str} 开始下载: {url}")
file_path = f"{tmp_path}/{device}_{start_time}_{end_time}.mp4"
urllib.request.urlretrieve(url, file_path)
end = datetime.datetime.now()
print(
f"{device} {start_time}-{end_time}: 下载用时: {(end - start).total_seconds()}")
stats = os.stat(file_path)
print(f"文件 {file_path} 大小: {stats.st_size}")
if __name__ == '__main__':
check_or_mk_tmp_dir()
check_or_create_devices_file()
print(work_path)
# workers = os.cpu_count()
workers = 32
print(f"最大并发数: {workers}")
with ThreadPoolExecutor(max_workers=workers) as worker:
devices = read_devices_file()
# day = datetime.datetime.today()
day = datetime.date(year=2024, month=3, day=11)
# 开始时间
start = datetime.time(hour=8, minute=11, second=15)
# 结束时间
end = datetime.time(hour=8, minute=11, second=30)
start_time = datetime.datetime.combine(day, start).strftime(
"%Y%m%d%H%M%S")
end_time = datetime.datetime.combine(day, end).strftime("%Y%m%d%H%M%S")
for device in devices:
worker.submit(tasks, device, start_time, end_time)

66
test/merge.py Normal file
View File

@ -0,0 +1,66 @@
import re
import subprocess
import os
import os.path as path
import sys
work_path = sys.path[0]
ffmpeg_path = "ffmpeg"
ffprobe_path = "ffprobe"
record_dir = "record"
record_path = path.join(work_path, record_dir)
if not path.exists(record_path):
os.mkdir(record_path)
merge_file = path.join(work_path, "test.mp4")
cmd = f"{ffmpeg_path} -y -loglevel error -f concat -safe 0 -i %s -c copy {merge_file}"
def natural_sort_key(s):
"""
按文件名中的自然数排序
"""
# 将字符串按照数字和非数字部分分割,返回分割后的子串列表
sub_strings = re.split(r'(\d+)', s)
# 如果当前子串由数字组成,则将它转换为整数;否则将其替换成空字符串
sub_strings = [int(c) if c.isdigit() else '' for c in sub_strings]
# 返回子串列表
return sub_strings
file_list = []
for item in os.listdir(record_path):
p = path.join(record_path, item)
if not path.isfile(p) or (not p.endswith(".mp4") and not p.endswith(".rec")):
continue
else:
file_list.append(p)
sorted_file_list = sorted(file_list, key=natural_sort_key)
tmp_merge_file = path.join(work_path, "merge.tmp")
with open(tmp_merge_file, mode="w", encoding="utf8") as f:
for record in sorted_file_list:
f.write(f"file '{record}'\n")
proc = subprocess.Popen(
cmd % tmp_merge_file,
stdin=None,
stdout=None,
shell=True
)
proc.communicate()
os.remove(tmp_merge_file)
meta_cmd = f"{ffprobe_path} -v error -i {merge_file} -print_format json -show_format -show_streams -pretty > {merge_file}.meta.json"
proc = subprocess.Popen(
meta_cmd,
stdin=None,
stdout=None,
shell=True
)
proc.communicate()

84
test/push_zlm.py Normal file
View File

@ -0,0 +1,84 @@
import os
import os.path as path
import select
import sys
import urllib.parse
from concurrent.futures import ThreadPoolExecutor
import subprocess
import platform
work_path = sys.path[0]
# 下载目录
tmp_dir = "download"
tmp_path = path.join(work_path, tmp_dir)
zlm_server = "192.168.3.12"
zlm_rtmp_port = 1936
zlm_auth_params = {
"sign": "41db35390ddad33f83944f44b8b75ded"
}
ffmpeg_path = "ffmpeg"
ffmpeg_read_rate = 1
ffplay_path = "ffplay"
enable_preview = True
workers = os.cpu_count()
def get_rtmp_url(app: str, stream_id: str):
params = urllib.parse.urlencode(zlm_auth_params)
return "rtmp://{}:{}/{}/{}?{}".format(zlm_server, zlm_rtmp_port, app,
stream_id, params)
def check_or_mk_tmp_dir():
if not path.exists(tmp_path):
os.mkdir(tmp_path)
def push_stream(file: str):
stream_id = path.basename(file)
target = get_rtmp_url("ffmpeg", stream_id)
print("开始 ffmpeg 推流 {} => {}", file, target)
cmd = "{} -loglevel error -stream_loop -1 -readrate {} -i {} -t 60 -c copy -f flv {}".format(
ffmpeg_path, ffmpeg_read_rate, file, target)
print(cmd)
proc = subprocess.Popen(
cmd,
stdin=None,
stdout=None,
shell=True
)
if enable_preview and len(
ffplay_path) > 0 and platform.system() == "Windows":
subprocess.Popen(
"ffplay {} -x 400 -y 300 -autoexit".format(target),
stdin=subprocess.DEVNULL,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
shell=True
)
proc.communicate()
print("ffmpeg 结束 {} =/= {}", file, target)
if __name__ == '__main__':
if len(sys.argv) > 1:
try:
workers = int(sys.argv[1])
except:
print("参数解析错误, 将使用默认线程数 {} ".format(workers))
print("最大并发数: {}".format(workers))
with ThreadPoolExecutor(max_workers=workers) as worker:
for item in os.listdir(tmp_path):
p = path.join(tmp_path, item)
if not path.isfile(p) or not p.endswith(".mp4"):
continue
else:
worker.submit(push_stream, p)

44
test/video.py Normal file
View File

@ -0,0 +1,44 @@
import os.path as path
import sys
import shutil
work_path = sys.path[0]
# 设备列表
devices_file = "devices.txt"
devices_file_path = path.join(work_path, devices_file)
# 源文件
source_video_file = "20240311081115_20240311081130.mp4"
def check_or_create_devices_file():
if not path.exists(devices_file_path):
with open(devices_file_path, mode="w", encoding="utf8") as f:
f.write("# 设备编码 一行一个\n")
def read_devices_file():
check_or_create_devices_file()
devices = []
with open(devices_file_path, mode="r", encoding="utf8") as f:
while True:
line = f.readline()
if not line:
break
line = line.strip()
if line.startswith("#") or len(line) == 0:
continue
else:
devices.append(line)
print("读取设备数量: {}".format(len(devices)))
return devices
if __name__ == '__main__':
check_or_create_devices_file()
devices = read_devices_file()
src = path.join(work_path, source_video_file)
for device in devices:
dst = path.join(work_path, "{}_{}".format(device, source_video_file))
shutil.copyfile(src, dst)