Merge branch 'master' into 1078

This commit is contained in:
648540858 2024-03-08 16:22:13 +08:00
commit 79dc7e79d2
452 changed files with 16881 additions and 33260 deletions

View File

@ -49,6 +49,7 @@ https://gitee.com/pan648540858/wvp-GB28181-pro.git
- [X] 支持电子地图支持接入WGS84和GCJ02两种坐标系并且自动转化为合适的坐标系进行展示和分发
- [X] 接入设备
- [X] 视频预览
- [X] 支持主码流子码流切换
- [X] 无限制接入路数,能接入多少设备只取决于你的服务器性能
- [X] 云台控制,控制设备转向,拉近,拉远
- [X] 预置位查询,使用与设置
@ -66,6 +67,7 @@ https://gitee.com/pan648540858/wvp-GB28181-pro.git
- [X] 支持国标网络校时
- [X] 支持播放H264和H265
- [X] 报警信息处理,支持向前端推送报警信息
- [X] 语音对讲
- [X] 支持订阅与通知方法
- [X] 移动位置订阅
- [X] 移动位置通知处理
@ -92,6 +94,7 @@ https://gitee.com/pan648540858/wvp-GB28181-pro.git
- [X] 目录订阅与通知
- [X] 录像查看与播放
- [X] GPS订阅与通知直播推流
- [X] 语音对讲
- [X] 支持自动配置ZLM媒体服务, 减少因配置问题所出现的问题;
- [X] 多流媒体节点,自动选择负载最低的节点使用。
- [X] 支持启用udp多端口模式, 提高udp模式下媒体传输性能;
@ -133,3 +136,11 @@ https://gitee.com/pan648540858/wvp-GB28181-pro.git
[mk1990](https://github.com/mk1990) [SaltFish001](https://github.com/SaltFish001)
ffmpeg -re -i 123.mp3 -acodec pcm_alaw -ar 8000 -ac 1 -f rtsp rtsp://192.168.1.3:30554/broadcast/34020000001320000101_34020000001310000001
ffmpeg -re -i 123.mp3 -acodec pcm_alaw -ar 8000 -ac 1 -f rtsp rtsp://192.168.1.3:30554/talk/34020000001320000011_34020000001370000001
ffmpeg -re -i 123.mp3 -acodec pcm_alaw -ar 8000 -ac 1 -f rtsp rtsp://192.168.1.3:30554/talk/34020000001320000101_34020000001310000001

View File

@ -55,8 +55,8 @@
- [X] 移动设备位置订阅
- [X] 报警订阅
- [X] 目录订阅
- [ ] 语音广播
- [ ] 语音对讲
- [X] 语音广播
- [X] 语音喊话
**作为下级平台**
- [X] 注册
@ -94,8 +94,8 @@
- [X] 移动设备位置订阅
- [ ] 报警订阅
- [X] 目录订阅
- [ ] 语音广播
- [ ] 语音对讲
- [X] 语音广播
- [X] 语音喊话

View File

@ -0,0 +1,76 @@
# 语音对讲
## 流程和原理
语音对讲在国标28181-2016中分为broadcast广播和talk对讲两种模式broadcast模式是从服务端把音频传送到设备端是单向的
需要结合点播视频来实现双向对讲talk模式支持双向不过wvp只处理了和broadcast一样的把音频传递设备这样两种模式可以使用一样的逻辑处理即可。
不同的设备对于两种模式的支持不同且通常差异很大不同的设备对同一个设备的支持也有一些不同所以语音对讲中的兼容和适配也是问题最多的。talk模式因为在国标28181-2022中已经移除所以这里不再讨论它了。
### 1. broadcast模式流程
```plantuml
@startuml
"WVP-PRO" -> "设备": 语音广播通知
"WVP-PRO" <-- "设备": 200OK
"WVP-PRO" <- "设备": 语音广播应答
"WVP-PRO" --> "设备": 200OK
"WVP-PRO" <- "设备": Invite
"WVP-PRO" --> "设备": 200OK(携带SDP消息体)
"WVP-PRO" <-- "设备": ACK
"ZLMediaKit" -> "设备": 向设备发送语音流
@enduml
```
与点播的流程不同的是这里的invite消息是由设备发送给wvp的wvp按照invite协商的方式给设备推送语音流所有对讲的使用那种方式UDP/TCP被动/TCP主动传输语音流由设备决定
## 使用条件与限制
因为invite消息是由设备发送给wvp的这决定了发送语音流的方式这也就决定了有的设备不能用于公网对讲比如大部分的海康设备只支持udp方式收流(目前新版的海康设备已经在着手解决这个问题)那么wvp发流时只能按照sdp中指定的ip端口发流所以如果wvp在公网设备在内网中那么wvp无法连接设备提供的IP发流也就失败了。
与海康不同的大华以及很多执法记录仪厂商是支持tcp主动方式取流的这样是可以实现公网对讲的。
## 使用ffmpeg快速测试
由于浏览器对于音频的采集需要网页支持https才可以所以如果想要实现网页音频对讲那么你必须给wvp和zlm配置证书以使用https。
测试阶段如果只是想测试功能可以用ffmpeg来模拟语音流推送到wvp后可以实现把音频文件推送到摄像头。
测试命令格式如下:
```shell
ffmpeg -re -i {音频文件} -acodec pcm_alaw -ar 8000 -ac 1 -f rtsp 'rtsp://{zlm的IP}:{zlm的RTSP端口}/broadcast/{设备国标编号}_{通道国标编号}?sign={md5(pushKey)}'
```
例如
```shell
ffmpeg -re -i test.mp3 -acodec pcm_alaw -ar 8000 -ac 1 -f rtsp 'rtsp://192.168.1.3:22554/broadcast/34020000001320000001_34020000001320000001?sign=41db35390ddad33f83944f44b8b75ded'
```
测试流程如下:
```plantuml
@startuml
"FFMPEG" -> "ZLMediaKit": 推流到zlm
"WVP-PRO" <- "ZLMediaKit": 通知收到语音对讲推流携带设备和通道信息
"WVP-PRO" -> "设备": 开始语音对讲
"WVP-PRO" <-- "设备": 语音对讲建立成功携带收流端口
"WVP-PRO" -> "ZLMediaKit": 通知zlm将流推送到设备收流端口
"ZLMediaKit" -> "设备": 向设备推流
@enduml
```
如果听到设备播放你推送的音频,那么意味着调用成功,此过程推流即可需要调用任何接口
## 生产环境网页发起语音对讲
生产环境下使用语音对讲如果是自己的客户端设备那么直接上面的ffmpeg测试方式按照固定格式推流到zlm即可。
对于WEB程序主要是局域网和公网的区别两个原因
1. 很多设备不支持公网对讲
2. 公网和局域网获取证书实现https支持的方式不同
### 公网使用
公网你可以直接使用证书厂商或者云服务器厂商提供的证书,这是很方便的。
### 局域网使用
局域网你需要为wvp和zlm生成自签名证书这里我推荐一种生成自签名证书相对方便的方式,
此方式为linux下的一种方式。
下载证书生成工具:
[https://github.com/FiloSottile/mkcert/releases/tag/v1.4.4](https://github.com/FiloSottile/mkcert/releases/tag/v1.4.4)
安装此工具, 进入解压的工具目录,执行
```shell
./mkcert-v1.4.4-linux-amd64 -install
```
生成pem证书
```shell
./mkcert-v1.4.4-linux-amd64 局域网IP 局域网IP2 局域网IP3
```
你会得到两文件*-key.pem和*.pem, 此文件配置到wvp后既可实现证书的加载
生成zlm使用的证书
```shell
cat *.pem *-key.pem> ./zlm.pem
```
得到的文件就是可以给zlm使用的证书
zlm下使用证书有两种方式
1. 替换zlm下的default.pem, 即删除此文件并把zlm.pem重命名为default.pem
2. 在启动zlm的使用添加 `-s zlm.pem`

27
doc/_content/broadcast.md Normal file
View File

@ -0,0 +1,27 @@
# 原理图
## 使用ffmpeg测试语音对讲原理
```plantuml
@startuml
"FFMPEG" -> "ZLMediaKit": 推流到zlm
"WVP-PRO" <- "ZLMediaKit": 通知收到语音对讲推流携带设备和通道信息
"WVP-PRO" -> "设备": 开始语音对讲
"WVP-PRO" <-- "设备": 语音对讲建立成功携带收流端口
"WVP-PRO" -> "ZLMediaKit": 通知zlm将流推送到设备收流端口
"ZLMediaKit" -> "设备": 向设备推流
@enduml
```
## 使用网页测试语音对讲原理
```plantuml
@startuml
"前端页面" -> "WVP-PRO": 请求推流地址
"前端页面" <-- "WVP-PRO": 返回推流地址
"前端页面" -> "ZLMediaKit": 使用webrtc推流到zlm以下过程相同
"WVP-PRO" <- "ZLMediaKit": 通知收到语音对讲推流携带设备和通道信息
"WVP-PRO" -> "设备": 开始语音对讲
"WVP-PRO" <-- "设备": 语音对讲建立成功携带收流端口
"WVP-PRO" -> "ZLMediaKit": 通知zlm将流推送到设备收流端口
"ZLMediaKit" -> "设备": 向设备推流
@enduml
```

View File

@ -153,11 +153,11 @@ user-settings:
# 国标是否录制
record-sip: true
# 是否将日志存储进数据库
logInDatebase: true
logInDatabase: true
# 第三方匹配用于从stream钟获取有效信息
thirdPartyGBIdReg: [\s\S]*
```
如果配置信息无误你可以启动zlm再启动wvp来测试了启动成功的话你可以在wvp的日志下看到zlm已连接的提示。
接下来[部署到服务器](./_content/introduction/deployment.md)何你只是本地运行直接再本地运行即可。
接下来[部署到服务器](./_content/introduction/deployment.md)果你只是本地运行直接在本地运行即可。

View File

@ -21,9 +21,9 @@
4. WVP-PRO与ZLM支持分开部署但是wvp-pro-assist必须与zlm部署在同一台主机;
5. 生产环境按需开放端口但是建议修改默认端口尤其是5060端口易受到攻击;
6. zlm使用docker部署的情况要求端口映射一致比如映射5060,应将外部端口也映射为5060端口;
7. 启动服务以linux为例
### 启动WVP-PRO
**jar包**
7. zlm与wvp会保持高频率的通信所以不要去将wvp与zlm分属在两个网络比如wvp在内网zlm却在公网的情况。
8. 启动服务以linux为例
**启动WVP-PRO**
```shell
nohup java -jar wvp-pro-*.jar &
```

View File

@ -0,0 +1,46 @@
<!-- 点播流程 -->
# 点播流程
> 以下为WVP-PRO级联语音喊话流程。
```plantuml
@startuml
"上级平台" -> "下级平台": 1. 发起语音喊话请求
"上级平台" <-- "下级平台": 2. 200OK
"上级平台" <- "下级平台": 3. 回复Result OK
"上级平台" --> "下级平台": 4. 200OK
"下级平台" -> "设备": 5. 发起语音喊话请求
"下级平台" <-- "设备": 6. 200OK
"下级平台" <- "设备": 7. 回复Result OK
"下级平台" --> "设备": 8. 200OK
"下级平台" <- "设备": 9. invite(broadcast)
"下级平台" --> "设备": 10. 100 trying
"下级平台" --> "设备": 11. 200OK SDP
"下级平台" <-- "设备": 12. ack
"上级平台" <- "下级平台": 13. invite(broadcast)
"上级平台" --> "下级平台": 14. 100 trying
"上级平台" --> "下级平台": 15. 200OK SDP
"上级平台" <-- "下级平台": 16. ack
"上级平台" -> "下级平台": 17. 推送RTP
"下级平台" -> "设备": 18. 推送RTP
@enduml
```
## 注册流程描述如下:
1. 用户从网页或调用接口发起点播请求;
2. WVP-PRO向摄像机发送Invite消息,消息头域中携带 Subject字段,表明点播的视频源ID、发送方媒体流序列号、ZLMediaKit接收流使用的IP、端口号、
接收端媒体流序列号等参数,SDP消息体中 s字段为“Play”代表实时点播y字段描述SSRC值,f字段描述媒体参数。
3. 摄像机向WVP-PRO回复200OK消息体中描述了媒体流发送者发送媒体流的IP、端口、媒体格式、SSRC字段等内容。
4. WVP-PRO向设备回复Ack 会话建立成功。
5. 设备向ZLMediaKit发送实时流。
6. ZLMediaKit向WVP-PRO发送流改变事件。
7. WVP-PRO向WEB用户回复播放地址。
8. ZLMediaKit向WVP发送流无人观看事件。
9. WVP-PRO向设备回复Bye 结束会话。
10. 设备回复200OK会话结束成功。

View File

@ -15,11 +15,13 @@
* [节点管理](_content/ability/node_manger.md)
* [云端录像](_content/ability/cloud_record.md)
* [不间断录像](_content/ability/continuous_recording.md)
* [语音对讲](_content/ability/continuous_broadcast.md)
* **流程与原理**
* [统一编码规则](_content/theory/code.md)
* [树形结构](_content/theory/channel_tree.md)
* [注册流程](_content/theory/register.md)
* [点播流程](_content/theory/play.md)
* [级联语音喊话流程](_content/theory/broadcast_cascade.md)
* **必备技巧**
* [抓包](_content/skill/tcpdump.md)

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

84
pom.xml
View File

@ -6,12 +6,12 @@
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.2</version>
<version>2.7.17</version>
</parent>
<groupId>com.genersoft</groupId>
<artifactId>wvp-pro</artifactId>
<version>2.6.9</version>
<version>2.7.0</version>
<name>web video platform</name>
<description>国标28181视频平台</description>
<packaging>${project.packaging}</packaging>
@ -30,6 +30,7 @@
</releases>
</repository>
</repositories>
<pluginRepositories>
<pluginRepository>
<id>nexus-aliyun</id>
@ -130,9 +131,9 @@
<!-- mysql数据库 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.30</version>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<version>8.2.0</version>
</dependency>
<!--postgresql-->
@ -153,6 +154,13 @@
<scope>system</scope>
<systemPath>${basedir}/libs/jdbc-aarch/kingbase8-8.6.0.jar</systemPath>
</dependency>
<dependency>
<groupId>com.kingbase</groupId>
<artifactId>kingbase8</artifactId>
<version>8.6.0</version>
<scope>system</scope>
<systemPath>${basedir}/libs/jdbc-x86/kingbase8-8.6.0.jar</systemPath>
</dependency>
<!--Mybatis分页插件 -->
<dependency>
@ -161,6 +169,26 @@
<version>1.4.6</version>
</dependency>
<!--在线文档 -->
<!--在线文档 -->
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-ui</artifactId>
<version>1.6.10</version>
</dependency>
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-security</artifactId>
<version>1.6.10</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.baomidou/dynamic-datasource-spring-boot-starter -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>dynamic-datasource-spring-boot-starter</artifactId>
<version>3.6.1</version>
</dependency>
<!--在线文档 -->
<dependency>
<groupId>org.springdoc</groupId>
@ -241,11 +269,11 @@
</dependency>
<!-- https://mvnrepository.com/artifact/net.sf.kxml/kxml2 -->
<!-- <dependency>-->
<!-- <groupId>net.sf.kxml</groupId>-->
<!-- <artifactId>kxml2</artifactId>-->
<!-- <version>2.3.0</version>-->
<!-- </dependency>-->
<!-- <dependency>-->
<!-- <groupId>net.sf.kxml</groupId>-->
<!-- <artifactId>kxml2</artifactId>-->
<!-- <version>2.3.0</version>-->
<!-- </dependency>-->
<!-- jwt实现 -->
<dependency>
@ -265,7 +293,18 @@
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>easyexcel</artifactId>
<version>3.1.1</version>
<version>3.3.2</version>
<exclusions>
<exclusion>
<groupId>org.apache.commons</groupId>
<artifactId>commons-compress</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-compress</artifactId>
<version>1.24.0</version>
</dependency>
<!-- 获取系统信息 -->
@ -280,31 +319,28 @@
<artifactId>spring-session-core</artifactId>
</dependency>
<!-- &lt;!&ndash; 检测文件编码 &ndash;&gt;-->
<!-- &lt;!&ndash; https://mvnrepository.com/artifact/cpdetector/cpdetector &ndash;&gt;-->
<!-- <dependency>-->
<!-- <groupId>cpdetector</groupId>-->
<!-- <artifactId>cpdetector</artifactId>-->
<!-- <version>1.0.8</version>-->
<!-- </dependency>-->
<!-- 检测文件编码 -->
<!-- https://mvnrepository.com/artifact/cpdetector/cpdetector -->
<!--<dependency>-->
<!-- <groupId>cpdetector</groupId>-->
<!-- <artifactId>cpdetector</artifactId>-->
<!-- <version>1.0.8</version>-->
<!--</dependency>-->
<!-- https://mvnrepository.com/artifact/com.google.guava/guava -->
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>31.1-jre</version>
<version>32.1.3-jre</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<!-- <scope>test</scope>-->
<scope>test</scope>
</dependency>
</dependencies>
<build>
<finalName>${project.artifactId}-${project.version}-${maven.build.timestamp}</finalName>
<plugins>
@ -316,6 +352,7 @@
<includeSystemScope>true</includeSystemScope>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
@ -345,7 +382,6 @@
<skipTests>true</skipTests>
</configuration>
</plugin>
</plugins>
<resources>
<resource>

View File

@ -1,2 +0,0 @@
alter table wvp_device_channel
change stream_id stream_id varying(255)

View File

@ -1,7 +1,6 @@
package com.genersoft.iot.vmp.common;
import com.genersoft.iot.vmp.service.bean.SSRCInfo;
import io.swagger.v3.oas.annotations.media.Schema;
/**
* 记录每次发送invite消息的状态
@ -125,20 +124,4 @@ public class InviteInfo {
this.streamMode = streamMode;
}
/*=========================设备主子码流逻辑START====================*/
@Schema(description = "是否为子码流(true-是false-主码流)")
private boolean subStream;
public boolean isSubStream() {
return subStream;
}
public void setSubStream(boolean subStream) {
this.subStream = subStream;
}
}

View File

@ -3,5 +3,7 @@ package com.genersoft.iot.vmp.common;
public enum InviteSessionType {
PLAY,
PLAYBACK,
DOWNLOAD
DOWNLOAD,
BROADCAST,
TALK
}

View File

@ -1,5 +1,6 @@
package com.genersoft.iot.vmp.common;
import com.genersoft.iot.vmp.service.bean.DownloadFileInfo;
import io.swagger.v3.oas.annotations.media.Schema;
import java.io.Serializable;
@ -76,6 +77,8 @@ public class StreamInfo implements Serializable, Cloneable{
private String endTime;
@Schema(description = "进度(录像下载使用)")
private double progress;
@Schema(description = "文件下载地址(录像下载使用)")
private DownloadFileInfo downLoadFilePath;
@Schema(description = "是否暂停(录像回放使用)")
private boolean pause;
@ -237,11 +240,11 @@ public class StreamInfo implements Serializable, Cloneable{
}
}
public void setRtc(String host, int port, int sslPort, String app, String stream, String callIdParam) {
public void setRtc(String host, int port, int sslPort, String app, String stream, String callIdParam, boolean isPlay) {
if (callIdParam != null) {
callIdParam = Objects.equals(callIdParam, "") ? callIdParam : callIdParam.replace("?", "&");
}
String file = String.format("index/api/webrtc?app=%s&stream=%s&type=play%s", app, stream, callIdParam);
String file = String.format("index/api/webrtc?app=%s&stream=%s&type=%s%s", app, stream, isPlay?"play":"push", callIdParam);
if (port > 0) {
this.rtc = new StreamURL("http", host, port, file);
}
@ -523,6 +526,69 @@ public class StreamInfo implements Serializable, Cloneable{
StreamInfo instance = null;
try{
instance = (StreamInfo)super.clone();
if (this.flv != null) {
instance.flv=this.flv.clone();
}
if (this.ws_flv != null ){
instance.ws_flv= this.ws_flv.clone();
}
if (this.hls != null ) {
instance.hls= this.hls.clone();
}
if (this.ws_hls != null ) {
instance.ws_hls= this.ws_hls.clone();
}
if (this.ts != null ) {
instance.ts= this.ts.clone();
}
if (this.ws_ts != null ) {
instance.ws_ts= this.ws_ts.clone();
}
if (this.fmp4 != null ) {
instance.fmp4= this.fmp4.clone();
}
if (this.ws_fmp4 != null ) {
instance.ws_fmp4= this.ws_fmp4.clone();
}
if (this.rtc != null ) {
instance.rtc= this.rtc.clone();
}
if (this.https_flv != null) {
instance.https_flv= this.https_flv.clone();
}
if (this.wss_flv != null) {
instance.wss_flv= this.wss_flv.clone();
}
if (this.https_hls != null) {
instance.https_hls= this.https_hls.clone();
}
if (this.wss_hls != null) {
instance.wss_hls= this.wss_hls.clone();
}
if (this.wss_ts != null) {
instance.wss_ts= this.wss_ts.clone();
}
if (this.https_fmp4 != null) {
instance.https_fmp4= this.https_fmp4.clone();
}
if (this.wss_fmp4 != null) {
instance.wss_fmp4= this.wss_fmp4.clone();
}
if (this.rtcs != null) {
instance.rtcs= this.rtcs.clone();
}
if (this.rtsp != null) {
instance.rtsp= this.rtsp.clone();
}
if (this.rtsps != null) {
instance.rtsps= this.rtsps.clone();
}
if (this.rtmp != null) {
instance.rtmp= this.rtmp.clone();
}
if (this.rtmps != null) {
instance.rtmps= this.rtmps.clone();
}
}catch(CloneNotSupportedException e) {
e.printStackTrace();
}
@ -542,5 +608,11 @@ public class StreamInfo implements Serializable, Cloneable{
this.subStream = subStream;
}
public DownloadFileInfo getDownLoadFilePath() {
return downLoadFilePath;
}
public void setDownLoadFilePath(DownloadFileInfo downLoadFilePath) {
this.downLoadFilePath = downLoadFilePath;
}
}

View File

@ -6,7 +6,7 @@ import java.io.Serializable;
@Schema(description = "流地址信息")
public class StreamURL implements Serializable {
public class StreamURL implements Serializable,Cloneable {
@Schema(description = "协议")
private String protocol;
@ -77,4 +77,8 @@ public class StreamURL implements Serializable {
return null;
}
}
@Override
public StreamURL clone() throws CloneNotSupportedException {
return (StreamURL) super.clone();
}
}

View File

@ -53,7 +53,7 @@ public class VideoManagerConstants {
public static final String MEDIA_TRANSACTION_USED_PREFIX = "VMP_MEDIA_TRANSACTION_";
public static final String MEDIA_STREAM_AUTHORITY = "MEDIA_STREAM_AUTHORITY_";
public static final String MEDIA_STREAM_AUTHORITY = "VMP_MEDIA_STREAM_AUTHORITY_";
public static final String SIP_CSEQ_PREFIX = "VMP_SIP_CSEQ_";
@ -68,8 +68,10 @@ public class VideoManagerConstants {
public static final String SYSTEM_INFO_NET_PREFIX = "VMP_SYSTEM_INFO_NET_";
public static final String SYSTEM_INFO_DISK_PREFIX = "VMP_SYSTEM_INFO_DISK_";
public static final String BROADCAST_WAITE_INVITE = "task_broadcast_waite_invite_";
public static final String REGISTER_EXPIRE_TASK_KEY_PREFIX = "VMP_device_register_expire_";
public static final String PUSH_STREAM_LIST = "VMP_PUSH_STREAM_LIST_";
@ -158,7 +160,9 @@ public class VideoManagerConstants {
public static final String WVP_STREAM_GB_ID_PREFIX = "memberNo_";
public static final String WVP_STREAM_GPS_MSG_PREFIX = "WVP_STREAM_GPS_MSG_";
public static final String WVP_OTHER_SEND_RTP_INFO = "VMP_OTHER_SEND_RTP_INFO_";
public static final String WVP_OTHER_SEND_PS_INFO = "VMP_OTHER_SEND_PS_INFO_";
public static final String WVP_OTHER_RECEIVE_RTP_INFO = "VMP_OTHER_RECEIVE_RTP_INFO_";
public static final String WVP_OTHER_RECEIVE_PS_INFO = "VMP_OTHER_RECEIVE_PS_INFO_";
/**
* Redis Const

View File

@ -51,7 +51,7 @@ public class ApiAccessFilter extends OncePerRequestFilter {
filterChain.doFilter(servletRequest, servletResponse);
if (uriName != null && userSetting != null && userSetting.getLogInDatebase() != null && userSetting.getLogInDatebase()) {
if (uriName != null && userSetting != null && userSetting.getLogInDatabase() != null && userSetting.getLogInDatabase()) {
LogDto logDto = new LogDto();
logDto.setName(uriName);

View File

@ -12,7 +12,10 @@ import org.springframework.core.annotation.Order;
import org.springframework.core.io.ClassPathResource;
import org.springframework.util.ObjectUtils;
import java.io.*;
import java.io.BufferedReader;
import java.io.File;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.file.Files;
import java.util.Map;

View File

@ -0,0 +1,83 @@
package com.genersoft.iot.vmp.conf;
import com.alibaba.fastjson2.JSONObject;
import com.genersoft.iot.vmp.media.zlm.AssistRESTfulUtils;
import com.genersoft.iot.vmp.media.zlm.ZLMRESTfulUtils;
import com.genersoft.iot.vmp.media.zlm.dto.MediaServerItem;
import com.genersoft.iot.vmp.service.IMediaServerService;
import com.genersoft.iot.vmp.service.bean.CloudRecordItem;
import com.genersoft.iot.vmp.storager.dao.CloudRecordServiceMapper;
import com.genersoft.iot.vmp.vmanager.cloudRecord.CloudRecordController;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.io.File;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.List;
/**
* 录像文件定时删除
*/
@Component
public class CloudRecordTimer {
private final static Logger logger = LoggerFactory.getLogger(CloudRecordTimer.class);
@Autowired
private IMediaServerService mediaServerService;
@Autowired
private CloudRecordServiceMapper cloudRecordServiceMapper;
@Autowired
private ZLMRESTfulUtils zlmresTfulUtils;
/**
* 定时查询待删除的录像文件
*/
// @Scheduled(fixedRate = 10000) //每五秒执行一次方便测试
@Scheduled(cron = "0 0 0 * * ?") //每天的0点执行
public void execute(){
logger.info("[录像文件定时清理] 开始清理过期录像文件");
// 获取配置了assist的流媒体节点
List<MediaServerItem> mediaServerItemList = mediaServerService.getAllOnline();
if (mediaServerItemList.isEmpty()) {
return;
}
long result = 0;
for (MediaServerItem mediaServerItem : mediaServerItemList) {
Calendar lastCalendar = Calendar.getInstance();
if (mediaServerItem.getRecordDay() > 0) {
lastCalendar.setTime(new Date());
// 获取保存的最后截至日[因为每个节点都有一个日期也就是支持每个节点设置不同的保存日期
lastCalendar.add(Calendar.DAY_OF_MONTH, -mediaServerItem.getRecordDay());
Long lastDate = lastCalendar.getTimeInMillis();
// 获取到截至日期之前的录像文件列表文件列表满足未被收藏和保持的这两个字段目前共能一致
// 为我自己业务系统相关的代码大家使用的时候直接使用收藏collect这一个类型即可
List<CloudRecordItem> cloudRecordItemList = cloudRecordServiceMapper.queryRecordListForDelete(lastDate, mediaServerItem.getId());
if (cloudRecordItemList.isEmpty()) {
continue;
}
// TODO 后续可以删除空了的过期日期文件夹
for (CloudRecordItem cloudRecordItem : cloudRecordItemList) {
String date = new File(cloudRecordItem.getFilePath()).getParentFile().getName();
JSONObject jsonObject = zlmresTfulUtils.deleteRecordDirectory(mediaServerItem, cloudRecordItem.getApp(),
cloudRecordItem.getStream(), date, cloudRecordItem.getFileName());
if (jsonObject.getInteger("code") != 0) {
logger.warn("[录像文件定时清理] 删除磁盘文件错误: {}:{}", cloudRecordItem.getFilePath(), jsonObject);
}
}
result += cloudRecordServiceMapper.deleteList(cloudRecordItemList);
}
}
logger.info("[录像文件定时清理] 共清理{}个过期录像文件", result);
}
}

View File

@ -1,5 +1,6 @@
package com.genersoft.iot.vmp.conf;
import org.apache.commons.lang3.ObjectUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.scheduling.annotation.Scheduled;
@ -45,6 +46,9 @@ public class DynamicTask {
* @return
*/
public void startCron(String key, Runnable task, int cycleForCatalog) {
if(ObjectUtils.isEmpty(key)) {
return;
}
ScheduledFuture<?> future = futureMap.get(key);
if (future != null) {
if (future.isCancelled()) {
@ -73,6 +77,9 @@ public class DynamicTask {
* @return
*/
public void startDelay(String key, Runnable task, int delay) {
if(ObjectUtils.isEmpty(key)) {
return;
}
stop(key);
// 获取执行的时刻
@ -99,8 +106,11 @@ public class DynamicTask {
}
public boolean stop(String key) {
if(ObjectUtils.isEmpty(key)) {
return false;
}
boolean result = false;
if (futureMap.get(key) != null && !futureMap.get(key).isCancelled() && !futureMap.get(key).isDone()) {
if (!ObjectUtils.isEmpty(futureMap.get(key)) && !futureMap.get(key).isCancelled() && !futureMap.get(key).isDone()) {
result = futureMap.get(key).cancel(false);
futureMap.remove(key);
runnableMap.remove(key);
@ -109,6 +119,9 @@ public class DynamicTask {
}
public boolean contains(String key) {
if(ObjectUtils.isEmpty(key)) {
return false;
}
return futureMap.get(key) != null;
}
@ -117,6 +130,9 @@ public class DynamicTask {
}
public Runnable get(String key) {
if(ObjectUtils.isEmpty(key)) {
return null;
}
return runnableMap.get(key);
}
@ -127,7 +143,8 @@ public class DynamicTask {
public void execute(){
if (futureMap.size() > 0) {
for (String key : futureMap.keySet()) {
if (futureMap.get(key).isDone() || futureMap.get(key).isCancelled()) {
ScheduledFuture<?> future = futureMap.get(key);
if (future.isDone() || future.isCancelled()) {
futureMap.remove(key);
runnableMap.remove(key);
}

View File

@ -32,7 +32,7 @@ public class GlobalResponseAdvice implements ResponseBodyAdvice<Object> {
@Override
public Object beforeBodyWrite(Object body, @NotNull MethodParameter returnType, @NotNull MediaType selectedContentType, @NotNull Class<? extends HttpMessageConverter<?>> selectedConverterType, @NotNull ServerHttpRequest request, @NotNull ServerHttpResponse response) {
// 排除api文档的接口这个接口不需要统一
String[] excludePath = {"/v3/api-docs","/api/v1","/index/hook"};
String[] excludePath = {"/v3/api-docs","/api/v1","/index/hook","/api/video-"};
for (String path : excludePath) {
if (request.getURI().getPath().startsWith(path)) {
return body;
@ -59,8 +59,8 @@ public class GlobalResponseAdvice implements ResponseBodyAdvice<Object> {
* 防止返回string时出错
* @return
*/
@Bean
public HttpMessageConverters fast() {
/*@Bean
public HttpMessageConverters custHttpMessageConverter() {
return new HttpMessageConverters(new FastJsonHttpMessageConverter());
}
}*/
}

View File

@ -81,6 +81,12 @@ public class MediaConfig{
@Value("${media.record-assist-port:0}")
private Integer recordAssistPort = 0;
@Value("${media.record-day:7}")
private Integer recordDay;
@Value("${media.record-path:}")
private String recordPath;
public String getId() {
return id;
}
@ -91,7 +97,7 @@ public class MediaConfig{
public String getHookIp() {
if (ObjectUtils.isEmpty(hookIp)){
return sipIp.split(",")[0];
return sipIp;
}else {
return hookIp;
}
@ -212,13 +218,32 @@ public class MediaConfig{
mediaServerItem.setSendRtpPortRange(rtpSendPortRange);
mediaServerItem.setRecordAssistPort(recordAssistPort);
mediaServerItem.setHookAliveInterval(30.00f);
mediaServerItem.setRecordDay(recordDay);
if (recordPath != null) {
mediaServerItem.setRecordPath(recordPath);
}
mediaServerItem.setCreateTime(DateUtil.getNow());
mediaServerItem.setUpdateTime(DateUtil.getNow());
return mediaServerItem;
}
public Integer getRecordDay() {
return recordDay;
}
public void setRecordDay(Integer recordDay) {
this.recordDay = recordDay;
}
public String getRecordPath() {
return recordPath;
}
public void setRecordPath(String recordPath) {
this.recordPath = recordPath;
}
public String getRtpSendPortRange() {
return rtpSendPortRange;
}

View File

@ -18,6 +18,7 @@ import org.springframework.util.ObjectUtils;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.net.ConnectException;
@ -64,6 +65,18 @@ public class ProxyServletConfig {
return queryStr;
}
@Override
protected HttpResponse doExecute(HttpServletRequest servletRequest, HttpServletResponse servletResponse,
HttpRequest proxyRequest) throws IOException {
HttpResponse response = super.doExecute(servletRequest, servletResponse, proxyRequest);
response.removeHeaders("Access-Control-Allow-Origin");
response.setHeader("Access-Control-Allow-Credentials","true");
response.removeHeaders("Access-Control-Allow-Credentials");
return response;
}
/**
* 异常处理
*/
@ -181,6 +194,18 @@ public class ProxyServletConfig {
return queryStr;
}
@Override
protected HttpResponse doExecute(HttpServletRequest servletRequest, HttpServletResponse servletResponse,
HttpRequest proxyRequest) throws IOException {
HttpResponse response = super.doExecute(servletRequest, servletResponse, proxyRequest);
String origin = servletRequest.getHeader("origin");
response.setHeader("Access-Control-Allow-Origin",origin);
response.setHeader("Access-Control-Allow-Credentials","true");
return response;
}
/**
* 异常处理
*/

View File

@ -4,8 +4,11 @@ import com.genersoft.iot.vmp.gb28181.bean.ParentPlatform;
import com.genersoft.iot.vmp.gb28181.bean.ParentPlatformCatch;
import com.genersoft.iot.vmp.gb28181.transmit.cmd.ISIPCommanderForPlatform;
import com.genersoft.iot.vmp.service.IPlatformService;
import com.genersoft.iot.vmp.service.impl.PlatformServiceImpl;
import com.genersoft.iot.vmp.storager.IRedisCatchStorage;
import com.genersoft.iot.vmp.storager.IVideoManagerStorage;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.core.annotation.Order;
@ -33,6 +36,7 @@ public class SipPlatformRunner implements CommandLineRunner {
@Autowired
private ISIPCommanderForPlatform sipCommanderForPlatform;
private final static Logger logger = LoggerFactory.getLogger(PlatformServiceImpl.class);
@Override
public void run(String... args) throws Exception {
@ -50,9 +54,15 @@ public class SipPlatformRunner implements CommandLineRunner {
redisCatchStorage.updatePlatformCatchInfo(parentPlatformCatch);
if (parentPlatformCatchOld != null) {
// 取消订阅
try {
sipCommanderForPlatform.unregister(parentPlatform, parentPlatformCatchOld.getSipTransactionInfo(), null, (eventResult)->{
platformService.login(parentPlatform);
});
} catch (Exception e) {
logger.error("[命令发送失败] 国标级联 注销: {}", e.getMessage());
platformService.offline(parentPlatform, true);
continue;
}
}
// 设置所有平台离线

View File

@ -1,9 +1,12 @@
package com.genersoft.iot.vmp.conf;
import com.genersoft.iot.vmp.conf.security.JwtUtils;
import io.swagger.v3.oas.models.Components;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Contact;
import io.swagger.v3.oas.models.info.Info;
import io.swagger.v3.oas.models.info.License;
import io.swagger.v3.oas.models.security.SecurityScheme;
import org.springframework.core.annotation.Order;
import org.springdoc.core.GroupedOpenApi;
import org.springframework.beans.factory.annotation.Value;
@ -26,10 +29,14 @@ public class SpringDocConfig {
contact.setName("pan");
contact.setEmail("648540858@qq.com");
return new OpenAPI()
.components(new Components()
.addSecuritySchemes(JwtUtils.HEADER, new SecurityScheme()
.type(SecurityScheme.Type.HTTP)
.bearerFormat("JWT")))
.info(new Info().title("WVP-PRO 接口文档")
.contact(contact)
.description("开箱即用的28181协议视频平台")
.version("v2.0")
.version("v3.1.0")
.license(new License().name("Apache 2.0").url("http://springdoc.org")));
}

View File

@ -39,4 +39,6 @@ public class SystemInfoTimerTask {
}
}
}

View File

@ -23,7 +23,7 @@ public class UserSetting {
private Integer playTimeout = 18000;
private int platformPlayTimeout = 60000;
private int platformPlayTimeout = 20000;
private Boolean interfaceAuthentication = Boolean.TRUE;
@ -31,9 +31,9 @@ public class UserSetting {
private Boolean recordSip = Boolean.TRUE;
private Boolean logInDatebase = Boolean.TRUE;
private Boolean logInDatabase = Boolean.TRUE;
private Boolean usePushingAsStatus = Boolean.TRUE;
private Boolean usePushingAsStatus = Boolean.FALSE;
private Boolean useSourceIpAsStreamIp = Boolean.FALSE;
@ -43,8 +43,6 @@ public class UserSetting {
private Boolean pushAuthority = Boolean.TRUE;
private Boolean gbSendStreamStrict = Boolean.FALSE;
private Boolean syncChannelOnDeviceOnline = Boolean.FALSE;
private Boolean sipLog = Boolean.FALSE;
@ -53,15 +51,15 @@ public class UserSetting {
private Boolean refuseChannelStatusChannelFormNotify = Boolean.FALSE;
private Boolean deviceStatusNotify = Boolean.FALSE;
private Boolean deviceStatusNotify = Boolean.TRUE;
private Boolean useCustomSsrcForParentInvite = Boolean.TRUE;
private String serverId = "000000";
private String recordPath = null;
private String thirdPartyGBIdReg = "[\\s\\S]*";
private String broadcastForPlatform = "UDP";
private String civilCodeFile = "classpath:civilCode.csv";
private List<String> interfaceAuthenticationExcludes = new ArrayList<>();
@ -134,12 +132,12 @@ public class UserSetting {
this.interfaceAuthenticationExcludes = interfaceAuthenticationExcludes;
}
public Boolean getLogInDatebase() {
return logInDatebase;
public Boolean getLogInDatabase() {
return logInDatabase;
}
public void setLogInDatebase(Boolean logInDatebase) {
this.logInDatebase = logInDatebase;
public void setLogInDatabase(Boolean logInDatabase) {
this.logInDatabase = logInDatabase;
}
public String getServerId() {
@ -206,14 +204,6 @@ public class UserSetting {
this.pushAuthority = pushAuthority;
}
public Boolean getGbSendStreamStrict() {
return gbSendStreamStrict;
}
public void setGbSendStreamStrict(Boolean gbSendStreamStrict) {
this.gbSendStreamStrict = gbSendStreamStrict;
}
public Boolean getSyncChannelOnDeviceOnline() {
return syncChannelOnDeviceOnline;
}
@ -222,6 +212,14 @@ public class UserSetting {
this.syncChannelOnDeviceOnline = syncChannelOnDeviceOnline;
}
public String getBroadcastForPlatform() {
return broadcastForPlatform;
}
public void setBroadcastForPlatform(String broadcastForPlatform) {
this.broadcastForPlatform = broadcastForPlatform;
}
public Boolean getSipUseSourceIpAsRemoteAddress() {
return sipUseSourceIpAsRemoteAddress;
}
@ -262,14 +260,6 @@ public class UserSetting {
this.refuseChannelStatusChannelFormNotify = refuseChannelStatusChannelFormNotify;
}
public String getRecordPath() {
return recordPath;
}
public void setRecordPath(String recordPath) {
this.recordPath = recordPath;
}
public int getMaxNotifyCountQueue() {
return maxNotifyCountQueue;
}

View File

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

View File

@ -1,8 +1,10 @@
package com.genersoft.iot.vmp.conf.security;
import com.genersoft.iot.vmp.conf.security.dto.JwtUser;
import org.jose4j.json.JsonUtil;
import com.genersoft.iot.vmp.service.IUserService;
import com.genersoft.iot.vmp.storager.dao.dto.User;
import org.jose4j.jwk.RsaJsonWebKey;
import org.jose4j.jwk.RsaJwkGenerator;
import org.jose4j.jws.AlgorithmIdentifiers;
import org.jose4j.jws.JsonWebSignature;
import org.jose4j.jwt.JwtClaims;
@ -14,45 +16,69 @@ import org.jose4j.jwt.consumer.JwtConsumerBuilder;
import org.jose4j.lang.JoseException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.stereotype.Component;
import java.security.PrivateKey;
import javax.annotation.Resource;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
public class JwtUtils {
@Component
public class JwtUtils implements InitializingBean {
private static final Logger logger = LoggerFactory.getLogger(JwtUtils.class);
private static final String HEADER = "access-token";
public static final String HEADER = "access-token";
private static final String AUDIENCE = "Audience";
private static final long EXPIRED_THRESHOLD = 10 * 60;
private static final String keyId = "3e79646c4dbc408383a9eed09f2b85ae";
private static final String privateKeyStr = "{\"kty\":\"RSA\",\"kid\":\"3e79646c4dbc408383a9eed09f2b85ae\",\"alg\":\"RS256\",\"n\":\"gndmVdiOTSJ5et2HIeTM5f1m61x5ojLUi5HDfvr-jRrESQ5kbKuySGHVwR4QhwinpY1wQqBnwc80tx7cb_6SSqsTOoGln6T_l3k2Pb54ClVnGWiW_u1kmX78V2TZOsVmZmwtdZCMi-2zWIyAdIEXE-gncIehoAgEoq2VAhaCURbJWro_EwzzQwNmCTkDodLAx4npXRd_qSu0Ayp0txym9OFovBXBULRvk4DPiy3i_bPUmCDxzC46pTtFOe9p82uybTehZfULZtXXqRm85FL9n5zkrsTllPNAyEGhgb0RK9sE5nK1m_wNNysDyfLC4EFf1VXTrKm14XNVjc2vqLb7Mw\",\"e\":\"AQAB\",\"d\":\"ed7U_k3rJ4yTk70JtRSIfjKGiEb67BO1TabcymnljKO7RU8nage84zZYuSu_XpQsHk6P1f0Gzxkicghm_Er-FrfVn2pp70Xu52z3yRd6BJUgWLDFk97ngScIyw5OiULKU9SrZk2frDpftNCSUcIgb50F8m0QAnBa_CdPsQKbuuhLv8V8tBAV7F_lAwvSBgu56wRo3hPz5dWH8YeXM7XBfQ9viFMNEKd21sP_j5C7ueUnXT66nBxe3ZJEU3iuMYM6D6dB_KW2GfZC6WmTgvGhhxJD0h7aYmfjkD99MDleB7SkpbvoODOqiQ5Epb7Nyh6kv5u4KUv2CJYtATLZkUeMkQ\",\"p\":\"uBUjWPWtlGksmOqsqCNWksfqJvMcnP_8TDYN7e4-WnHL4N-9HjRuPDnp6kHvCIEi9SEfxm7gNxlRcWegvNQr3IZCz7TnCTexXc5NOklB9OavWFla6u-s3Thn6Tz45-EUjpJr0VJMxhO-KxGmuTwUXBBp4vN6K2qV6rQNFmgkWzk\",\"q\":\"tW_i7cCec56bHkhITL_79dXHz_PLC_f7xlynmlZJGU_d6mqOKmLBNBbTMLnYW8uAFiFzWxDeDHh1o5uF0mSQR-Z1Fg35OftnpbWpy0Cbc2la5WgXQjOwtG1eLYIY2BD3-wQ1VYDBCvowr4FDi-sngxwLqvwmrJ0xjhi99O-Gzcs\",\"dp\":\"q1d5jE85Hz_6M-eTh_lEluEf0NtPEc-vvhw-QO4V-cecNpbrCBdTWBmr4dE3NdpFeJc5ZVFEv-SACyei1MBEh0ItI_pFZi4BmMfy2ELh8ptaMMkTOESYyVy8U7veDq9RnBcr5i1Nqr0rsBkA77-9T6gzdvycBZdzLYAkAmwzEvk\",\"dq\":\"q29A2K08Crs-jmp2Bi8Q_8QzvIX6wSBbwZ4ir24AO-5_HNP56IrPS0yV2GCB0pqCOGb6_Hz_koDvhtuYoqdqvMVAtMoXR3YJBUaVXPt65p4RyNmFwIPe31zHs_BNUTsXVRMw4c16mci03-Af1sEm4HdLfxAp6sfM3xr5wcnhcek\",\"qi\":\"rHPgVTyHUHuYzcxfouyBfb1XAY8nshwn0ddo81o1BccD4Z7zo5It6SefDHjxCAbcmbiCcXBSooLcY-NF5FMv3fg19UE21VyLQltHcVjRRp2tRs4OHcM8yaXIU2x6N6Z6BP2tOksHb9MOBY1wAQzFOAKg_G4Sxev6-_6ud6RISuc\"}";
private static final String publicKeyStr = "{\"kty\":\"RSA\",\"kid\":\"3e79646c4dbc408383a9eed09f2b85ae\",\"alg\":\"RS256\",\"n\":\"gndmVdiOTSJ5et2HIeTM5f1m61x5ojLUi5HDfvr-jRrESQ5kbKuySGHVwR4QhwinpY1wQqBnwc80tx7cb_6SSqsTOoGln6T_l3k2Pb54ClVnGWiW_u1kmX78V2TZOsVmZmwtdZCMi-2zWIyAdIEXE-gncIehoAgEoq2VAhaCURbJWro_EwzzQwNmCTkDodLAx4npXRd_qSu0Ayp0txym9OFovBXBULRvk4DPiy3i_bPUmCDxzC46pTtFOe9p82uybTehZfULZtXXqRm85FL9n5zkrsTllPNAyEGhgb0RK9sE5nK1m_wNNysDyfLC4EFf1VXTrKm14XNVjc2vqLb7Mw\",\"e\":\"AQAB\"}";
/**
* token过期时间(分钟)
*/
public static final long expirationTime = 30;
public static final long expirationTime = 30 * 24 * 60;
public static String createToken(String username, String password, Integer roleId) {
private static RsaJsonWebKey rsaJsonWebKey;
private static IUserService userService;
@Resource
public void setUserService(IUserService userService) {
JwtUtils.userService = userService;
}
@Override
public void afterPropertiesSet() {
try {
rsaJsonWebKey = generateRsaJsonWebKey();
} catch (JoseException e) {
logger.error("生成RsaJsonWebKey报错。", e);
}
}
/**
* 创建密钥对
* @throws JoseException JoseException
*/
private RsaJsonWebKey generateRsaJsonWebKey() throws JoseException {
// 生成一个RSA密钥对该密钥对将用于JWT的签名和验证包装在JWK中
RsaJsonWebKey rsaJsonWebKey = RsaJwkGenerator.generateJwk(2048);
// 给JWK一个密钥ID
rsaJsonWebKey.setKeyId(keyId);
return rsaJsonWebKey;
}
public static String createToken(String username) {
try {
/*
* iss (issuer) 发行人
*
* sub (subject) 主题
*
* aud (audience) 接收方 用户
*
* exp (expiration time) 到期时间
*
* nbf (not before) 在此之前不可用
*
* iat (issued at) jwt的签发时间
*/
//Payload
JwtClaims claims = new JwtClaims();
claims.setGeneratedJwtId();
claims.setIssuedAtToNow();
@ -62,9 +88,7 @@ public class JwtUtils {
claims.setSubject("login");
claims.setAudience(AUDIENCE);
//添加自定义参数,必须是字符串类型
claims.setClaim("username", username);
claims.setClaim("password", password);
claims.setClaim("roleId", roleId);
claims.setClaim("userName", username);
//jws
JsonWebSignature jws = new JsonWebSignature();
@ -73,12 +97,10 @@ public class JwtUtils {
jws.setKeyIdHeaderValue(keyId);
jws.setPayload(claims.toJson());
PrivateKey privateKey = new RsaJsonWebKey(JsonUtil.parseJson(privateKeyStr)).getPrivateKey();
jws.setKey(privateKey);
jws.setKey(rsaJsonWebKey.getPrivateKey());
//get token
String idToken = jws.getCompactSerialization();
return idToken;
return jws.getCompactSerialization();
} catch (JoseException e) {
logger.error("[Token生成失败] {}", e.getMessage());
}
@ -90,7 +112,6 @@ public class JwtUtils {
return HEADER;
}
public static JwtUser verifyToken(String token) {
JwtUser jwtUser = new JwtUser();
@ -103,7 +124,7 @@ public class JwtUtils {
.setRequireSubject()
//.setExpectedIssuer("")
.setExpectedAudience(AUDIENCE)
.setVerificationKey(new RsaJsonWebKey(JsonUtil.parseJson(publicKeyStr)).getPublicKey())
.setVerificationKey(rsaJsonWebKey.getPublicKey())
.build();
JwtClaims claims = consumer.processToClaims(token);
@ -113,26 +134,27 @@ public class JwtUtils {
long timeRemaining = LocalDateTime.now().toEpochSecond(ZoneOffset.ofHours(8)) - expirationTime.getValue();
if (timeRemaining < 5 * 60) {
jwtUser.setStatus(JwtUser.TokenStatus.EXPIRING_SOON);
}else {
} else {
jwtUser.setStatus(JwtUser.TokenStatus.NORMAL);
}
String username = (String) claims.getClaimValue("username");
String password = (String) claims.getClaimValue("password");
Long roleId = (Long) claims.getClaimValue("roleId");
String username = (String) claims.getClaimValue("userName");
User user = userService.getUserByUsername(username);
jwtUser.setUserName(username);
jwtUser.setPassword(password);
jwtUser.setRoleId(roleId.intValue());
jwtUser.setPassword(user.getPassword());
jwtUser.setRoleId(user.getRole().getId());
jwtUser.setUserId(user.getId());
return jwtUser;
} catch (InvalidJwtException e) {
if (e.hasErrorCode(ErrorCodes.EXPIRED)) {
jwtUser.setStatus(JwtUser.TokenStatus.EXPIRED);
}else {
} else {
jwtUser.setStatus(JwtUser.TokenStatus.EXCEPTION);
}
return jwtUser;
}catch (Exception e) {
} catch (Exception e) {
logger.error("[Token解析失败] {}", e.getMessage());
jwtUser.setStatus(JwtUser.TokenStatus.EXPIRED);
return jwtUser;

View File

@ -1,12 +1,12 @@
package com.genersoft.iot.vmp.conf.security;
import com.genersoft.iot.vmp.conf.UserSetting;
import org.springframework.core.annotation.Order;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
@ -25,9 +25,11 @@ import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
/**
* 配置Spring Security
*
* @author lin
*/
@Configuration
@ -67,6 +69,8 @@ public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
matchers.add("/");
matchers.add("/#/**");
matchers.add("/static/**");
matchers.add("/swagger-ui.html");
matchers.add("/swagger-ui/");
matchers.add("/index.html");
matchers.add("/doc.html");
matchers.add("/webjars/**");
@ -75,7 +79,8 @@ public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
matchers.add("/js/**");
matchers.add("/api/device/query/snap/**");
matchers.add("/record_proxy/*/**");
matchers.addAll(userSetting.getInterfaceAuthenticationExcludes());
matchers.add("/api/emit");
matchers.add("/favicon.ico");
// 可以直接访问的静态数据
web.ignoring().antMatchers(matchers.toArray(new String[0]));
}
@ -83,6 +88,7 @@ public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
/**
* 配置认证方式
*
* @param auth
* @throws Exception
*/
@ -111,7 +117,7 @@ public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
.authorizeRequests()
.requestMatchers(CorsUtils::isPreFlightRequest).permitAll()
.antMatchers(userSetting.getInterfaceAuthenticationExcludes().toArray(new String[0])).permitAll()
.antMatchers("/api/user/login","/index/hook/**","/zlm_Proxy/FhTuMYqB2HeCuNOb/record/t/1/2023-03-25/16:35:07-16:35:16-9353.mp4").permitAll()
.antMatchers("/api/user/login", "/index/hook/**", "/swagger-ui/**", "/doc.html").permitAll()
.anyRequest().authenticated()
// 异常处理器
.and()
@ -124,18 +130,24 @@ public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
}
CorsConfigurationSource configurationSource(){
CorsConfigurationSource configurationSource() {
// 配置跨域
CorsConfiguration corsConfiguration = new CorsConfiguration();
corsConfiguration.setAllowedHeaders(Arrays.asList("*"));
corsConfiguration.setAllowedMethods(Arrays.asList("*"));
corsConfiguration.setMaxAge(3600L);
if (userSetting.getAllowedOrigins() != null && !userSetting.getAllowedOrigins().isEmpty()) {
corsConfiguration.setAllowCredentials(true);
corsConfiguration.setAllowedOrigins(userSetting.getAllowedOrigins());
}else {
corsConfiguration.setAllowCredentials(false);
corsConfiguration.setAllowedOrigins(Collections.singletonList(CorsConfiguration.ALL));
}
corsConfiguration.setExposedHeaders(Arrays.asList(JwtUtils.getHeader()));
UrlBasedCorsConfigurationSource url = new UrlBasedCorsConfigurationSource();
url.registerCorsConfiguration("/**",corsConfiguration);
url.registerCorsConfiguration("/**", corsConfiguration);
return url;
}

View File

@ -21,6 +21,7 @@ public class JwtUser {
EXCEPTION
}
private int userId;
private String userName;
private String password;
@ -29,6 +30,14 @@ public class JwtUser {
private TokenStatus status;
public int getUserId() {
return userId;
}
public void setUserId(int userId) {
this.userId = userId;
}
public String getUserName() {
return userName;
}

View File

@ -100,6 +100,9 @@ public class LoginUser implements UserDetails, CredentialsContainer {
return user.getRole();
}
public String getPushKey() {
return user.getPushKey();
}
public String getAccessToken() {
return accessToken;

View File

@ -0,0 +1,159 @@
package com.genersoft.iot.vmp.gb28181.bean;
import com.genersoft.iot.vmp.media.zlm.dto.MediaServerItem;
import com.genersoft.iot.vmp.vmanager.gb28181.play.bean.AudioBroadcastEvent;
import gov.nist.javax.sip.message.SIPResponse;
/**
* 缓存语音广播的状态
* @author lin
*/
public class AudioBroadcastCatch {
public AudioBroadcastCatch(
String deviceId,
String channelId,
MediaServerItem mediaServerItem,
String app,
String stream,
AudioBroadcastEvent event,
AudioBroadcastCatchStatus status,
boolean isFromPlatform
) {
this.deviceId = deviceId;
this.channelId = channelId;
this.status = status;
this.event = event;
this.isFromPlatform = isFromPlatform;
this.app = app;
this.stream = stream;
this.mediaServerItem = mediaServerItem;
}
public AudioBroadcastCatch() {
}
/**
* 设备编号
*/
private String deviceId;
/**
* 通道编号
*/
private String channelId;
/**
* 流媒体信息
*/
private MediaServerItem mediaServerItem;
/**
* 关联的流APP
*/
private String app;
/**
* 关联的流STREAM
*/
private String stream;
/**
* 是否是级联语音喊话
*/
private boolean isFromPlatform;
/**
* 语音广播状态
*/
private AudioBroadcastCatchStatus status;
/**
* 请求信息
*/
private SipTransactionInfo sipTransactionInfo;
/**
* 请求结果回调
*/
private AudioBroadcastEvent event;
public String getDeviceId() {
return deviceId;
}
public void setDeviceId(String deviceId) {
this.deviceId = deviceId;
}
public String getChannelId() {
return channelId;
}
public void setChannelId(String channelId) {
this.channelId = channelId;
}
public AudioBroadcastCatchStatus getStatus() {
return status;
}
public void setStatus(AudioBroadcastCatchStatus status) {
this.status = status;
}
public SipTransactionInfo getSipTransactionInfo() {
return sipTransactionInfo;
}
public MediaServerItem getMediaServerItem() {
return mediaServerItem;
}
public void setMediaServerItem(MediaServerItem mediaServerItem) {
this.mediaServerItem = mediaServerItem;
}
public String getApp() {
return app;
}
public void setApp(String app) {
this.app = app;
}
public String getStream() {
return stream;
}
public void setStream(String stream) {
this.stream = stream;
}
public boolean isFromPlatform() {
return isFromPlatform;
}
public void setFromPlatform(boolean fromPlatform) {
isFromPlatform = fromPlatform;
}
public void setSipTransactionInfo(SipTransactionInfo sipTransactionInfo) {
this.sipTransactionInfo = sipTransactionInfo;
}
public AudioBroadcastEvent getEvent() {
return event;
}
public void setEvent(AudioBroadcastEvent event) {
this.event = event;
}
public void setSipTransactionInfoByRequset(SIPResponse sipResponse) {
this.sipTransactionInfo = new SipTransactionInfo(sipResponse);
}
}

View File

@ -0,0 +1,15 @@
package com.genersoft.iot.vmp.gb28181.bean;
/**
* 语音广播状态
* @author lin
*/
public enum AudioBroadcastCatchStatus {
// 发送语音广播消息等待对方回复语音广播
Ready,
// 收到回复等待invite消息
WaiteInvite,
// 收到invite消息
Ok,
}

View File

@ -165,7 +165,7 @@ public class Device {
* 是否开启ssrc校验默认关闭开启可以防止串流
*/
@Schema(description = "是否开启ssrc校验默认关闭开启可以防止串流")
private boolean ssrcCheck = true;
private boolean ssrcCheck = false;
/**
* 地理坐标系 目前支持 WGS84,GCJ02
@ -188,8 +188,8 @@ public class Device {
@Schema(description = "设备注册的事务信息")
private SipTransactionInfo sipTransactionInfo;
@Schema(description = "控制语音对讲流程释放收到ACK后发流")
private boolean broadcastPushAfterAck;
public String getDeviceId() {
return deviceId;
@ -452,20 +452,11 @@ public class Device {
public void setSipTransactionInfo(SipTransactionInfo sipTransactionInfo) {
this.sipTransactionInfo = sipTransactionInfo;
}
/*======================设备主子码流逻辑START=========================*/
@Schema(description = "开启主子码流切换的开关false-不开启true-开启)")
private boolean switchPrimarySubStream;
public boolean isSwitchPrimarySubStream() {
return switchPrimarySubStream;
public boolean isBroadcastPushAfterAck() {
return broadcastPushAfterAck;
}
public void setSwitchPrimarySubStream(boolean switchPrimarySubStream) {
this.switchPrimarySubStream = switchPrimarySubStream;
public void setBroadcastPushAfterAck(boolean broadcastPushAfterAck) {
this.broadcastPushAfterAck = broadcastPushAfterAck;
}
/*======================设备主子码流逻辑END=========================*/
}

View File

@ -246,6 +246,10 @@ public class DeviceChannel {
@Schema(description = "GPS的更新时间")
private String gpsTime;
@Schema(description = "码流标识,优先级高于设备中码流标识," +
"用于选择码流时组成码流标识。默认为null不设置。可选值: stream/streamnumber/streamprofile/streamMode")
private String streamIdentification;
public int getId() {
return id;
}
@ -574,4 +578,12 @@ public class DeviceChannel {
public void setGpsTime(String gpsTime) {
this.gpsTime = gpsTime;
}
public String getStreamIdentification() {
return streamIdentification;
}
public void setStreamIdentification(String streamIdentification) {
this.streamIdentification = streamIdentification;
}
}

View File

@ -0,0 +1,44 @@
package com.genersoft.iot.vmp.gb28181.bean;
/**
* 码流索引标识
*/
public enum GbSteamIdentification {
/**
* 主码流 stream:0
* 子码流 stream:1s
*/
streamMain("stream", new String[]{"0","1"}),
/**
* 国标28181-2022定义的方式
* 主码流 streamnumber:0
* 子码流 streamnumber:1
*/
streamnumber("streamnumber", new String[]{"0","1"}),
/**
* 主码流 streamprofile:0
* 子码流 streamprofile:1
*/
streamprofile("streamprofile", new String[]{"0","1"}),
/**
* 适用的品牌 TP-LINK
*/
streamMode("streamMode", new String[]{"main","sub"}),
;
GbSteamIdentification(String value, String[] indexArray) {
this.value = value;
this.indexArray = indexArray;
}
private String value;
private String[] indexArray;
public String getValue() {
return value;
}
public String[] getIndexArray() {
return indexArray;
}
}

View File

View File

View File

View File

View File

@ -2,7 +2,7 @@ package com.genersoft.iot.vmp.gb28181.bean;
public enum InviteStreamType {
PLAY,PLAYBACK,DOWNLOAD,PUSH,PROXY,CLOUD_RECORD_PUSH,CLOUD_RECORD_PROXY
PLAY,PLAYBACK,DOWNLOAD,PUSH,PROXY,CLOUD_RECORD_PUSH,CLOUD_RECORD_PROXY,BROADCAST,TALK
}

View File

View File

@ -66,7 +66,7 @@ public class ParentPlatform {
* 设备端口
*/
@Schema(description = "设备端口")
private String devicePort;
private int devicePort;
/**
* SIP认证用户名(默认使用设备国标编号)
@ -186,6 +186,9 @@ public class ParentPlatform {
@Schema(description = "是否作为消息通道")
private boolean asMessageChannel;
@Schema(description = "是否作为消息通道")
private boolean autoPushChannel;
public Integer getId() {
return id;
}
@ -258,11 +261,11 @@ public class ParentPlatform {
this.deviceIp = deviceIp;
}
public String getDevicePort() {
public int getDevicePort() {
return devicePort;
}
public void setDevicePort(String devicePort) {
public void setDevicePort(int devicePort) {
this.devicePort = devicePort;
}
@ -425,4 +428,12 @@ public class ParentPlatform {
public void setAsMessageChannel(boolean asMessageChannel) {
this.asMessageChannel = asMessageChannel;
}
public boolean isAutoPushChannel() {
return autoPushChannel;
}
public void setAutoPushChannel(boolean autoPushChannel) {
this.autoPushChannel = autoPushChannel;
}
}

View File

View File

View File

View File

View File

View File

View File

@ -49,7 +49,7 @@ public class SendRtpItem {
/**
* 设备推流的streamId
*/
private String streamId;
private String stream;
/**
* 是否为tcp
@ -117,6 +117,11 @@ public class SendRtpItem {
*/
private InviteStreamType playType;
/**
* 发流的同时收流
*/
private String receiveStream;
public String getIp() {
return ip;
}
@ -181,12 +186,12 @@ public class SendRtpItem {
this.app = app;
}
public String getStreamId() {
return streamId;
public String getStream() {
return stream;
}
public void setStreamId(String streamId) {
this.streamId = streamId;
public void setStream(String stream) {
this.stream = stream;
}
public boolean isTcp() {
@ -292,4 +297,12 @@ public class SendRtpItem {
public void setRtcp(boolean rtcp) {
this.rtcp = rtcp;
}
public String getReceiveStream() {
return receiveStream;
}
public void setReceiveStream(String receiveStream) {
this.receiveStream = receiveStream;
}
}

View File

View File

@ -1,6 +1,5 @@
package com.genersoft.iot.vmp.gb28181.bean;
import gov.nist.javax.sip.message.SIPRequest;
import gov.nist.javax.sip.message.SIPResponse;
public class SipTransactionInfo {
@ -10,11 +9,23 @@ public class SipTransactionInfo {
private String toTag;
private String viaBranch;
// 自己是否媒体流发送者
private boolean asSender;
public SipTransactionInfo(SIPResponse response, boolean asSender) {
this.callId = response.getCallIdHeader().getCallId();
this.fromTag = response.getFromTag();
this.toTag = response.getToTag();
this.viaBranch = response.getTopmostViaHeader().getBranch();
this.asSender = asSender;
}
public SipTransactionInfo(SIPResponse response) {
this.callId = response.getCallIdHeader().getCallId();
this.fromTag = response.getFromTag();
this.toTag = response.getToTag();
this.viaBranch = response.getTopmostViaHeader().getBranch();
this.asSender = false;
}
public SipTransactionInfo() {
@ -51,4 +62,12 @@ public class SipTransactionInfo {
public void setViaBranch(String viaBranch) {
this.viaBranch = viaBranch;
}
public boolean isAsSender() {
return asSender;
}
public void setAsSender(boolean asSender) {
this.asSender = asSender;
}
}

View File

View File

@ -2,12 +2,9 @@ package com.genersoft.iot.vmp.gb28181.bean;
import com.genersoft.iot.vmp.common.VideoManagerConstants;
import com.genersoft.iot.vmp.conf.DynamicTask;
import com.genersoft.iot.vmp.conf.UserSetting;
import com.genersoft.iot.vmp.gb28181.task.ISubscribeTask;
import com.genersoft.iot.vmp.gb28181.task.impl.MobilePositionSubscribeHandlerTask;
import com.genersoft.iot.vmp.gb28181.transmit.cmd.ISIPCommanderForPlatform;
import com.genersoft.iot.vmp.service.IPlatformService;
import com.genersoft.iot.vmp.storager.IRedisCatchStorage;
import com.genersoft.iot.vmp.storager.IVideoManagerStorage;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@ -24,6 +21,9 @@ public class SubscribeHolder {
@Autowired
private DynamicTask dynamicTask;
@Autowired
private UserSetting userSetting;
private final String taskOverduePrefix = "subscribe_overdue_";
private static ConcurrentHashMap<String, SubscribeInfo> catalogMap = new ConcurrentHashMap<>();
@ -32,12 +32,14 @@ public class SubscribeHolder {
public void putCatalogSubscribe(String platformId, SubscribeInfo subscribeInfo) {
catalogMap.put(platformId, subscribeInfo);
if (subscribeInfo.getExpires() > 0) {
// 添加订阅到期
String taskOverdueKey = taskOverduePrefix + "catalog_" + platformId;
// 添加任务处理订阅过期
dynamicTask.startDelay(taskOverdueKey, () -> removeCatalogSubscribe(subscribeInfo.getId()),
subscribeInfo.getExpires() * 1000);
}
}
public SubscribeInfo getCatalogSubscribe(String platformId) {
return catalogMap.get(platformId);
@ -50,7 +52,7 @@ public class SubscribeHolder {
Runnable runnable = dynamicTask.get(taskOverdueKey);
if (runnable instanceof ISubscribeTask) {
ISubscribeTask subscribeTask = (ISubscribeTask) runnable;
subscribeTask.stop();
subscribeTask.stop(null);
}
// 添加任务处理订阅过期
dynamicTask.stop(taskOverdueKey);
@ -58,17 +60,19 @@ public class SubscribeHolder {
public void putMobilePositionSubscribe(String platformId, SubscribeInfo subscribeInfo) {
mobilePositionMap.put(platformId, subscribeInfo);
String key = VideoManagerConstants.SIP_SUBSCRIBE_PREFIX + "MobilePosition_" + platformId;
String key = VideoManagerConstants.SIP_SUBSCRIBE_PREFIX + userSetting.getServerId() + "MobilePosition_" + platformId;
// 添加任务处理GPS定时推送
dynamicTask.startCron(key, new MobilePositionSubscribeHandlerTask(platformId),
subscribeInfo.getGpsInterval() * 1000);
String taskOverdueKey = taskOverduePrefix + "MobilePosition_" + platformId;
if (subscribeInfo.getExpires() > 0) {
// 添加任务处理订阅过期
dynamicTask.startDelay(taskOverdueKey, () -> {
removeMobilePositionSubscribe(subscribeInfo.getId());
},
subscribeInfo.getExpires() * 1000);
}
}
public SubscribeInfo getMobilePositionSubscribe(String platformId) {
return mobilePositionMap.get(platformId);
@ -76,14 +80,14 @@ public class SubscribeHolder {
public void removeMobilePositionSubscribe(String platformId) {
mobilePositionMap.remove(platformId);
String key = VideoManagerConstants.SIP_SUBSCRIBE_PREFIX + "MobilePosition_" + platformId;
String key = VideoManagerConstants.SIP_SUBSCRIBE_PREFIX + userSetting.getServerId() + "MobilePosition_" + platformId;
// 结束任务处理GPS定时推送
dynamicTask.stop(key);
String taskOverdueKey = taskOverduePrefix + "MobilePosition_" + platformId;
Runnable runnable = dynamicTask.get(taskOverdueKey);
if (runnable instanceof ISubscribeTask) {
ISubscribeTask subscribeTask = (ISubscribeTask) runnable;
subscribeTask.stop();
subscribeTask.stop(null);
}
// 添加任务处理订阅过期
dynamicTask.stop(taskOverdueKey);

View File

@ -18,6 +18,9 @@ public class SubscribeInfo {
}
public SubscribeInfo() {
}
private String id;
private SIPRequest request;
@ -33,6 +36,21 @@ public class SubscribeInfo {
private String sn;
private int gpsInterval;
/**
* 模拟的FromTag
*/
private String simulatedFromTag;
/**
* 模拟的ToTag
*/
private String simulatedToTag;
/**
* 模拟的CallID
*/
private String simulatedCallId;
public String getId() {
return id;
}
@ -96,4 +114,28 @@ public class SubscribeInfo {
public void setGpsInterval(int gpsInterval) {
this.gpsInterval = gpsInterval;
}
public String getSimulatedFromTag() {
return simulatedFromTag;
}
public void setSimulatedFromTag(String simulatedFromTag) {
this.simulatedFromTag = simulatedFromTag;
}
public String getSimulatedCallId() {
return simulatedCallId;
}
public void setSimulatedCallId(String simulatedCallId) {
this.simulatedCallId = simulatedCallId;
}
public String getSimulatedToTag() {
return simulatedToTag;
}
public void setSimulatedToTag(String simulatedToTag) {
this.simulatedToTag = simulatedToTag;
}
}

View File

View File

View File

@ -1,8 +1,8 @@
package com.genersoft.iot.vmp.gb28181.conf;
import gov.nist.core.StackLogger;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.spi.LocationAwareLogger;
import org.springframework.stereotype.Component;
import java.util.Properties;
@ -10,7 +10,39 @@ import java.util.Properties;
@Component
public class StackLoggerImpl implements StackLogger {
private final static Logger logger = LoggerFactory.getLogger(StackLoggerImpl.class);
/**
* 完全限定类名(Fully Qualified Class Name)用于定位日志位置
*/
private static final String FQCN = StackLoggerImpl.class.getName();
/**
* 获取栈中类信息(以便底层日志记录系统能够提取正确的位置信息(方法名行号))
* @return LocationAwareLogger
*/
private static LocationAwareLogger getLocationAwareLogger() {
return (LocationAwareLogger) LoggerFactory.getLogger(new Throwable().getStackTrace()[4].getClassName());
}
/**
* 封装打印日志的位置信息
* @param level 日志级别
* @param message 日志事件的消息
*/
private static void log(int level, String message) {
LocationAwareLogger locationAwareLogger = getLocationAwareLogger();
locationAwareLogger.log(null, FQCN, level, message, null, null);
}
/**
* 封装打印日志的位置信息
* @param level 日志级别
* @param message 日志事件的消息
*/
private static void log(int level, String message, Throwable throwable) {
LocationAwareLogger locationAwareLogger = getLocationAwareLogger();
locationAwareLogger.log(null, FQCN, level, message, null, throwable);
}
@Override
public void logStackTrace() {
@ -34,27 +66,27 @@ public class StackLoggerImpl implements StackLogger {
@Override
public void logDebug(String message) {
// logger.debug(message);
log(LocationAwareLogger.INFO_INT, message);
}
@Override
public void logDebug(String message, Exception ex) {
// logger.debug(message);
log(LocationAwareLogger.INFO_INT, message, ex);
}
@Override
public void logTrace(String message) {
logger.trace(message);
log(LocationAwareLogger.INFO_INT, message);
}
@Override
public void logFatalError(String message) {
// logger.error(message);
log(LocationAwareLogger.INFO_INT, message);
}
@Override
public void logError(String message) {
// logger.error(message);
log(LocationAwareLogger.INFO_INT, message);
}
@Override
@ -69,17 +101,17 @@ public class StackLoggerImpl implements StackLogger {
@Override
public void logError(String message, Exception ex) {
// logger.error(message);
log(LocationAwareLogger.INFO_INT, message, ex);
}
@Override
public void logWarning(String message) {
logger.warn(message);
log(LocationAwareLogger.INFO_INT, message);
}
@Override
public void logInfo(String message) {
logger.info(message);
log(LocationAwareLogger.INFO_INT, message);
}
@Override

View File

View File

@ -2,6 +2,8 @@ package com.genersoft.iot.vmp.gb28181.event;
import com.genersoft.iot.vmp.gb28181.bean.DeviceNotFoundEvent;
import gov.nist.javax.sip.message.SIPRequest;
import gov.nist.javax.sip.message.SIPResponse;
import org.apache.commons.lang3.ObjectUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.scheduling.annotation.Scheduled;
@ -11,8 +13,7 @@ import javax.sip.DialogTerminatedEvent;
import javax.sip.ResponseEvent;
import javax.sip.TimeoutEvent;
import javax.sip.TransactionTerminatedEvent;
import javax.sip.header.CallIdHeader;
import javax.sip.message.Response;
import javax.sip.header.WarningHeader;
import java.time.Instant;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@ -60,7 +61,7 @@ public class SipSubscribe {
logger.debug("errorSubscribes.size:{}",errorSubscribes.size());
}
public interface Event { void response(EventResult eventResult) ;
public interface Event { void response(EventResult eventResult);
}
/**
@ -77,8 +78,10 @@ public class SipSubscribe {
dialogTerminated,
// 设备未找到
deviceNotFoundEvent,
// 设备未找到
cmdSendFailEvent
// 消息发送失败
cmdSendFailEvent,
// 消息发送失败
failedToGetPort
}
public static class EventResult<EventObject>{
@ -95,14 +98,27 @@ public class SipSubscribe {
this.event = event;
if (event instanceof ResponseEvent) {
ResponseEvent responseEvent = (ResponseEvent)event;
Response response = responseEvent.getResponse();
SIPResponse response = (SIPResponse)responseEvent.getResponse();
this.type = EventResultType.response;
if (response != null) {
this.msg = response.getReasonPhrase();
this.statusCode = response.getStatusCode();
WarningHeader warningHeader = (WarningHeader)response.getHeader(WarningHeader.NAME);
if (warningHeader != null && !ObjectUtils.isEmpty(warningHeader.getText())) {
this.msg = "";
if (warningHeader.getCode() > 0) {
this.msg += warningHeader.getCode() + ":";
}
if (warningHeader.getAgent() != null) {
this.msg += warningHeader.getCode() + ":";
}
if (warningHeader.getText() != null) {
this.msg += warningHeader.getText();
}
}else {
this.msg = response.getReasonPhrase();
}
this.statusCode = response.getStatusCode();
this.callId = response.getCallIdHeader().getCallId();
}
this.callId = ((CallIdHeader)response.getHeader(CallIdHeader.NAME)).getCallId();
}else if (event instanceof TimeoutEvent) {
TimeoutEvent timeoutEvent = (TimeoutEvent)event;
this.type = EventResultType.timeout;

View File

@ -1,55 +1,68 @@
package com.genersoft.iot.vmp.gb28181.event.alarm;
import org.springframework.context.ApplicationListener;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import java.io.IOException;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.Map;
import org.jetbrains.annotations.NotNull;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.ApplicationListener;
import org.springframework.stereotype.Component;
import java.io.PrintWriter;
import java.util.Iterator;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* @description: 报警事件监听
* @author: lawrencehj
* @data: 2021-01-20
* 报警事件监听器.
*
* @author lawrencehj
* @author <a href="mailto:xiaoQQya@126.com">xiaoQQya</a>
* @since 2021/01/20
*/
@Component
public class AlarmEventListener implements ApplicationListener<AlarmEvent> {
private final static Logger logger = LoggerFactory.getLogger(AlarmEventListener.class);
private static final Logger logger = LoggerFactory.getLogger(AlarmEventListener.class);
private static Map<String, SseEmitter> sseEmitters = new Hashtable<>();
private static final Map<String, PrintWriter> SSE_CACHE = new ConcurrentHashMap<>();
public void addSseEmitters(String browserId, SseEmitter sseEmitter) {
sseEmitters.put(browserId, sseEmitter);
public void addSseEmitter(String browserId, PrintWriter writer) {
SSE_CACHE.put(browserId, writer);
logger.info("SSE 在线数量: {}", SSE_CACHE.size());
}
public void removeSseEmitter(String browserId, PrintWriter writer) {
SSE_CACHE.remove(browserId, writer);
logger.info("SSE 在线数量: {}", SSE_CACHE.size());
}
@Override
public void onApplicationEvent(AlarmEvent event) {
public void onApplicationEvent(@NotNull AlarmEvent event) {
if (logger.isDebugEnabled()) {
logger.debug("设备报警事件触发deviceId" + event.getAlarmInfo().getDeviceId() + ", "
+ event.getAlarmInfo().getAlarmDescription());
logger.debug("设备报警事件触发, deviceId: {}, {}", event.getAlarmInfo().getDeviceId(), event.getAlarmInfo().getAlarmDescription());
}
String msg = "<strong>设备编码:</strong> <i>" + event.getAlarmInfo().getDeviceId() + "</i>"
+ "<br><strong>报警描述:</strong> <i>" + event.getAlarmInfo().getAlarmDescription() + "</i>"
+ "<br><strong>报警时间:</strong> <i>" + event.getAlarmInfo().getAlarmTime() + "</i>"
+ "<br><strong>报警位置:</strong> <i>" + event.getAlarmInfo().getLongitude() + "</i>"
+ ", <i>" + event.getAlarmInfo().getLatitude() + "</i>";
for (Iterator<Map.Entry<String, SseEmitter>> it = sseEmitters.entrySet().iterator(); it.hasNext();) {
Map.Entry<String, SseEmitter> emitter = it.next();
logger.info("推送到SSE连接浏览器ID: " + emitter.getKey());
String msg = "<strong>设备编号:</strong> <i>" + event.getAlarmInfo().getDeviceId() + "</i>"
+ "<br><strong>通道编号:</strong> <i>" + event.getAlarmInfo().getChannelId() + "</i>"
+ "<br><strong>报警描述:</strong> <i>" + event.getAlarmInfo().getAlarmDescription() + "</i>"
+ "<br><strong>报警时间:</strong> <i>" + event.getAlarmInfo().getAlarmTime() + "</i>";
for (Iterator<Map.Entry<String, PrintWriter>> it = SSE_CACHE.entrySet().iterator(); it.hasNext(); ) {
Map.Entry<String, PrintWriter> response = it.next();
logger.info("推送到 SSE 连接, 浏览器 ID: {}", response.getKey());
try {
emitter.getValue().send(msg);
} catch (IOException | IllegalStateException e) {
if (logger.isDebugEnabled()) {
logger.debug("SSE连接已关闭");
PrintWriter writer = response.getValue();
if (writer.checkError()) {
it.remove();
continue;
}
// 移除已关闭的连接
String sseMsg = "event:message\n" +
"data:" + msg + "\n" +
"\n";
writer.write(sseMsg);
writer.flush();
} catch (Exception e) {
it.remove();
}
}

View File

@ -35,7 +35,7 @@ public class RecordEndEventListener implements ApplicationListener<RecordEndEven
int sumNum = event.getRecordInfo().getSumNum();
logger.info("录像查询完成事件触发deviceId{}, channelId: {}, 录像数量{}/{}条", event.getRecordInfo().getDeviceId(),
event.getRecordInfo().getChannelId(), count,sumNum);
if (handlerMap.size() > 0) {
if (!handlerMap.isEmpty()) {
RecordEndEventHandler handler = handlerMap.get(deviceId + channelId);
if (handler !=null){
handler.handler(event.getRecordInfo());
@ -43,6 +43,9 @@ public class RecordEndEventListener implements ApplicationListener<RecordEndEven
handlerMap.remove(deviceId + channelId);
}
}
}else {
logger.info("录像查询完成事件触发, 但是订阅为空取消发送deviceId{}, channelId: {}",
event.getRecordInfo().getDeviceId(), event.getRecordInfo().getChannelId());
}
}
@ -53,6 +56,7 @@ public class RecordEndEventListener implements ApplicationListener<RecordEndEven
* @param recordEndEventHandler
*/
public void addEndEventHandler(String device, String channelId, RecordEndEventHandler recordEndEventHandler) {
logger.info("录像查询事件添加监听deviceId{}, channelId: {}", device, channelId);
handlerMap.put(device + channelId, recordEndEventHandler);
}
/**
@ -61,6 +65,7 @@ public class RecordEndEventListener implements ApplicationListener<RecordEndEven
* @param channelId
*/
public void delEndEventHandler(String device, String channelId) {
logger.info("录像查询事件移除监听deviceId{}, channelId: {}", device, channelId);
handlerMap.remove(device + channelId);
}

View File

@ -93,7 +93,10 @@ public class CatalogEventLister implements ApplicationListener<CatalogEvent> {
}
if (event.getGbStreams() != null && event.getGbStreams().size() > 0){
for (GbStream gbStream : event.getGbStreams()) {
if (gbStream.getStreamType().equals("push") && !userSetting.isUsePushingAsStatus()) {
if (gbStream != null
&& gbStream.getStreamType() != null
&& gbStream.getStreamType().equals("push")
&& !userSetting.isUsePushingAsStatus()) {
continue;
}
DeviceChannel deviceChannelByStream = gbStreamService.getDeviceChannelListByStream(gbStream, gbStream.getCatalogId(), parentPlatform);
@ -145,13 +148,13 @@ public class CatalogEventLister implements ApplicationListener<CatalogEvent> {
if (event.getDeviceChannels() != null) {
deviceChannelList.addAll(event.getDeviceChannels());
}
if (event.getGbStreams() != null && event.getGbStreams().size() > 0){
if (event.getGbStreams() != null && !event.getGbStreams().isEmpty()){
for (GbStream gbStream : event.getGbStreams()) {
deviceChannelList.add(
gbStreamService.getDeviceChannelListByStreamWithStatus(gbStream, gbStream.getCatalogId(), parentPlatform));
}
}
if (deviceChannelList.size() > 0) {
if (!deviceChannelList.isEmpty()) {
logger.info("[Catalog事件: {}]平台:{},影响通道{}个", event.getType(), event.getPlatformId(), deviceChannelList.size());
try {
sipCommanderFroPlatform.sendNotifyForCatalogAddOrUpdate(event.getType(), parentPlatform, deviceChannelList, subscribe, null);
@ -160,10 +163,10 @@ public class CatalogEventLister implements ApplicationListener<CatalogEvent> {
logger.error("[命令发送失败] 国标级联 Catalog通知: {}", e.getMessage());
}
}
}else if (parentPlatformMap.keySet().size() > 0) {
}else if (!parentPlatformMap.keySet().isEmpty()) {
for (String gbId : parentPlatformMap.keySet()) {
List<ParentPlatform> parentPlatforms = parentPlatformMap.get(gbId);
if (parentPlatforms != null && parentPlatforms.size() > 0) {
if (parentPlatforms != null && !parentPlatforms.isEmpty()) {
for (ParentPlatform platform : parentPlatforms) {
SubscribeInfo subscribeInfo = subscribeHolder.getCatalogSubscribe(platform.getServerGBId());
if (subscribeInfo == null) {

View File

@ -0,0 +1,103 @@
package com.genersoft.iot.vmp.gb28181.session;
import com.genersoft.iot.vmp.conf.SipConfig;
import com.genersoft.iot.vmp.gb28181.bean.AudioBroadcastCatch;
import com.genersoft.iot.vmp.gb28181.utils.SipUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
import java.util.stream.Stream;
/**
* 语音广播消息管理类
* @author lin
*/
@Component
public class AudioBroadcastManager {
@Autowired
private SipConfig config;
public static Map<String, AudioBroadcastCatch> data = new ConcurrentHashMap<>();
public void update(AudioBroadcastCatch audioBroadcastCatch) {
if (SipUtils.isFrontEnd(audioBroadcastCatch.getDeviceId())) {
data.put(audioBroadcastCatch.getDeviceId(), audioBroadcastCatch);
}else {
data.put(audioBroadcastCatch.getDeviceId() + audioBroadcastCatch.getChannelId(), audioBroadcastCatch);
}
}
public void del(String deviceId, String channelId) {
if (SipUtils.isFrontEnd(deviceId)) {
data.remove(deviceId);
}else {
data.remove(deviceId + channelId);
}
}
public void delByDeviceId(String deviceId) {
for (String key : data.keySet()) {
if (key.startsWith(deviceId)) {
data.remove(key);
}
}
}
public List<AudioBroadcastCatch> getAll(){
Collection<AudioBroadcastCatch> values = data.values();
return new ArrayList<>(values);
}
public boolean exit(String deviceId, String channelId) {
for (String key : data.keySet()) {
if (SipUtils.isFrontEnd(deviceId)) {
return key.equals(deviceId);
}else {
return key.equals(deviceId + channelId);
}
}
return false;
}
public AudioBroadcastCatch get(String deviceId, String channelId) {
AudioBroadcastCatch audioBroadcastCatch;
if (SipUtils.isFrontEnd(deviceId)) {
audioBroadcastCatch = data.get(deviceId);
}else {
audioBroadcastCatch = data.get(deviceId + channelId);
}
if (audioBroadcastCatch == null) {
Stream<AudioBroadcastCatch> allAudioBroadcastCatchStreamForDevice = data.values().stream().filter(
audioBroadcastCatchItem -> Objects.equals(audioBroadcastCatchItem.getDeviceId(), deviceId));
List<AudioBroadcastCatch> audioBroadcastCatchList = allAudioBroadcastCatchStreamForDevice.collect(Collectors.toList());
if (audioBroadcastCatchList.size() == 1 && Objects.equals(config.getId(), channelId)) {
audioBroadcastCatch = audioBroadcastCatchList.get(0);
}
}
return audioBroadcastCatch;
}
public List<AudioBroadcastCatch> get(String deviceId) {
List<AudioBroadcastCatch> audioBroadcastCatchList= new ArrayList<>();
if (SipUtils.isFrontEnd(deviceId)) {
if (data.get(deviceId) != null) {
audioBroadcastCatchList.add(data.get(deviceId));
}
}else {
for (String key : data.keySet()) {
if (key.startsWith(deviceId)) {
audioBroadcastCatchList.add(data.get(key));
}
}
}
return audioBroadcastCatchList;
}
}

View File

@ -0,0 +1,86 @@
package com.genersoft.iot.vmp.gb28181.session;
import com.genersoft.iot.vmp.common.CommonCallback;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.util.Calendar;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* 通用回调管理
*/
@Component
public class CommonSessionManager {
public static Map<String, CommonSession> callbackMap = new ConcurrentHashMap<>();
/**
* 存储回调相关的信息
*/
class CommonSession{
public String session;
public long createTime;
public int timeout;
public CommonCallback<Object> callback;
public CommonCallback<String> timeoutCallback;
}
/**
* 添加回调
* @param sessionId 唯一标识
* @param callback 回调
* @param timeout 超时时间, 单位分钟
*/
public void add(String sessionId, CommonCallback<Object> callback, CommonCallback<String> timeoutCallback,
Integer timeout) {
CommonSession commonSession = new CommonSession();
commonSession.session = sessionId;
commonSession.callback = callback;
commonSession.createTime = System.currentTimeMillis();
if (timeoutCallback != null) {
commonSession.timeoutCallback = timeoutCallback;
}
if (timeout != null) {
commonSession.timeout = timeout;
}
callbackMap.put(sessionId, commonSession);
}
public void add(String sessionId, CommonCallback<Object> callback) {
add(sessionId, callback, null, 1);
}
public CommonCallback<Object> get(String sessionId, boolean destroy) {
CommonSession commonSession = callbackMap.get(sessionId);
if (destroy) {
callbackMap.remove(sessionId);
}
return commonSession.callback;
}
public CommonCallback<Object> get(String sessionId) {
return get(sessionId, false);
}
public void delete(String sessionID) {
callbackMap.remove(sessionID);
}
@Scheduled(fixedRate= 60) //每分钟执行一次
public void execute(){
Calendar cal = Calendar.getInstance();
cal.add(Calendar.MINUTE, -1);
for (String session : callbackMap.keySet()) {
if (callbackMap.get(session).createTime < cal.getTimeInMillis()) {
// 超时
if (callbackMap.get(session).timeoutCallback != null) {
callbackMap.get(session).timeoutCallback.run("timeout");
}
callbackMap.remove(session);
}
}
}
}

View File

@ -8,6 +8,7 @@ import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.Set;
/**
@ -37,7 +38,8 @@ public class SSRCFactory {
public void initMediaServerSSRC(String mediaServerId, Set<String> usedSet) {
String ssrcPrefix = sipConfig.getDomain().substring(3, 8);
String sipDomain = sipConfig.getDomain();
String ssrcPrefix = sipDomain.length() >= 8 ? sipDomain.substring(3, 8) : sipDomain;
String redisKey = SSRC_INFO_KEY + userSetting.getServerId() + "_" + mediaServerId;
List<String> ssrcList = new ArrayList<>();
for (int i = 1; i < MAX_STREAM_COUNT; i++) {
@ -118,7 +120,7 @@ public class SSRCFactory {
*/
public boolean hasMediaServerSSRC(String mediaServerId) {
String redisKey = SSRC_INFO_KEY + userSetting.getServerId() + "_" + mediaServerId;
return redisTemplate.opsForSet().members(redisKey) != null;
return Boolean.TRUE.equals(redisTemplate.hasKey(redisKey));
}
}

View File

@ -75,6 +75,33 @@ public class VideoStreamSessionManager {
return (SsrcTransaction)redisTemplate.opsForValue().get(scanResult.get(0));
}
public SsrcTransaction getSsrcTransactionByCallId(String callId){
if (ObjectUtils.isEmpty(callId)) {
return null;
}
String key = VideoManagerConstants.MEDIA_TRANSACTION_USED_PREFIX + userSetting.getServerId() + "_*_*_" + callId+ "_*";
List<Object> scanResult = RedisUtil.scan(redisTemplate, key);
if (!scanResult.isEmpty()) {
return (SsrcTransaction)redisTemplate.opsForValue().get(scanResult.get(0));
}else {
key = VideoManagerConstants.MEDIA_TRANSACTION_USED_PREFIX + userSetting.getServerId() + "_*_*_play_*";
scanResult = RedisUtil.scan(redisTemplate, key);
if (scanResult.isEmpty()) {
return null;
}
for (Object keyObj : scanResult) {
SsrcTransaction ssrcTransaction = (SsrcTransaction)redisTemplate.opsForValue().get(keyObj);
if (ssrcTransaction.getSipTransactionInfo() != null &&
ssrcTransaction.getSipTransactionInfo().getCallId().equals(callId)) {
return ssrcTransaction;
}
}
return null;
}
}
public List<SsrcTransaction> getSsrcTransactionForAll(String deviceId, String channelId, String callId, String stream){
if (ObjectUtils.isEmpty(deviceId)) {
deviceId ="*";
@ -117,8 +144,19 @@ public class VideoStreamSessionManager {
}
public void remove(String deviceId, String channelId, String stream) {
SsrcTransaction ssrcTransaction = getSsrcTransaction(deviceId, channelId, null, stream);
if (ssrcTransaction == null) {
List<SsrcTransaction> ssrcTransactionList = getSsrcTransactionForAll(deviceId, channelId, null, stream);
if (ssrcTransactionList == null || ssrcTransactionList.isEmpty()) {
return;
}
for (SsrcTransaction ssrcTransaction : ssrcTransactionList) {
redisTemplate.delete(VideoManagerConstants.MEDIA_TRANSACTION_USED_PREFIX + userSetting.getServerId() + "_"
+ deviceId + "_" + channelId + "_" + ssrcTransaction.getCallId() + "_" + ssrcTransaction.getStream());
}
}
public void removeByCallId(String deviceId, String channelId, String callId) {
SsrcTransaction ssrcTransaction = getSsrcTransaction(deviceId, channelId, callId, null);
if (ssrcTransaction == null ) {
return;
}
redisTemplate.delete(VideoManagerConstants.MEDIA_TRANSACTION_USED_PREFIX + userSetting.getServerId() + "_"

View File

@ -1,10 +1,10 @@
package com.genersoft.iot.vmp.gb28181.task;
import javax.sip.DialogState;
import com.genersoft.iot.vmp.common.CommonCallback;
/**
* @author lin
*/
public interface ISubscribeTask extends Runnable{
void stop();
void stop(CommonCallback<Boolean> callback);
}

View File

@ -12,13 +12,19 @@ import com.genersoft.iot.vmp.media.zlm.dto.MediaServerItem;
import com.genersoft.iot.vmp.service.IDeviceService;
import com.genersoft.iot.vmp.service.IMediaServerService;
import com.genersoft.iot.vmp.service.IPlatformService;
import com.genersoft.iot.vmp.service.impl.PlatformServiceImpl;
import com.genersoft.iot.vmp.storager.IRedisCatchStorage;
import com.genersoft.iot.vmp.storager.IVideoManagerStorage;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import javax.sip.InvalidArgumentException;
import javax.sip.SipException;
import java.text.ParseException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@ -59,6 +65,8 @@ public class SipRunner implements CommandLineRunner {
@Autowired
private ISIPCommanderForPlatform commanderForPlatform;
private final static Logger logger = LoggerFactory.getLogger(PlatformServiceImpl.class);
@Override
public void run(String... args) throws Exception {
List<Device> deviceList = deviceService.getAllOnlineDevice();
@ -98,23 +106,29 @@ public class SipRunner implements CommandLineRunner {
if (sendRtpItems.size() > 0) {
for (SendRtpItem sendRtpItem : sendRtpItems) {
MediaServerItem mediaServerItem = mediaServerService.getOne(sendRtpItem.getMediaServerId());
redisCatchStorage.deleteSendRTPServer(sendRtpItem.getPlatformId(),sendRtpItem.getChannelId(), sendRtpItem.getCallId(),sendRtpItem.getStreamId());
redisCatchStorage.deleteSendRTPServer(sendRtpItem.getPlatformId(),sendRtpItem.getChannelId(), sendRtpItem.getCallId(),sendRtpItem.getStream());
if (mediaServerItem != null) {
ssrcFactory.releaseSsrc(sendRtpItem.getMediaServerId(), sendRtpItem.getSsrc());
Map<String, Object> param = new HashMap<>();
param.put("vhost","__defaultVhost__");
param.put("app",sendRtpItem.getApp());
param.put("stream",sendRtpItem.getStreamId());
param.put("stream",sendRtpItem.getStream());
param.put("ssrc",sendRtpItem.getSsrc());
JSONObject jsonObject = zlmresTfulUtils.stopSendRtp(mediaServerItem, param);
if (jsonObject != null && jsonObject.getInteger("code") == 0) {
ParentPlatform platform = platformService.queryPlatformByServerGBId(sendRtpItem.getPlatformId());
if (platform != null) {
try {
commanderForPlatform.streamByeCmd(platform, sendRtpItem.getCallId());
} catch (InvalidArgumentException | ParseException | SipException e) {
logger.error("[命令发送失败] 国标级联 发送BYE: {}", e.getMessage());
}
}
}
}
}
}
}
}

View File

@ -1,5 +1,6 @@
package com.genersoft.iot.vmp.gb28181.task.impl;
import com.genersoft.iot.vmp.common.CommonCallback;
import com.genersoft.iot.vmp.conf.DynamicTask;
import com.genersoft.iot.vmp.gb28181.bean.Device;
import com.genersoft.iot.vmp.gb28181.task.ISubscribeTask;
@ -7,14 +8,13 @@ import com.genersoft.iot.vmp.gb28181.transmit.cmd.ISIPCommander;
import gov.nist.javax.sip.message.SIPRequest;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.scheduling.annotation.Async;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import javax.sip.*;
import javax.sip.DialogState;
import javax.sip.InvalidArgumentException;
import javax.sip.ResponseEvent;
import javax.sip.SipException;
import javax.sip.header.ToHeader;
import java.text.ParseException;
import java.util.Timer;
import java.util.TimerTask;
/**
* 目录订阅任务
@ -71,7 +71,7 @@ public class CatalogSubscribeTask implements ISubscribeTask {
}
@Override
public void stop() {
public void stop(CommonCallback<Boolean> callback) {
/**
* dialog 的各个状态
* EARLY-> Early state状态-初始请求发送以后收到了一个临时响应消息
@ -89,17 +89,20 @@ public class CatalogSubscribeTask implements ISubscribeTask {
ResponseEvent event = (ResponseEvent) eventResult.event;
if (event.getResponse().getRawContent() != null) {
// 成功
logger.info("[取消目录订阅订阅]成功: {}", device.getDeviceId());
logger.info("[取消目录订阅]成功: {}", device.getDeviceId());
}else {
// 成功
logger.info("[取消目录订阅订阅]成功: {}", device.getDeviceId());
logger.info("[取消目录订阅]成功: {}", device.getDeviceId());
}
if (callback != null) {
callback.run(event.getResponse().getRawContent() != null);
}
},eventResult -> {
// 失败
logger.warn("[取消目录订阅订阅]失败,信令发送失败: {}-{} ", device.getDeviceId(), eventResult.msg);
logger.warn("[取消目录订阅]失败,信令发送失败: {}-{} ", device.getDeviceId(), eventResult.msg);
});
} catch (InvalidArgumentException | SipException | ParseException e) {
logger.error("[命令发送失败] 取消目录订阅订阅: {}", e.getMessage());
logger.error("[命令发送失败] 取消目录订阅: {}", e.getMessage());
}
}
}

View File

@ -1,20 +1,9 @@
package com.genersoft.iot.vmp.gb28181.task.impl;
import com.genersoft.iot.vmp.conf.DynamicTask;
import com.genersoft.iot.vmp.gb28181.bean.*;
import com.genersoft.iot.vmp.common.CommonCallback;
import com.genersoft.iot.vmp.gb28181.task.ISubscribeTask;
import com.genersoft.iot.vmp.gb28181.transmit.cmd.ISIPCommanderForPlatform;
import com.genersoft.iot.vmp.service.IPlatformService;
import com.genersoft.iot.vmp.service.bean.GPSMsgInfo;
import com.genersoft.iot.vmp.storager.IRedisCatchStorage;
import com.genersoft.iot.vmp.storager.IVideoManagerStorage;
import com.genersoft.iot.vmp.utils.SpringBeanFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.scheduling.annotation.Async;
import javax.sip.DialogState;
import java.util.List;
/**
* 向已经订阅(移动位置)的上级发送MobilePosition消息
@ -38,7 +27,7 @@ public class MobilePositionSubscribeHandlerTask implements ISubscribeTask {
}
@Override
public void stop() {
public void stop(CommonCallback<Boolean> callback) {
}
}

View File

@ -1,21 +1,19 @@
package com.genersoft.iot.vmp.gb28181.task.impl;
import com.genersoft.iot.vmp.common.CommonCallback;
import com.genersoft.iot.vmp.conf.DynamicTask;
import com.genersoft.iot.vmp.gb28181.bean.Device;
import com.genersoft.iot.vmp.gb28181.task.ISubscribeTask;
import com.genersoft.iot.vmp.gb28181.transmit.cmd.ISIPCommander;
import gov.nist.javax.sip.message.SIPRequest;
import gov.nist.javax.sip.message.SIPResponse;
import org.dom4j.Element;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.scheduling.annotation.Async;
import javax.sip.*;
import javax.sip.InvalidArgumentException;
import javax.sip.ResponseEvent;
import javax.sip.SipException;
import javax.sip.header.ToHeader;
import java.text.ParseException;
import java.util.Timer;
import java.util.TimerTask;
/**
* 移动位置订阅的定时更新
@ -70,7 +68,7 @@ public class MobilePositionSubscribeTask implements ISubscribeTask {
}
@Override
public void stop() {
public void stop(CommonCallback<Boolean> callback) {
/**
* dialog 的各个状态
* EARLY-> Early state状态-初始请求发送以后收到了一个临时响应消息
@ -92,6 +90,9 @@ public class MobilePositionSubscribeTask implements ISubscribeTask {
// 成功
logger.info("[取消移动位置订阅]成功: {}", device.getDeviceId());
}
if (callback != null) {
callback.run(event.getResponse().getRawContent() != null);
}
},eventResult -> {
// 失败
logger.warn("[取消移动位置订阅]失败,信令发送失败: {}-{} ", device.getDeviceId(), eventResult.msg);

View File

@ -66,17 +66,17 @@ public class SIPSender {
// 添加错误订阅
if (errorEvent != null) {
sipSubscribe.addErrorSubscribe(callIdHeader.getCallId(), (eventResult -> {
errorEvent.response(eventResult);
sipSubscribe.removeErrorSubscribe(eventResult.callId);
sipSubscribe.removeOkSubscribe(eventResult.callId);
errorEvent.response(eventResult);
}));
}
// 添加订阅
if (okEvent != null) {
sipSubscribe.addOkSubscribe(callIdHeader.getCallId(), eventResult -> {
okEvent.response(eventResult);
sipSubscribe.removeOkSubscribe(eventResult.callId);
sipSubscribe.removeErrorSubscribe(eventResult.callId);
okEvent.response(eventResult);
});
}
if ("TCP".equals(transport)) {

View File

@ -2,8 +2,10 @@ package com.genersoft.iot.vmp.gb28181.transmit.cmd;
import com.genersoft.iot.vmp.common.StreamInfo;
import com.genersoft.iot.vmp.conf.exception.SsrcTransactionNotFoundException;
import com.genersoft.iot.vmp.gb28181.bean.*;
import com.genersoft.iot.vmp.gb28181.bean.Device;
import com.genersoft.iot.vmp.gb28181.bean.DeviceAlarm;
import com.genersoft.iot.vmp.gb28181.bean.DeviceChannel;
import com.genersoft.iot.vmp.gb28181.event.SipSubscribe;
import com.genersoft.iot.vmp.media.zlm.ZlmHttpHookSubscribe;
import com.genersoft.iot.vmp.media.zlm.dto.MediaServerItem;
@ -96,9 +98,9 @@ public interface ISIPCommander {
/**
* 请求预览视频流
* @param device 视频设备
* @param channelId 预览通道
* @param channel 预览通道
*/
void playStreamCmd(MediaServerItem mediaServerItem, SSRCInfo ssrcInfo, Device device, String channelId, ZlmHttpHookSubscribe.Event event, SipSubscribe.Event okEvent, SipSubscribe.Event errorEvent) throws InvalidArgumentException, SipException, ParseException;
void playStreamCmd(MediaServerItem mediaServerItem, SSRCInfo ssrcInfo, Device device, DeviceChannel channel, ZlmHttpHookSubscribe.Event event, SipSubscribe.Event okEvent, SipSubscribe.Event errorEvent) throws InvalidArgumentException, SipException, ParseException;
/**
* 请求回放视频流
@ -123,13 +125,19 @@ public interface ISIPCommander {
String startTime, String endTime, int downloadSpeed, ZlmHttpHookSubscribe.Event hookEvent,
SipSubscribe.Event errorEvent,SipSubscribe.Event okEvent) throws InvalidArgumentException, SipException, ParseException;
/**
* 视频流停止
*/
void streamByeCmd(Device device, String channelId, String stream, String callId, SipSubscribe.Event okEvent) throws InvalidArgumentException, SipException, ParseException, SsrcTransactionNotFoundException;
void talkStreamCmd(MediaServerItem mediaServerItem, SendRtpItem sendRtpItem, Device device, String channelId, String callId, ZlmHttpHookSubscribe.Event event, ZlmHttpHookSubscribe.Event eventForPush, SipSubscribe.Event okEvent, SipSubscribe.Event errorEvent) throws InvalidArgumentException, SipException, ParseException;
void streamByeCmd(Device device, String channelId, String stream, String callId) throws InvalidArgumentException, ParseException, SipException, SsrcTransactionNotFoundException;
void streamByeCmd(Device device, String channelId, SipTransactionInfo sipTransactionInfo, SipSubscribe.Event okEvent) throws InvalidArgumentException, SipException, ParseException, SsrcTransactionNotFoundException;
/**
* 回放暂停
*/
@ -159,21 +167,15 @@ public interface ISIPCommander {
void playbackControlCmd(Device device, StreamInfo streamInfo, String content,SipSubscribe.Event errorEvent, SipSubscribe.Event okEvent) throws SipException, InvalidArgumentException, ParseException;
/**
* 语音广播
*
* @param device 视频设备
* @param channelId 预览通道
*/
void audioBroadcastCmd(Device device,String channelId);
void streamByeCmdForDeviceInvite(Device device, String channelId, SipTransactionInfo sipTransactionInfo, SipSubscribe.Event okEvent) throws InvalidArgumentException, SipException, ParseException, SsrcTransactionNotFoundException;
/**
* /**
* 语音广播
*
* @param device 视频设备
*/
void audioBroadcastCmd(Device device, SipSubscribe.Event okEvent) throws InvalidArgumentException, SipException, ParseException;
void audioBroadcastCmd(Device device) throws InvalidArgumentException, SipException, ParseException;
void audioBroadcastCmd(Device device, String channelId, SipSubscribe.Event okEvent, SipSubscribe.Event errorEvent) throws InvalidArgumentException, SipException, ParseException;
/**
* 音视频录像控制
@ -334,7 +336,7 @@ public interface ISIPCommander {
* @param endTime 报警发生终止时间可选
* @return true = 命令发送成功
*/
void alarmSubscribe(Device device, int expires, String startPriority, String endPriority, String alarmMethod, String alarmType, String startTime, String endTime) throws InvalidArgumentException, SipException, ParseException;
void alarmSubscribe(Device device, int expires, String startPriority, String endPriority, String alarmMethod, String startTime, String endTime) throws InvalidArgumentException, SipException, ParseException;
/**
* 订阅取消订阅目录信息
@ -361,4 +363,5 @@ public interface ISIPCommander {
*/
void sendAlarmMessage(Device device, DeviceAlarm deviceAlarm) throws InvalidArgumentException, SipException, ParseException;
}

View File

@ -1,8 +1,12 @@
package com.genersoft.iot.vmp.gb28181.transmit.cmd;
import com.genersoft.iot.vmp.conf.exception.SsrcTransactionNotFoundException;
import com.genersoft.iot.vmp.gb28181.bean.*;
import com.genersoft.iot.vmp.gb28181.event.SipSubscribe;
import com.genersoft.iot.vmp.media.zlm.ZlmHttpHookSubscribe;
import com.genersoft.iot.vmp.media.zlm.dto.MediaServerItem;
import com.genersoft.iot.vmp.service.bean.GPSMsgInfo;
import com.genersoft.iot.vmp.service.bean.SSRCInfo;
import javax.sip.InvalidArgumentException;
import javax.sip.SipException;
@ -14,16 +18,20 @@ public interface ISIPCommanderForPlatform {
/**
* 向上级平台注册
*
* @param parentPlatform
* @return
*/
void register(ParentPlatform parentPlatform, SipSubscribe.Event errorEvent , SipSubscribe.Event okEvent) throws InvalidArgumentException, ParseException, SipException;
void register(ParentPlatform parentPlatform, SipTransactionInfo sipTransactionInfo, SipSubscribe.Event errorEvent , SipSubscribe.Event okEvent) throws InvalidArgumentException, ParseException, SipException;
void register(ParentPlatform parentPlatform, SipTransactionInfo sipTransactionInfo, WWWAuthenticateHeader www, SipSubscribe.Event errorEvent , SipSubscribe.Event okEvent, boolean isRegister) throws SipException, InvalidArgumentException, ParseException;
/**
* 向上级平台注销
*
* @param parentPlatform
* @return
*/
@ -32,14 +40,17 @@ public interface ISIPCommanderForPlatform {
/**
* 向上级平发送心跳信息
*
* @param parentPlatform
* @return callId(作为接受回复的判定)
*/
String keepalive(ParentPlatform parentPlatform,SipSubscribe.Event errorEvent , SipSubscribe.Event okEvent) throws SipException, InvalidArgumentException, ParseException;
String keepalive(ParentPlatform parentPlatform, SipSubscribe.Event errorEvent, SipSubscribe.Event okEvent)
throws SipException, InvalidArgumentException, ParseException;
/**
* 向上级回复通道信息
*
* @param channel 通道信息
* @param parentPlatform 平台信息
* @param sn
@ -47,11 +58,15 @@ public interface ISIPCommanderForPlatform {
* @param size
* @return
*/
void catalogQuery(DeviceChannel channel, ParentPlatform parentPlatform, String sn, String fromTag, int size) throws SipException, InvalidArgumentException, ParseException;
void catalogQuery(List<DeviceChannel> channels, ParentPlatform parentPlatform, String sn, String fromTag) throws InvalidArgumentException, ParseException, SipException;
void catalogQuery(DeviceChannel channel, ParentPlatform parentPlatform, String sn, String fromTag, int size)
throws SipException, InvalidArgumentException, ParseException;
void catalogQuery(List<DeviceChannel> channels, ParentPlatform parentPlatform, String sn, String fromTag)
throws InvalidArgumentException, ParseException, SipException;
/**
* 向上级回复DeviceInfo查询信息
*
* @param parentPlatform 平台信息
* @param sn SN
* @param fromTag FROM头的tag信息
@ -61,6 +76,7 @@ public interface ISIPCommanderForPlatform {
/**
* 向上级回复DeviceStatus查询信息
*
* @param parentPlatform 平台信息
* @param sn
* @param fromTag
@ -70,15 +86,18 @@ public interface ISIPCommanderForPlatform {
/**
* 向上级回复移动位置订阅消息
*
* @param parentPlatform 平台信息
* @param gpsMsgInfo GPS信息
* @param subscribeInfo 订阅相关的信息
* @return
*/
void sendNotifyMobilePosition(ParentPlatform parentPlatform, GPSMsgInfo gpsMsgInfo, SubscribeInfo subscribeInfo) throws InvalidArgumentException, ParseException, NoSuchFieldException, SipException, IllegalAccessException;
void sendNotifyMobilePosition(ParentPlatform parentPlatform, GPSMsgInfo gpsMsgInfo, SubscribeInfo subscribeInfo)
throws InvalidArgumentException, ParseException, NoSuchFieldException, SipException, IllegalAccessException;
/**
* 向上级回复报警消息
*
* @param parentPlatform 平台信息
* @param deviceAlarm 报警信息信息
* @return
@ -87,6 +106,7 @@ public interface ISIPCommanderForPlatform {
/**
* 回复catalog事件-增加/更新
*
* @param parentPlatform
* @param deviceChannels
*/
@ -94,22 +114,28 @@ public interface ISIPCommanderForPlatform {
/**
* 回复catalog事件-删除
*
* @param parentPlatform
* @param deviceChannels
*/
void sendNotifyForCatalogOther(String type, ParentPlatform parentPlatform, List<DeviceChannel> deviceChannels, SubscribeInfo subscribeInfo, Integer index) throws InvalidArgumentException, ParseException, NoSuchFieldException, SipException, IllegalAccessException;
void sendNotifyForCatalogOther(String type, ParentPlatform parentPlatform, List<DeviceChannel> deviceChannels,
SubscribeInfo subscribeInfo, Integer index) throws InvalidArgumentException,
ParseException, NoSuchFieldException, SipException, IllegalAccessException;
/**
* 回复recordInfo
*
* @param deviceChannel 通道信息
* @param parentPlatform 平台信息
* @param fromTag fromTag
* @param recordInfo 录像信息
*/
void recordInfo(DeviceChannel deviceChannel, ParentPlatform parentPlatform, String fromTag, RecordInfo recordInfo) throws SipException, InvalidArgumentException, ParseException;
void recordInfo(DeviceChannel deviceChannel, ParentPlatform parentPlatform, String fromTag, RecordInfo recordInfo)
throws SipException, InvalidArgumentException, ParseException;
/**
* 录像播放推送完成时发送MediaStatus消息
*
* @param platform
* @param sendRtpItem
* @return
@ -118,9 +144,19 @@ public interface ISIPCommanderForPlatform {
/**
* 向发起点播的上级回复bye
*
* @param platform 平台信息
* @param callId callId
*/
void streamByeCmd(ParentPlatform platform, String callId) throws SipException, InvalidArgumentException, ParseException;
void streamByeCmd(ParentPlatform platform, SendRtpItem sendRtpItem) throws SipException, InvalidArgumentException, ParseException;
void streamByeCmd(ParentPlatform platform, String channelId, String stream, String callId, SipSubscribe.Event okEvent) throws InvalidArgumentException, SipException, ParseException, SsrcTransactionNotFoundException;
void broadcastInviteCmd(ParentPlatform platform, String channelId, MediaServerItem mediaServerItem,
SSRCInfo ssrcInfo, ZlmHttpHookSubscribe.Event event, SipSubscribe.Event okEvent,
SipSubscribe.Event errorEvent) throws ParseException, SipException, InvalidArgumentException;
void broadcastResultCmd(ParentPlatform platform, DeviceChannel deviceChannel, String sn, boolean result, SipSubscribe.Event errorEvent, SipSubscribe.Event okEvent) throws InvalidArgumentException, SipException, ParseException;
}

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