From 3dfbc843adf2d4b6affd3d1d14684941a09561fb Mon Sep 17 00:00:00 2001 From: 648540858 <648540858@qq.com> Date: Mon, 3 Apr 2023 10:53:54 +0800 Subject: [PATCH 01/48] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E5=85=B3=E9=97=AD?= =?UTF-8?q?=E6=8E=A5=E5=8F=A3=E9=89=B4=E6=9D=83=E6=97=B6=EF=BC=8C=E5=A4=84?= =?UTF-8?q?=E4=BA=8E=E5=BF=BD=E7=95=A5=E5=9C=B0=E5=9D=80=E4=B8=AD=E7=9A=84?= =?UTF-8?q?=E6=8E=A5=E5=8F=A3=E4=B8=8D=E5=8F=AF=E7=94=A8=E7=9A=84=E9=97=AE?= =?UTF-8?q?=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../security/JwtAuthenticationFilter.java | 1 - .../vmp/conf/security/WebSecurityConfig.java | 32 ++++++++++--------- 2 files changed, 17 insertions(+), 16 deletions(-) diff --git a/src/main/java/com/genersoft/iot/vmp/conf/security/JwtAuthenticationFilter.java b/src/main/java/com/genersoft/iot/vmp/conf/security/JwtAuthenticationFilter.java index e50a8b0ec..27151eeed 100644 --- a/src/main/java/com/genersoft/iot/vmp/conf/security/JwtAuthenticationFilter.java +++ b/src/main/java/com/genersoft/iot/vmp/conf/security/JwtAuthenticationFilter.java @@ -38,7 +38,6 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter { return; } if (!userSetting.isInterfaceAuthentication()) { - // 构建UsernamePasswordAuthenticationToken,这里密码为null,是因为提供了正确的JWT,实现自动登录 UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(null, null, new ArrayList<>() ); SecurityContextHolder.getContext().setAuthentication(token); chain.doFilter(request, response); diff --git a/src/main/java/com/genersoft/iot/vmp/conf/security/WebSecurityConfig.java b/src/main/java/com/genersoft/iot/vmp/conf/security/WebSecurityConfig.java index c9a1233bd..cea19f81c 100644 --- a/src/main/java/com/genersoft/iot/vmp/conf/security/WebSecurityConfig.java +++ b/src/main/java/com/genersoft/iot/vmp/conf/security/WebSecurityConfig.java @@ -72,21 +72,23 @@ public class WebSecurityConfig extends WebSecurityConfigurerAdapter { **/ @Override public void configure(WebSecurity web) { - - ArrayList matchers = new ArrayList<>(); - matchers.add("/"); - matchers.add("/#/**"); - matchers.add("/static/**"); - matchers.add("/index.html"); - matchers.add("/doc.html"); - matchers.add("/webjars/**"); - matchers.add("/swagger-resources/**"); - matchers.add("/v3/api-docs/**"); - matchers.add("/js/**"); - matchers.add("/api/device/query/snap/**"); - matchers.addAll(userSetting.getInterfaceAuthenticationExcludes()); - // 可以直接访问的静态数据 - web.ignoring().antMatchers(matchers.toArray(new String[0])); + if (userSetting.isInterfaceAuthentication()) { + ArrayList matchers = new ArrayList<>(); + matchers.add("/"); + matchers.add("/#/**"); + matchers.add("/static/**"); + matchers.add("/index.html"); + matchers.add("/doc.html"); + matchers.add("/webjars/**"); + matchers.add("/swagger-resources/**"); + matchers.add("/v3/api-docs/**"); + matchers.add("/js/**"); + matchers.add("/api/device/query/snap/**"); + matchers.add("/record_proxy/*/**"); + matchers.addAll(userSetting.getInterfaceAuthenticationExcludes()); + // 可以直接访问的静态数据 + web.ignoring().antMatchers(matchers.toArray(new String[0])); + } } /** From 59ab2adb2ecd0af71b36b92f820248da848fc592 Mon Sep 17 00:00:00 2001 From: 648540858 <648540858@qq.com> Date: Tue, 4 Apr 2023 11:14:08 +0800 Subject: [PATCH 02/48] =?UTF-8?q?=E5=8E=BB=E9=99=A4flyway=E9=85=8D?= =?UTF-8?q?=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../iot/vmp/service/impl/PlatformServiceImpl.java | 1 + .../vmp/storager/impl/VideoManagerStorageImpl.java | 3 --- src/main/resources/application.yml | 13 +------------ 3 files changed, 2 insertions(+), 15 deletions(-) diff --git a/src/main/java/com/genersoft/iot/vmp/service/impl/PlatformServiceImpl.java b/src/main/java/com/genersoft/iot/vmp/service/impl/PlatformServiceImpl.java index d05626a6b..9233ca9d9 100644 --- a/src/main/java/com/genersoft/iot/vmp/service/impl/PlatformServiceImpl.java +++ b/src/main/java/com/genersoft/iot/vmp/service/impl/PlatformServiceImpl.java @@ -176,6 +176,7 @@ public class PlatformServiceImpl implements IPlatformService { // 保存时启用就发送注册 // 注册成功时由程序直接调用了online方法 try { + logger.info("[国标级联] 平台注册 {}", parentPlatform.getDeviceGBId()); commanderForPlatform.register(parentPlatform, eventResult -> { logger.info("[国标级联] {},添加向上级注册失败,请确定上级平台可用时重新保存", parentPlatform.getServerGBId()); }, null); diff --git a/src/main/java/com/genersoft/iot/vmp/storager/impl/VideoManagerStorageImpl.java b/src/main/java/com/genersoft/iot/vmp/storager/impl/VideoManagerStorageImpl.java index 1ce01df3b..206456d93 100644 --- a/src/main/java/com/genersoft/iot/vmp/storager/impl/VideoManagerStorageImpl.java +++ b/src/main/java/com/genersoft/iot/vmp/storager/impl/VideoManagerStorageImpl.java @@ -441,9 +441,6 @@ public class VideoManagerStorageImpl implements IVideoManagerStorage { */ @Override public synchronized boolean insertMobilePosition(MobilePosition mobilePosition) { - if (mobilePosition.getDeviceId().equals(mobilePosition.getChannelId())) { - mobilePosition.setChannelId(null); - } return deviceMobilePositionMapper.insertNewPosition(mobilePosition) > 0; } diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 4efb527d5..3f478442e 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -2,15 +2,4 @@ spring: application: name: wvp profiles: - active: local - # flayway相关配置 - flyway: - enabled: true #是否启用flyway(默认true) - locations: classpath:db/migration #这个路径指的是fly版本控制的sql语句存放的路径,可以多个,可以给每个环境使用不同位置,比如classpath:db/migration,classpath:test/db/migration - baseline-on-migrate: true #开启自动创建flyway元数据表标识 默认: false - # 与 baseline-on-migrate: true 搭配使用,将当前数据库初始版本设置为0 - baseline-version: 0 - clean-disabled: true #禁止flyway执行清理 - # 假如已经执行了版本1和版本3,如果增加了一个版本2,下面这个选项将会允许执行版本2的脚本 - out-of-order: true - table: flyway_schema_history_${spring.application.name} #用于记录所有的版本变化记录 \ No newline at end of file + active: local \ No newline at end of file From e21e419361ca69c31c8eed7349ce4d44ca42313a Mon Sep 17 00:00:00 2001 From: canghai809 Date: Tue, 4 Apr 2023 18:52:13 +0800 Subject: [PATCH 03/48] =?UTF-8?q?=E5=BC=80=E5=8F=91=E5=8F=8ADocker?= =?UTF-8?q?=E9=85=8D=E7=BD=AE=E6=96=87=E4=BB=B6=E6=95=B0=E6=8D=AE=E6=BA=90?= =?UTF-8?q?=E6=9B=B4=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/resources/application-dev.yml | 26 +++++++---------------- src/main/resources/application-docker.yml | 2 +- 2 files changed, 9 insertions(+), 19 deletions(-) diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml index f06bd3e9b..d02cdaf75 100644 --- a/src/main/resources/application-dev.yml +++ b/src/main/resources/application-dev.yml @@ -18,29 +18,19 @@ spring: timeout: 10000 # mysql数据源 datasource: - type: com.alibaba.druid.pool.DruidDataSource + type: com.zaxxer.hikari.HikariDataSource driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://127.0.0.1:3306/wvp2?useUnicode=true&characterEncoding=UTF8&rewriteBatchedStatements=true&serverTimezone=PRC&useSSL=false&allowMultiQueries=true username: root password: 123456 - druid: + hikari: + connection-timeout: 20000 # 是客户端等待连接池连接的最大毫秒数 initialSize: 10 # 连接池初始化连接数 - maxActive: 200 # 连接池最大连接数 - minIdle: 5 # 连接池最小空闲连接数 - maxWait: 60000 # 获取连接时最大等待时间,单位毫秒。配置了maxWait之后,缺省启用公平锁,并发效率会有所下降,如果需要可以通过配置useUnfairLock属性为true使用非公平锁。 - keepAlive: true # 连接池中的minIdle数量以内的连接,空闲时间超过minEvictableIdleTimeMillis,则会执行keepAlive操作。 - validationQuery: select 1 # 检测连接是否有效sql,要求是查询语句,常用select 'x'。如果validationQuery为null,testOnBorrow、testOnReturn、testWhileIdle都不会起作用。 - testWhileIdle: true # 建议配置为true,不影响性能,并且保证安全性。申请连接的时候检测,如果空闲时间大于timeBetweenEvictionRunsMillis,执行validationQuery检测连接是否有效。 - testOnBorrow: false # 申请连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能。 - testOnReturn: false # 归还连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能。 - poolPreparedStatements: false # 是否開啟PSCache,並且指定每個連線上PSCache的大小 - timeBetweenEvictionRunsMillis: 60000 # 配置間隔多久才進行一次檢測,檢測需要關閉的空閒連線,單位是毫秒 - minEvictableIdleTimeMillis: 300000 # 配置一個連線在池中最小生存的時間,單位是毫秒 - filters: stat,slf4j # 配置监控统计拦截的filters,监控统计用的filter:sta, 日志用的filter:log4j - useGlobalDataSourceStat: true # 合并多个DruidDataSource的监控数据 - # 通过connectProperties属性来打开mergeSql功能;慢SQL记录 - connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=1000 - #stat-view-servlet.url-pattern: /admin/druid/* + maximum-pool-size: 200 # 连接池最大连接数 + minimum-idle: 5 # 连接池最小空闲连接数 + idle-timeout: 300000 # 允许连接在连接池中空闲的最长时间(以毫秒为单位) + max-lifetime: 1200000 # 是池中连接关闭后的最长生命周期(以毫秒为单位) + #[可选] WVP监听的HTTP端口, 网页和接口调用都是这个端口 server: diff --git a/src/main/resources/application-docker.yml b/src/main/resources/application-docker.yml index a722435c4..f5c3d0f41 100644 --- a/src/main/resources/application-docker.yml +++ b/src/main/resources/application-docker.yml @@ -23,7 +23,7 @@ spring: url: jdbc:mysql://127.0.0.1:3306/wvp?useUnicode=true&characterEncoding=UTF8&rewriteBatchedStatements=true&allowMultiQueries=true&useSSL=false&allowMultiQueries=true username: root password: root - type: com.alibaba.druid.pool.DruidDataSource + type: com.zaxxer.hikari.HikariDataSource driver-class-name: com.mysql.cj.jdbc.Driver # [可选] WVP监听的HTTP端口, 网页和接口调用都是这个端口 From 0858f7995b8236d79c6e39a5974cb7a13bcb4e3e Mon Sep 17 00:00:00 2001 From: 648540858 <648540858@qq.com> Date: Sat, 8 Apr 2023 15:22:18 +0800 Subject: [PATCH 04/48] =?UTF-8?q?=E4=BC=98=E5=8C=96ssrc=E5=AD=98=E5=82=A8?= =?UTF-8?q?=E7=BB=93=E6=9E=84=EF=BC=8C=E9=81=BF=E5=85=8D=E5=9B=A0=E4=B8=BA?= =?UTF-8?q?=E5=A4=A7=E9=87=8Fssrc=E5=AD=98=E5=9C=A8MediaServer=E4=B8=AD?= =?UTF-8?q?=E5=AF=BC=E8=87=B4redis=E8=AF=BB=E5=8F=96=E8=B6=85=E6=97=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../iot/vmp/conf/redis/RedisConfig.java | 18 --- .../vmp/conf/redis/RedisTemplateConfig.java | 28 ++++ .../iot/vmp/gb28181/session/SSRCFactory.java | 132 +++++++++++++++ .../iot/vmp/gb28181/session/SsrcConfig.java | 150 ------------------ .../request/impl/InviteRequestProcessor.java | 13 +- .../vmp/media/zlm/dto/MediaServerItem.java | 20 +-- .../impl/DeviceChannelServiceImpl.java | 2 + .../service/impl/MediaServerServiceImpl.java | 88 ++++------ .../iot/vmp/service/impl/PlayServiceImpl.java | 12 +- .../genersoft/iot/vmp/utils/ConfigConst.java | 8 - 10 files changed, 213 insertions(+), 258 deletions(-) create mode 100644 src/main/java/com/genersoft/iot/vmp/conf/redis/RedisTemplateConfig.java create mode 100644 src/main/java/com/genersoft/iot/vmp/gb28181/session/SSRCFactory.java delete mode 100644 src/main/java/com/genersoft/iot/vmp/gb28181/session/SsrcConfig.java delete mode 100644 src/main/java/com/genersoft/iot/vmp/utils/ConfigConst.java diff --git a/src/main/java/com/genersoft/iot/vmp/conf/redis/RedisConfig.java b/src/main/java/com/genersoft/iot/vmp/conf/redis/RedisConfig.java index a2dca6310..87b4d53da 100644 --- a/src/main/java/com/genersoft/iot/vmp/conf/redis/RedisConfig.java +++ b/src/main/java/com/genersoft/iot/vmp/conf/redis/RedisConfig.java @@ -9,12 +9,8 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.annotation.Order; import org.springframework.data.redis.connection.RedisConnectionFactory; -import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.listener.PatternTopic; import org.springframework.data.redis.listener.RedisMessageListenerContainer; -import org.springframework.data.redis.serializer.StringRedisSerializer; - -import com.genersoft.iot.vmp.utils.redis.FastJsonRedisSerializer; /** @@ -48,21 +44,7 @@ public class RedisConfig extends CachingConfigurerSupport { @Autowired private RedisPushStreamResponseListener redisPushStreamResponseListener; - @Bean - public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory) { - RedisTemplate redisTemplate = new RedisTemplate<>(); - // 使用fastJson序列化 - FastJsonRedisSerializer fastJsonRedisSerializer = new FastJsonRedisSerializer(Object.class); - // value值的序列化采用fastJsonRedisSerializer - redisTemplate.setValueSerializer(fastJsonRedisSerializer); - redisTemplate.setHashValueSerializer(fastJsonRedisSerializer); - // key的序列化采用StringRedisSerializer - redisTemplate.setKeySerializer(new StringRedisSerializer()); - redisTemplate.setHashKeySerializer(new StringRedisSerializer()); - redisTemplate.setConnectionFactory(redisConnectionFactory); - return redisTemplate; - } /** diff --git a/src/main/java/com/genersoft/iot/vmp/conf/redis/RedisTemplateConfig.java b/src/main/java/com/genersoft/iot/vmp/conf/redis/RedisTemplateConfig.java new file mode 100644 index 000000000..1868a5d48 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/conf/redis/RedisTemplateConfig.java @@ -0,0 +1,28 @@ +package com.genersoft.iot.vmp.conf.redis; + +import com.genersoft.iot.vmp.utils.redis.FastJsonRedisSerializer; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.serializer.StringRedisSerializer; + +@Configuration +public class RedisTemplateConfig { + + @Bean + public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory) { + RedisTemplate redisTemplate = new RedisTemplate<>(); + // 使用fastJson序列化 + FastJsonRedisSerializer fastJsonRedisSerializer = new FastJsonRedisSerializer(Object.class); + // value值的序列化采用fastJsonRedisSerializer + redisTemplate.setValueSerializer(fastJsonRedisSerializer); + redisTemplate.setHashValueSerializer(fastJsonRedisSerializer); + + // key的序列化采用StringRedisSerializer + redisTemplate.setKeySerializer(new StringRedisSerializer()); + redisTemplate.setHashKeySerializer(new StringRedisSerializer()); + redisTemplate.setConnectionFactory(redisConnectionFactory); + return redisTemplate; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/session/SSRCFactory.java b/src/main/java/com/genersoft/iot/vmp/gb28181/session/SSRCFactory.java new file mode 100644 index 000000000..ec8e0ba6a --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/session/SSRCFactory.java @@ -0,0 +1,132 @@ +package com.genersoft.iot.vmp.gb28181.session; + +import com.genersoft.iot.vmp.conf.SipConfig; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.stereotype.Component; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; + +/** + * ssrc使用 + */ +@Component +public class SSRCFactory { + + /** + * 播流最大并发个数 + */ + private static final Integer MAX_STREAM_COUNT = 10000; + + /** + * 播流最大并发个数 + */ + private static final String SSRC_INFO_KEY = "VMP_SSRC_INFO_"; + + @Autowired + private StringRedisTemplate redisTemplate; + + @Autowired + private SipConfig sipConfig; + + + public void initMediaServerSSRC(String mediaServerId, Set usedSet) { + String ssrcPrefix = sipConfig.getDomain().substring(3, 8); + String redisKey = SSRC_INFO_KEY + mediaServerId; + List ssrcList = new ArrayList<>(); + for (int i = 1; i < MAX_STREAM_COUNT; i++) { + String ssrc = String.format("%s%04d", ssrcPrefix, i); + + if (null == usedSet || !usedSet.contains(ssrc)) { + ssrcList.add(ssrc); + + } + } + if (redisTemplate.opsForSet().size(redisKey) != null) { + redisTemplate.delete(redisKey); + } + redisTemplate.opsForSet().add(redisKey, ssrcList.toArray(new String[0])); + } + + + /** + * 获取视频预览的SSRC值,第一位固定为0 + * + * @return ssrc + */ + public String getPlaySsrc(String mediaServerId) { + return "0" + getSN(mediaServerId); + } + + /** + * 获取录像回放的SSRC值,第一位固定为1 + */ + public String getPlayBackSsrc(String mediaServerId) { + return "1" + getSN(mediaServerId); + } + + /** + * 释放ssrc,主要用完的ssrc一定要释放,否则会耗尽 + * + * @param ssrc 需要重置的ssrc + */ + public void releaseSsrc(String mediaServerId, String ssrc) { + if (ssrc == null) { + return; + } + String sn = ssrc.substring(1); + String redisKey = SSRC_INFO_KEY + mediaServerId; + redisTemplate.opsForSet().add(redisKey, sn); + } + + /** + * 获取后四位数SN,随机数 + */ + private String getSN(String mediaServerId) { + String sn = null; + String redisKey = SSRC_INFO_KEY + mediaServerId; + Long size = redisTemplate.opsForSet().size(redisKey); + if (size == null || size == 0) { + throw new RuntimeException("ssrc已经用完"); + } else { + // 在集合中移除并返回一个随机成员。 + sn = (String) redisTemplate.opsForSet().pop(redisKey); + redisTemplate.opsForSet().remove(redisKey, sn); + } + return sn; + } + + /** + * 重置一个流媒体服务的所有ssrc + * + * @param mediaServerId 流媒体服务ID + */ + public void reset(String mediaServerId) { + this.initMediaServerSSRC(mediaServerId, null); + } + + /** + * 是否已经存在了某个MediaServer的SSRC信息 + * + * @param mediaServerId 流媒体服务ID + */ + public boolean hasMediaServerSSRC(String mediaServerId) { + String redisKey = SSRC_INFO_KEY + mediaServerId; + return redisTemplate.opsForSet().members(redisKey) != null; + } + + /** + * 查询ssrc是否可用 + * + * @param mediaServerId + * @param ssrc + * @return + */ + public boolean checkSsrc(String mediaServerId, String ssrc) { + String sn = ssrc.substring(1); + String redisKey = SSRC_INFO_KEY + mediaServerId; + return redisTemplate.opsForSet().isMember(redisKey, sn) != null; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/session/SsrcConfig.java b/src/main/java/com/genersoft/iot/vmp/gb28181/session/SsrcConfig.java deleted file mode 100644 index cc303c8bd..000000000 --- a/src/main/java/com/genersoft/iot/vmp/gb28181/session/SsrcConfig.java +++ /dev/null @@ -1,150 +0,0 @@ -package com.genersoft.iot.vmp.gb28181.session; - -import com.genersoft.iot.vmp.utils.ConfigConst; -import io.swagger.v3.oas.annotations.media.Schema; - -import java.util.ArrayList; -import java.util.List; -import java.util.Random; -import java.util.Set; - -@Schema(description = "ssrc信息") -public class SsrcConfig { - - /** - * zlm流媒体服务器Id - */ - @Schema(description = "流媒体服务器Id") - private String mediaServerId; - - @Schema(description = "SSRC前缀") - private String ssrcPrefix; - - /** - * zlm流媒体服务器已用会话句柄 - */ - @Schema(description = "zlm流媒体服务器已用会话句柄") - private List isUsed; - - /** - * zlm流媒体服务器可用会话句柄 - */ - @Schema(description = "zlm流媒体服务器可用会话句柄") - private List notUsed; - - public SsrcConfig() { - } - - public SsrcConfig(String mediaServerId, Set usedSet, String sipDomain) { - this.mediaServerId = mediaServerId; - this.isUsed = new ArrayList<>(); - this.ssrcPrefix = sipDomain.substring(3, 8); - this.notUsed = new ArrayList<>(); - for (int i = 1; i < ConfigConst.MAX_STRTEAM_COUNT; i++) { - String ssrc; - if (i < 10) { - ssrc = "000" + i; - } else if (i < 100) { - ssrc = "00" + i; - } else if (i < 1000) { - ssrc = "0" + i; - } else { - ssrc = String.valueOf(i); - } - if (null == usedSet || !usedSet.contains(ssrc)) { - this.notUsed.add(ssrc); - } else { - this.isUsed.add(ssrc); - } - } - } - - - /** - * 获取视频预览的SSRC值,第一位固定为0 - * @return ssrc - */ - public String getPlaySsrc() { - return "0" + getSsrcPrefix() + getSN(); - } - - /** - * 获取录像回放的SSRC值,第一位固定为1 - * - */ - public String getPlayBackSsrc() { - return "1" + getSsrcPrefix() + getSN(); - } - - /** - * 释放ssrc,主要用完的ssrc一定要释放,否则会耗尽 - * @param ssrc 需要重置的ssrc - */ - public void releaseSsrc(String ssrc) { - if (ssrc == null) { - return; - } - String sn = ssrc.substring(6); - try { - isUsed.remove(sn); - notUsed.add(sn); - }catch (NullPointerException e){ - } - } - - /** - * 获取后四位数SN,随机数 - * - */ - private String getSN() { - String sn = null; - int index = 0; - if (notUsed.size() == 0) { - throw new RuntimeException("ssrc已经用完"); - } else if (notUsed.size() == 1) { - sn = notUsed.get(0); - } else { - index = new Random().nextInt(notUsed.size() - 1); - sn = notUsed.get(index); - } - notUsed.remove(index); - isUsed.add(sn); - return sn; - } - - public String getSsrcPrefix() { - return ssrcPrefix; - } - - public String getMediaServerId() { - return mediaServerId; - } - - public void setMediaServerId(String mediaServerId) { - this.mediaServerId = mediaServerId; - } - - public void setSsrcPrefix(String ssrcPrefix) { - this.ssrcPrefix = ssrcPrefix; - } - - public List getIsUsed() { - return isUsed; - } - - public void setIsUsed(List isUsed) { - this.isUsed = isUsed; - } - - public List getNotUsed() { - return notUsed; - } - - public void setNotUsed(List notUsed) { - this.notUsed = notUsed; - } - - public boolean checkSsrc(String ssrcInResponse) { - return !isUsed.contains(ssrcInResponse); - } -} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/InviteRequestProcessor.java b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/InviteRequestProcessor.java index 1a6358b03..80734177f 100644 --- a/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/InviteRequestProcessor.java +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/InviteRequestProcessor.java @@ -5,7 +5,7 @@ import com.genersoft.iot.vmp.conf.DynamicTask; import com.genersoft.iot.vmp.conf.UserSetting; import com.genersoft.iot.vmp.gb28181.bean.*; import com.genersoft.iot.vmp.gb28181.event.SipSubscribe; -import com.genersoft.iot.vmp.gb28181.session.SsrcConfig; +import com.genersoft.iot.vmp.gb28181.session.SSRCFactory; import com.genersoft.iot.vmp.gb28181.session.VideoStreamSessionManager; import com.genersoft.iot.vmp.gb28181.transmit.SIPProcessorObserver; import com.genersoft.iot.vmp.gb28181.transmit.SIPSender; @@ -74,6 +74,9 @@ public class InviteRequestProcessor extends SIPRequestProcessorParent implements @Autowired private IRedisCatchStorage redisCatchStorage; + @Autowired + private SSRCFactory ssrcFactory; + @Autowired private DynamicTask dynamicTask; @@ -491,12 +494,8 @@ public class InviteRequestProcessor extends SIPRequestProcessorParent implements } else if (gbStream != null) { if(ssrc.equals(ssrcDefault)) { - SsrcConfig ssrcConfig = mediaServerItem.getSsrcConfig(); - if(ssrcConfig != null) - { - ssrc = ssrcConfig.getPlaySsrc(); - ssrcConfig.releaseSsrc(ssrc); - } + ssrc = ssrcFactory.getPlaySsrc(mediaServerItem.getId()); + ssrcFactory.releaseSsrc(mediaServerItem.getId(), ssrc); } if("push".equals(gbStream.getStreamType())) { if (streamPushItem != null && streamPushItem.isPushIng()) { diff --git a/src/main/java/com/genersoft/iot/vmp/media/zlm/dto/MediaServerItem.java b/src/main/java/com/genersoft/iot/vmp/media/zlm/dto/MediaServerItem.java index f3eb3d679..ea248fde4 100644 --- a/src/main/java/com/genersoft/iot/vmp/media/zlm/dto/MediaServerItem.java +++ b/src/main/java/com/genersoft/iot/vmp/media/zlm/dto/MediaServerItem.java @@ -1,7 +1,7 @@ package com.genersoft.iot.vmp.media.zlm.dto; -import com.genersoft.iot.vmp.gb28181.session.SsrcConfig; +import com.genersoft.iot.vmp.gb28181.session.SSRCFactory; import com.genersoft.iot.vmp.media.zlm.ZLMServerConfig; import io.swagger.v3.oas.annotations.media.Schema; import org.springframework.util.ObjectUtils; @@ -80,8 +80,8 @@ public class MediaServerItem{ @Schema(description = "是否是默认ZLM") private boolean defaultServer; - @Schema(description = "SSRC信息") - private SsrcConfig ssrcConfig; +// @Schema(description = "SSRC信息") +// private SsrcConfig ssrcConfig; @Schema(description = "当前使用到的端口") private int currentPort; @@ -92,7 +92,7 @@ public class MediaServerItem{ * 在ApplicationCheckRunner里对mediaServerSsrcMap进行初始化 */ @Schema(description = "ID") - private HashMap mediaServerSsrcMap; + private HashMap mediaServerSsrcMap; public MediaServerItem() { } @@ -279,22 +279,14 @@ public class MediaServerItem{ this.updateTime = updateTime; } - public HashMap getMediaServerSsrcMap() { + public HashMap getMediaServerSsrcMap() { return mediaServerSsrcMap; } - public void setMediaServerSsrcMap(HashMap mediaServerSsrcMap) { + public void setMediaServerSsrcMap(HashMap mediaServerSsrcMap) { this.mediaServerSsrcMap = mediaServerSsrcMap; } - public SsrcConfig getSsrcConfig() { - return ssrcConfig; - } - - public void setSsrcConfig(SsrcConfig ssrcConfig) { - this.ssrcConfig = ssrcConfig; - } - public int getCurrentPort() { return currentPort; } diff --git a/src/main/java/com/genersoft/iot/vmp/service/impl/DeviceChannelServiceImpl.java b/src/main/java/com/genersoft/iot/vmp/service/impl/DeviceChannelServiceImpl.java index 336082f50..9223ced0c 100644 --- a/src/main/java/com/genersoft/iot/vmp/service/impl/DeviceChannelServiceImpl.java +++ b/src/main/java/com/genersoft/iot/vmp/service/impl/DeviceChannelServiceImpl.java @@ -45,6 +45,8 @@ public class DeviceChannelServiceImpl implements IDeviceChannelService { device = deviceMapper.getDeviceByDeviceId(deviceChannel.getDeviceId()); } + + if ("WGS84".equals(device.getGeoCoordSys())) { deviceChannel.setLongitudeWgs84(deviceChannel.getLongitude()); deviceChannel.setLatitudeWgs84(deviceChannel.getLatitude()); diff --git a/src/main/java/com/genersoft/iot/vmp/service/impl/MediaServerServiceImpl.java b/src/main/java/com/genersoft/iot/vmp/service/impl/MediaServerServiceImpl.java index eab8edb3d..9bd64f1e4 100644 --- a/src/main/java/com/genersoft/iot/vmp/service/impl/MediaServerServiceImpl.java +++ b/src/main/java/com/genersoft/iot/vmp/service/impl/MediaServerServiceImpl.java @@ -9,7 +9,7 @@ import com.genersoft.iot.vmp.conf.SipConfig; import com.genersoft.iot.vmp.conf.UserSetting; import com.genersoft.iot.vmp.conf.exception.ControllerException; import com.genersoft.iot.vmp.gb28181.event.EventPublisher; -import com.genersoft.iot.vmp.gb28181.session.SsrcConfig; +import com.genersoft.iot.vmp.gb28181.session.SSRCFactory; import com.genersoft.iot.vmp.gb28181.session.VideoStreamSessionManager; import com.genersoft.iot.vmp.media.zlm.ZLMRESTfulUtils; import com.genersoft.iot.vmp.media.zlm.ZLMRTPServerFactory; @@ -54,6 +54,9 @@ public class MediaServerServiceImpl implements IMediaServerService { @Autowired private SipConfig sipConfig; + @Autowired + private SSRCFactory ssrcFactory; + @Value("${server.ssl.enabled:false}") private boolean sslEnabled; @@ -90,6 +93,7 @@ public class MediaServerServiceImpl implements IMediaServerService { @Autowired private IRedisCatchStorage redisCatchStorage; + /** * 初始化 */ @@ -101,9 +105,8 @@ public class MediaServerServiceImpl implements IMediaServerService { continue; } // 更新 - if (mediaServerItem.getSsrcConfig() == null) { - SsrcConfig ssrcConfig = new SsrcConfig(mediaServerItem.getId(), null, sipConfig.getDomain()); - mediaServerItem.setSsrcConfig(ssrcConfig); + if (ssrcFactory.hasMediaServerSSRC(mediaServerItem.getId())) { + ssrcFactory.initMediaServerSSRC(mediaServerItem.getId(), null); RedisUtil.set(VideoManagerConstants.MEDIA_SERVER_PREFIX + userSetting.getServerId() + "_" + mediaServerItem.getId(), mediaServerItem); } // 查询redis是否存在此mediaServer @@ -127,36 +130,27 @@ public class MediaServerServiceImpl implements IMediaServerService { return null; } // 获取mediaServer可用的ssrc - String key = VideoManagerConstants.MEDIA_SERVER_PREFIX + userSetting.getServerId() + "_" + mediaServerItem.getId(); - - SsrcConfig ssrcConfig = mediaServerItem.getSsrcConfig(); - if (ssrcConfig == null) { - logger.info("media server [ {} ] ssrcConfig is null", mediaServerItem.getId()); - return null; + String ssrc; + if (presetSsrc != null) { + ssrc = presetSsrc; }else { - String ssrc; - if (presetSsrc != null) { - ssrc = presetSsrc; + if (isPlayback) { + ssrc = ssrcFactory.getPlayBackSsrc(mediaServerItem.getId()); }else { - if (isPlayback) { - ssrc = ssrcConfig.getPlayBackSsrc(); - }else { - ssrc = ssrcConfig.getPlaySsrc(); - } + ssrc = ssrcFactory.getPlaySsrc(mediaServerItem.getId()); } - - if (streamId == null) { - streamId = String.format("%08x", Integer.parseInt(ssrc)).toUpperCase(); - } - int rtpServerPort; - if (mediaServerItem.isRtpEnable()) { - rtpServerPort = zlmrtpServerFactory.createRTPServer(mediaServerItem, streamId, ssrcCheck?Integer.parseInt(ssrc):0, port); - } else { - rtpServerPort = mediaServerItem.getRtpProxyPort(); - } - RedisUtil.set(key, mediaServerItem); - return new SSRCInfo(rtpServerPort, ssrc, streamId); } + + if (streamId == null) { + streamId = String.format("%08x", Integer.parseInt(ssrc)).toUpperCase(); + } + int rtpServerPort; + if (mediaServerItem.isRtpEnable()) { + rtpServerPort = zlmrtpServerFactory.createRTPServer(mediaServerItem, streamId, ssrcCheck?Integer.parseInt(ssrc):0, port); + } else { + rtpServerPort = mediaServerItem.getRtpProxyPort(); + } + return new SSRCInfo(rtpServerPort, ssrc, streamId); } @Override @@ -184,11 +178,7 @@ public class MediaServerServiceImpl implements IMediaServerService { if (mediaServerItem == null || ssrc == null) { return; } - SsrcConfig ssrcConfig = mediaServerItem.getSsrcConfig(); - ssrcConfig.releaseSsrc(ssrc); - mediaServerItem.setSsrcConfig(ssrcConfig); - String key = VideoManagerConstants.MEDIA_SERVER_PREFIX + userSetting.getServerId() + "_" + mediaServerItem.getId(); - RedisUtil.set(key, mediaServerItem); + ssrcFactory.releaseSsrc(mediaServerItemId, ssrc); } /** @@ -196,8 +186,7 @@ public class MediaServerServiceImpl implements IMediaServerService { */ @Override public void clearRTPServer(MediaServerItem mediaServerItem) { - mediaServerItem.setSsrcConfig(new SsrcConfig(mediaServerItem.getId(), null, sipConfig.getDomain())); - RedisUtil.zAdd(VideoManagerConstants.MEDIA_SERVERS_ONLINE_PREFIX + userSetting.getServerId(), mediaServerItem.getId(), 0); + ssrcFactory.reset(mediaServerItem.getId()); } @@ -207,16 +196,8 @@ public class MediaServerServiceImpl implements IMediaServerService { mediaServerMapper.update(mediaSerItem); MediaServerItem mediaServerItemInRedis = getOne(mediaSerItem.getId()); MediaServerItem mediaServerItemInDataBase = mediaServerMapper.queryOne(mediaSerItem.getId()); - if (mediaServerItemInRedis != null && mediaServerItemInRedis.getSsrcConfig() != null) { - mediaServerItemInDataBase.setSsrcConfig(mediaServerItemInRedis.getSsrcConfig()); - }else { - mediaServerItemInDataBase.setSsrcConfig( - new SsrcConfig( - mediaServerItemInDataBase.getId(), - null, - sipConfig.getDomain() - ) - ); + if (mediaServerItemInRedis == null || ssrcFactory.hasMediaServerSSRC(mediaSerItem.getId())) { + ssrcFactory.initMediaServerSSRC(mediaServerItemInDataBase.getId(),null); } String key = VideoManagerConstants.MEDIA_SERVER_PREFIX + userSetting.getServerId() + "_" + mediaServerItemInDataBase.getId(); RedisUtil.set(key, mediaServerItemInDataBase); @@ -396,14 +377,8 @@ public class MediaServerServiceImpl implements IMediaServerService { } mediaServerMapper.update(serverItem); String key = VideoManagerConstants.MEDIA_SERVER_PREFIX + userSetting.getServerId() + "_" + zlmServerConfig.getGeneralMediaServerId(); - if (RedisUtil.get(key) == null) { - SsrcConfig ssrcConfig = new SsrcConfig(zlmServerConfig.getGeneralMediaServerId(), null, sipConfig.getDomain()); - serverItem.setSsrcConfig(ssrcConfig); - }else { - MediaServerItem mediaServerItemInRedis = JsonUtil.redisJsonToObject(key, MediaServerItem.class); - if (Objects.nonNull(mediaServerItemInRedis)) { - serverItem.setSsrcConfig(mediaServerItemInRedis.getSsrcConfig()); - } + if (ssrcFactory.hasMediaServerSSRC(serverItem.getId())) { + ssrcFactory.initMediaServerSSRC(zlmServerConfig.getGeneralMediaServerId(), null); } RedisUtil.set(key, serverItem); resetOnlineServerItem(serverItem); @@ -682,8 +657,7 @@ public class MediaServerServiceImpl implements IMediaServerService { } // zlm连接重试 logger.warn("[更新ZLM 保活信息]尝试链接zml id {}", mediaServerId); - SsrcConfig ssrcConfig = new SsrcConfig(mediaServerItem.getId(), null, sipConfig.getDomain()); - mediaServerItem.setSsrcConfig(ssrcConfig); + ssrcFactory.initMediaServerSSRC(mediaServerItem.getId(), null); String key = VideoManagerConstants.MEDIA_SERVER_PREFIX + userSetting.getServerId() + "_" + mediaServerItem.getId(); RedisUtil.set(key, mediaServerItem); clearRTPServer(mediaServerItem); diff --git a/src/main/java/com/genersoft/iot/vmp/service/impl/PlayServiceImpl.java b/src/main/java/com/genersoft/iot/vmp/service/impl/PlayServiceImpl.java index 0c9243ef1..a072e8af7 100644 --- a/src/main/java/com/genersoft/iot/vmp/service/impl/PlayServiceImpl.java +++ b/src/main/java/com/genersoft/iot/vmp/service/impl/PlayServiceImpl.java @@ -11,6 +11,7 @@ import com.genersoft.iot.vmp.conf.exception.ServiceException; import com.genersoft.iot.vmp.conf.exception.SsrcTransactionNotFoundException; import com.genersoft.iot.vmp.gb28181.bean.*; import com.genersoft.iot.vmp.gb28181.event.SipSubscribe; +import com.genersoft.iot.vmp.gb28181.session.SSRCFactory; import com.genersoft.iot.vmp.gb28181.session.VideoStreamSessionManager; import com.genersoft.iot.vmp.gb28181.transmit.callback.DeferredResultHolder; import com.genersoft.iot.vmp.gb28181.transmit.callback.RequestMessage; @@ -100,6 +101,9 @@ public class PlayServiceImpl implements IPlayService { @Autowired private ZlmHttpHookSubscribe subscribe; + @Autowired + private SSRCFactory ssrcFactory; + @Override public void play(MediaServerItem mediaServerItem, String deviceId, String channelId, @@ -295,10 +299,10 @@ public class PlayServiceImpl implements IPlayService { if (!mediaServerItem.isRtpEnable() || device.isSsrcCheck()) { logger.info("[点播消息] SSRC修正 {}->{}", ssrcInfo.getSsrc(), ssrcInResponse); - if (!mediaServerItem.getSsrcConfig().checkSsrc(ssrcInResponse)) { + if (!ssrcFactory.checkSsrc(mediaServerItem.getId(),ssrcInResponse)) { // ssrc 不可用 // 释放ssrc - mediaServerService.releaseSsrc(mediaServerItem.getId(), ssrcInfo.getSsrc()); + ssrcFactory.releaseSsrc(mediaServerItem.getId(), ssrcInfo.getSsrc()); streamSession.remove(device.getDeviceId(), channelId, ssrcInfo.getStream()); event.msg = "下级自定义了ssrc,但是此ssrc不可用"; event.statusCode = 400; @@ -536,7 +540,7 @@ public class PlayServiceImpl implements IPlayService { if (!mediaServerItem.isRtpEnable() || device.isSsrcCheck()) { logger.info("[回放消息] SSRC修正 {}->{}", ssrcInfo.getSsrc(), ssrcInResponse); - if (!mediaServerItem.getSsrcConfig().checkSsrc(ssrcInResponse)) { + if (!ssrcFactory.checkSsrc(mediaServerItem.getId(),ssrcInResponse)) { // ssrc 不可用 // 释放ssrc mediaServerService.releaseSsrc(mediaServerItem.getId(), ssrcInfo.getSsrc()); @@ -675,7 +679,7 @@ public class PlayServiceImpl implements IPlayService { if (!mediaServerItem.isRtpEnable() || device.isSsrcCheck()) { logger.info("[回放消息] SSRC修正 {}->{}", ssrcInfo.getSsrc(), ssrcInResponse); - if (!mediaServerItem.getSsrcConfig().checkSsrc(ssrcInResponse)) { + if (!ssrcFactory.checkSsrc(mediaServerItem.getId(),ssrcInResponse)) { // ssrc 不可用 // 释放ssrc mediaServerService.releaseSsrc(mediaServerItem.getId(), ssrcInfo.getSsrc()); diff --git a/src/main/java/com/genersoft/iot/vmp/utils/ConfigConst.java b/src/main/java/com/genersoft/iot/vmp/utils/ConfigConst.java deleted file mode 100644 index 125d8180d..000000000 --- a/src/main/java/com/genersoft/iot/vmp/utils/ConfigConst.java +++ /dev/null @@ -1,8 +0,0 @@ -package com.genersoft.iot.vmp.utils; - -public class ConfigConst { - /** - * 播流最大并发个数 - */ - public static final Integer MAX_STRTEAM_COUNT = 10000; -} From 403f1e16a39b9df292d50108197f25375d0fc471 Mon Sep 17 00:00:00 2001 From: 648540858 <648540858@qq.com> Date: Sat, 8 Apr 2023 15:36:59 +0800 Subject: [PATCH 05/48] =?UTF-8?q?=E5=90=88=E5=B9=B6=E4=BC=98=E5=8C=96ssrc?= =?UTF-8?q?=E5=AD=98=E5=82=A8=E7=BB=93=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../iot/vmp/conf/redis/RedisConfig.java | 42 ------------------- .../vmp/media/zlm/dto/MediaServerItem.java | 21 ---------- .../service/impl/MediaServerServiceImpl.java | 2 - 3 files changed, 65 deletions(-) delete mode 100644 src/main/java/com/genersoft/iot/vmp/conf/redis/RedisConfig.java diff --git a/src/main/java/com/genersoft/iot/vmp/conf/redis/RedisConfig.java b/src/main/java/com/genersoft/iot/vmp/conf/redis/RedisConfig.java deleted file mode 100644 index 0035248c0..000000000 --- a/src/main/java/com/genersoft/iot/vmp/conf/redis/RedisConfig.java +++ /dev/null @@ -1,42 +0,0 @@ -package com.genersoft.iot.vmp.conf.redis; - - -import com.alibaba.fastjson2.support.spring.data.redis.GenericFastJsonRedisSerializer; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.core.annotation.Order; -import org.springframework.data.redis.connection.RedisConnectionFactory; -import org.springframework.data.redis.core.RedisTemplate; -import org.springframework.data.redis.serializer.StringRedisSerializer; - -import org.springframework.data.redis.listener.PatternTopic; -import org.springframework.data.redis.listener.RedisMessageListenerContainer; - - -/** - * Redis中间件配置类,使用spring-data-redis集成,自动从application.yml中加载redis配置 - * swwheihei - * 2019年5月30日 上午10:58:25 - * - */ -@Configuration -@Order(value=1) -public class RedisConfig { - - - @Bean - public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory) { - RedisTemplate redisTemplate = new RedisTemplate<>(); - // 使用fastJson序列化 - GenericFastJsonRedisSerializer fastJsonRedisSerializer = new GenericFastJsonRedisSerializer(); - // value值的序列化采用fastJsonRedisSerializer - redisTemplate.setValueSerializer(fastJsonRedisSerializer); - redisTemplate.setHashValueSerializer(fastJsonRedisSerializer); - - // key的序列化采用StringRedisSerializer - redisTemplate.setKeySerializer(new StringRedisSerializer()); - redisTemplate.setHashKeySerializer(new StringRedisSerializer()); - redisTemplate.setConnectionFactory(redisConnectionFactory); - return redisTemplate; - } -} diff --git a/src/main/java/com/genersoft/iot/vmp/media/zlm/dto/MediaServerItem.java b/src/main/java/com/genersoft/iot/vmp/media/zlm/dto/MediaServerItem.java index ea248fde4..e6bbb5fac 100644 --- a/src/main/java/com/genersoft/iot/vmp/media/zlm/dto/MediaServerItem.java +++ b/src/main/java/com/genersoft/iot/vmp/media/zlm/dto/MediaServerItem.java @@ -1,13 +1,10 @@ package com.genersoft.iot.vmp.media.zlm.dto; -import com.genersoft.iot.vmp.gb28181.session.SSRCFactory; import com.genersoft.iot.vmp.media.zlm.ZLMServerConfig; import io.swagger.v3.oas.annotations.media.Schema; import org.springframework.util.ObjectUtils; -import java.util.HashMap; - @Schema(description = "流媒体服务信息") public class MediaServerItem{ @@ -80,20 +77,10 @@ public class MediaServerItem{ @Schema(description = "是否是默认ZLM") private boolean defaultServer; -// @Schema(description = "SSRC信息") -// private SsrcConfig ssrcConfig; - @Schema(description = "当前使用到的端口") private int currentPort; - /** - * 每一台ZLM都有一套独立的SSRC列表 - * 在ApplicationCheckRunner里对mediaServerSsrcMap进行初始化 - */ - @Schema(description = "ID") - private HashMap mediaServerSsrcMap; - public MediaServerItem() { } @@ -279,14 +266,6 @@ public class MediaServerItem{ this.updateTime = updateTime; } - public HashMap getMediaServerSsrcMap() { - return mediaServerSsrcMap; - } - - public void setMediaServerSsrcMap(HashMap mediaServerSsrcMap) { - this.mediaServerSsrcMap = mediaServerSsrcMap; - } - public int getCurrentPort() { return currentPort; } diff --git a/src/main/java/com/genersoft/iot/vmp/service/impl/MediaServerServiceImpl.java b/src/main/java/com/genersoft/iot/vmp/service/impl/MediaServerServiceImpl.java index 07177f089..856359cd8 100644 --- a/src/main/java/com/genersoft/iot/vmp/service/impl/MediaServerServiceImpl.java +++ b/src/main/java/com/genersoft/iot/vmp/service/impl/MediaServerServiceImpl.java @@ -10,8 +10,6 @@ import com.genersoft.iot.vmp.conf.UserSetting; import com.genersoft.iot.vmp.conf.exception.ControllerException; import com.genersoft.iot.vmp.gb28181.event.EventPublisher; import com.genersoft.iot.vmp.gb28181.session.SSRCFactory; -import com.genersoft.iot.vmp.gb28181.session.VideoStreamSessionManager; -import com.genersoft.iot.vmp.gb28181.session.SsrcConfig; import com.genersoft.iot.vmp.media.zlm.AssistRESTfulUtils; import com.genersoft.iot.vmp.media.zlm.ZLMRESTfulUtils; import com.genersoft.iot.vmp.media.zlm.ZLMRTPServerFactory; From 15f718bd646d6566ca9c4e702dc5985198565efd Mon Sep 17 00:00:00 2001 From: 648540858 <648540858@qq.com> Date: Tue, 11 Apr 2023 11:23:36 +0800 Subject: [PATCH 06/48] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E7=A9=BA=E6=8C=87?= =?UTF-8?q?=E9=92=88=E5=BC=82=E5=B8=B8=20#813?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/genersoft/iot/vmp/conf/SipPlatformRunner.java | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/genersoft/iot/vmp/conf/SipPlatformRunner.java b/src/main/java/com/genersoft/iot/vmp/conf/SipPlatformRunner.java index 0a8405b16..55363efbf 100644 --- a/src/main/java/com/genersoft/iot/vmp/conf/SipPlatformRunner.java +++ b/src/main/java/com/genersoft/iot/vmp/conf/SipPlatformRunner.java @@ -48,10 +48,13 @@ public class SipPlatformRunner implements CommandLineRunner { parentPlatformCatch.setParentPlatform(parentPlatform); parentPlatformCatch.setId(parentPlatform.getServerGBId()); redisCatchStorage.updatePlatformCatchInfo(parentPlatformCatch); - // 取消订阅 - sipCommanderForPlatform.unregister(parentPlatform, parentPlatformCatchOld.getSipTransactionInfo(), null, (eventResult)->{ - platformService.login(parentPlatform); - }); + if (parentPlatformCatchOld != null) { + // 取消订阅 + sipCommanderForPlatform.unregister(parentPlatform, parentPlatformCatchOld.getSipTransactionInfo(), null, (eventResult)->{ + platformService.login(parentPlatform); + }); + } + // 设置所有平台离线 platformService.offline(parentPlatform, true); } From 88779b983ec38b6206eed03c9f9213c0db8d913b Mon Sep 17 00:00:00 2001 From: xubinbin <1323875150@qq.com> Date: Thu, 13 Apr 2023 10:22:28 +0800 Subject: [PATCH 07/48] =?UTF-8?q?=E5=A4=84=E7=90=86=E4=B8=8A=E7=BA=A7?= =?UTF-8?q?=E5=B9=B3=E5=8F=B0=E5=8F=91=E9=80=81=E7=9A=84invite=E8=AF=B7?= =?UTF-8?q?=E6=B1=82=E4=B8=8D=E6=90=BA=E5=B8=A6=E2=80=9Cy=3D=E2=80=9Dsdp?= =?UTF-8?q?=E4=BF=A1=E6=81=AF=E6=97=B6=EF=BC=8C=E5=B9=B6=E4=B8=94=E8=AE=BE?= =?UTF-8?q?=E5=A4=87=E5=B7=B2=E7=BB=8F=E5=9C=A8=E5=BD=93=E5=89=8D=E5=B9=B3?= =?UTF-8?q?=E5=8F=B0=E4=B8=AD=E7=82=B9=E6=92=AD=E4=BA=86=E3=80=82=E7=BB=99?= =?UTF-8?q?=E4=B8=8A=E7=BA=A7=E5=B9=B3=E5=8F=B0=E5=9B=9E=E5=A4=8D=E7=9A=84?= =?UTF-8?q?ssrc=E4=BD=BF=E7=94=A8=E9=BB=98=E8=AE=A4=E2=80=9Cy=3D0000000000?= =?UTF-8?q?=E2=80=9D=EF=BC=8C=E4=B8=8A=E7=BA=A7=E5=B9=B3=E5=8F=B0=E6=97=A0?= =?UTF-8?q?=E6=B3=95=E6=92=AD=E6=94=BE=E8=A7=86=E9=A2=91=E7=9A=84=E9=97=AE?= =?UTF-8?q?=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../transmit/event/request/impl/InviteRequestProcessor.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/InviteRequestProcessor.java b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/InviteRequestProcessor.java index 80734177f..01a2a6e91 100644 --- a/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/InviteRequestProcessor.java +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/InviteRequestProcessor.java @@ -482,6 +482,12 @@ public class InviteRequestProcessor extends SIPRequestProcessorParent implements redisCatchStorage.deleteSendRTPServer(platform.getServerGBId(), channelId, callIdHeader.getCallId(), null); }); } else { + // 当前系统作为下级平台使用,当上级平台点播时不携带ssrc时,并且设备在当前系统中已经点播了。这个时候需要重新给生成一个ssrc,不使用默认的"0000000000"。 + if (ssrc.equals(ssrcDefault)) { + ssrc = ssrcFactory.getPlaySsrc(mediaServerItem.getId()); + sendRtpItem.setSsrc(ssrc); + } + sendRtpItem.setStreamId(playTransaction.getStream()); // 写入redis, 超时时回复 redisCatchStorage.updateSendRTPSever(sendRtpItem); From 5c5699ae11ef02a675f7f9d81783b4ad02f312ea Mon Sep 17 00:00:00 2001 From: 648540858 <648540858@qq.com> Date: Thu, 13 Apr 2023 17:14:04 +0800 Subject: [PATCH 08/48] =?UTF-8?q?redis=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../vmp/conf/redis/RedisTemplateConfig.java | 4 +- .../genersoft/iot/vmp/gb28181/SipLayer.java | 11 +- .../iot/vmp/gb28181/transmit/SIPSender.java | 2 +- .../cmd/SIPRequestHeaderPlarformProvider.java | 133 +++++------ .../cmd/SIPRequestHeaderProvider.java | 213 +++++++++--------- .../transmit/cmd/impl/SIPCommander.java | 5 +- .../cmd/impl/SIPCommanderFroPlatform.java | 3 +- .../request/impl/NotifyRequestProcessor.java | 1 + .../impl/RegisterRequestProcessor.java | 13 -- .../impl/InviteResponseProcessor.java | 24 +- .../iot/vmp/gb28181/utils/SipUtils.java | 4 +- .../vmp/media/zlm/ZLMHttpHookListener.java | 2 +- .../RedisPushStreamStatusMsgListener.java | 20 +- 13 files changed, 206 insertions(+), 229 deletions(-) diff --git a/src/main/java/com/genersoft/iot/vmp/conf/redis/RedisTemplateConfig.java b/src/main/java/com/genersoft/iot/vmp/conf/redis/RedisTemplateConfig.java index 1868a5d48..df3345eef 100644 --- a/src/main/java/com/genersoft/iot/vmp/conf/redis/RedisTemplateConfig.java +++ b/src/main/java/com/genersoft/iot/vmp/conf/redis/RedisTemplateConfig.java @@ -1,6 +1,6 @@ package com.genersoft.iot.vmp.conf.redis; -import com.genersoft.iot.vmp.utils.redis.FastJsonRedisSerializer; +import com.alibaba.fastjson2.support.spring.data.redis.GenericFastJsonRedisSerializer; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.redis.connection.RedisConnectionFactory; @@ -14,7 +14,7 @@ public class RedisTemplateConfig { public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory) { RedisTemplate redisTemplate = new RedisTemplate<>(); // 使用fastJson序列化 - FastJsonRedisSerializer fastJsonRedisSerializer = new FastJsonRedisSerializer(Object.class); + GenericFastJsonRedisSerializer fastJsonRedisSerializer = new GenericFastJsonRedisSerializer(); // value值的序列化采用fastJsonRedisSerializer redisTemplate.setValueSerializer(fastJsonRedisSerializer); redisTemplate.setHashValueSerializer(fastJsonRedisSerializer); diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/SipLayer.java b/src/main/java/com/genersoft/iot/vmp/gb28181/SipLayer.java index d452771ec..782384623 100644 --- a/src/main/java/com/genersoft/iot/vmp/gb28181/SipLayer.java +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/SipLayer.java @@ -36,8 +36,6 @@ public class SipLayer implements CommandLineRunner { private final Map tcpSipProviderMap = new ConcurrentHashMap<>(); private final Map udpSipProviderMap = new ConcurrentHashMap<>(); - private SipFactory sipFactory; - @Override public void run(String... args) { List monitorIps = new ArrayList<>(); @@ -50,8 +48,7 @@ public class SipLayer implements CommandLineRunner { monitorIps.add(sipConfig.getIp()); } - sipFactory = SipFactory.getInstance(); - sipFactory.setPathName("gov.nist"); + SipFactory.getInstance().setPathName("gov.nist"); if (monitorIps.size() > 0) { for (String monitorIp : monitorIps) { addListeningPoint(monitorIp, sipConfig.getPort()); @@ -65,7 +62,7 @@ public class SipLayer implements CommandLineRunner { private void addListeningPoint(String monitorIp, int port){ SipStackImpl sipStack; try { - sipStack = (SipStackImpl)sipFactory.createSipStack(DefaultProperties.getProperties(monitorIp, userSetting.getSipLog())); + sipStack = (SipStackImpl)SipFactory.getInstance().createSipStack(DefaultProperties.getProperties(monitorIp, userSetting.getSipLog())); } catch (PeerUnavailableException e) { logger.error("[Sip Server] SIP服务启动失败, 监听地址{}失败,请检查ip是否正确", monitorIp); return; @@ -106,10 +103,6 @@ public class SipLayer implements CommandLineRunner { } } - public SipFactory getSipFactory() { - return sipFactory; - } - public SipProviderImpl getUdpSipProvider(String ip) { if (ObjectUtils.isEmpty(ip)) { return null; diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/SIPSender.java b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/SIPSender.java index 03ce619c9..89e6e32f2 100644 --- a/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/SIPSender.java +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/SIPSender.java @@ -57,7 +57,7 @@ public class SIPSender { } if (message.getHeader(UserAgentHeader.NAME) == null) { try { - message.addHeader(SipUtils.createUserAgentHeader(sipLayer.getSipFactory(), gitUtil)); + message.addHeader(SipUtils.createUserAgentHeader(gitUtil)); } catch (ParseException e) { logger.error("添加UserAgentHeader失败", e); } diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/cmd/SIPRequestHeaderPlarformProvider.java b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/cmd/SIPRequestHeaderPlarformProvider.java index f14d0d114..831897a7c 100644 --- a/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/cmd/SIPRequestHeaderPlarformProvider.java +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/cmd/SIPRequestHeaderPlarformProvider.java @@ -16,6 +16,7 @@ import org.springframework.util.DigestUtils; import javax.sip.InvalidArgumentException; import javax.sip.PeerUnavailableException; +import javax.sip.SipFactory; import javax.sip.address.Address; import javax.sip.address.SipURI; import javax.sip.header.*; @@ -49,39 +50,39 @@ public class SIPRequestHeaderPlarformProvider { Request request = null; String sipAddress = parentPlatform.getDeviceIp() + ":" + parentPlatform.getDevicePort(); //请求行 - SipURI requestLine = sipLayer.getSipFactory().createAddressFactory().createSipURI(parentPlatform.getServerGBId(), + SipURI requestLine = SipFactory.getInstance().createAddressFactory().createSipURI(parentPlatform.getServerGBId(), parentPlatform.getServerIP() + ":" + parentPlatform.getServerPort()); //via ArrayList viaHeaders = new ArrayList(); - ViaHeader viaHeader = sipLayer.getSipFactory().createHeaderFactory().createViaHeader(parentPlatform.getServerIP(), + ViaHeader viaHeader = SipFactory.getInstance().createHeaderFactory().createViaHeader(parentPlatform.getServerIP(), parentPlatform.getServerPort(), parentPlatform.getTransport(), SipUtils.getNewViaTag()); viaHeader.setRPort(); viaHeaders.add(viaHeader); //from - SipURI fromSipURI = sipLayer.getSipFactory().createAddressFactory().createSipURI(parentPlatform.getDeviceGBId(), sipConfig.getDomain()); - Address fromAddress = sipLayer.getSipFactory().createAddressFactory().createAddress(fromSipURI); - FromHeader fromHeader = sipLayer.getSipFactory().createHeaderFactory().createFromHeader(fromAddress, fromTag); + SipURI fromSipURI = SipFactory.getInstance().createAddressFactory().createSipURI(parentPlatform.getDeviceGBId(), sipConfig.getDomain()); + Address fromAddress = SipFactory.getInstance().createAddressFactory().createAddress(fromSipURI); + FromHeader fromHeader = SipFactory.getInstance().createHeaderFactory().createFromHeader(fromAddress, fromTag); //to - SipURI toSipURI = sipLayer.getSipFactory().createAddressFactory().createSipURI(parentPlatform.getDeviceGBId(), sipConfig.getDomain()); - Address toAddress = sipLayer.getSipFactory().createAddressFactory().createAddress(toSipURI); - ToHeader toHeader = sipLayer.getSipFactory().createHeaderFactory().createToHeader(toAddress,toTag); + SipURI toSipURI = SipFactory.getInstance().createAddressFactory().createSipURI(parentPlatform.getDeviceGBId(), sipConfig.getDomain()); + Address toAddress = SipFactory.getInstance().createAddressFactory().createAddress(toSipURI); + ToHeader toHeader = SipFactory.getInstance().createHeaderFactory().createToHeader(toAddress,toTag); //Forwards - MaxForwardsHeader maxForwards = sipLayer.getSipFactory().createHeaderFactory().createMaxForwardsHeader(70); + MaxForwardsHeader maxForwards = SipFactory.getInstance().createHeaderFactory().createMaxForwardsHeader(70); //ceq - CSeqHeader cSeqHeader = sipLayer.getSipFactory().createHeaderFactory().createCSeqHeader(CSeq, Request.REGISTER); - request = sipLayer.getSipFactory().createMessageFactory().createRequest(requestLine, Request.REGISTER, callIdHeader, + CSeqHeader cSeqHeader = SipFactory.getInstance().createHeaderFactory().createCSeqHeader(CSeq, Request.REGISTER); + request = SipFactory.getInstance().createMessageFactory().createRequest(requestLine, Request.REGISTER, callIdHeader, cSeqHeader,fromHeader, toHeader, viaHeaders, maxForwards); - Address concatAddress = sipLayer.getSipFactory().createAddressFactory().createAddress(sipLayer.getSipFactory().createAddressFactory() + Address concatAddress = SipFactory.getInstance().createAddressFactory().createAddress(SipFactory.getInstance().createAddressFactory() .createSipURI(parentPlatform.getDeviceGBId(), sipAddress)); - request.addHeader(sipLayer.getSipFactory().createHeaderFactory().createContactHeader(concatAddress)); + request.addHeader(SipFactory.getInstance().createHeaderFactory().createContactHeader(concatAddress)); - ExpiresHeader expiresHeader = sipLayer.getSipFactory().createHeaderFactory().createExpiresHeader(expires); + ExpiresHeader expiresHeader = SipFactory.getInstance().createHeaderFactory().createExpiresHeader(expires); request.addHeader(expiresHeader); - request.addHeader(SipUtils.createUserAgentHeader(sipLayer.getSipFactory(), gitUtil)); + request.addHeader(SipUtils.createUserAgentHeader(gitUtil)); return request; } @@ -91,9 +92,9 @@ public class SIPRequestHeaderPlarformProvider { Request registerRequest = createRegisterRequest(parentPlatform, redisCatchStorage.getCSEQ(), fromTag, toTag, callIdHeader, expires); - SipURI requestURI = sipLayer.getSipFactory().createAddressFactory().createSipURI(parentPlatform.getServerGBId(), parentPlatform.getServerIP() + ":" + parentPlatform.getServerPort()); + SipURI requestURI = SipFactory.getInstance().createAddressFactory().createSipURI(parentPlatform.getServerGBId(), parentPlatform.getServerIP() + ":" + parentPlatform.getServerPort()); if (www == null) { - AuthorizationHeader authorizationHeader = sipLayer.getSipFactory().createHeaderFactory().createAuthorizationHeader("Digest"); + AuthorizationHeader authorizationHeader = SipFactory.getInstance().createHeaderFactory().createAuthorizationHeader("Digest"); String username = parentPlatform.getUsername(); if ( username == null || username == "" ) { @@ -146,7 +147,7 @@ public class SIPRequestHeaderPlarformProvider { String RESPONSE = DigestUtils.md5DigestAsHex(reStr.toString().getBytes()); - AuthorizationHeader authorizationHeader = sipLayer.getSipFactory().createHeaderFactory().createAuthorizationHeader(scheme); + AuthorizationHeader authorizationHeader = SipFactory.getInstance().createHeaderFactory().createAuthorizationHeader(scheme); authorizationHeader.setUsername(parentPlatform.getDeviceGBId()); authorizationHeader.setRealm(realm); authorizationHeader.setNonce(nonce); @@ -164,7 +165,7 @@ public class SIPRequestHeaderPlarformProvider { } public Request createMessageRequest(ParentPlatform parentPlatform, String content, SendRtpItem sendRtpItem) throws PeerUnavailableException, ParseException, InvalidArgumentException { - CallIdHeader callIdHeader = sipLayer.getSipFactory().createHeaderFactory().createCallIdHeader(sendRtpItem.getCallId()); + CallIdHeader callIdHeader = SipFactory.getInstance().createHeaderFactory().createCallIdHeader(sendRtpItem.getCallId()); return createMessageRequest(parentPlatform, content, sendRtpItem.getToTag(), SipUtils.getNewViaTag(), sendRtpItem.getFromTag(), callIdHeader); } @@ -177,36 +178,36 @@ public class SIPRequestHeaderPlarformProvider { Request request = null; String serverAddress = parentPlatform.getServerIP()+ ":" + parentPlatform.getServerPort(); // sipuri - SipURI requestURI = sipLayer.getSipFactory().createAddressFactory().createSipURI(parentPlatform.getServerGBId(), serverAddress); + SipURI requestURI = SipFactory.getInstance().createAddressFactory().createSipURI(parentPlatform.getServerGBId(), serverAddress); // via ArrayList viaHeaders = new ArrayList(); - ViaHeader viaHeader = sipLayer.getSipFactory().createHeaderFactory().createViaHeader(parentPlatform.getDeviceIp(), Integer.parseInt(parentPlatform.getDevicePort()), + ViaHeader viaHeader = SipFactory.getInstance().createHeaderFactory().createViaHeader(parentPlatform.getDeviceIp(), Integer.parseInt(parentPlatform.getDevicePort()), parentPlatform.getTransport(), viaTag); viaHeader.setRPort(); viaHeaders.add(viaHeader); // from - // SipURI fromSipURI = sipLayer.getSipFactory().createAddressFactory().createSipURI(parentPlatform.getDeviceGBId(), parentPlatform.getDeviceIp() + ":" + parentPlatform.getDeviceIp()); - SipURI fromSipURI = sipLayer.getSipFactory().createAddressFactory().createSipURI(parentPlatform.getDeviceGBId(), sipConfig.getDomain()); - Address fromAddress = sipLayer.getSipFactory().createAddressFactory().createAddress(fromSipURI); - FromHeader fromHeader = sipLayer.getSipFactory().createHeaderFactory().createFromHeader(fromAddress, fromTag); + // SipURI fromSipURI = SipFactory.getInstance().createAddressFactory().createSipURI(parentPlatform.getDeviceGBId(), parentPlatform.getDeviceIp() + ":" + parentPlatform.getDeviceIp()); + SipURI fromSipURI = SipFactory.getInstance().createAddressFactory().createSipURI(parentPlatform.getDeviceGBId(), sipConfig.getDomain()); + Address fromAddress = SipFactory.getInstance().createAddressFactory().createAddress(fromSipURI); + FromHeader fromHeader = SipFactory.getInstance().createHeaderFactory().createFromHeader(fromAddress, fromTag); // to - SipURI toSipURI = sipLayer.getSipFactory().createAddressFactory().createSipURI(parentPlatform.getServerGBId(), serverAddress); - Address toAddress = sipLayer.getSipFactory().createAddressFactory().createAddress(toSipURI); - ToHeader toHeader = sipLayer.getSipFactory().createHeaderFactory().createToHeader(toAddress, toTag); + SipURI toSipURI = SipFactory.getInstance().createAddressFactory().createSipURI(parentPlatform.getServerGBId(), serverAddress); + Address toAddress = SipFactory.getInstance().createAddressFactory().createAddress(toSipURI); + ToHeader toHeader = SipFactory.getInstance().createHeaderFactory().createToHeader(toAddress, toTag); // Forwards - MaxForwardsHeader maxForwards = sipLayer.getSipFactory().createHeaderFactory().createMaxForwardsHeader(70); + MaxForwardsHeader maxForwards = SipFactory.getInstance().createHeaderFactory().createMaxForwardsHeader(70); // ceq - CSeqHeader cSeqHeader = sipLayer.getSipFactory().createHeaderFactory().createCSeqHeader(redisCatchStorage.getCSEQ(), Request.MESSAGE); - MessageFactoryImpl messageFactory = (MessageFactoryImpl) sipLayer.getSipFactory().createMessageFactory(); + CSeqHeader cSeqHeader = SipFactory.getInstance().createHeaderFactory().createCSeqHeader(redisCatchStorage.getCSEQ(), Request.MESSAGE); + MessageFactoryImpl messageFactory = (MessageFactoryImpl) SipFactory.getInstance().createMessageFactory(); // 设置编码, 防止中文乱码 messageFactory.setDefaultContentEncodingCharset(parentPlatform.getCharacterSet()); request = messageFactory.createRequest(requestURI, Request.MESSAGE, callIdHeader, cSeqHeader, fromHeader, toHeader, viaHeaders, maxForwards); - request.addHeader(SipUtils.createUserAgentHeader(sipLayer.getSipFactory(), gitUtil)); + request.addHeader(SipUtils.createUserAgentHeader(gitUtil)); - ContentTypeHeader contentTypeHeader = sipLayer.getSipFactory().createHeaderFactory().createContentTypeHeader("Application", "MANSCDP+xml"); + ContentTypeHeader contentTypeHeader = SipFactory.getInstance().createHeaderFactory().createContentTypeHeader("Application", "MANSCDP+xml"); request.setContent(content, contentTypeHeader); return request; } @@ -214,54 +215,54 @@ public class SIPRequestHeaderPlarformProvider { public SIPRequest createNotifyRequest(ParentPlatform parentPlatform, String content, SubscribeInfo subscribeInfo) throws PeerUnavailableException, ParseException, InvalidArgumentException { SIPRequest request = null; // sipuri - SipURI requestURI = sipLayer.getSipFactory().createAddressFactory().createSipURI(parentPlatform.getServerGBId(), parentPlatform.getServerIP()+ ":" + parentPlatform.getServerPort()); + SipURI requestURI = SipFactory.getInstance().createAddressFactory().createSipURI(parentPlatform.getServerGBId(), parentPlatform.getServerIP()+ ":" + parentPlatform.getServerPort()); // via ArrayList viaHeaders = new ArrayList<>(); - ViaHeader viaHeader = sipLayer.getSipFactory().createHeaderFactory().createViaHeader(parentPlatform.getDeviceIp(), Integer.parseInt(parentPlatform.getDevicePort()), + ViaHeader viaHeader = SipFactory.getInstance().createHeaderFactory().createViaHeader(parentPlatform.getDeviceIp(), Integer.parseInt(parentPlatform.getDevicePort()), parentPlatform.getTransport(), SipUtils.getNewViaTag()); viaHeader.setRPort(); viaHeaders.add(viaHeader); // from - SipURI fromSipURI = sipLayer.getSipFactory().createAddressFactory().createSipURI(parentPlatform.getDeviceGBId(), + SipURI fromSipURI = SipFactory.getInstance().createAddressFactory().createSipURI(parentPlatform.getDeviceGBId(), parentPlatform.getDeviceIp() + ":" + parentPlatform.getDevicePort()); - Address fromAddress = sipLayer.getSipFactory().createAddressFactory().createAddress(fromSipURI); - FromHeader fromHeader = sipLayer.getSipFactory().createHeaderFactory().createFromHeader(fromAddress, subscribeInfo.getResponse().getToTag()); + Address fromAddress = SipFactory.getInstance().createAddressFactory().createAddress(fromSipURI); + FromHeader fromHeader = SipFactory.getInstance().createHeaderFactory().createFromHeader(fromAddress, subscribeInfo.getResponse().getToTag()); // to - SipURI toSipURI = sipLayer.getSipFactory().createAddressFactory().createSipURI(parentPlatform.getServerGBId(), parentPlatform.getServerGBDomain()); - Address toAddress = sipLayer.getSipFactory().createAddressFactory().createAddress(toSipURI); - ToHeader toHeader = sipLayer.getSipFactory().createHeaderFactory().createToHeader(toAddress, subscribeInfo.getRequest().getFromTag()); + SipURI toSipURI = SipFactory.getInstance().createAddressFactory().createSipURI(parentPlatform.getServerGBId(), parentPlatform.getServerGBDomain()); + Address toAddress = SipFactory.getInstance().createAddressFactory().createAddress(toSipURI); + ToHeader toHeader = SipFactory.getInstance().createHeaderFactory().createToHeader(toAddress, subscribeInfo.getRequest().getFromTag()); // Forwards - MaxForwardsHeader maxForwards = sipLayer.getSipFactory().createHeaderFactory().createMaxForwardsHeader(70); + MaxForwardsHeader maxForwards = SipFactory.getInstance().createHeaderFactory().createMaxForwardsHeader(70); // ceq - CSeqHeader cSeqHeader = sipLayer.getSipFactory().createHeaderFactory().createCSeqHeader(redisCatchStorage.getCSEQ(), Request.NOTIFY); - MessageFactoryImpl messageFactory = (MessageFactoryImpl) sipLayer.getSipFactory().createMessageFactory(); + CSeqHeader cSeqHeader = SipFactory.getInstance().createHeaderFactory().createCSeqHeader(redisCatchStorage.getCSEQ(), Request.NOTIFY); + MessageFactoryImpl messageFactory = (MessageFactoryImpl) SipFactory.getInstance().createMessageFactory(); // 设置编码, 防止中文乱码 messageFactory.setDefaultContentEncodingCharset("gb2312"); - CallIdHeader callIdHeader = sipLayer.getSipFactory().createHeaderFactory().createCallIdHeader(subscribeInfo.getRequest().getCallIdHeader().getCallId()); + CallIdHeader callIdHeader = SipFactory.getInstance().createHeaderFactory().createCallIdHeader(subscribeInfo.getRequest().getCallIdHeader().getCallId()); request = (SIPRequest) messageFactory.createRequest(requestURI, Request.NOTIFY, callIdHeader, cSeqHeader, fromHeader, toHeader, viaHeaders, maxForwards); - request.addHeader(SipUtils.createUserAgentHeader(sipLayer.getSipFactory(), gitUtil)); + request.addHeader(SipUtils.createUserAgentHeader(gitUtil)); - EventHeader event = sipLayer.getSipFactory().createHeaderFactory().createEventHeader(subscribeInfo.getEventType()); + EventHeader event = SipFactory.getInstance().createHeaderFactory().createEventHeader(subscribeInfo.getEventType()); if (subscribeInfo.getEventId() != null) { event.setEventId(subscribeInfo.getEventId()); } request.addHeader(event); - SubscriptionStateHeader active = sipLayer.getSipFactory().createHeaderFactory().createSubscriptionStateHeader("active"); + SubscriptionStateHeader active = SipFactory.getInstance().createHeaderFactory().createSubscriptionStateHeader("active"); request.setHeader(active); String sipAddress = parentPlatform.getDeviceIp() + ":" + parentPlatform.getDevicePort(); - Address concatAddress = sipLayer.getSipFactory().createAddressFactory().createAddress(sipLayer.getSipFactory().createAddressFactory() + Address concatAddress = SipFactory.getInstance().createAddressFactory().createAddress(SipFactory.getInstance().createAddressFactory() .createSipURI(parentPlatform.getDeviceGBId(), sipAddress)); - request.addHeader(sipLayer.getSipFactory().createHeaderFactory().createContactHeader(concatAddress)); + request.addHeader(SipFactory.getInstance().createHeaderFactory().createContactHeader(concatAddress)); - ContentTypeHeader contentTypeHeader = sipLayer.getSipFactory().createHeaderFactory().createContentTypeHeader("Application", "MANSCDP+xml"); + ContentTypeHeader contentTypeHeader = SipFactory.getInstance().createHeaderFactory().createContentTypeHeader("Application", "MANSCDP+xml"); request.setContent(content, contentTypeHeader); return request; } @@ -274,42 +275,42 @@ public class SIPRequestHeaderPlarformProvider { SIPRequest request = null; // sipuri - SipURI requestURI = sipLayer.getSipFactory().createAddressFactory().createSipURI(platform.getServerGBId(), platform.getServerIP()+ ":" + platform.getServerPort()); + SipURI requestURI = SipFactory.getInstance().createAddressFactory().createSipURI(platform.getServerGBId(), platform.getServerIP()+ ":" + platform.getServerPort()); // via ArrayList viaHeaders = new ArrayList<>(); - ViaHeader viaHeader = sipLayer.getSipFactory().createHeaderFactory().createViaHeader(platform.getDeviceIp(), Integer.parseInt(platform.getDevicePort()), + ViaHeader viaHeader = SipFactory.getInstance().createHeaderFactory().createViaHeader(platform.getDeviceIp(), Integer.parseInt(platform.getDevicePort()), platform.getTransport(), SipUtils.getNewViaTag()); viaHeader.setRPort(); viaHeaders.add(viaHeader); // from - SipURI fromSipURI = sipLayer.getSipFactory().createAddressFactory().createSipURI(platform.getDeviceGBId(), + SipURI fromSipURI = SipFactory.getInstance().createAddressFactory().createSipURI(platform.getDeviceGBId(), platform.getDeviceIp() + ":" + platform.getDevicePort()); - Address fromAddress = sipLayer.getSipFactory().createAddressFactory().createAddress(fromSipURI); - FromHeader fromHeader = sipLayer.getSipFactory().createHeaderFactory().createFromHeader(fromAddress, sendRtpItem.getToTag()); + Address fromAddress = SipFactory.getInstance().createAddressFactory().createAddress(fromSipURI); + FromHeader fromHeader = SipFactory.getInstance().createHeaderFactory().createFromHeader(fromAddress, sendRtpItem.getToTag()); // to - SipURI toSipURI = sipLayer.getSipFactory().createAddressFactory().createSipURI(platform.getServerGBId(), platform.getServerGBDomain()); - Address toAddress = sipLayer.getSipFactory().createAddressFactory().createAddress(toSipURI); - ToHeader toHeader = sipLayer.getSipFactory().createHeaderFactory().createToHeader(toAddress, sendRtpItem.getFromTag()); + SipURI toSipURI = SipFactory.getInstance().createAddressFactory().createSipURI(platform.getServerGBId(), platform.getServerGBDomain()); + Address toAddress = SipFactory.getInstance().createAddressFactory().createAddress(toSipURI); + ToHeader toHeader = SipFactory.getInstance().createHeaderFactory().createToHeader(toAddress, sendRtpItem.getFromTag()); // Forwards - MaxForwardsHeader maxForwards = sipLayer.getSipFactory().createHeaderFactory().createMaxForwardsHeader(70); + MaxForwardsHeader maxForwards = SipFactory.getInstance().createHeaderFactory().createMaxForwardsHeader(70); // ceq - CSeqHeader cSeqHeader = sipLayer.getSipFactory().createHeaderFactory().createCSeqHeader(redisCatchStorage.getCSEQ(), Request.BYE); - MessageFactoryImpl messageFactory = (MessageFactoryImpl) sipLayer.getSipFactory().createMessageFactory(); + CSeqHeader cSeqHeader = SipFactory.getInstance().createHeaderFactory().createCSeqHeader(redisCatchStorage.getCSEQ(), Request.BYE); + MessageFactoryImpl messageFactory = (MessageFactoryImpl) SipFactory.getInstance().createMessageFactory(); // 设置编码, 防止中文乱码 messageFactory.setDefaultContentEncodingCharset("gb2312"); - CallIdHeader callIdHeader = sipLayer.getSipFactory().createHeaderFactory().createCallIdHeader(sendRtpItem.getCallId()); + CallIdHeader callIdHeader = SipFactory.getInstance().createHeaderFactory().createCallIdHeader(sendRtpItem.getCallId()); request = (SIPRequest) messageFactory.createRequest(requestURI, Request.BYE, callIdHeader, cSeqHeader, fromHeader, toHeader, viaHeaders, maxForwards); - request.addHeader(SipUtils.createUserAgentHeader(sipLayer.getSipFactory(), gitUtil)); + request.addHeader(SipUtils.createUserAgentHeader(gitUtil)); String sipAddress = platform.getDeviceIp() + ":" + platform.getDevicePort(); - Address concatAddress = sipLayer.getSipFactory().createAddressFactory().createAddress(sipLayer.getSipFactory().createAddressFactory() + Address concatAddress = SipFactory.getInstance().createAddressFactory().createAddress(SipFactory.getInstance().createAddressFactory() .createSipURI(platform.getDeviceGBId(), sipAddress)); - request.addHeader(sipLayer.getSipFactory().createHeaderFactory().createContactHeader(concatAddress)); + request.addHeader(SipFactory.getInstance().createHeaderFactory().createContactHeader(concatAddress)); return request; } diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/cmd/SIPRequestHeaderProvider.java b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/cmd/SIPRequestHeaderProvider.java index 40d049cbf..899643700 100644 --- a/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/cmd/SIPRequestHeaderProvider.java +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/cmd/SIPRequestHeaderProvider.java @@ -16,6 +16,7 @@ import org.springframework.stereotype.Component; import javax.sip.InvalidArgumentException; import javax.sip.PeerUnavailableException; import javax.sip.SipException; +import javax.sip.SipFactory; import javax.sip.address.Address; import javax.sip.address.SipURI; import javax.sip.header.*; @@ -49,32 +50,32 @@ public class SIPRequestHeaderProvider { public Request createMessageRequest(Device device, String content, String viaTag, String fromTag, String toTag, CallIdHeader callIdHeader) throws ParseException, InvalidArgumentException, PeerUnavailableException { Request request = null; // sipuri - SipURI requestURI = sipLayer.getSipFactory().createAddressFactory().createSipURI(device.getDeviceId(), device.getHostAddress()); + SipURI requestURI = SipFactory.getInstance().createAddressFactory().createSipURI(device.getDeviceId(), device.getHostAddress()); // via ArrayList viaHeaders = new ArrayList(); - ViaHeader viaHeader = sipLayer.getSipFactory().createHeaderFactory().createViaHeader(sipLayer.getLocalIp(device.getLocalIp()), sipConfig.getPort(), device.getTransport(), viaTag); + ViaHeader viaHeader = SipFactory.getInstance().createHeaderFactory().createViaHeader(sipLayer.getLocalIp(device.getLocalIp()), sipConfig.getPort(), device.getTransport(), viaTag); viaHeader.setRPort(); viaHeaders.add(viaHeader); // from - SipURI fromSipURI = sipLayer.getSipFactory().createAddressFactory().createSipURI(sipConfig.getId(), sipConfig.getDomain()); - Address fromAddress = sipLayer.getSipFactory().createAddressFactory().createAddress(fromSipURI); - FromHeader fromHeader = sipLayer.getSipFactory().createHeaderFactory().createFromHeader(fromAddress, fromTag); + SipURI fromSipURI = SipFactory.getInstance().createAddressFactory().createSipURI(sipConfig.getId(), sipConfig.getDomain()); + Address fromAddress = SipFactory.getInstance().createAddressFactory().createAddress(fromSipURI); + FromHeader fromHeader = SipFactory.getInstance().createHeaderFactory().createFromHeader(fromAddress, fromTag); // to - SipURI toSipURI = sipLayer.getSipFactory().createAddressFactory().createSipURI(device.getDeviceId(), device.getHostAddress()); - Address toAddress = sipLayer.getSipFactory().createAddressFactory().createAddress(toSipURI); - ToHeader toHeader = sipLayer.getSipFactory().createHeaderFactory().createToHeader(toAddress, toTag); + SipURI toSipURI = SipFactory.getInstance().createAddressFactory().createSipURI(device.getDeviceId(), device.getHostAddress()); + Address toAddress = SipFactory.getInstance().createAddressFactory().createAddress(toSipURI); + ToHeader toHeader = SipFactory.getInstance().createHeaderFactory().createToHeader(toAddress, toTag); // Forwards - MaxForwardsHeader maxForwards = sipLayer.getSipFactory().createHeaderFactory().createMaxForwardsHeader(70); + MaxForwardsHeader maxForwards = SipFactory.getInstance().createHeaderFactory().createMaxForwardsHeader(70); // ceq - CSeqHeader cSeqHeader = sipLayer.getSipFactory().createHeaderFactory().createCSeqHeader(redisCatchStorage.getCSEQ(), Request.MESSAGE); + CSeqHeader cSeqHeader = SipFactory.getInstance().createHeaderFactory().createCSeqHeader(redisCatchStorage.getCSEQ(), Request.MESSAGE); - request = sipLayer.getSipFactory().createMessageFactory().createRequest(requestURI, Request.MESSAGE, callIdHeader, cSeqHeader, fromHeader, + request = SipFactory.getInstance().createMessageFactory().createRequest(requestURI, Request.MESSAGE, callIdHeader, cSeqHeader, fromHeader, toHeader, viaHeaders, maxForwards); - request.addHeader(SipUtils.createUserAgentHeader(sipLayer.getSipFactory(), gitUtil)); + request.addHeader(SipUtils.createUserAgentHeader(gitUtil)); - ContentTypeHeader contentTypeHeader = sipLayer.getSipFactory().createHeaderFactory().createContentTypeHeader("Application", "MANSCDP+xml"); + ContentTypeHeader contentTypeHeader = SipFactory.getInstance().createHeaderFactory().createContentTypeHeader("Application", "MANSCDP+xml"); request.setContent(content, contentTypeHeader); return request; } @@ -82,39 +83,39 @@ public class SIPRequestHeaderProvider { public Request createInviteRequest(Device device, String channelId, String content, String viaTag, String fromTag, String toTag, String ssrc, CallIdHeader callIdHeader) throws ParseException, InvalidArgumentException, PeerUnavailableException { Request request = null; //请求行 - SipURI requestLine = sipLayer.getSipFactory().createAddressFactory().createSipURI(channelId, device.getHostAddress()); + SipURI requestLine = SipFactory.getInstance().createAddressFactory().createSipURI(channelId, device.getHostAddress()); //via ArrayList viaHeaders = new ArrayList(); - HeaderFactory headerFactory = sipLayer.getSipFactory().createHeaderFactory(); - ViaHeader viaHeader = sipLayer.getSipFactory().createHeaderFactory().createViaHeader(sipLayer.getLocalIp(device.getLocalIp()), sipConfig.getPort(), device.getTransport(), viaTag); + HeaderFactory headerFactory = SipFactory.getInstance().createHeaderFactory(); + ViaHeader viaHeader = SipFactory.getInstance().createHeaderFactory().createViaHeader(sipLayer.getLocalIp(device.getLocalIp()), sipConfig.getPort(), device.getTransport(), viaTag); viaHeader.setRPort(); viaHeaders.add(viaHeader); //from - SipURI fromSipURI = sipLayer.getSipFactory().createAddressFactory().createSipURI(sipConfig.getId(), sipConfig.getDomain()); - Address fromAddress = sipLayer.getSipFactory().createAddressFactory().createAddress(fromSipURI); - FromHeader fromHeader = sipLayer.getSipFactory().createHeaderFactory().createFromHeader(fromAddress, fromTag); //必须要有标记,否则无法创建会话,无法回应ack + SipURI fromSipURI = SipFactory.getInstance().createAddressFactory().createSipURI(sipConfig.getId(), sipConfig.getDomain()); + Address fromAddress = SipFactory.getInstance().createAddressFactory().createAddress(fromSipURI); + FromHeader fromHeader = SipFactory.getInstance().createHeaderFactory().createFromHeader(fromAddress, fromTag); //必须要有标记,否则无法创建会话,无法回应ack //to - SipURI toSipURI = sipLayer.getSipFactory().createAddressFactory().createSipURI(channelId, device.getHostAddress()); - Address toAddress = sipLayer.getSipFactory().createAddressFactory().createAddress(toSipURI); - ToHeader toHeader = sipLayer.getSipFactory().createHeaderFactory().createToHeader(toAddress,null); + SipURI toSipURI = SipFactory.getInstance().createAddressFactory().createSipURI(channelId, device.getHostAddress()); + Address toAddress = SipFactory.getInstance().createAddressFactory().createAddress(toSipURI); + ToHeader toHeader = SipFactory.getInstance().createHeaderFactory().createToHeader(toAddress,null); //Forwards - MaxForwardsHeader maxForwards = sipLayer.getSipFactory().createHeaderFactory().createMaxForwardsHeader(70); + MaxForwardsHeader maxForwards = SipFactory.getInstance().createHeaderFactory().createMaxForwardsHeader(70); //ceq - CSeqHeader cSeqHeader = sipLayer.getSipFactory().createHeaderFactory().createCSeqHeader(redisCatchStorage.getCSEQ(), Request.INVITE); - request = sipLayer.getSipFactory().createMessageFactory().createRequest(requestLine, Request.INVITE, callIdHeader, cSeqHeader,fromHeader, toHeader, viaHeaders, maxForwards); + CSeqHeader cSeqHeader = SipFactory.getInstance().createHeaderFactory().createCSeqHeader(redisCatchStorage.getCSEQ(), Request.INVITE); + request = SipFactory.getInstance().createMessageFactory().createRequest(requestLine, Request.INVITE, callIdHeader, cSeqHeader,fromHeader, toHeader, viaHeaders, maxForwards); - request.addHeader(SipUtils.createUserAgentHeader(sipLayer.getSipFactory(), gitUtil)); + request.addHeader(SipUtils.createUserAgentHeader(gitUtil)); - Address concatAddress = sipLayer.getSipFactory().createAddressFactory().createAddress(sipLayer.getSipFactory().createAddressFactory().createSipURI(sipConfig.getId(), sipLayer.getLocalIp(device.getLocalIp())+":"+sipConfig.getPort())); - // Address concatAddress = sipLayer.getSipFactory().createAddressFactory().createAddress(sipLayer.getSipFactory().createAddressFactory().createSipURI(sipConfig.getId(), device.getHost().getIp()+":"+device.getHost().getPort())); - request.addHeader(sipLayer.getSipFactory().createHeaderFactory().createContactHeader(concatAddress)); + Address concatAddress = SipFactory.getInstance().createAddressFactory().createAddress(SipFactory.getInstance().createAddressFactory().createSipURI(sipConfig.getId(), sipLayer.getLocalIp(device.getLocalIp())+":"+sipConfig.getPort())); + // Address concatAddress = SipFactory.getInstance().createAddressFactory().createAddress(SipFactory.getInstance().createAddressFactory().createSipURI(sipConfig.getId(), device.getHost().getIp()+":"+device.getHost().getPort())); + request.addHeader(SipFactory.getInstance().createHeaderFactory().createContactHeader(concatAddress)); // Subject - SubjectHeader subjectHeader = sipLayer.getSipFactory().createHeaderFactory().createSubjectHeader(String.format("%s:%s,%s:%s", channelId, ssrc, sipConfig.getId(), 0)); + SubjectHeader subjectHeader = SipFactory.getInstance().createHeaderFactory().createSubjectHeader(String.format("%s:%s,%s:%s", channelId, ssrc, sipConfig.getId(), 0)); request.addHeader(subjectHeader); - ContentTypeHeader contentTypeHeader = sipLayer.getSipFactory().createHeaderFactory().createContentTypeHeader("APPLICATION", "SDP"); + ContentTypeHeader contentTypeHeader = SipFactory.getInstance().createHeaderFactory().createContentTypeHeader("APPLICATION", "SDP"); request.setContent(content, contentTypeHeader); return request; } @@ -122,39 +123,39 @@ public class SIPRequestHeaderProvider { public Request createPlaybackInviteRequest(Device device, String channelId, String content, String viaTag, String fromTag, String toTag, CallIdHeader callIdHeader, String ssrc) throws ParseException, InvalidArgumentException, PeerUnavailableException { Request request = null; //请求行 - SipURI requestLine = sipLayer.getSipFactory().createAddressFactory().createSipURI(channelId, device.getHostAddress()); + SipURI requestLine = SipFactory.getInstance().createAddressFactory().createSipURI(channelId, device.getHostAddress()); // via ArrayList viaHeaders = new ArrayList(); - ViaHeader viaHeader = sipLayer.getSipFactory().createHeaderFactory().createViaHeader(sipLayer.getLocalIp(device.getLocalIp()), sipConfig.getPort(), device.getTransport(), viaTag); + ViaHeader viaHeader = SipFactory.getInstance().createHeaderFactory().createViaHeader(sipLayer.getLocalIp(device.getLocalIp()), sipConfig.getPort(), device.getTransport(), viaTag); viaHeader.setRPort(); viaHeaders.add(viaHeader); //from - SipURI fromSipURI = sipLayer.getSipFactory().createAddressFactory().createSipURI(sipConfig.getId(), sipConfig.getDomain()); - Address fromAddress = sipLayer.getSipFactory().createAddressFactory().createAddress(fromSipURI); - FromHeader fromHeader = sipLayer.getSipFactory().createHeaderFactory().createFromHeader(fromAddress, fromTag); //必须要有标记,否则无法创建会话,无法回应ack + SipURI fromSipURI = SipFactory.getInstance().createAddressFactory().createSipURI(sipConfig.getId(), sipConfig.getDomain()); + Address fromAddress = SipFactory.getInstance().createAddressFactory().createAddress(fromSipURI); + FromHeader fromHeader = SipFactory.getInstance().createHeaderFactory().createFromHeader(fromAddress, fromTag); //必须要有标记,否则无法创建会话,无法回应ack //to - SipURI toSipURI = sipLayer.getSipFactory().createAddressFactory().createSipURI(channelId, device.getHostAddress()); - Address toAddress = sipLayer.getSipFactory().createAddressFactory().createAddress(toSipURI); - ToHeader toHeader = sipLayer.getSipFactory().createHeaderFactory().createToHeader(toAddress,null); + SipURI toSipURI = SipFactory.getInstance().createAddressFactory().createSipURI(channelId, device.getHostAddress()); + Address toAddress = SipFactory.getInstance().createAddressFactory().createAddress(toSipURI); + ToHeader toHeader = SipFactory.getInstance().createHeaderFactory().createToHeader(toAddress,null); //Forwards - MaxForwardsHeader maxForwards = sipLayer.getSipFactory().createHeaderFactory().createMaxForwardsHeader(70); + MaxForwardsHeader maxForwards = SipFactory.getInstance().createHeaderFactory().createMaxForwardsHeader(70); //ceq - CSeqHeader cSeqHeader = sipLayer.getSipFactory().createHeaderFactory().createCSeqHeader(redisCatchStorage.getCSEQ(), Request.INVITE); - request = sipLayer.getSipFactory().createMessageFactory().createRequest(requestLine, Request.INVITE, callIdHeader, cSeqHeader,fromHeader, toHeader, viaHeaders, maxForwards); + CSeqHeader cSeqHeader = SipFactory.getInstance().createHeaderFactory().createCSeqHeader(redisCatchStorage.getCSEQ(), Request.INVITE); + request = SipFactory.getInstance().createMessageFactory().createRequest(requestLine, Request.INVITE, callIdHeader, cSeqHeader,fromHeader, toHeader, viaHeaders, maxForwards); - Address concatAddress = sipLayer.getSipFactory().createAddressFactory().createAddress(sipLayer.getSipFactory().createAddressFactory().createSipURI(sipConfig.getId(), sipLayer.getLocalIp(device.getLocalIp())+":"+sipConfig.getPort())); - // Address concatAddress = sipLayer.getSipFactory().createAddressFactory().createAddress(sipLayer.getSipFactory().createAddressFactory().createSipURI(sipConfig.getId(), device.getHost().getIp()+":"+device.getHost().getPort())); - request.addHeader(sipLayer.getSipFactory().createHeaderFactory().createContactHeader(concatAddress)); + Address concatAddress = SipFactory.getInstance().createAddressFactory().createAddress(SipFactory.getInstance().createAddressFactory().createSipURI(sipConfig.getId(), sipLayer.getLocalIp(device.getLocalIp())+":"+sipConfig.getPort())); + // Address concatAddress = SipFactory.getInstance().createAddressFactory().createAddress(SipFactory.getInstance().createAddressFactory().createSipURI(sipConfig.getId(), device.getHost().getIp()+":"+device.getHost().getPort())); + request.addHeader(SipFactory.getInstance().createHeaderFactory().createContactHeader(concatAddress)); - request.addHeader(SipUtils.createUserAgentHeader(sipLayer.getSipFactory(), gitUtil)); + request.addHeader(SipUtils.createUserAgentHeader(gitUtil)); // Subject - SubjectHeader subjectHeader = sipLayer.getSipFactory().createHeaderFactory().createSubjectHeader(String.format("%s:%s,%s:%s", channelId, ssrc, sipConfig.getId(), 0)); + SubjectHeader subjectHeader = SipFactory.getInstance().createHeaderFactory().createSubjectHeader(String.format("%s:%s,%s:%s", channelId, ssrc, sipConfig.getId(), 0)); request.addHeader(subjectHeader); - ContentTypeHeader contentTypeHeader = sipLayer.getSipFactory().createHeaderFactory().createContentTypeHeader("APPLICATION", "SDP"); + ContentTypeHeader contentTypeHeader = SipFactory.getInstance().createHeaderFactory().createContentTypeHeader("APPLICATION", "SDP"); request.setContent(content, contentTypeHeader); return request; } @@ -162,34 +163,34 @@ public class SIPRequestHeaderProvider { public Request createByteRequest(Device device, String channelId, SipTransactionInfo transactionInfo) throws ParseException, InvalidArgumentException, PeerUnavailableException { Request request = null; //请求行 - SipURI requestLine = sipLayer.getSipFactory().createAddressFactory().createSipURI(channelId, device.getHostAddress()); + SipURI requestLine = SipFactory.getInstance().createAddressFactory().createSipURI(channelId, device.getHostAddress()); // via ArrayList viaHeaders = new ArrayList(); - ViaHeader viaHeader = sipLayer.getSipFactory().createHeaderFactory().createViaHeader(sipLayer.getLocalIp(device.getLocalIp()), sipConfig.getPort(), device.getTransport(), SipUtils.getNewViaTag()); + ViaHeader viaHeader = SipFactory.getInstance().createHeaderFactory().createViaHeader(sipLayer.getLocalIp(device.getLocalIp()), sipConfig.getPort(), device.getTransport(), SipUtils.getNewViaTag()); viaHeaders.add(viaHeader); //from - SipURI fromSipURI = sipLayer.getSipFactory().createAddressFactory().createSipURI(sipConfig.getId(),sipConfig.getDomain()); - Address fromAddress = sipLayer.getSipFactory().createAddressFactory().createAddress(fromSipURI); - FromHeader fromHeader = sipLayer.getSipFactory().createHeaderFactory().createFromHeader(fromAddress, transactionInfo.getFromTag()); + SipURI fromSipURI = SipFactory.getInstance().createAddressFactory().createSipURI(sipConfig.getId(),sipConfig.getDomain()); + Address fromAddress = SipFactory.getInstance().createAddressFactory().createAddress(fromSipURI); + FromHeader fromHeader = SipFactory.getInstance().createHeaderFactory().createFromHeader(fromAddress, transactionInfo.getFromTag()); //to - SipURI toSipURI = sipLayer.getSipFactory().createAddressFactory().createSipURI(channelId,device.getHostAddress()); - Address toAddress = sipLayer.getSipFactory().createAddressFactory().createAddress(toSipURI); - ToHeader toHeader = sipLayer.getSipFactory().createHeaderFactory().createToHeader(toAddress, transactionInfo.getToTag()); + SipURI toSipURI = SipFactory.getInstance().createAddressFactory().createSipURI(channelId,device.getHostAddress()); + Address toAddress = SipFactory.getInstance().createAddressFactory().createAddress(toSipURI); + ToHeader toHeader = SipFactory.getInstance().createHeaderFactory().createToHeader(toAddress, transactionInfo.getToTag()); //Forwards - MaxForwardsHeader maxForwards = sipLayer.getSipFactory().createHeaderFactory().createMaxForwardsHeader(70); + MaxForwardsHeader maxForwards = SipFactory.getInstance().createHeaderFactory().createMaxForwardsHeader(70); //ceq - CSeqHeader cSeqHeader = sipLayer.getSipFactory().createHeaderFactory().createCSeqHeader(redisCatchStorage.getCSEQ(), Request.BYE); - CallIdHeader callIdHeader = sipLayer.getSipFactory().createHeaderFactory().createCallIdHeader(transactionInfo.getCallId()); - request = sipLayer.getSipFactory().createMessageFactory().createRequest(requestLine, Request.BYE, callIdHeader, cSeqHeader,fromHeader, toHeader, viaHeaders, maxForwards); + CSeqHeader cSeqHeader = SipFactory.getInstance().createHeaderFactory().createCSeqHeader(redisCatchStorage.getCSEQ(), Request.BYE); + CallIdHeader callIdHeader = SipFactory.getInstance().createHeaderFactory().createCallIdHeader(transactionInfo.getCallId()); + request = SipFactory.getInstance().createMessageFactory().createRequest(requestLine, Request.BYE, callIdHeader, cSeqHeader,fromHeader, toHeader, viaHeaders, maxForwards); - request.addHeader(SipUtils.createUserAgentHeader(sipLayer.getSipFactory(), gitUtil)); + request.addHeader(SipUtils.createUserAgentHeader(gitUtil)); - Address concatAddress = sipLayer.getSipFactory().createAddressFactory().createAddress(sipLayer.getSipFactory().createAddressFactory().createSipURI(sipConfig.getId(), sipLayer.getLocalIp(device.getLocalIp())+":"+sipConfig.getPort())); - request.addHeader(sipLayer.getSipFactory().createHeaderFactory().createContactHeader(concatAddress)); + Address concatAddress = SipFactory.getInstance().createAddressFactory().createAddress(SipFactory.getInstance().createAddressFactory().createSipURI(sipConfig.getId(), sipLayer.getLocalIp(device.getLocalIp())+":"+sipConfig.getPort())); + request.addHeader(SipFactory.getInstance().createHeaderFactory().createContactHeader(concatAddress)); - request.addHeader(SipUtils.createUserAgentHeader(sipLayer.getSipFactory(), gitUtil)); + request.addHeader(SipUtils.createUserAgentHeader(gitUtil)); return request; } @@ -197,50 +198,50 @@ public class SIPRequestHeaderProvider { public Request createSubscribeRequest(Device device, String content, SIPRequest requestOld, Integer expires, String event, CallIdHeader callIdHeader) throws ParseException, InvalidArgumentException, PeerUnavailableException { Request request = null; // sipuri - SipURI requestURI = sipLayer.getSipFactory().createAddressFactory().createSipURI(device.getDeviceId(), device.getHostAddress()); + SipURI requestURI = SipFactory.getInstance().createAddressFactory().createSipURI(device.getDeviceId(), device.getHostAddress()); // via ArrayList viaHeaders = new ArrayList(); - ViaHeader viaHeader = sipLayer.getSipFactory().createHeaderFactory().createViaHeader(sipLayer.getLocalIp(device.getLocalIp()), sipConfig.getPort(), + ViaHeader viaHeader = SipFactory.getInstance().createHeaderFactory().createViaHeader(sipLayer.getLocalIp(device.getLocalIp()), sipConfig.getPort(), device.getTransport(), SipUtils.getNewViaTag()); viaHeader.setRPort(); viaHeaders.add(viaHeader); // from - SipURI fromSipURI = sipLayer.getSipFactory().createAddressFactory().createSipURI(sipConfig.getId(), sipConfig.getDomain()); - Address fromAddress = sipLayer.getSipFactory().createAddressFactory().createAddress(fromSipURI); - FromHeader fromHeader = sipLayer.getSipFactory().createHeaderFactory().createFromHeader(fromAddress, requestOld == null ? SipUtils.getNewFromTag() :requestOld.getFromTag()); + SipURI fromSipURI = SipFactory.getInstance().createAddressFactory().createSipURI(sipConfig.getId(), sipConfig.getDomain()); + Address fromAddress = SipFactory.getInstance().createAddressFactory().createAddress(fromSipURI); + FromHeader fromHeader = SipFactory.getInstance().createHeaderFactory().createFromHeader(fromAddress, requestOld == null ? SipUtils.getNewFromTag() :requestOld.getFromTag()); // to - SipURI toSipURI = sipLayer.getSipFactory().createAddressFactory().createSipURI(device.getDeviceId(), device.getHostAddress()); - Address toAddress = sipLayer.getSipFactory().createAddressFactory().createAddress(toSipURI); - ToHeader toHeader = sipLayer.getSipFactory().createHeaderFactory().createToHeader(toAddress, requestOld == null ? null :requestOld.getToTag()); + SipURI toSipURI = SipFactory.getInstance().createAddressFactory().createSipURI(device.getDeviceId(), device.getHostAddress()); + Address toAddress = SipFactory.getInstance().createAddressFactory().createAddress(toSipURI); + ToHeader toHeader = SipFactory.getInstance().createHeaderFactory().createToHeader(toAddress, requestOld == null ? null :requestOld.getToTag()); // Forwards - MaxForwardsHeader maxForwards = sipLayer.getSipFactory().createHeaderFactory().createMaxForwardsHeader(70); + MaxForwardsHeader maxForwards = SipFactory.getInstance().createHeaderFactory().createMaxForwardsHeader(70); // ceq - CSeqHeader cSeqHeader = sipLayer.getSipFactory().createHeaderFactory().createCSeqHeader(redisCatchStorage.getCSEQ(), Request.SUBSCRIBE); + CSeqHeader cSeqHeader = SipFactory.getInstance().createHeaderFactory().createCSeqHeader(redisCatchStorage.getCSEQ(), Request.SUBSCRIBE); - request = sipLayer.getSipFactory().createMessageFactory().createRequest(requestURI, Request.SUBSCRIBE, callIdHeader, cSeqHeader, fromHeader, + request = SipFactory.getInstance().createMessageFactory().createRequest(requestURI, Request.SUBSCRIBE, callIdHeader, cSeqHeader, fromHeader, toHeader, viaHeaders, maxForwards); - Address concatAddress = sipLayer.getSipFactory().createAddressFactory().createAddress(sipLayer.getSipFactory().createAddressFactory().createSipURI(sipConfig.getId(), sipLayer.getLocalIp(device.getLocalIp())+":"+sipConfig.getPort())); - request.addHeader(sipLayer.getSipFactory().createHeaderFactory().createContactHeader(concatAddress)); + Address concatAddress = SipFactory.getInstance().createAddressFactory().createAddress(SipFactory.getInstance().createAddressFactory().createSipURI(sipConfig.getId(), sipLayer.getLocalIp(device.getLocalIp())+":"+sipConfig.getPort())); + request.addHeader(SipFactory.getInstance().createHeaderFactory().createContactHeader(concatAddress)); // Expires - ExpiresHeader expireHeader = sipLayer.getSipFactory().createHeaderFactory().createExpiresHeader(expires); + ExpiresHeader expireHeader = SipFactory.getInstance().createHeaderFactory().createExpiresHeader(expires); request.addHeader(expireHeader); // Event - EventHeader eventHeader = sipLayer.getSipFactory().createHeaderFactory().createEventHeader(event); + EventHeader eventHeader = SipFactory.getInstance().createHeaderFactory().createEventHeader(event); int random = (int) Math.floor(Math.random() * 10000); eventHeader.setEventId(random + ""); request.addHeader(eventHeader); - ContentTypeHeader contentTypeHeader = sipLayer.getSipFactory().createHeaderFactory().createContentTypeHeader("Application", "MANSCDP+xml"); + ContentTypeHeader contentTypeHeader = SipFactory.getInstance().createHeaderFactory().createContentTypeHeader("Application", "MANSCDP+xml"); request.setContent(content, contentTypeHeader); - request.addHeader(SipUtils.createUserAgentHeader(sipLayer.getSipFactory(), gitUtil)); + request.addHeader(SipUtils.createUserAgentHeader(gitUtil)); return request; } @@ -252,37 +253,37 @@ public class SIPRequestHeaderProvider { } SIPRequest request = null; //请求行 - SipURI requestLine = sipLayer.getSipFactory().createAddressFactory().createSipURI(channelId, device.getHostAddress()); + SipURI requestLine = SipFactory.getInstance().createAddressFactory().createSipURI(channelId, device.getHostAddress()); // via ArrayList viaHeaders = new ArrayList(); - ViaHeader viaHeader = sipLayer.getSipFactory().createHeaderFactory().createViaHeader(sipLayer.getLocalIp(device.getLocalIp()), sipConfig.getPort(), device.getTransport(), SipUtils.getNewViaTag()); + ViaHeader viaHeader = SipFactory.getInstance().createHeaderFactory().createViaHeader(sipLayer.getLocalIp(device.getLocalIp()), sipConfig.getPort(), device.getTransport(), SipUtils.getNewViaTag()); viaHeaders.add(viaHeader); //from - SipURI fromSipURI = sipLayer.getSipFactory().createAddressFactory().createSipURI(sipConfig.getId(),sipConfig.getDomain()); - Address fromAddress = sipLayer.getSipFactory().createAddressFactory().createAddress(fromSipURI); - FromHeader fromHeader = sipLayer.getSipFactory().createHeaderFactory().createFromHeader(fromAddress, transactionInfo.getFromTag()); + SipURI fromSipURI = SipFactory.getInstance().createAddressFactory().createSipURI(sipConfig.getId(),sipConfig.getDomain()); + Address fromAddress = SipFactory.getInstance().createAddressFactory().createAddress(fromSipURI); + FromHeader fromHeader = SipFactory.getInstance().createHeaderFactory().createFromHeader(fromAddress, transactionInfo.getFromTag()); //to - SipURI toSipURI = sipLayer.getSipFactory().createAddressFactory().createSipURI(channelId,device.getHostAddress()); - Address toAddress = sipLayer.getSipFactory().createAddressFactory().createAddress(toSipURI); - ToHeader toHeader = sipLayer.getSipFactory().createHeaderFactory().createToHeader(toAddress, transactionInfo.getToTag()); + SipURI toSipURI = SipFactory.getInstance().createAddressFactory().createSipURI(channelId,device.getHostAddress()); + Address toAddress = SipFactory.getInstance().createAddressFactory().createAddress(toSipURI); + ToHeader toHeader = SipFactory.getInstance().createHeaderFactory().createToHeader(toAddress, transactionInfo.getToTag()); //Forwards - MaxForwardsHeader maxForwards = sipLayer.getSipFactory().createHeaderFactory().createMaxForwardsHeader(70); + MaxForwardsHeader maxForwards = SipFactory.getInstance().createHeaderFactory().createMaxForwardsHeader(70); //ceq - CSeqHeader cSeqHeader = sipLayer.getSipFactory().createHeaderFactory().createCSeqHeader(redisCatchStorage.getCSEQ(), Request.INFO); - CallIdHeader callIdHeader = sipLayer.getSipFactory().createHeaderFactory().createCallIdHeader(transactionInfo.getCallId()); - request = (SIPRequest)sipLayer.getSipFactory().createMessageFactory().createRequest(requestLine, Request.INFO, callIdHeader, cSeqHeader,fromHeader, toHeader, viaHeaders, maxForwards); + CSeqHeader cSeqHeader = SipFactory.getInstance().createHeaderFactory().createCSeqHeader(redisCatchStorage.getCSEQ(), Request.INFO); + CallIdHeader callIdHeader = SipFactory.getInstance().createHeaderFactory().createCallIdHeader(transactionInfo.getCallId()); + request = (SIPRequest)SipFactory.getInstance().createMessageFactory().createRequest(requestLine, Request.INFO, callIdHeader, cSeqHeader,fromHeader, toHeader, viaHeaders, maxForwards); - request.addHeader(SipUtils.createUserAgentHeader(sipLayer.getSipFactory(), gitUtil)); + request.addHeader(SipUtils.createUserAgentHeader(gitUtil)); - Address concatAddress = sipLayer.getSipFactory().createAddressFactory().createAddress(sipLayer.getSipFactory().createAddressFactory().createSipURI(sipConfig.getId(), sipLayer.getLocalIp(device.getLocalIp())+":"+sipConfig.getPort())); - request.addHeader(sipLayer.getSipFactory().createHeaderFactory().createContactHeader(concatAddress)); + Address concatAddress = SipFactory.getInstance().createAddressFactory().createAddress(SipFactory.getInstance().createAddressFactory().createSipURI(sipConfig.getId(), sipLayer.getLocalIp(device.getLocalIp())+":"+sipConfig.getPort())); + request.addHeader(SipFactory.getInstance().createHeaderFactory().createContactHeader(concatAddress)); - request.addHeader(SipUtils.createUserAgentHeader(sipLayer.getSipFactory(), gitUtil)); + request.addHeader(SipUtils.createUserAgentHeader(gitUtil)); if (content != null) { - ContentTypeHeader contentTypeHeader = sipLayer.getSipFactory().createHeaderFactory().createContentTypeHeader("Application", + ContentTypeHeader contentTypeHeader = SipFactory.getInstance().createHeaderFactory().createContentTypeHeader("Application", "MANSRTSP"); request.setContent(content, contentTypeHeader); } @@ -294,23 +295,23 @@ public class SIPRequestHeaderProvider { // via ArrayList viaHeaders = new ArrayList(); - ViaHeader viaHeader = sipLayer.getSipFactory().createHeaderFactory().createViaHeader(localIp, sipConfig.getPort(), sipResponse.getTopmostViaHeader().getTransport(), SipUtils.getNewViaTag()); + ViaHeader viaHeader = SipFactory.getInstance().createHeaderFactory().createViaHeader(localIp, sipConfig.getPort(), sipResponse.getTopmostViaHeader().getTransport(), SipUtils.getNewViaTag()); viaHeaders.add(viaHeader); //Forwards - MaxForwardsHeader maxForwards = sipLayer.getSipFactory().createHeaderFactory().createMaxForwardsHeader(70); + MaxForwardsHeader maxForwards = SipFactory.getInstance().createHeaderFactory().createMaxForwardsHeader(70); //ceq - CSeqHeader cSeqHeader = sipLayer.getSipFactory().createHeaderFactory().createCSeqHeader(sipResponse.getCSeqHeader().getSeqNumber(), Request.ACK); + CSeqHeader cSeqHeader = SipFactory.getInstance().createHeaderFactory().createCSeqHeader(sipResponse.getCSeqHeader().getSeqNumber(), Request.ACK); - Request request = sipLayer.getSipFactory().createMessageFactory().createRequest(sipURI, Request.ACK, sipResponse.getCallIdHeader(), cSeqHeader, sipResponse.getFromHeader(), sipResponse.getToHeader(), viaHeaders, maxForwards); + Request request = SipFactory.getInstance().createMessageFactory().createRequest(sipURI, Request.ACK, sipResponse.getCallIdHeader(), cSeqHeader, sipResponse.getFromHeader(), sipResponse.getToHeader(), viaHeaders, maxForwards); - request.addHeader(SipUtils.createUserAgentHeader(sipLayer.getSipFactory(), gitUtil)); + request.addHeader(SipUtils.createUserAgentHeader(gitUtil)); - Address concatAddress = sipLayer.getSipFactory().createAddressFactory().createAddress(sipLayer.getSipFactory().createAddressFactory().createSipURI(sipConfig.getId(), localIp + ":"+sipConfig.getPort())); - request.addHeader(sipLayer.getSipFactory().createHeaderFactory().createContactHeader(concatAddress)); + Address concatAddress = SipFactory.getInstance().createAddressFactory().createAddress(SipFactory.getInstance().createAddressFactory().createSipURI(sipConfig.getId(), localIp + ":"+sipConfig.getPort())); + request.addHeader(SipFactory.getInstance().createHeaderFactory().createContactHeader(concatAddress)); - request.addHeader(SipUtils.createUserAgentHeader(sipLayer.getSipFactory(), gitUtil)); + request.addHeader(SipUtils.createUserAgentHeader(gitUtil)); return request; } diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/cmd/impl/SIPCommander.java b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/cmd/impl/SIPCommander.java index 03050169d..00e2613b4 100644 --- a/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/cmd/impl/SIPCommander.java +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/cmd/impl/SIPCommander.java @@ -33,6 +33,7 @@ import org.springframework.util.ObjectUtils; import javax.sip.InvalidArgumentException; import javax.sip.ResponseEvent; import javax.sip.SipException; +import javax.sip.SipFactory; import javax.sip.header.CallIdHeader; import javax.sip.message.Request; import java.text.ParseException; @@ -1192,7 +1193,7 @@ public class SIPCommander implements ISIPCommander { CallIdHeader callIdHeader; if (requestOld != null) { - callIdHeader = sipLayer.getSipFactory().createHeaderFactory().createCallIdHeader(requestOld.getCallIdHeader().getCallId()); + callIdHeader = SipFactory.getInstance().createHeaderFactory().createCallIdHeader(requestOld.getCallIdHeader().getCallId()); } else { callIdHeader = sipSender.getNewCallIdHeader(sipLayer.getLocalIp(device.getLocalIp()),device.getTransport()); } @@ -1267,7 +1268,7 @@ public class SIPCommander implements ISIPCommander { CallIdHeader callIdHeader; if (requestOld != null) { - callIdHeader = sipLayer.getSipFactory().createHeaderFactory().createCallIdHeader(requestOld.getCallIdHeader().getCallId()); + callIdHeader = SipFactory.getInstance().createHeaderFactory().createCallIdHeader(requestOld.getCallIdHeader().getCallId()); } else { callIdHeader = sipSender.getNewCallIdHeader(sipLayer.getLocalIp(device.getLocalIp()),device.getTransport()); } diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/cmd/impl/SIPCommanderFroPlatform.java b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/cmd/impl/SIPCommanderFroPlatform.java index 7f0499a9c..582fbafe9 100644 --- a/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/cmd/impl/SIPCommanderFroPlatform.java +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/cmd/impl/SIPCommanderFroPlatform.java @@ -29,6 +29,7 @@ import org.springframework.util.ObjectUtils; import javax.sip.InvalidArgumentException; import javax.sip.SipException; +import javax.sip.SipFactory; import javax.sip.header.CallIdHeader; import javax.sip.header.WWWAuthenticateHeader; import javax.sip.message.Request; @@ -497,7 +498,7 @@ public class SIPCommanderFroPlatform implements ISIPCommanderForPlatform { private void sendNotify(ParentPlatform parentPlatform, String catalogXmlContent, SubscribeInfo subscribeInfo, SipSubscribe.Event errorEvent, SipSubscribe.Event okEvent ) throws SipException, ParseException, InvalidArgumentException { - MessageFactoryImpl messageFactory = (MessageFactoryImpl) sipLayer.getSipFactory().createMessageFactory(); + MessageFactoryImpl messageFactory = (MessageFactoryImpl) SipFactory.getInstance().createMessageFactory(); String characterSet = parentPlatform.getCharacterSet(); // 设置编码, 防止中文乱码 messageFactory.setDefaultContentEncodingCharset(characterSet); diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/NotifyRequestProcessor.java b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/NotifyRequestProcessor.java index a0038ca64..426064116 100644 --- a/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/NotifyRequestProcessor.java +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/NotifyRequestProcessor.java @@ -96,6 +96,7 @@ public class NotifyRequestProcessor extends SIPRequestProcessorParent implements logger.error("未处理的异常 ", e); } boolean runed = !taskQueue.isEmpty(); + logger.info("[notify] 待处理消息数量: {}", taskQueue.size()); taskQueue.offer(new HandlerCatchData(evt, null, null)); if (!runed) { taskExecutor.execute(()-> { diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/RegisterRequestProcessor.java b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/RegisterRequestProcessor.java index 0c8c3f689..9f69950f1 100644 --- a/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/RegisterRequestProcessor.java +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/RegisterRequestProcessor.java @@ -82,19 +82,6 @@ public class RegisterRequestProcessor extends SIPRequestProcessorParent implemen RequestEventExt evtExt = (RequestEventExt) evt; String requestAddress = evtExt.getRemoteIpAddress() + ":" + evtExt.getRemotePort(); -// MBeanServer beanServer = ManagementFactory.getPlatformMBeanServer(); -// QueryExp protocol = Query.match(Query.attr("protocol"), Query.value("HTTP/1.1")); -//// ObjectName name = new ObjectName("*:type=Connector,*"); -// ObjectName name = new ObjectName("*:*"); -// Set objectNames = beanServer.queryNames(name, protocol); -// for (ObjectName objectName : objectNames) { -// String catalina = objectName.getDomain(); -// if ("Catalina".equals(catalina)) { -// System.out.println(objectName.getKeyProperty("port")); -// } -// } - -// System.out.println(ServiceInfo.getServerPort()); SIPRequest request = (SIPRequest)evt.getRequest(); Response response = null; boolean passwordCorrect = false; diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/response/impl/InviteResponseProcessor.java b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/response/impl/InviteResponseProcessor.java index b5a9ee7b1..f647b96b9 100644 --- a/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/response/impl/InviteResponseProcessor.java +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/response/impl/InviteResponseProcessor.java @@ -1,39 +1,25 @@ package com.genersoft.iot.vmp.gb28181.transmit.event.response.impl; -import com.genersoft.iot.vmp.conf.SipConfig; import com.genersoft.iot.vmp.gb28181.SipLayer; -import com.genersoft.iot.vmp.gb28181.bean.Device; -import com.genersoft.iot.vmp.gb28181.bean.SsrcTransaction; -import com.genersoft.iot.vmp.gb28181.session.VideoStreamSessionManager; import com.genersoft.iot.vmp.gb28181.transmit.SIPProcessorObserver; import com.genersoft.iot.vmp.gb28181.transmit.SIPSender; -import com.genersoft.iot.vmp.gb28181.transmit.cmd.ISIPCommander; import com.genersoft.iot.vmp.gb28181.transmit.cmd.SIPRequestHeaderProvider; import com.genersoft.iot.vmp.gb28181.transmit.event.response.SIPResponseProcessorAbstract; -import com.genersoft.iot.vmp.gb28181.utils.SipUtils; -import com.genersoft.iot.vmp.service.IDeviceService; -import com.genersoft.iot.vmp.utils.GitUtil; import gov.nist.javax.sip.ResponseEventExt; -import gov.nist.javax.sip.SipProviderImpl; import gov.nist.javax.sip.message.SIPResponse; -import gov.nist.javax.sip.stack.SIPClientTransaction; -import gov.nist.javax.sip.stack.SIPDialog; -import gov.nist.javax.sip.stack.SIPTransaction; -import gov.nist.javax.sip.stack.SIPTransactionImpl; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.stereotype.Component; import javax.sdp.SdpFactory; import javax.sdp.SdpParseException; import javax.sdp.SessionDescription; -import javax.sip.*; -import javax.sip.address.Address; +import javax.sip.InvalidArgumentException; +import javax.sip.ResponseEvent; +import javax.sip.SipException; +import javax.sip.SipFactory; import javax.sip.address.SipURI; -import javax.sip.header.CSeqHeader; -import javax.sip.header.UserAgentHeader; import javax.sip.message.Request; import javax.sip.message.Response; import java.text.ParseException; @@ -105,7 +91,7 @@ public class InviteResponseProcessor extends SIPResponseProcessorAbstract { sdp = SdpFactory.getInstance().createSessionDescription(contentString); } - SipURI requestUri = sipLayer.getSipFactory().createAddressFactory().createSipURI(sdp.getOrigin().getUsername(), event.getRemoteIpAddress() + ":" + event.getRemotePort()); + SipURI requestUri = SipFactory.getInstance().createAddressFactory().createSipURI(sdp.getOrigin().getUsername(), event.getRemoteIpAddress() + ":" + event.getRemotePort()); Request reqAck = headerProvider.createAckRequest(response.getLocalAddress().getHostAddress(), requestUri, response); logger.info("[回复ack] {}-> {}:{} ", sdp.getOrigin().getUsername(), event.getRemoteIpAddress(), event.getRemotePort()); diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/utils/SipUtils.java b/src/main/java/com/genersoft/iot/vmp/gb28181/utils/SipUtils.java index afe11838c..f98315001 100644 --- a/src/main/java/com/genersoft/iot/vmp/gb28181/utils/SipUtils.java +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/utils/SipUtils.java @@ -54,7 +54,7 @@ public class SipUtils { return "z9hG4bK" + System.currentTimeMillis(); } - public static UserAgentHeader createUserAgentHeader(SipFactory sipFactory, GitUtil gitUtil) throws PeerUnavailableException, ParseException { + public static UserAgentHeader createUserAgentHeader(GitUtil gitUtil) throws PeerUnavailableException, ParseException { List agentParam = new ArrayList<>(); agentParam.add("WVP-Pro "); if (gitUtil != null ) { @@ -66,7 +66,7 @@ public class SipUtils { agentParam.add(gitUtil.getCommitTime()); } } - return sipFactory.createHeaderFactory().createUserAgentHeader(agentParam); + return SipFactory.getInstance().createHeaderFactory().createUserAgentHeader(agentParam); } public static String getNewFromTag(){ diff --git a/src/main/java/com/genersoft/iot/vmp/media/zlm/ZLMHttpHookListener.java b/src/main/java/com/genersoft/iot/vmp/media/zlm/ZLMHttpHookListener.java index f2eed1126..b62e114f0 100644 --- a/src/main/java/com/genersoft/iot/vmp/media/zlm/ZLMHttpHookListener.java +++ b/src/main/java/com/genersoft/iot/vmp/media/zlm/ZLMHttpHookListener.java @@ -395,7 +395,7 @@ public class ZLMHttpHookListener { List sendRtpItems = redisCatchStorage.querySendRTPServerByStream(param.getStream()); if (sendRtpItems.size() > 0) { for (SendRtpItem sendRtpItem : sendRtpItems) { - if (sendRtpItem.getApp().equals(param.getApp())) { + if (sendRtpItem != null && sendRtpItem.getApp().equals(param.getApp())) { String platformId = sendRtpItem.getPlatformId(); ParentPlatform platform = storager.queryParentPlatByServerGBId(platformId); Device device = deviceService.getDevice(platformId); diff --git a/src/main/java/com/genersoft/iot/vmp/service/redisMsg/RedisPushStreamStatusMsgListener.java b/src/main/java/com/genersoft/iot/vmp/service/redisMsg/RedisPushStreamStatusMsgListener.java index d7e02f594..c8f4b2ae8 100644 --- a/src/main/java/com/genersoft/iot/vmp/service/redisMsg/RedisPushStreamStatusMsgListener.java +++ b/src/main/java/com/genersoft/iot/vmp/service/redisMsg/RedisPushStreamStatusMsgListener.java @@ -3,6 +3,7 @@ package com.genersoft.iot.vmp.service.redisMsg; import com.alibaba.fastjson2.JSON; import com.genersoft.iot.vmp.common.VideoManagerConstants; import com.genersoft.iot.vmp.conf.DynamicTask; +import com.genersoft.iot.vmp.conf.UserSetting; import com.genersoft.iot.vmp.service.IStreamPushService; import com.genersoft.iot.vmp.service.bean.PushStreamStatusChangeFromRedisDto; import com.genersoft.iot.vmp.storager.IRedisCatchStorage; @@ -38,6 +39,9 @@ public class RedisPushStreamStatusMsgListener implements MessageListener, Applic @Autowired private DynamicTask dynamicTask; + @Autowired + private UserSetting userSetting; + private ConcurrentLinkedQueue taskQueue = new ConcurrentLinkedQueue<>(); @@ -89,13 +93,15 @@ public class RedisPushStreamStatusMsgListener implements MessageListener, Applic @Override public void run(ApplicationArguments args) throws Exception { - // 启动时设置所有推流通道离线,发起查询请求 - redisCatchStorage.sendStreamPushRequestedMsgForStatus(); - dynamicTask.startDelay(VideoManagerConstants.VM_MSG_GET_ALL_ONLINE_REQUESTED, ()->{ - logger.info("[REDIS消息]未收到redis回复推流设备状态,执行推流设备离线"); - // 五秒收不到请求就设置通道离线,然后通知上级离线 - streamPushService.allStreamOffline(); - }, 5000); + if (!userSetting.isUsePushingAsStatus()) { + // 启动时设置所有推流通道离线,发起查询请求 + redisCatchStorage.sendStreamPushRequestedMsgForStatus(); + dynamicTask.startDelay(VideoManagerConstants.VM_MSG_GET_ALL_ONLINE_REQUESTED, ()->{ + logger.info("[REDIS消息]未收到redis回复推流设备状态,执行推流设备离线"); + // 五秒收不到请求就设置通道离线,然后通知上级离线 + streamPushService.allStreamOffline(); + }, 5000); + } } } From 72f59e1cd4ee35ab3750f94eefe9c66613e1bbdd Mon Sep 17 00:00:00 2001 From: 648540858 <648540858@qq.com> Date: Thu, 13 Apr 2023 17:28:54 +0800 Subject: [PATCH 09/48] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E7=9F=A5=E8=AF=86?= =?UTF-8?q?=E6=98=9F=E7=90=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index bb8161159..4df03c35c 100644 --- a/README.md +++ b/README.md @@ -106,13 +106,13 @@ https://gitee.com/pan648540858/wvp-GB28181-pro.git 国标最麻烦的地方在于设备的兼容性,所以需要大量的设备来测试,目前作者手里的设备有限,再加上作者水平有限,所以遇到问题在所难免; 1. 查看文档网站,仔细的阅读可以帮你避免几乎所有的问题 2. 搜索issues,这里有大部分的答案 -3. 加QQ群(901799015),这里有大量热心的小伙伴,但是前提新希望你已经仔细阅读了wiki和搜索了issues。 -4. 你可以请作者为你解答,但是我不是免费的。 -5. 你可以把遇到问题的设备寄给我,可以更容易的兼容设备和解决问题。 +3. 你可以把遇到问题的设备寄给我,可以更容易的兼容设备和解决问题。 +4. 欢迎加入[知识星球](https://t.zsxq.com/0drbw002x)支持本项目,同时可以得到更加快速的解答。 # 使用帮助 -QQ群: 901799015, ZLM使用文档[https://github.com/ZLMediaKit/ZLMediaKit](https://github.com/ZLMediaKit/ZLMediaKit) -QQ私信一般不回, 精力有限.欢迎大家在群里讨论.觉得项目对你有帮助,欢迎star和提交pr。 +ZLM使用文档[https://github.com/ZLMediaKit/ZLMediaKit](https://github.com/ZLMediaKit/ZLMediaKit) +wvp官方文档[doc.wvp-pro.cn](https://doc.wvp-pro.cn) +QQ群不再接受新成员直接进入,希望大家多多参考文档,用户可加入[知识星球](https://t.zsxq.com/0drbw002x)提问以支持本项目,欢迎star和提交pr。 # 授权协议 本项目自有代码使用宽松的MIT协议,在保留版权信息的情况下可以自由应用于各自商用、非商业的项目。 但是本项目也零碎的使用了一些其他的开源代码,在商用的情况下请自行替代或剔除; 由于使用本项目而产生的商业纠纷或侵权行为一概与本项目及开发者无关,请自行承担法律风险。 在使用本项目代码时,也应该在授权协议中同时表明本项目依赖的第三方库的协议 From 71030ca6bc2a4a2eb05f4b63fa83dda5e89ac68b Mon Sep 17 00:00:00 2001 From: 648540858 <648540858@qq.com> Date: Thu, 13 Apr 2023 17:50:33 +0800 Subject: [PATCH 10/48] =?UTF-8?q?=E6=9B=B4=E6=96=B0readme?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index 4df03c35c..9f803675e 100644 --- a/README.md +++ b/README.md @@ -117,6 +117,9 @@ QQ群不再接受新成员直接进入,希望大家多多参考文档,用户 # 授权协议 本项目自有代码使用宽松的MIT协议,在保留版权信息的情况下可以自由应用于各自商用、非商业的项目。 但是本项目也零碎的使用了一些其他的开源代码,在商用的情况下请自行替代或剔除; 由于使用本项目而产生的商业纠纷或侵权行为一概与本项目及开发者无关,请自行承担法律风险。 在使用本项目代码时,也应该在授权协议中同时表明本项目依赖的第三方库的协议 +# 付费技术支持 +如果项目需要一对一的技术支持,或者棘手的问题需要解决,请发送邮件到648540858@qq.com + # 致谢 感谢作者[夏楚](https://github.com/xia-chu) 提供这么棒的开源流媒体服务框架,并在开发过程中给予支持与帮助。 感谢作者[dexter langhuihui](https://github.com/langhuihui) 开源这么好用的WEB播放器。 From 1f02cb9178befba039ee10f344d7a23fb3864a53 Mon Sep 17 00:00:00 2001 From: 648540858 <648540858@qq.com> Date: Thu, 13 Apr 2023 20:08:25 +0800 Subject: [PATCH 11/48] pr #817 --- .../transmit/event/request/impl/InviteRequestProcessor.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/InviteRequestProcessor.java b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/InviteRequestProcessor.java index 01a2a6e91..aef01e4cf 100644 --- a/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/InviteRequestProcessor.java +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/InviteRequestProcessor.java @@ -485,6 +485,7 @@ public class InviteRequestProcessor extends SIPRequestProcessorParent implements // 当前系统作为下级平台使用,当上级平台点播时不携带ssrc时,并且设备在当前系统中已经点播了。这个时候需要重新给生成一个ssrc,不使用默认的"0000000000"。 if (ssrc.equals(ssrcDefault)) { ssrc = ssrcFactory.getPlaySsrc(mediaServerItem.getId()); + ssrcFactory.releaseSsrc(mediaServerItem.getId(), ssrc); sendRtpItem.setSsrc(ssrc); } From d46fc9de827fe85a48f447cf1550444573a6f1a5 Mon Sep 17 00:00:00 2001 From: 648540858 <648540858@qq.com> Date: Fri, 14 Apr 2023 14:59:22 +0800 Subject: [PATCH 12/48] =?UTF-8?q?=E4=BC=98=E5=8C=96=E4=B8=8B=E7=BA=A7?= =?UTF-8?q?=E5=B9=B3=E5=8F=B0=E8=87=AA=E5=AE=9A=E4=B9=89ssrc=E7=9A=84?= =?UTF-8?q?=E6=83=85=E5=86=B5=EF=BC=8C=E4=BC=98=E5=8C=96=E5=9B=BD=E6=A0=87?= =?UTF-8?q?=E5=BD=95=E5=83=8F=E4=B8=8B=E8=BD=BD=E6=B5=81=E7=A8=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../iot/vmp/common/CommonCallback.java | 5 + .../transmit/cmd/impl/SIPCommander.java | 6 +- .../cmd/MediaStatusNotifyMessageHandler.java | 9 ++ .../iot/vmp/media/zlm/ZLMRESTfulUtils.java | 4 + .../vmp/media/zlm/ZLMRTPServerFactory.java | 26 ++++++ .../iot/vmp/service/IMediaServerService.java | 3 + .../service/impl/MediaServerServiceImpl.java | 10 ++ .../iot/vmp/service/impl/PlayServiceImpl.java | 93 ++++++++++++++++--- .../src/components/dialog/recordDownload.vue | 5 +- 9 files changed, 141 insertions(+), 20 deletions(-) create mode 100644 src/main/java/com/genersoft/iot/vmp/common/CommonCallback.java diff --git a/src/main/java/com/genersoft/iot/vmp/common/CommonCallback.java b/src/main/java/com/genersoft/iot/vmp/common/CommonCallback.java new file mode 100644 index 000000000..819fe0dd5 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/common/CommonCallback.java @@ -0,0 +1,5 @@ +package com.genersoft.iot.vmp.common; + +public interface CommonCallback{ + public void run(T t); +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/cmd/impl/SIPCommander.java b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/cmd/impl/SIPCommander.java index 00e2613b4..4f0dc11a5 100644 --- a/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/cmd/impl/SIPCommander.java +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/cmd/impl/SIPCommander.java @@ -546,7 +546,7 @@ public class SIPCommander implements ISIPCommander { HookSubscribeForStreamChange hookSubscribe = HookSubscribeFactory.on_stream_changed("rtp", ssrcInfo.getStream(), true, null, mediaServerItem.getId()); // 添加订阅 CallIdHeader newCallIdHeader = sipSender.getNewCallIdHeader(sipLayer.getLocalIp(device.getLocalIp()), device.getTransport()); - String callId=newCallIdHeader.getCallId(); + String callId= newCallIdHeader.getCallId(); subscribe.addSubscribe(hookSubscribe, (MediaServerItem mediaServerItemInUse, JSONObject json) -> { logger.debug("sipc 添加订阅===callId {}",callId); hookEvent.call(new InviteStreamInfo(mediaServerItem, json,callId, "rtp", ssrcInfo.getStream())); @@ -558,7 +558,7 @@ public class SIPCommander implements ISIPCommander { (MediaServerItem mediaServerItemForEnd, JSONObject jsonForEnd) -> { logger.info("[录像]下载结束, 发送BYE"); try { - streamByeCmd(device, channelId, ssrcInfo.getStream(),callId); + streamByeCmd(device, channelId, ssrcInfo.getStream(), callId); } catch (InvalidArgumentException | ParseException | SipException | SsrcTransactionNotFoundException e) { logger.error("[录像]下载结束, 发送BYE失败 {}", e.getMessage()); @@ -580,8 +580,6 @@ public class SIPCommander implements ISIPCommander { if (ssrcIndex >= 0) { ssrc = contentString.substring(ssrcIndex + 2, ssrcIndex + 12); } - logger.debug("接收到的下载响应ssrc====>{}",ssrcInfo.getSsrc()); - logger.debug("接收到的下载响应ssrc====>{}",ssrc); streamSession.put(device.getDeviceId(), channelId, response.getCallIdHeader().getCallId(), ssrcInfo.getStream(), ssrc, mediaServerItem.getId(), response, VideoStreamSessionManager.SessionType.download); okEvent.response(event); }); diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/notify/cmd/MediaStatusNotifyMessageHandler.java b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/notify/cmd/MediaStatusNotifyMessageHandler.java index b15003cca..728ff0e38 100644 --- a/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/notify/cmd/MediaStatusNotifyMessageHandler.java +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/notify/cmd/MediaStatusNotifyMessageHandler.java @@ -12,6 +12,9 @@ import com.genersoft.iot.vmp.gb28181.transmit.cmd.impl.SIPCommanderFroPlatform; import com.genersoft.iot.vmp.gb28181.transmit.event.request.SIPRequestProcessorParent; import com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.IMessageHandler; import com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.notify.NotifyMessageHandler; +import com.genersoft.iot.vmp.media.zlm.ZlmHttpHookSubscribe; +import com.genersoft.iot.vmp.media.zlm.dto.HookSubscribeFactory; +import com.genersoft.iot.vmp.media.zlm.dto.HookSubscribeForStreamChange; import com.genersoft.iot.vmp.storager.IRedisCatchStorage; import com.genersoft.iot.vmp.storager.IVideoManagerStorage; import gov.nist.javax.sip.message.SIPRequest; @@ -58,6 +61,9 @@ public class MediaStatusNotifyMessageHandler extends SIPRequestProcessorParent i @Autowired private VideoStreamSessionManager sessionManager; + @Autowired + private ZlmHttpHookSubscribe subscribe; + @Override public void afterPropertiesSet() throws Exception { notifyMessageHandler.addHandler(cmdType, this); @@ -93,6 +99,9 @@ public class MediaStatusNotifyMessageHandler extends SIPRequestProcessorParent i } catch (InvalidArgumentException | ParseException | SsrcTransactionNotFoundException | SipException e) { logger.error("[录像流]推送完毕,收到关流通知, 发送BYE失败 {}", e.getMessage()); } + // 去除监听流注销自动停止下载的监听 + HookSubscribeForStreamChange hookSubscribe = HookSubscribeFactory.on_stream_changed("rtp", ssrcTransaction.getStream(), false, "rtsp", ssrcTransaction.getMediaServerId()); + subscribe.removeSubscribe(hookSubscribe); // 如果级联播放,需要给上级发送此通知 TODO 多个上级同时观看一个下级 可能存在停错的问题,需要将点播CallId进行上下级绑定 SendRtpItem sendRtpItem = redisCatchStorage.querySendRTPServer(null, ssrcTransaction.getChannelId(), null, null); diff --git a/src/main/java/com/genersoft/iot/vmp/media/zlm/ZLMRESTfulUtils.java b/src/main/java/com/genersoft/iot/vmp/media/zlm/ZLMRESTfulUtils.java index a28919732..77758a3f5 100644 --- a/src/main/java/com/genersoft/iot/vmp/media/zlm/ZLMRESTfulUtils.java +++ b/src/main/java/com/genersoft/iot/vmp/media/zlm/ZLMRESTfulUtils.java @@ -276,6 +276,10 @@ public class ZLMRESTfulUtils { return sendPost(mediaServerItem, "closeRtpServer",param, null); } + public void closeRtpServer(MediaServerItem mediaServerItem, Map param, RequestCallback callback) { + sendPost(mediaServerItem, "closeRtpServer",param, callback); + } + public JSONObject listRtpServer(MediaServerItem mediaServerItem) { return sendPost(mediaServerItem, "listRtpServer",null, null); } diff --git a/src/main/java/com/genersoft/iot/vmp/media/zlm/ZLMRTPServerFactory.java b/src/main/java/com/genersoft/iot/vmp/media/zlm/ZLMRTPServerFactory.java index 9bf1a3ade..ce38b10de 100644 --- a/src/main/java/com/genersoft/iot/vmp/media/zlm/ZLMRTPServerFactory.java +++ b/src/main/java/com/genersoft/iot/vmp/media/zlm/ZLMRTPServerFactory.java @@ -3,6 +3,7 @@ package com.genersoft.iot.vmp.media.zlm; import com.alibaba.fastjson2.JSON; import com.alibaba.fastjson2.JSONArray; import com.alibaba.fastjson2.JSONObject; +import com.genersoft.iot.vmp.common.CommonCallback; import com.genersoft.iot.vmp.conf.UserSetting; import com.genersoft.iot.vmp.gb28181.bean.SendRtpItem; import com.genersoft.iot.vmp.media.zlm.dto.*; @@ -164,6 +165,31 @@ public class ZLMRTPServerFactory { return result; } + public void closeRtpServer(MediaServerItem serverItem, String streamId, CommonCallback callback) { + if (serverItem == null) { + callback.run(false); + return; + } + Map param = new HashMap<>(); + param.put("stream_id", streamId); + zlmresTfulUtils.closeRtpServer(serverItem, param, jsonObject -> { + if (jsonObject != null ) { + if (jsonObject.getInteger("code") == 0) { + callback.run(jsonObject.getInteger("hit") == 1); + return; + }else { + logger.error("关闭RTP Server 失败: " + jsonObject.getString("msg")); + } + }else { + // 检查ZLM状态 + logger.error("关闭RTP Server 失败: 请检查ZLM服务"); + } + callback.run(false); + }); + + + } + /** * 创建一个国标推流 diff --git a/src/main/java/com/genersoft/iot/vmp/service/IMediaServerService.java b/src/main/java/com/genersoft/iot/vmp/service/IMediaServerService.java index 1233455f2..a5b9034db 100644 --- a/src/main/java/com/genersoft/iot/vmp/service/IMediaServerService.java +++ b/src/main/java/com/genersoft/iot/vmp/service/IMediaServerService.java @@ -1,5 +1,6 @@ package com.genersoft.iot.vmp.service; +import com.genersoft.iot.vmp.common.CommonCallback; import com.genersoft.iot.vmp.media.zlm.ZLMServerConfig; import com.genersoft.iot.vmp.media.zlm.dto.MediaServerItem; import com.genersoft.iot.vmp.media.zlm.dto.ServerKeepaliveData; @@ -51,6 +52,8 @@ public interface IMediaServerService { void closeRTPServer(MediaServerItem mediaServerItem, String streamId); + void closeRTPServer(MediaServerItem mediaServerItem, String streamId, CommonCallback callback); + void closeRTPServer(String mediaServerId, String streamId); void clearRTPServer(MediaServerItem mediaServerItem); diff --git a/src/main/java/com/genersoft/iot/vmp/service/impl/MediaServerServiceImpl.java b/src/main/java/com/genersoft/iot/vmp/service/impl/MediaServerServiceImpl.java index 856359cd8..5a2db63b0 100644 --- a/src/main/java/com/genersoft/iot/vmp/service/impl/MediaServerServiceImpl.java +++ b/src/main/java/com/genersoft/iot/vmp/service/impl/MediaServerServiceImpl.java @@ -3,6 +3,7 @@ package com.genersoft.iot.vmp.service.impl; import com.alibaba.fastjson2.JSON; import com.alibaba.fastjson2.JSONArray; import com.alibaba.fastjson2.JSONObject; +import com.genersoft.iot.vmp.common.CommonCallback; import com.genersoft.iot.vmp.common.VideoManagerConstants; import com.genersoft.iot.vmp.conf.DynamicTask; import com.genersoft.iot.vmp.conf.SipConfig; @@ -172,6 +173,15 @@ public class MediaServerServiceImpl implements IMediaServerService { zlmrtpServerFactory.closeRtpServer(mediaServerItem, streamId); } + @Override + public void closeRTPServer(MediaServerItem mediaServerItem, String streamId, CommonCallback callback) { + if (mediaServerItem == null) { + callback.run(false); + return; + } + zlmrtpServerFactory.closeRtpServer(mediaServerItem, streamId, callback); + } + @Override public void closeRTPServer(String mediaServerId, String streamId) { MediaServerItem mediaServerItem = this.getOne(mediaServerId); diff --git a/src/main/java/com/genersoft/iot/vmp/service/impl/PlayServiceImpl.java b/src/main/java/com/genersoft/iot/vmp/service/impl/PlayServiceImpl.java index f143a15cf..6c2ee7ee6 100644 --- a/src/main/java/com/genersoft/iot/vmp/service/impl/PlayServiceImpl.java +++ b/src/main/java/com/genersoft/iot/vmp/service/impl/PlayServiceImpl.java @@ -328,9 +328,30 @@ public class PlayServiceImpl implements IPlayService { }); } // 关闭rtp server - mediaServerService.closeRTPServer(mediaServerItem, ssrcInfo.getStream()); - // 重新开启ssrc server - mediaServerService.openRTPServer(mediaServerItem, ssrcInfo.getStream(), ssrcInResponse, device.isSsrcCheck(), false, ssrcInfo.getPort()); + mediaServerService.closeRTPServer(mediaServerItem, ssrcInfo.getStream(), result->{ + if (result) { + // 重新开启ssrc server + mediaServerService.openRTPServer(mediaServerItem, ssrcInfo.getStream(), ssrcInResponse, device.isSsrcCheck(), false, ssrcInfo.getPort()); + }else { + try { + logger.warn("[停止点播] {}/{}", device.getDeviceId(), channelId); + cmder.streamByeCmd(device, channelId, ssrcInfo.getStream(), null, null); + } catch (InvalidArgumentException | SipException | ParseException | SsrcTransactionNotFoundException e) { + logger.error("[命令发送失败] 停止点播, 发送BYE: {}", e.getMessage()); + throw new ControllerException(ErrorCode.ERROR100.getCode(), "命令发送失败: " + e.getMessage()); + } + + dynamicTask.stop(timeOutTaskKey); + // 释放ssrc + mediaServerService.releaseSsrc(mediaServerItem.getId(), ssrcInfo.getSsrc()); + + streamSession.remove(device.getDeviceId(), channelId, ssrcInfo.getStream()); + event.msg = "下级自定义了ssrc,重新设置收流信息失败"; + event.statusCode = 500; + errorEvent.response(event); + } + }); + } } @@ -472,7 +493,7 @@ public class PlayServiceImpl implements IPlayService { if (device == null) { throw new ControllerException(ErrorCode.ERROR100.getCode(), "设备: " + deviceId + "不存在"); } - + logger.info("[回放消息] deviceId: {}, channelId: {},收流端口: {}, 收流模式:{}, SSRC: {}, SSRC校验:{}", device.getDeviceId(), channelId, ssrcInfo.getPort(), device.getStreamMode(), ssrcInfo.getSsrc(), device.isSsrcCheck()); PlayBackResult playBackResult = new PlayBackResult<>(); String playBackTimeOutTaskKey = UUID.randomUUID().toString(); dynamicTask.startDelay(playBackTimeOutTaskKey, () -> { @@ -546,6 +567,7 @@ public class PlayServiceImpl implements IPlayService { if (!ssrcFactory.checkSsrc(mediaServerItem.getId(),ssrcInResponse)) { // ssrc 不可用 // 释放ssrc + dynamicTask.stop(playBackTimeOutTaskKey); mediaServerService.releaseSsrc(mediaServerItem.getId(), ssrcInfo.getSsrc()); streamSession.remove(device.getDeviceId(), channelId, ssrcInfo.getStream()); eventResult.msg = "下级自定义了ssrc,但是此ssrc不可用"; @@ -568,10 +590,31 @@ public class PlayServiceImpl implements IPlayService { hookEvent.call(new InviteStreamInfo(mediaServerItem, null, eventResult.callId, "rtp", ssrcInfo.getStream())); }); } + // 关闭rtp server - mediaServerService.closeRTPServer(mediaServerItem, ssrcInfo.getStream()); - // 重新开启ssrc server - mediaServerService.openRTPServer(mediaServerItem, ssrcInfo.getStream(), ssrcInResponse, device.isSsrcCheck(), true, ssrcInfo.getPort()); + mediaServerService.closeRTPServer(mediaServerItem, ssrcInfo.getStream(), result->{ + if (result) { + // 重新开启ssrc server + mediaServerService.openRTPServer(mediaServerItem, ssrcInfo.getStream(), ssrcInResponse, device.isSsrcCheck(), true, ssrcInfo.getPort()); + }else { + try { + logger.warn("[回放消息]停止 {}/{}", device.getDeviceId(), channelId); + cmder.streamByeCmd(device, channelId, ssrcInfo.getStream(), null, null); + } catch (InvalidArgumentException | SipException | ParseException | SsrcTransactionNotFoundException e) { + logger.error("[命令发送失败] 停止点播 停止, 发送BYE: {}", e.getMessage()); + throw new ControllerException(ErrorCode.ERROR100.getCode(), "命令发送失败: " + e.getMessage()); + } + + dynamicTask.stop(playBackTimeOutTaskKey); + // 释放ssrc + mediaServerService.releaseSsrc(mediaServerItem.getId(), ssrcInfo.getSsrc()); + streamSession.remove(device.getDeviceId(), channelId, ssrcInfo.getStream()); + errorEvent.response(eventResult); + eventResult.msg = "下级自定义了ssrc,重新设置收流信息失败"; + eventResult.statusCode = 500; + errorEvent.response(eventResult); + } + }); } } } @@ -619,7 +662,7 @@ public class PlayServiceImpl implements IPlayService { throw new ControllerException(ErrorCode.ERROR400.getCode(), "设备:" + deviceId + "不存在"); } PlayBackResult downloadResult = new PlayBackResult<>(); - + logger.info("[录像下载] deviceId: {}, channelId: {},收流端口: {}, 收流模式:{}, SSRC: {}, SSRC校验:{}", device.getDeviceId(), channelId, ssrcInfo.getPort(), device.getStreamMode(), ssrcInfo.getSsrc(), device.isSsrcCheck()); String downLoadTimeOutTaskKey = UUID.randomUUID().toString(); dynamicTask.startDelay(downLoadTimeOutTaskKey, () -> { logger.warn(String.format("录像下载请求超时,deviceId:%s ,channelId:%s", deviceId, channelId)); @@ -648,7 +691,7 @@ public class PlayServiceImpl implements IPlayService { streamSession.remove(device.getDeviceId(), channelId, ssrcInfo.getStream()); }; InviteStreamCallback hookEvent = (InviteStreamInfo inviteStreamInfo) -> { - logger.info("收到订阅消息: " + inviteStreamInfo.getCallId()); + logger.info("[录像下载]收到订阅消息: " + inviteStreamInfo.getCallId()); dynamicTask.stop(downLoadTimeOutTaskKey); StreamInfo streamInfo = onPublishHandler(inviteStreamInfo.getMediaServerItem(), inviteStreamInfo.getResponse(), deviceId, channelId); streamInfo.setStartTime(startTime); @@ -678,9 +721,9 @@ public class PlayServiceImpl implements IPlayService { if (ssrcInfo.getSsrc().equals(ssrcInResponse)) { return; } - logger.info("[回放消息] 收到invite 200, 发现下级自定义了ssrc: {}", ssrcInResponse); + logger.info("[录像下载] 收到invite 200, 发现下级自定义了ssrc: {}", ssrcInResponse); if (!mediaServerItem.isRtpEnable() || device.isSsrcCheck()) { - logger.info("[回放消息] SSRC修正 {}->{}", ssrcInfo.getSsrc(), ssrcInResponse); + logger.info("[录像下载] SSRC修正 {}->{}", ssrcInfo.getSsrc(), ssrcInResponse); if (!ssrcFactory.checkSsrc(mediaServerItem.getId(),ssrcInResponse)) { // ssrc 不可用 @@ -707,14 +750,34 @@ public class PlayServiceImpl implements IPlayService { hookEvent.call(new InviteStreamInfo(mediaServerItem, null, eventResult.callId, "rtp", ssrcInfo.getStream())); }); } + // 关闭rtp server - mediaServerService.closeRTPServer(mediaServerItem, ssrcInfo.getStream()); - // 重新开启ssrc server - mediaServerService.openRTPServer(mediaServerItem, ssrcInfo.getStream(), ssrcInResponse, device.isSsrcCheck(), true, ssrcInfo.getPort()); + mediaServerService.closeRTPServer(mediaServerItem, ssrcInfo.getStream(), result->{ + if (result) { + // 重新开启ssrc server + mediaServerService.openRTPServer(mediaServerItem, ssrcInfo.getStream(), ssrcInResponse, device.isSsrcCheck(), true, ssrcInfo.getPort()); + }else { + try { + logger.warn("[录像下载] 停止{}/{}", device.getDeviceId(), channelId); + cmder.streamByeCmd(device, channelId, ssrcInfo.getStream(), null, null); + } catch (InvalidArgumentException | SipException | ParseException | SsrcTransactionNotFoundException e) { + logger.error("[命令发送失败] 录像下载停止, 发送BYE: {}", e.getMessage()); + throw new ControllerException(ErrorCode.ERROR100.getCode(), "命令发送失败: " + e.getMessage()); + } + + dynamicTask.stop(downLoadTimeOutTaskKey); + // 释放ssrc + mediaServerService.releaseSsrc(mediaServerItem.getId(), ssrcInfo.getSsrc()); + + streamSession.remove(device.getDeviceId(), channelId, ssrcInfo.getStream()); + eventResult.msg = "下级自定义了ssrc,重新设置收流信息失败"; + eventResult.statusCode = 500; + errorEvent.response(eventResult); + } + }); } } } - }); } catch (InvalidArgumentException | SipException | ParseException e) { logger.error("[命令发送失败] 录像下载: {}", e.getMessage()); diff --git a/web_src/src/components/dialog/recordDownload.vue b/web_src/src/components/dialog/recordDownload.vue index 3e8c42713..b3f46c879 100644 --- a/web_src/src/components/dialog/recordDownload.vue +++ b/web_src/src/components/dialog/recordDownload.vue @@ -96,7 +96,10 @@ export default { }); }, close: function (){ - this.stopDownloadRecord(); + if (this.streamInfo.progress < 1) { + this.stopDownloadRecord(); + } + if (this.timer !== null) { window.clearTimeout(this.timer); this.timer = null; From cf1696e0d6f148445bb21dca6f066d8e07bc3234 Mon Sep 17 00:00:00 2001 From: 648540858 <648540858@qq.com> Date: Sat, 15 Apr 2023 09:03:41 +0800 Subject: [PATCH 13/48] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E4=BD=BF=E7=94=A8jwt?= =?UTF-8?q?=E5=90=8E=E5=AF=BC=E8=87=B4=E7=9A=84=E7=94=A8=E6=88=B7=E7=AE=A1?= =?UTF-8?q?=E7=90=86=E5=8A=9F=E8=83=BD=E5=BC=82=E5=B8=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 3 +- .../security/JwtAuthenticationFilter.java | 10 ++- .../iot/vmp/conf/security/JwtUtils.java | 5 +- .../conf/security/LoginFailureHandler.java | 65 ------------------- .../conf/security/LoginSuccessHandler.java | 36 ---------- .../iot/vmp/conf/security/SecurityUtils.java | 10 +-- .../vmp/conf/security/WebSecurityConfig.java | 10 --- .../iot/vmp/conf/security/dto/JwtUser.java | 10 +++ .../iot/vmp/vmanager/user/UserController.java | 2 +- 9 files changed, 29 insertions(+), 122 deletions(-) delete mode 100644 src/main/java/com/genersoft/iot/vmp/conf/security/LoginFailureHandler.java delete mode 100644 src/main/java/com/genersoft/iot/vmp/conf/security/LoginSuccessHandler.java diff --git a/README.md b/README.md index 9f803675e..d3ad3ac47 100644 --- a/README.md +++ b/README.md @@ -117,7 +117,8 @@ QQ群不再接受新成员直接进入,希望大家多多参考文档,用户 # 授权协议 本项目自有代码使用宽松的MIT协议,在保留版权信息的情况下可以自由应用于各自商用、非商业的项目。 但是本项目也零碎的使用了一些其他的开源代码,在商用的情况下请自行替代或剔除; 由于使用本项目而产生的商业纠纷或侵权行为一概与本项目及开发者无关,请自行承担法律风险。 在使用本项目代码时,也应该在授权协议中同时表明本项目依赖的第三方库的协议 -# 付费技术支持 +# 技术支持 +建议加入[知识星球](https://t.zsxq.com/0drbw002x)可以获取更多的教程以及更加及时的回复。 如果项目需要一对一的技术支持,或者棘手的问题需要解决,请发送邮件到648540858@qq.com # 致谢 diff --git a/src/main/java/com/genersoft/iot/vmp/conf/security/JwtAuthenticationFilter.java b/src/main/java/com/genersoft/iot/vmp/conf/security/JwtAuthenticationFilter.java index 27151eeed..f35b5bd86 100644 --- a/src/main/java/com/genersoft/iot/vmp/conf/security/JwtAuthenticationFilter.java +++ b/src/main/java/com/genersoft/iot/vmp/conf/security/JwtAuthenticationFilter.java @@ -2,6 +2,8 @@ package com.genersoft.iot.vmp.conf.security; import com.genersoft.iot.vmp.conf.UserSetting; import com.genersoft.iot.vmp.conf.security.dto.JwtUser; +import com.genersoft.iot.vmp.storager.dao.dto.Role; +import com.genersoft.iot.vmp.storager.dao.dto.User; import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; @@ -75,7 +77,13 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter { } // 构建UsernamePasswordAuthenticationToken,这里密码为null,是因为提供了正确的JWT,实现自动登录 - UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(username, jwtUser.getPassword(), new ArrayList<>() ); + User user = new User(); + user.setUsername(jwtUser.getUserName()); + user.setPassword(jwtUser.getPassword()); + Role role = new Role(); + role.setId(jwtUser.getRoleId()); + user.setRole(role); + UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(user, jwtUser.getPassword(), new ArrayList<>() ); SecurityContextHolder.getContext().setAuthentication(token); chain.doFilter(request, response); } diff --git a/src/main/java/com/genersoft/iot/vmp/conf/security/JwtUtils.java b/src/main/java/com/genersoft/iot/vmp/conf/security/JwtUtils.java index 57911b045..c9c7b680f 100644 --- a/src/main/java/com/genersoft/iot/vmp/conf/security/JwtUtils.java +++ b/src/main/java/com/genersoft/iot/vmp/conf/security/JwtUtils.java @@ -37,7 +37,7 @@ public class JwtUtils { */ public static final long expirationTime = 30; - public static String createToken(String username, String password) { + public static String createToken(String username, String password, Integer roleId) { try { /** * “iss” (issuer) 发行人 @@ -64,6 +64,7 @@ public class JwtUtils { //添加自定义参数,必须是字符串类型 claims.setClaim("username", username); claims.setClaim("password", password); + claims.setClaim("roleId", roleId); //jws JsonWebSignature jws = new JsonWebSignature(); @@ -118,8 +119,10 @@ public class JwtUtils { String username = (String) claims.getClaimValue("username"); String password = (String) claims.getClaimValue("password"); + Long roleId = (Long) claims.getClaimValue("roleId"); jwtUser.setUserName(username); jwtUser.setPassword(password); + jwtUser.setRoleId(roleId.intValue()); return jwtUser; } catch (InvalidJwtException e) { diff --git a/src/main/java/com/genersoft/iot/vmp/conf/security/LoginFailureHandler.java b/src/main/java/com/genersoft/iot/vmp/conf/security/LoginFailureHandler.java deleted file mode 100644 index 9bbf2e7d8..000000000 --- a/src/main/java/com/genersoft/iot/vmp/conf/security/LoginFailureHandler.java +++ /dev/null @@ -1,65 +0,0 @@ -package com.genersoft.iot.vmp.conf.security; - -import com.fasterxml.jackson.databind.ObjectMapper; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.security.authentication.*; -import org.springframework.security.core.AuthenticationException; -import org.springframework.security.web.authentication.AuthenticationFailureHandler; -import org.springframework.stereotype.Component; - -import javax.servlet.ServletException; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import java.io.IOException; -import java.util.HashMap; -import java.util.Map; - -@Component -public class LoginFailureHandler implements AuthenticationFailureHandler { - - private final static Logger logger = LoggerFactory.getLogger(LoginFailureHandler.class); - - @Autowired - private ObjectMapper objectMapper; - - @Override - public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException { - - String username = request.getParameter("username"); - if (e instanceof AccountExpiredException) { - // 账号过期 - logger.info("[登录失败] - 用户[{}]账号过期", username); - - } else if (e instanceof BadCredentialsException) { - // 密码错误 - logger.info("[登录失败] - 用户[{}]密码/SIP服务器ID 错误", username); - - } else if (e instanceof CredentialsExpiredException) { - // 密码过期 - logger.info("[登录失败] - 用户[{}]密码过期", username); - - } else if (e instanceof DisabledException) { - // 用户被禁用 - logger.info("[登录失败] - 用户[{}]被禁用", username); - - } else if (e instanceof LockedException) { - // 用户被锁定 - logger.info("[登录失败] - 用户[{}]被锁定", username); - - } else if (e instanceof InternalAuthenticationServiceException) { - // 内部错误 - logger.error(String.format("[登录失败] - [%s]内部错误", username), e); - - } else { - // 其他错误 - logger.error(String.format("[登录失败] - [%s]其他错误", username), e); - } - Map map = new HashMap<>(); - map.put("code","0"); - map.put("msg","登录失败"); - response.setContentType("application/json;charset=UTF-8"); - response.getWriter().write(objectMapper.writeValueAsString(map)); - } -} diff --git a/src/main/java/com/genersoft/iot/vmp/conf/security/LoginSuccessHandler.java b/src/main/java/com/genersoft/iot/vmp/conf/security/LoginSuccessHandler.java deleted file mode 100644 index d26342ef5..000000000 --- a/src/main/java/com/genersoft/iot/vmp/conf/security/LoginSuccessHandler.java +++ /dev/null @@ -1,36 +0,0 @@ -package com.genersoft.iot.vmp.conf.security; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.security.core.Authentication; -import org.springframework.security.web.authentication.AuthenticationSuccessHandler; -import org.springframework.stereotype.Component; - -import javax.servlet.ServletException; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import java.io.IOException; - -/** - * @author lin - */ -@Component -public class LoginSuccessHandler implements AuthenticationSuccessHandler { - - private final static Logger logger = LoggerFactory.getLogger(LoginSuccessHandler.class); - - @Override - public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException { -// String username = request.getParameter("username"); -// httpServletResponse.setContentType("application/json;charset=UTF-8"); -// // 生成JWT,并放置到请求头中 -// String jwt = JwtUtils.createToken(authentication.getName(), ); -// httpServletResponse.setHeader(JwtUtils.getHeader(), jwt); -// ServletOutputStream outputStream = httpServletResponse.getOutputStream(); -// outputStream.write(JSON.toJSONString(ErrorCode.SUCCESS).getBytes(StandardCharsets.UTF_8)); -// outputStream.flush(); -// outputStream.close(); - -// logger.info("[登录成功] - [{}]", username); - } -} diff --git a/src/main/java/com/genersoft/iot/vmp/conf/security/SecurityUtils.java b/src/main/java/com/genersoft/iot/vmp/conf/security/SecurityUtils.java index a8d35681e..f012f7efb 100644 --- a/src/main/java/com/genersoft/iot/vmp/conf/security/SecurityUtils.java +++ b/src/main/java/com/genersoft/iot/vmp/conf/security/SecurityUtils.java @@ -53,14 +53,10 @@ public class SecurityUtils { Authentication authentication = getAuthentication(); if(authentication!=null){ Object principal = authentication.getPrincipal(); - if(principal!=null && !"anonymousUser".equals(principal)){ -// LoginUser user = (LoginUser) authentication.getPrincipal(); + if(principal!=null && !"anonymousUser".equals(principal.toString())){ - String username = (String) principal; - User user = new User(); - user.setUsername(username); - LoginUser loginUser = new LoginUser(user, LocalDateTime.now()); - return loginUser; + User user = (User) principal; + return new LoginUser(user, LocalDateTime.now()); } } return null; diff --git a/src/main/java/com/genersoft/iot/vmp/conf/security/WebSecurityConfig.java b/src/main/java/com/genersoft/iot/vmp/conf/security/WebSecurityConfig.java index 96ae6b91c..1fbe3a4eb 100644 --- a/src/main/java/com/genersoft/iot/vmp/conf/security/WebSecurityConfig.java +++ b/src/main/java/com/genersoft/iot/vmp/conf/security/WebSecurityConfig.java @@ -47,16 +47,6 @@ public class WebSecurityConfig extends WebSecurityConfigurerAdapter { * 登出成功的处理 */ @Autowired - private LoginFailureHandler loginFailureHandler; - /** - * 登录成功的处理 - */ - @Autowired - private LoginSuccessHandler loginSuccessHandler; - /** - * 登出成功的处理 - */ - @Autowired private LogoutHandler logoutHandler; /** * 未登录的处理 diff --git a/src/main/java/com/genersoft/iot/vmp/conf/security/dto/JwtUser.java b/src/main/java/com/genersoft/iot/vmp/conf/security/dto/JwtUser.java index 1639d1fc2..8921a3083 100644 --- a/src/main/java/com/genersoft/iot/vmp/conf/security/dto/JwtUser.java +++ b/src/main/java/com/genersoft/iot/vmp/conf/security/dto/JwtUser.java @@ -25,6 +25,8 @@ public class JwtUser { private String password; + private int roleId; + private TokenStatus status; public String getUserName() { @@ -50,4 +52,12 @@ public class JwtUser { public void setPassword(String password) { this.password = password; } + + public int getRoleId() { + return roleId; + } + + public void setRoleId(int roleId) { + this.roleId = roleId; + } } diff --git a/src/main/java/com/genersoft/iot/vmp/vmanager/user/UserController.java b/src/main/java/com/genersoft/iot/vmp/vmanager/user/UserController.java index 157a3a819..5ffb02cb2 100644 --- a/src/main/java/com/genersoft/iot/vmp/vmanager/user/UserController.java +++ b/src/main/java/com/genersoft/iot/vmp/vmanager/user/UserController.java @@ -57,7 +57,7 @@ public class UserController { if (user == null) { throw new ControllerException(ErrorCode.ERROR100.getCode(), "用户名或密码错误"); }else { - String jwt = JwtUtils.createToken(username, password); + String jwt = JwtUtils.createToken(username, password, user.getRole().getId()); response.setHeader(JwtUtils.getHeader(), jwt); user.setAccessToken(jwt); } From f5d07c4c14a6555c0f1c80f5f47881a431570622 Mon Sep 17 00:00:00 2001 From: 648540858 <648540858@qq.com> Date: Sat, 15 Apr 2023 09:16:21 +0800 Subject: [PATCH 14/48] Update --bug---.md --- .github/ISSUE_TEMPLATE/--bug---.md | 29 ++++++++++++++++++----------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/--bug---.md b/.github/ISSUE_TEMPLATE/--bug---.md index ff09d5cf2..a39276ec9 100644 --- a/.github/ISSUE_TEMPLATE/--bug---.md +++ b/.github/ISSUE_TEMPLATE/--bug---.md @@ -7,23 +7,30 @@ assignees: '' --- +**环境信息:** + + - 1. 部署方式 wvp-pro docker / zlm(docker) + 编译wvp-pro/ wvp-prp + zlm都是编译部署/ + - 2. 部署环境 windows / ubuntu/ centos ... + - 3. 端口开放情况 + - 4. 是否是公网部署 + - 5. 是否使用https + - 6. 方便的话提供下使用的设备品牌或平台 + - 7. 你做过哪些尝试 + - 8. 代码更新时间 + **描述错误** 描述下您遇到的问题 **如何复现** 有明确复现步骤的问题会很容易被解决 -**预期行为** -清晰简洁的描述您期望发生的事情 +**截图** -**截图** +**抓包文件** + +**日志** +``` +日志内容放这里, 文件的话请直接上传 +``` -**环境信息:** - - 1. 部署方式 wvp-pro docker / zlm(docker) + 编译wvp-pro/ wvp-prp + zlm都是编译部署/ - - 2. 部署环境 windows / ubuntu/ centos ... - - 3. 端口开放情况 - - 4. 是否是公网部署 - - 5. 是否使用https - - 6. 方便的话提供下使用的设备品牌或平台 - - 7. 你做过哪些尝试 From cbd8b074af2f079da786c8ee47e30387f38597e1 Mon Sep 17 00:00:00 2001 From: 648540858 <648540858@qq.com> Date: Sat, 15 Apr 2023 09:33:35 +0800 Subject: [PATCH 15/48] Create solve.md --- .github/ISSUE_TEMPLATE/solve.md | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/solve.md diff --git a/.github/ISSUE_TEMPLATE/solve.md b/.github/ISSUE_TEMPLATE/solve.md new file mode 100644 index 000000000..75a0eed76 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/solve.md @@ -0,0 +1,31 @@ +--- +name: "[ 技术咨询 ] " +about: 对于使用中遇到问题 +title: '技术咨询' +labels: '技术咨询' +assignees: '' + +--- + +**环境信息:** + + - 1. 部署方式 wvp-pro docker / zlm(docker) + 编译wvp-pro/ wvp-prp + zlm都是编译部署/ + - 2. 部署环境 windows / ubuntu/ centos ... + - 3. 端口开放情况 + - 4. 是否是公网部署 + - 5. 是否使用https + - 6. 方便的话提供下使用的设备品牌或平台 + - 7. 你做过哪些尝试 + - 8. 代码更新时间 + + +**内容描述:** + +**截图** + +**抓包文件** + +**日志** +``` +日志内容放这里, 文件的话请直接上传 +``` From 30ad3fca99bb0d206dafe0881ec71a0bd53200f8 Mon Sep 17 00:00:00 2001 From: 648540858 <648540858@qq.com> Date: Sat, 15 Apr 2023 09:35:36 +0800 Subject: [PATCH 16/48] Update --bug---.md --- .github/ISSUE_TEMPLATE/--bug---.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/--bug---.md b/.github/ISSUE_TEMPLATE/--bug---.md index a39276ec9..7e61cd7c8 100644 --- a/.github/ISSUE_TEMPLATE/--bug---.md +++ b/.github/ISSUE_TEMPLATE/--bug---.md @@ -1,8 +1,8 @@ --- name: "[ BUG ] " -about: Create a report to help us improve -title: '' -labels: '' +about: 关于wvp的bug,与zlm有关的建议直接在zlm的issue中提问 +title: 'BUG' +labels: 'wvp的bug' assignees: '' --- From 03070db3fc5e10635f9ff81d42f36e94d8860101 Mon Sep 17 00:00:00 2001 From: 648540858 <648540858@qq.com> Date: Sat, 15 Apr 2023 09:36:12 +0800 Subject: [PATCH 17/48] Rename --bug---.md to bug.md --- .github/ISSUE_TEMPLATE/{--bug---.md => bug.md} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename .github/ISSUE_TEMPLATE/{--bug---.md => bug.md} (100%) diff --git a/.github/ISSUE_TEMPLATE/--bug---.md b/.github/ISSUE_TEMPLATE/bug.md similarity index 100% rename from .github/ISSUE_TEMPLATE/--bug---.md rename to .github/ISSUE_TEMPLATE/bug.md From 08a20f907bc4223619365299e8761eeeff64f128 Mon Sep 17 00:00:00 2001 From: 648540858 <648540858@qq.com> Date: Sat, 15 Apr 2023 09:38:03 +0800 Subject: [PATCH 18/48] Update and rename -------.md to new.md --- .github/ISSUE_TEMPLATE/-------.md | 10 ---------- .github/ISSUE_TEMPLATE/new.md | 13 +++++++++++++ 2 files changed, 13 insertions(+), 10 deletions(-) delete mode 100644 .github/ISSUE_TEMPLATE/-------.md create mode 100644 .github/ISSUE_TEMPLATE/new.md diff --git a/.github/ISSUE_TEMPLATE/-------.md b/.github/ISSUE_TEMPLATE/-------.md deleted file mode 100644 index 461ce76a6..000000000 --- a/.github/ISSUE_TEMPLATE/-------.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -name: "[ 新功能 ]" -about: 新功能 -title: '' -labels: '' -assignees: '' - ---- - - diff --git a/.github/ISSUE_TEMPLATE/new.md b/.github/ISSUE_TEMPLATE/new.md new file mode 100644 index 000000000..796142189 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/new.md @@ -0,0 +1,13 @@ +--- +name: "[ 新功能 ]" +about: 新功能 +title: '希望wVP实现的新功能,此功能应与你的具体业务无关' +labels: '' +assignees: '' + +--- + +**项目的详细需求** + +**这样的实现什么作用** + From db2ccfedfa17eb3cb5ca73ac3b6bc4b5a05d4148 Mon Sep 17 00:00:00 2001 From: 648540858 <648540858@qq.com> Date: Wed, 19 Apr 2023 11:09:26 +0800 Subject: [PATCH 19/48] =?UTF-8?q?=E4=BC=98=E5=8C=96notify=E6=80=A7?= =?UTF-8?q?=E8=83=BD=EF=BC=8C=E5=A2=9E=E5=8A=A0notify=E8=B6=85=E5=87=BA?= =?UTF-8?q?=E5=A4=84=E7=90=86=E8=83=BD=E5=8A=9B=E6=97=B6=E7=9B=B4=E6=8E=A5?= =?UTF-8?q?=E5=9B=9E=E5=A4=8D=E9=94=99=E8=AF=AF=E7=A0=81=EF=BC=8C=E4=B8=8D?= =?UTF-8?q?=E5=81=9A=E5=A4=84=E7=90=86=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- doc/_content/ability/_media/img_16.png | Bin 42517 -> 114388 bytes .../genersoft/iot/vmp/conf/UserSetting.java | 10 + .../NotifyRequestForCatalogProcessor.java | 264 ++++++++++++++++++ .../request/impl/NotifyRequestProcessor.java | 27 +- .../vmp/service/IDeviceChannelService.java | 31 ++ .../impl/DeviceChannelServiceImpl.java | 41 +++ .../vmp/service/impl/DeviceServiceImpl.java | 2 + .../vmp/storager/dao/DeviceChannelMapper.java | 114 +++++--- src/main/resources/all-application.yml | 2 + 9 files changed, 451 insertions(+), 40 deletions(-) create mode 100644 src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/NotifyRequestForCatalogProcessor.java diff --git a/doc/_content/ability/_media/img_16.png b/doc/_content/ability/_media/img_16.png index f7ce9e7aa7b6ba28c414ce2e1f9b00667064aaa7..b09e8cd39a747a1f16bc51cd7b9446df36ac2b4e 100644 GIT binary patch literal 114388 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 z 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

PxMp+X#{%@a4mseg9rm zVNOSm&My)7S`Y6q@*o)9xtpTv00oX?H_=?)>ly8HxDdBvKAoH%MCs4FuIwYA%(p){duA3?Q9XarwSr;{Vb2B#vju?k z&J3CuT z9&6JcYo|Z4W2?8EWmX;xFfcH?I1#rpI5GFBk$3enP{Y{xc^qY;*yHRb69>x{NShv| z-?G80xIau#H`4;VbQKj95Tnq77{s(K7BroRY|u**TRUrnJW1T~yp-TbBL_mHNIj z)LYJ3DL=Oq3*F*V$S}`CnKOw8+Yl-r<~gOtagylP5vemH8{?t7?`$xTr6nUH1LRqD zDW)9g{t>ns?4LwFQr4cR59S#r%QK(ERg2b3ki2|OUV8ja`}e>S{(N%Lg1E+*MqpUl zq6W2!R@;1MgPgqmG(quJDqE38s`@YliM8+kv!d4u1jx(CsMimMT-9XNLWV%h67J;J zAZ8Pw8%Ej+H;{ueGJ?b)1JQ49wzx95+GuwH3LI#qd3P0W5i@8277h_)s8zDvlSeA3 z?=fdghhv|x3sLo)ug6;^1|+bT15ZAMEY$VY_ml`oYs1tYv(~8SXJfR3Tej_-KhRv6+mE-AE}VutKjt5_hiF55G!#Z>L^Ta2*Wc zaCArfO80@n{<(i@=#(xwfNkg@?}y6*$Zf_iYKLPqe&0R=YYYh@u75!wKzQ?4%AbpGS+xr3j& z9^HlYV{(2qQeU-UYYDF9d_w;1r~mZZr+6dgC-#J0ktDa@RGs=dF31Ytnp9&he2npXPBO3~J*pxamD-;cYj%?}{gqxTYmt zL(!+xl1UyqchZz39uD8ZjhNuZ6eb(k$K zog0DlHqFGz((h|VaE$P~ca87)rr$gnLp-wO$jMf(?k0*ozmXg=6bpS$O6+&ev_Fi8 zB+UQOC~Aw2y!=4YWD6#7kyprS;AYo1^Ox9Z%5&|0c#n=0miYJ0?lkNrsdpqF*-aqV zG4qQk#krnYcvI^IwHWv^@amQlIPAj@NK@|IUrK!D4x%;SC?~AD&FAl^rK&m-1|Sn} zwdv0BOb>7LC03%Kwub;rjWM&Fd(tl>g_W`Y+mhf~{jf z>i+?e-ljO~8R5DGH_#6YQ5lOKE4Xoe_o>f-9RdAfk}KgamV5VbN#?(Fwp%S7#daNv zs($?Y{_XdCd@eBo%|u;<(IKirIA3m5{I#XD2QVZF&;;XgoUcH82RrskO!W?PVh6lA zog+=c3G`o4v4g&6)7h^qV<}6krD$pgh>2Rl#_?uzLItdfrA?Grpu~*JHXe_-me{)mN7d6kQvm zs-ewW2vLX=oPl^N1gWQum7GVP6L8#@$$1W_w}F9dI@uhB;KB0_VvceE6IqBu<9{`0 zSpJm>FTNSQgIrxFH?cTHzg*8qRmMowA5*J9(As~Rr}>W=@!pbcTQBEAkpZrR=rF6x zzwI3DfH^8;(dnF*=Xx=xdB~0sKfn+sC`z)^#4HFz7+CJfgFcR&8lv`+?mu811&Ok^ z!v0Nn;qz74Y{ACHCS%ugT9~N{X1SPVL?8TIBx4}dED=^u#MfbH%@Va*&-CNp;eTUH z^pe%p30B=a1|2V9mJ4}NMB&)(nKI1t5lLH6++r6=nlTVNQxBlho15!MGz;_dkj!P@ zb{EWxnZf2DNS!)*_L|A`Jl<5c=>WJS;Y`CR$zrhR2_JBB0e(p}HlyLhe>4q5iE&$kIS>MTwhQJ)m|lR&24U_7KTRV8E)kspWSVJ( z^L$LyWqg4ryU=@1D_VBJ@B(InWk4w+&pW74Q&S(owE&=vsLnA3ObGpxd^<*Dap3D(6{y2-75@+`?P!Ur)NW*?wYtXb<&!$It5*lm zn9Q{A|AdbrwC9yW4@4}_J}aCf2Bq_^U;FTgo2E_P(J>1TqLaqcOf&-B8Fv_m5S!TD z`eXe{M|(`6f=xHoZrL_5`{VV4d265S_tg!~{==WnP^^sW#l47y)3FiGeK3>!_Q`>3MFb9CTq8@eG=TW;QU!nU2jP`F**6n z64IGbb2_<25I|Z$%Yf#~n>TL&u)!*&GN)47aTG6u5FNDnE{}R-)!iA31FfcHNl)$zex&bghgNYLm)y9DW+iuuOeguNNb)%QX!ul)5>AY zRu;-VnE6)e%OZA3Tdu=ng1chi($MtE*w4a%P=ny&0+Hr@Ka>FD#+orClcRfeg0B4u z#-B@TYabpl8W|XQJw@$A%jTQ=fR|)3i$t?uECPzLB!+w(*bQb)W7# ztvwUKyNucJ7h0t6@egF6c|n-gjzC};Vc*Ck0ji8Z)f9l(bJ2eeQO~O z2`TUC20~mg%X8syvsYE8H7x8Um$2&3<6xyVg!Tz~_jx0Q7*QLCHURAtvL&W~w6rcI z3E@T|i{-5sgh>Ps9Dv6a!L}EWIlsA0!-nfT zB*;wCwe{5ua%_$F&|>u|5FZ{R?F8j7kocMpsc)8HqDD*aqVWDkUcXGrgPEN^#eNU3 zNf?fHw!A#o4z~sbT1W^T+k+NPmuzl)yb;JrsE6fz2yqNF-IQ)IQgw8m1Z09IvC-?2 z;wWA3eSd$f59sDk;X%T0;Vgq|TaUI@cbKnMlcWc4TH{stI^fU-e-|sQ5-I~mmu@<0 zp)-WMAG*Y?>^xX(VPTc&w=lz6pl_g|!Q&_a>= z&fHr}%mf5L<~BVmOY?=PnHiL2M?kegEB4WO5RX@uM^?b~(VxF-RkWlrr%#b~yo6`o zc2fcaU5!8Im!?P2Uik-2UccTOd*i&(qc%%+cJ{D>J%DgA6Z-74c@jx-JzlIZR+=|0 zSX1p^jt5lJiezvqw0e1KezTCg-7Ux7qK$~IOG!6B0h564pSQ_E*K&~J7mc`xtt*K9 zIp(w{1Myz-Z)3%8HUU8}7XpUuJ<{VXJ zskgv~7h>R`RY1JiL~K{c2necBP8XXU9ap6GxX;cHrRskBRv74Dpm^;5c)}JL5s@)i zc|Tfc9{pCJZ-v+dI>zyReBJ6&FJKajsaCXb&sUAx9Z+-1fL;ZNDU(W&RD&CgtgOLi znf!daO;!Lcd4g!ceQ7W9H1Kc(?gZHbth|)+AIe(f6PcnH6UKy2!TUQ}!>mW%$Rr}!dGwh!x>t5=h0pTM}JUFhK6 z+0h)CI3a9ueTwI>L{Cn3Ev*!o5UY91SEN?*_T!kccIp=(JA>^7(!9L=5Jn>8$RG@6 zyOY7`Zh*hP#n;!o_NSo2vipu1I(SzcPP!Dx?A;45Wx{2_aKKC)8wByg&kd8PI&d5D zXn;uB^VHNBxDcEg`VQpr9PCyIW`8#C9*4gWARtWn z!pi}eCCWA|U=Svenp_l?fTaQI!RY9e`hGN9sW*DTiUCB{)2DGec2Y42xByWxrCkXb zZjZ%mT0>|X!|(w~yWip^iYj7BbH3mx2;z)ohQ#yjM)I;|d8_-CPW4Ta$Ol6b)M((P zg-3fO<15Noc+!|#Y}@Gd`1R{a=xdKZdIJTV4J}&XqYb{C-(_U;*P2#x_qU0te)_V5<1Kpys z=K+oq)GtudUnZokKBv~xc{Q469LzmEHLUrRSa}MqcT9s13kj)C z2jpVMARAbB^BA@wy6TW;#y-;2)m=afA>U=RbF9ezE?~j-uV48l-gg$Ch|V*q-j(oY z*0q-FNS3-!87dwaeBzme$D4565>*Jid)h2DRdGTp_Q}p*?b-Up7^d4NXJ+8~<{zX*JX<-8^;?Pon;r z3J6~=An+oihI;yQk|b6FXjom~dq6$X3DEch*<~j*sC?+9{#C?NW{~-`lnO zH3dp!b4RKR2t$P2Kj0C>!_5t|k^OctXH4;s#>JVa%`U7}J==ikr zoSt2mc`vh@yiD8(?i7bD`tc!$bA;}BZyd3gf=mNC2gs2paQ%3|1_dq-Wuj=fAfN7l z{K2?Ls1=h)Vj+w*A+ zhp!`2UNGwvr#l=Oq%{cEX1#{uidLf48D3BA;m?wzU(sn)m7_0YK%C+4z zY+;8Y4zPNNap;)Y>)N*;my`=&3RN~FJ1)*2LeJo2dfCJT{r(S)sC<_RJ|c>TuOl;? zyoz!5-MqO*5@1Pon1jS@UE@uhy3=&J_HsTrov_ww{fEFT>D!YDO$;-XRwE;-r%pW* zfsO+~`G?$=+a4zh<*ScXCJmxS561f(nvVSVTe}q9ud@f5DQA*DSMa|QT_Y+Zb0(k~ zT7j~}VcjVxCv{5Hxi8qwz1Yf>)}zjyYchL44lgMyn25=jVQ(NDv5^+$*SOQ zd%u)H+^TCFDzp#o-(Lm~JB)7@j|(Y9Mez@ev-4f`pewE!Guq67a2=rdqnzCn3QGlC zmul$T_Rzl6Nu>)zezNp~ePzEF0Zl*!eb6yJ7_CY|8SpL*yT(Ru+H{NPw2x+DIQlEr z*npb|!3BttC9j9|d>Vwaz>ti4w;zWq49B)`z-<6<=ed|?iq!hIE#7rAb51NL`8I-7@_mwp+t98ZugpnS(Y>thGH z4sz@cVtNCuA>~gQ&NiG3*XxRcX`evX$I=$x1n`0&^pp3%=tn0|rBUZVBNDPpNMB%@ zY-xGl*B6E=0A^;Kju2&I&IJ@&=oRJTKm+9%sFvwf)@p;2co@u9RaK>=rV0oO>erg? zEi)3^1^*DFs>Wpmn%Zq!Eq>u#2}2AmCyeFDKX#^_EfB(hSkO{Ze7##ZE*HaC>_GW3 z%=ZHXX$2`hZItx!O%x;P-Zq8Vh_fZTTvdAaSjG4V4EgN))({^2=n-5iyLQ$?Y7jdB z_8W+Ow)s2}rw50J#di@^aYK_buc;5+%*D=soLM+(p#l^q?I_4Z4Hc>L?Wqu>hzSqh zU%ox&x5Ro(Q$Ekk{L(woZ=Z{yxA&8a5N*OxC8T>{(Kd-Q1%fe(s4yU}LC9v#18E`j zE<_U+>Q1l=sE9F;L9HtofBv>ihQ$@HEUg1XR}Vr~EY7;!Zu`3nJXZ?q`_*D| zkshKs8dp#l2f@$pD9$X+%pSJ9RE&p&mEX3)q@)if>1_aEuqJK4Ksr_rECxKqvtKLr_N!mDj*8Kn;Q?(5c% zrK5Aa9?TkDYkpN~nT0qmLraM?0cyq9l5QD1_wm-76biwi3Qz(V!tByAG*RXf&lB+y z>x2j`@an`!)OUL+tPvbjz~;CYl31ay0zV)mE=c@vj0gx+8tu|S~!-^08PTEB2FNqh8>uy|>IvO^BPg7gqabw#PzE*1CX;lqdF-2(gz-TKO?@xu9^$s0aX-fkpy0##w^?UOc37LA z9s>yuh=Q!k#9R|>-#ZEy5?83@AD}sr2ilH7+E|F5xwU z;n44p!j6BlVuczqD0AKS4ab;v640;}2?E^&gRYVl+!6{k`Lsr%4_aZB-hZpJ|H1IL z>UM@-r!Zf&aUuS<0qUj1fARHv!`}k{Os!#{3NEp7=UZa$k5k z2F{R|i$Qa2H^9CaortJ5G3Lg&R($+k`wW0I*vDxJ!c>jT&2xQqd%Ldz3IAgfy zqDN8DpMRc;^~4b60|L2VNO;n|21PT32`98)0o=B=Jx7VHi!7B*O1Zn`om*8RTl@6N z28sh}!c0&?!MO}2Wu)miJE7GYnC{~03W&z!08%1AU#4)2xBnw81<{5?I$Ma*E%4xr z$II5`>)BTlESERQF2kgw)~Bgm_52pH6@607-o40v{aK`pIUNl5;EDZ39ab4Sa8Q0) zg#lk8Slp)7Piq{RaXk`?DYVTTs0uNyhAAhsWSFRJFQqlIu(Ar=#vBDSUcgI${Uojq z6B)K^QZ*K4idNbTN%*->+=D*8dGhCPlpN;_GZd2Qw9DQz zPAz#9jZ3ZCyW0;h{($MLHc%nOcse;WQQ~UCf`jdm|L#3;zi(D3Hl%gHbZbQ*X_Rkei$>m_H8k6ccs0r zB=UJ*YrSDzOgi;uPD#y@gpc9*YEygK->*R6uv0yXVOL;+U+ZhgvRaT`?LlW<)`wFL4GDk>_pdff4%fL%Fn6gH%Jt>R>GBIplD_my$$;B}387E8|i1FSPWAO#`_W%(Fbk4u)#>eJR9|-jb<8tQR&`Rw9yE3;^^2 z7ay!sJ~)eByS8=x+Tx49_i!Hve42CN1s?wv4#;3ZQ4MWT%p+o966ia?1w;?H5JJ#8 zuEtbs7oat7T3E+J)hs%mew)?H>$L~V*G`D^Q#WkOrb!hqQK+HTg`@gWF)=O!gzGbu zAeE6kSC*a2MkeMkDT`0F%efOC4lxy?wT9Pa@8AGV4thvWpl=ITpq)E$#aS@N5)c?d zltGy=^z$e7u!|CN^ouv~p_(aozXczfeXfw_dDrJqRd@z*uTkROo?Nfj**-^!dRGv1Y5`LYE}y>NWjZ|qG{PjDdUu>57%V8am!9I%vR_cq0enRNAp;}o_N!jHb*iP zYezA_?w#x+gDrw$W_tQMhdhvL#4E7)`W|6?6ORl#0~SC?f{2`_$kWrrq(`}^t#j*_ zZP^Cg9wM$me~=yKgx%(t^~_L2Ah!VDEI12g56ZU4u_O?JO)g)iLQBW8#u;Li znSUcq{yO3qzmcVzRbHr;{Bx`+tBj{Q9QI`{033*bJdH;pM~Nv!6v2qzn>kQS_1flO z01^4Gj-H;tZOCHU!mbjtmvv^*v9awySiqdp6AaTxCUml~y#)o^K^Mg}A@2pPC3xgh zzx}Jdk2^+IV9ZBMzQo3QAo)OBEAeU7;E3cqpElWCUkcE<=x zuw`HmN7+5oNEg)YU>-;?YNG9a<7z4?Y^)&RWA{4dV{ zt9n;h9zuG5P3eWj8BB1O}8I@Pr#ya&diW) zMUg7&r@>1Swpp!6U@++=swBlS13%r(S)L|6lbF5&kZ>xi7FoY1l1tl{w-C^fo7u;X zLDCnsO62KhAa!G49cu@@4KxS=hvS<*%~?1gDr7QI{={ms>((S+7h6vBEI6c2f{(f6 ztzz8knb;$|`}o|I!FB%%XcB-+`ilva`k_%#5aR2cSwO7XCp+ug;x&3=n}^8Sv;?-- zIzzvn{O~hxvVQkYxh4qGCM7XTAB(GT1bU3h4K7>n>q1xA9I#11aWd($M6`YJ2U(ct za@>`tpT6=*hQ+6yEcFm@Pum;u?d|Qb^$Pj`GZWAx`ud)i|4>pyJ_ ziUd@v2Cw`jN2keeWpq0p?Rc;fY?VpopZ~1og@srcs{)(|aMB?WhFdlQxGZ7gT+uds z^Vl2ELLsMt$5m9+(QUh#16wN>Sty_u6k0El?E=%5iuu@ST~bzd-O&*;{PA_!$lY)% zg4eL;{c^T0tA|~Xb#jCM&Kj? zm#@)G42Wf=TA^onxBVnS3l1AOO6U$x{Z!|W$C=Z&7WV?A4Yz=mf#*agcUn~y(DYTY zU9gShI-CGWYjn7*CGcf~%p}Ej>r5gx zR(RcP%^GWBKn4A8-h0I6uelyF5J1dMYb*#IZ_RD9bf$;SrFfJIbF@3CkI-LGQ&eDL zWpxKU5WSOnBhghdR=bVrk)Pkwj(bgmFOg-wQO1!_^`#u35}=Z)nHd<=!D{%7swz9U z!BAp+_6&-w5Yg8IoP%|ZCJ=FN3ro;!@V$Q@)^=Np7IbljY!*VuK<5arB_-uRdUJGS z*EQ=yAfj+LgvC;F0aVJA*xrVZUN*N0jV1OrrNDaubv_d7b3&1VoVdSHuC6Ob#v$Xm+-fGqz`y{zUqm6~qEISo8k$Da5aAv~RUE#l z8d1oBLPIrRcn;{iyJB4T51ek!fe z)bs???^6LeE>q^hOsAEVgO-@#dkD&`H!arSLB*sf>18Z3m|5hy!FJZ|wwnC-2P==F z6|g4Lcf0;}ZTpTLP!qYlvZtSL(tv0+bb=9DSa$3LMCPh!J#VZK(35Ff7kn2 zUnD%axl-M!fV5#$5+Xl@JgC8X$@Wg|SouzTC=N$Vx&<12Lz3bB(IMvoVq>ThvAEbx z`EG~TsZn^ukjtq2lHKkWMuvuf5W1UU@Gzxfhz2}(zL3}eOplr!&tJ|5oKV}SWY zpB^4PldQFVEKnXnkv{{!Nl5-M9G204$@F?DFdCSx%r=N&$QuN zS=lX$%$vx7-DEnD&mzjBHjs(|`<_t_dk6hE(|+Y_TD(0x^Y85OYtRHkVpdXKPKBJ! zaTOhhxGdfb+3_N1T`F{#0~86*>zEiAf8&{>#0jn+!Kb6792^|1J*%yrcl2bMK8@7^ zk^B5MEeyH{4LKhL{P7jNY793Yjb#AbO;3+>9X)4uc48D8MpisIu@%n^{Ce9oYe^vWjZ0!!p*ojZ)bpxp69-{X`BJAewCABx7z z&AnpM(k+)o!9*bbh|&^NxNwW<%`hksV5bsPD>|C*aSQ{x&56Jcn~-UN=u6{ zkBdA4h!q%c@pVnj%{h=-0YLQJ&LR)%2okS%RAIG=dT^&46ZOWd7J=K8>(`$m=t!Hq1)tZ@mt zwx`yafj4weatRq9^R8Ww(%_gezJnNsuuZvlSQWK5(pp5%&UExI?Q=cFQ(;}Ck=%yW z3~~&5O+>?Tt}#5#nIFK{!AHKF9h&mI8DGy>!3Jt_rc( zI70_`{^ujJ>qbq?mQgZZ{f&Ke!*aqvUfs(oSBwklAI_^hxZ*7-TIj4A-BZ-87gQOeJPs_$g zAgW8NV)K}1WlE-rZ&7@O+lY0$Q0Ox@d7Z~6tF$B4J6Ycr(~StL9}++v7qwpZV7%tO zy3aJ>VL1={rH0-{x_#B8+K73sh@B-&28FeYDK6dW{ZsR`>n|WKvJ6m!8((;}`|7hX z0ZUGE(MRgpe`*(fpSs3oqwM%D#YS?ZV8e~Ws_H6!3R-V@inqVm^5&L&h<97Wp)AiK zLw-a1Mb|YHBNAyfE92hFH~0RCGOblKR%AJ_CQaa*ZtE^FxyvV5wuQfX6~9%Pr6r1< zH9<+-jJ5EOddAL6dnSUnC%X&r^E8h#m|7^jJ`-fNT`yyMCEfaD@&ZdOyI{$#Y6&?YHX4h4;_( zzB#x^WvxRk+5R(p_Or-XC2Q$;rSF3Z-+cm{@`}DZd%cTQ6fRVpn|{6FhCvWMBha|P zggMN#%8^3(Rx+8f{ND}<#0CE^vCBS&#Wkrslc_B~^l~p!Q2ZXxTwa(1vqOr-U9f$n z*wb+9zQ2&d--e|BZPC#G>>2z&1`PlIM}c+ze+AWFp;4joe!bLko;-i2q;N+5h1|tk F{{vP918@KU literal 42517 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�LVIE` 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 allowedOrigins = new ArrayList<>(); + private int maxNotifyCountQueue = 10000; + public Boolean getSavePositionHistory() { return savePositionHistory; } @@ -257,4 +259,12 @@ public class UserSetting { public void setRecordPath(String recordPath) { this.recordPath = recordPath; } + + public int getMaxNotifyCountQueue() { + return maxNotifyCountQueue; + } + + public void setMaxNotifyCountQueue(int maxNotifyCountQueue) { + this.maxNotifyCountQueue = maxNotifyCountQueue; + } } diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/NotifyRequestForCatalogProcessor.java b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/NotifyRequestForCatalogProcessor.java new file mode 100644 index 000000000..56fa187ee --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/NotifyRequestForCatalogProcessor.java @@ -0,0 +1,264 @@ +package com.genersoft.iot.vmp.gb28181.transmit.event.request.impl; + +import com.genersoft.iot.vmp.conf.DynamicTask; +import com.genersoft.iot.vmp.conf.UserSetting; +import com.genersoft.iot.vmp.gb28181.bean.Device; +import com.genersoft.iot.vmp.gb28181.bean.DeviceChannel; +import com.genersoft.iot.vmp.gb28181.event.EventPublisher; +import com.genersoft.iot.vmp.gb28181.event.subscribe.catalog.CatalogEvent; +import com.genersoft.iot.vmp.gb28181.transmit.event.request.SIPRequestProcessorParent; +import com.genersoft.iot.vmp.gb28181.utils.SipUtils; +import com.genersoft.iot.vmp.gb28181.utils.XmlUtil; +import com.genersoft.iot.vmp.service.IDeviceChannelService; +import com.genersoft.iot.vmp.storager.IRedisCatchStorage; +import org.dom4j.DocumentException; +import org.dom4j.Element; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import javax.sip.RequestEvent; +import javax.sip.header.FromHeader; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CopyOnWriteArrayList; + +/** + * SIP命令类型: NOTIFY请求中的目录请求处理 + */ +@Component +public class NotifyRequestForCatalogProcessor extends SIPRequestProcessorParent { + + + private final static Logger logger = LoggerFactory.getLogger(NotifyRequestForCatalogProcessor.class); + + private final List updateChannelOnlineList = new CopyOnWriteArrayList<>(); + private final List updateChannelOfflineList = new CopyOnWriteArrayList<>(); + private final Map updateChannelMap = new ConcurrentHashMap<>(); + + private final Map addChannelMap = new ConcurrentHashMap<>(); + private final List deleteChannelList = new CopyOnWriteArrayList<>(); + + + @Autowired + private UserSetting userSetting; + + @Autowired + private EventPublisher eventPublisher; + + @Autowired + private IRedisCatchStorage redisCatchStorage; + + @Autowired + private IDeviceChannelService deviceChannelService; + + @Autowired + private DynamicTask dynamicTask; + + private final static String talkKey = "notify-request-for-catalog-task"; + + public void process(RequestEvent evt) { + try { + long start = System.currentTimeMillis(); + FromHeader fromHeader = (FromHeader) evt.getRequest().getHeader(FromHeader.NAME); + String deviceId = SipUtils.getUserIdFromFromHeader(fromHeader); + + Device device = redisCatchStorage.getDevice(deviceId); + if (device == null || device.getOnline() == 0) { + logger.warn("[收到目录订阅]:{}, 但是设备已经离线", (device != null ? device.getDeviceId():"" )); + return; + } + Element rootElement = getRootElement(evt, device.getCharset()); + if (rootElement == null) { + logger.warn("[ 收到目录订阅 ] content cannot be null, {}", evt.getRequest()); + return; + } + Element deviceListElement = rootElement.element("DeviceList"); + if (deviceListElement == null) { + return; + } + Iterator deviceListIterator = deviceListElement.elementIterator(); + if (deviceListIterator != null) { + + // 遍历DeviceList + while (deviceListIterator.hasNext()) { + Element itemDevice = deviceListIterator.next(); + Element channelDeviceElement = itemDevice.element("DeviceID"); + if (channelDeviceElement == null) { + continue; + } + Element eventElement = itemDevice.element("Event"); + String event; + if (eventElement == null) { + logger.warn("[收到目录订阅]:{}, 但是Event为空, 设为默认值 ADD", (device != null ? device.getDeviceId():"" )); + event = CatalogEvent.ADD; + }else { + event = eventElement.getText().toUpperCase(); + } + DeviceChannel channel = XmlUtil.channelContentHander(itemDevice, device, event); + + channel.setDeviceId(device.getDeviceId()); + logger.info("[收到目录订阅]:{}/{}", device.getDeviceId(), channel.getChannelId()); + switch (event) { + case CatalogEvent.ON: + // 上线 + logger.info("[收到通道上线通知] 来自设备: {}, 通道 {}", device.getDeviceId(), channel.getChannelId()); + updateChannelOnlineList.add(channel); + if (updateChannelOnlineList.size() > 300) { + executeSaveForOnline(); + } + break; + case CatalogEvent.OFF : + // 离线 + logger.info("[收到通道离线通知] 来自设备: {}, 通道 {}", device.getDeviceId(), channel.getChannelId()); + if (userSetting.getRefuseChannelStatusChannelFormNotify()) { + updateChannelOfflineList.add(channel); + if (updateChannelOfflineList.size() > 300) { + executeSaveForOffline(); + } + }else { + logger.info("[收到通道离线通知] 但是平台已配置拒绝此消息,来自设备: {}, 通道 {}", device.getDeviceId(), channel.getChannelId()); + } + break; + case CatalogEvent.VLOST: + // 视频丢失 + logger.info("[收到通道视频丢失通知] 来自设备: {}, 通道 {}", device.getDeviceId(), channel.getChannelId()); + if (userSetting.getRefuseChannelStatusChannelFormNotify()) { + updateChannelOfflineList.add(channel); + if (updateChannelOfflineList.size() > 300) { + executeSaveForOffline(); + } + }else { + logger.info("[收到通道视频丢失通知] 但是平台已配置拒绝此消息,来自设备: {}, 通道 {}", device.getDeviceId(), channel.getChannelId()); + } + break; + case CatalogEvent.DEFECT: + // 故障 + logger.info("[收到通道视频故障通知] 来自设备: {}, 通道 {}", device.getDeviceId(), channel.getChannelId()); + if (userSetting.getRefuseChannelStatusChannelFormNotify()) { + updateChannelOfflineList.add(channel); + if (updateChannelOfflineList.size() > 300) { + executeSaveForOffline(); + } + }else { + logger.info("[收到通道视频故障通知] 但是平台已配置拒绝此消息,来自设备: {}, 通道 {}", device.getDeviceId(), channel.getChannelId()); + } + break; + case CatalogEvent.ADD: + // 增加 + logger.info("[收到增加通道通知] 来自设备: {}, 通道 {}", device.getDeviceId(), channel.getChannelId()); + // 判断此通道是否存在 + DeviceChannel deviceChannel = deviceChannelService.getOne(deviceId, channel.getChannelId()); + if (deviceChannel != null) { + channel.setId(deviceChannel.getId()); + updateChannelMap.put(channel.getChannelId(), channel); + if (updateChannelMap.keySet().size() > 300) { + executeSaveForUpdate(); + } + }else { + addChannelMap.put(channel.getChannelId(), channel); + if (addChannelMap.keySet().size() > 300) { + executeSaveForAdd(); + } + } + + break; + case CatalogEvent.DEL: + // 删除 + logger.info("[收到删除通道通知] 来自设备: {}, 通道 {}", device.getDeviceId(), channel.getChannelId()); + deleteChannelList.add(channel); + if (deleteChannelList.size() > 300) { + executeSaveForDelete(); + } + break; + case CatalogEvent.UPDATE: + // 更新 + logger.info("[收到更新通道通知] 来自设备: {}, 通道 {}", device.getDeviceId(), channel.getChannelId()); + // 判断此通道是否存在 + DeviceChannel deviceChannelForUpdate = deviceChannelService.getOne(deviceId, channel.getChannelId()); + if (deviceChannelForUpdate != null) { + channel.setId(deviceChannelForUpdate.getId()); + updateChannelMap.put(channel.getChannelId(), channel); + if (updateChannelMap.keySet().size() > 300) { + executeSaveForUpdate(); + } + }else { + addChannelMap.put(channel.getChannelId(), channel); + if (addChannelMap.keySet().size() > 300) { + executeSaveForAdd(); + } + } + break; + default: + logger.warn("[ NotifyCatalog ] event not found : {}", event ); + + } + // 转发变化信息 + eventPublisher.catalogEventPublish(null, channel, event); + + if (updateChannelMap.keySet().size() > 0 + || addChannelMap.keySet().size() > 0 + || updateChannelOnlineList.size() > 0 + || updateChannelOfflineList.size() > 0 + || deleteChannelList.size() > 0) { + + if (!dynamicTask.contains(talkKey)) { + dynamicTask.startDelay(talkKey, this::executeSave, 1000); + } + } + } + } + } catch (DocumentException e) { + logger.error("未处理的异常 ", e); + } + } + + private void executeSave(){ + System.out.println("定时存储数据"); + executeSaveForUpdate(); + executeSaveForDelete(); + executeSaveForOnline(); + executeSaveForOffline(); + dynamicTask.stop(talkKey); + } + + private void executeSaveForUpdate(){ + if (updateChannelMap.values().size() > 0) { + ArrayList deviceChannels = new ArrayList<>(updateChannelMap.values()); + updateChannelMap.clear(); + deviceChannelService.batchUpdateChannel(deviceChannels); + } + + } + + private void executeSaveForAdd(){ + if (addChannelMap.values().size() > 0) { + ArrayList deviceChannels = new ArrayList<>(addChannelMap.values()); + addChannelMap.clear(); + deviceChannelService.batchAddChannel(deviceChannels); + } + } + + private void executeSaveForDelete(){ + if (deleteChannelList.size() > 0) { + deviceChannelService.deleteChannels(deleteChannelList); + deleteChannelList.clear(); + } + } + + private void executeSaveForOnline(){ + if (updateChannelOnlineList.size() > 0) { + deviceChannelService.channelsOnline(updateChannelOnlineList); + updateChannelOnlineList.clear(); + } + } + + private void executeSaveForOffline(){ + if (updateChannelOfflineList.size() > 0) { + deviceChannelService.channelsOffline(updateChannelOfflineList); + updateChannelOfflineList.clear(); + } + } + +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/NotifyRequestProcessor.java b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/NotifyRequestProcessor.java index 426064116..5dae82612 100644 --- a/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/NotifyRequestProcessor.java +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/NotifyRequestProcessor.java @@ -76,12 +76,17 @@ public class NotifyRequestProcessor extends SIPRequestProcessorParent implements @Autowired private IDeviceChannelService deviceChannelService; + @Autowired + private NotifyRequestForCatalogProcessor notifyRequestForCatalogProcessor; + private ConcurrentLinkedQueue taskQueue = new ConcurrentLinkedQueue<>(); @Qualifier("taskExecutor") @Autowired private ThreadPoolTaskExecutor taskExecutor; + private int maxQueueCount = 30000; + @Override public void afterPropertiesSet() throws Exception { // 添加消息处理的订阅 @@ -91,7 +96,15 @@ public class NotifyRequestProcessor extends SIPRequestProcessorParent implements @Override public void process(RequestEvent evt) { try { - responseAck((SIPRequest) evt.getRequest(), Response.OK, null, null); + + if (taskQueue.size() >= userSetting.getMaxNotifyCountQueue()) { + responseAck((SIPRequest) evt.getRequest(), Response.BUSY_HERE, null, null); + logger.error("[notify] 待处理消息队列已满 {},返回486 BUSY_HERE,消息不做处理", userSetting.getMaxNotifyCountQueue()); + return; + }else { + responseAck((SIPRequest) evt.getRequest(), Response.OK, null, null); + } + }catch (SipException | InvalidArgumentException | ParseException e) { logger.error("未处理的异常 ", e); } @@ -103,6 +116,9 @@ public class NotifyRequestProcessor extends SIPRequestProcessorParent implements while (!taskQueue.isEmpty()) { try { HandlerCatchData take = taskQueue.poll(); + if (take == null) { + continue; + } Element rootElement = getRootElement(take.getEvt()); if (rootElement == null) { logger.error("处理NOTIFY消息时未获取到消息体,{}", take.getEvt().getRequest()); @@ -112,7 +128,8 @@ public class NotifyRequestProcessor extends SIPRequestProcessorParent implements if (CmdType.CATALOG.equals(cmd)) { logger.info("接收到Catalog通知"); - processNotifyCatalogList(take.getEvt()); +// processNotifyCatalogList(take.getEvt()); + notifyRequestForCatalogProcessor.process(take.getEvt()); } else if (CmdType.ALARM.equals(cmd)) { logger.info("接收到Alarm通知"); processNotifyAlarm(take.getEvt()); @@ -132,7 +149,7 @@ public class NotifyRequestProcessor extends SIPRequestProcessorParent implements /** * 处理MobilePosition移动位置Notify - * + * * @param evt */ private void processNotifyMobilePosition(RequestEvent evt) { @@ -236,7 +253,7 @@ public class NotifyRequestProcessor extends SIPRequestProcessorParent implements /*** * 处理alarm设备报警Notify - * + * * @param evt */ private void processNotifyAlarm(RequestEvent evt) { @@ -346,7 +363,7 @@ public class NotifyRequestProcessor extends SIPRequestProcessorParent implements /*** * 处理catalog设备目录列表Notify - * + * * @param evt */ private void processNotifyCatalogList(RequestEvent evt) { diff --git a/src/main/java/com/genersoft/iot/vmp/service/IDeviceChannelService.java b/src/main/java/com/genersoft/iot/vmp/service/IDeviceChannelService.java index c192dd5a1..66dbe0772 100644 --- a/src/main/java/com/genersoft/iot/vmp/service/IDeviceChannelService.java +++ b/src/main/java/com/genersoft/iot/vmp/service/IDeviceChannelService.java @@ -56,4 +56,35 @@ public interface IDeviceChannelService { * 查询通道所属的设备 */ List getDeviceByChannelId(String channelId); + + /** + * 批量删除通道 + * @param deleteChannelList 待删除的通道列表 + */ + int deleteChannels(List deleteChannelList); + + /** + * 批量上线 + */ + int channelsOnline(List channels); + + /** + * 批量下线 + */ + int channelsOffline(List channels); + + /** + * 获取一个通道 + */ + DeviceChannel getOne(String deviceId, String channelId); + + /** + * 直接批量更新通道 + */ + void batchUpdateChannel(List channels); + + /** + * 直接批量添加 + */ + void batchAddChannel(List deviceChannels); } diff --git a/src/main/java/com/genersoft/iot/vmp/service/impl/DeviceChannelServiceImpl.java b/src/main/java/com/genersoft/iot/vmp/service/impl/DeviceChannelServiceImpl.java index 9223ced0c..229bc0d2c 100644 --- a/src/main/java/com/genersoft/iot/vmp/service/impl/DeviceChannelServiceImpl.java +++ b/src/main/java/com/genersoft/iot/vmp/service/impl/DeviceChannelServiceImpl.java @@ -209,6 +209,47 @@ public class DeviceChannelServiceImpl implements IDeviceChannelService { @Override public List getDeviceByChannelId(String channelId) { + return channelMapper.getDeviceByChannelId(channelId); } + + @Override + public int deleteChannels(List deleteChannelList) { + return channelMapper.batchDel(deleteChannelList); + } + + @Override + public int channelsOnline(List channels) { + return channelMapper.batchOnline(channels); + } + + @Override + public int channelsOffline(List channels) { + return channelMapper.batchOffline(channels); + } + + @Override + public DeviceChannel getOne(String deviceId, String channelId){ + return channelMapper.queryChannel(deviceId, channelId); + } + + @Override + public void batchUpdateChannel(List channels) { + channelMapper.batchUpdate(channels); + for (DeviceChannel channel : channels) { + if (channel.getParentId() != null) { + channelMapper.updateChannelSubCount(channel.getDeviceId(), channel.getParentId()); + } + } + } + + @Override + public void batchAddChannel(List channels) { + channelMapper.batchAdd(channels); + for (DeviceChannel channel : channels) { + if (channel.getParentId() != null) { + channelMapper.updateChannelSubCount(channel.getDeviceId(), channel.getParentId()); + } + } + } } diff --git a/src/main/java/com/genersoft/iot/vmp/service/impl/DeviceServiceImpl.java b/src/main/java/com/genersoft/iot/vmp/service/impl/DeviceServiceImpl.java index a3dd6a7ea..48de5d2bd 100644 --- a/src/main/java/com/genersoft/iot/vmp/service/impl/DeviceServiceImpl.java +++ b/src/main/java/com/genersoft/iot/vmp/service/impl/DeviceServiceImpl.java @@ -644,4 +644,6 @@ public class DeviceServiceImpl implements IDeviceService { public List getAll() { return deviceMapper.getAll(); } + + } diff --git a/src/main/java/com/genersoft/iot/vmp/storager/dao/DeviceChannelMapper.java b/src/main/java/com/genersoft/iot/vmp/storager/dao/DeviceChannelMapper.java index 93f2a0975..3f4d804c4 100644 --- a/src/main/java/com/genersoft/iot/vmp/storager/dao/DeviceChannelMapper.java +++ b/src/main/java/com/genersoft/iot/vmp/storager/dao/DeviceChannelMapper.java @@ -197,6 +197,60 @@ public interface DeviceChannelMapper { @Update(value = {"UPDATE device_channel SET status=0 WHERE deviceId=#{deviceId}"}) void offlineByDeviceId(String deviceId); +// @Insert("") +// int batchAdd(List addChannels); + + @Insert("") int batchAdd(List addChannels); @@ -264,7 +285,7 @@ public interface DeviceChannelMapper { ", owner=#{item.owner}" + ", civilCode=#{item.civilCode}" + ", block=#{item.block}" + - ", block=#{item.subCount}" + + ", subCount=#{item.subCount}" + ", address=#{item.address}" + ", parental=#{item.parental}" + ", parentId=#{item.parentId}" + @@ -289,7 +310,8 @@ public interface DeviceChannelMapper { ", latitudeWgs84=#{item.latitudeWgs84}" + ", businessGroupId=#{item.businessGroupId}" + ", gpsTime=#{item.gpsTime}" + - "WHERE deviceId=#{item.deviceId} AND channelId=#{item.channelId}"+ + "WHERE id=#{item.id}" + + "WHERE deviceId=#{item.deviceId} AND channelId=#{item.channelId}" + "" + ""}) int batchUpdate(List updateChannels); @@ -403,4 +425,26 @@ public interface DeviceChannelMapper { @Select("select de.* from device de left join device_channel dc on de.deviceId = dc.deviceId where dc.channelId=#{channelId}") List getDeviceByChannelId(String channelId); + + + @Delete({""}) + int batchDel(List deleteChannelList); + + @Update({""}) + int batchOnline(List channels); + + @Update({""}) + int batchOffline(List channels); } diff --git a/src/main/resources/all-application.yml b/src/main/resources/all-application.yml index d0a828958..bae990453 100644 --- a/src/main/resources/all-application.yml +++ b/src/main/resources/all-application.yml @@ -178,6 +178,8 @@ user-settings: send-to-platforms-when-id-lost: true # 保持通道状态,不接受notify通道状态变化, 兼容海康平台发送错误消息 refuse-channel-status-channel-form-notify: false + # 设置notify缓存队列最大长度,超过此长度的数据将返回486 BUSY_HERE,消息丢弃, 默认10000 + max-notify-count-queue: 10000 # 跨域配置,配置你访问前端页面的地址即可, 可以配置多个 allowed-origins: - http://localhost:8008 From f68f6d20e000b9e1926acb43999a2e6e3333cc4c Mon Sep 17 00:00:00 2001 From: 648540858 <648540858@qq.com> Date: Wed, 19 Apr 2023 14:53:06 +0800 Subject: [PATCH 20/48] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E6=B5=81=E6=B3=A8?= =?UTF-8?q?=E9=94=80=E6=83=B3=E4=B8=8A=E7=BA=A7=E5=8F=91=E9=80=81bye?= =?UTF-8?q?=E5=90=8E=E6=9C=AA=E6=B8=85=E7=90=86=E7=BC=93=E5=AD=98=E7=9A=84?= =?UTF-8?q?=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/genersoft/iot/vmp/media/zlm/ZLMHttpHookListener.java | 1 + .../genersoft/iot/vmp/service/impl/MediaServerServiceImpl.java | 1 + 2 files changed, 2 insertions(+) diff --git a/src/main/java/com/genersoft/iot/vmp/media/zlm/ZLMHttpHookListener.java b/src/main/java/com/genersoft/iot/vmp/media/zlm/ZLMHttpHookListener.java index b62e114f0..523b10fac 100644 --- a/src/main/java/com/genersoft/iot/vmp/media/zlm/ZLMHttpHookListener.java +++ b/src/main/java/com/genersoft/iot/vmp/media/zlm/ZLMHttpHookListener.java @@ -403,6 +403,7 @@ public class ZLMHttpHookListener { try { if (platform != null) { commanderFroPlatform.streamByeCmd(platform, sendRtpItem); + redisCatchStorage.deleteSendRTPServer(platformId, sendRtpItem.getChannelId(), sendRtpItem.getCallId(), sendRtpItem.getStreamId()); } else { cmder.streamByeCmd(device, sendRtpItem.getChannelId(), param.getStream(), sendRtpItem.getCallId()); } diff --git a/src/main/java/com/genersoft/iot/vmp/service/impl/MediaServerServiceImpl.java b/src/main/java/com/genersoft/iot/vmp/service/impl/MediaServerServiceImpl.java index 5a2db63b0..29eb6e974 100644 --- a/src/main/java/com/genersoft/iot/vmp/service/impl/MediaServerServiceImpl.java +++ b/src/main/java/com/genersoft/iot/vmp/service/impl/MediaServerServiceImpl.java @@ -742,4 +742,5 @@ public class MediaServerServiceImpl implements IMediaServerService { result.setGbSend(redisCatchStorage.getGbSendCount(mediaServerItem.getId())); return result; } + } From 0f3898910c42d4303347ac7295d3c7f0cc8e8457 Mon Sep 17 00:00:00 2001 From: 648540858 <648540858@qq.com> Date: Wed, 19 Apr 2023 16:21:28 +0800 Subject: [PATCH 21/48] =?UTF-8?q?=E6=94=B6=E6=B5=81=E6=97=B6=E8=AE=BE?= =?UTF-8?q?=E7=BD=AEre=5Fuse=5Fport=E5=8F=82=E6=95=B0=E8=A7=A3=E5=86=B3?= =?UTF-8?q?=E7=AB=AF=E5=8F=A3=E5=85=B3=E9=97=AD=E5=BF=AB=E9=80=9F=E6=89=93?= =?UTF-8?q?=E5=BC=80=E9=80=A0=E6=88=90=E7=9A=84=E7=AB=AF=E5=8F=A3=E6=9C=AA?= =?UTF-8?q?=E9=87=8A=E6=94=BE=E9=97=AE=E9=A2=98=EF=BC=8C=20=E6=94=AF?= =?UTF-8?q?=E6=8C=81=E7=82=B9=E6=92=AD=E7=9A=84tcp=E4=B8=BB=E5=8A=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../iot/vmp/gb28181/bean/Device.java | 11 ++++++ .../request/impl/InviteRequestProcessor.java | 4 +- .../iot/vmp/media/zlm/ZLMRESTfulUtils.java | 8 ++++ .../vmp/media/zlm/ZLMRTPServerFactory.java | 22 +++++++++-- .../iot/vmp/service/IMediaServerService.java | 7 +--- .../service/impl/MediaServerServiceImpl.java | 14 ++----- .../iot/vmp/service/impl/PlayServiceImpl.java | 39 +++++++++++++++---- web_src/src/components/DeviceList.vue | 4 +- 8 files changed, 79 insertions(+), 30 deletions(-) diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/bean/Device.java b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/Device.java index 9093970cb..daf709f9c 100644 --- a/src/main/java/com/genersoft/iot/vmp/gb28181/bean/Device.java +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/Device.java @@ -247,6 +247,17 @@ public class Device { return streamMode; } + public Integer getStreamModeForParam() { + if (streamMode.equalsIgnoreCase("UDP")) { + return 0; + }else if (streamMode.equalsIgnoreCase("TCP-PASSIVE")) { + return 1; + }else if (streamMode.equalsIgnoreCase("TCP-ACTIVE")) { + return 2; + } + return 0; + } + public void setStreamMode(String streamMode) { this.streamMode = streamMode; } diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/InviteRequestProcessor.java b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/InviteRequestProcessor.java index aef01e4cf..a4367b4a8 100644 --- a/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/InviteRequestProcessor.java +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/InviteRequestProcessor.java @@ -425,7 +425,7 @@ public class InviteRequestProcessor extends SIPRequestProcessorParent implements sendRtpItem.setApp("rtp"); if ("Playback".equalsIgnoreCase(sessionName)) { sendRtpItem.setPlayType(InviteStreamType.PLAYBACK); - SSRCInfo ssrcInfo = mediaServerService.openRTPServer(mediaServerItem, null, device.isSsrcCheck(), true); + SSRCInfo ssrcInfo = mediaServerService.openRTPServer(mediaServerItem, null, null, device.isSsrcCheck(), true, 0, false, device.getStreamModeForParam()); sendRtpItem.setStreamId(ssrcInfo.getStream()); // 写入redis, 超时时回复 redisCatchStorage.updateSendRTPSever(sendRtpItem); @@ -469,7 +469,7 @@ public class InviteRequestProcessor extends SIPRequestProcessorParent implements if (mediaServerItem.isRtpEnable()) { streamId = String.format("%s_%s", device.getDeviceId(), channelId); } - SSRCInfo ssrcInfo = mediaServerService.openRTPServer(mediaServerItem, streamId, null, device.isSsrcCheck(), false); + SSRCInfo ssrcInfo = mediaServerService.openRTPServer(mediaServerItem, streamId, null, device.isSsrcCheck(), false, 0, false, device.getStreamModeForParam()); logger.info(JSONObject.toJSONString(ssrcInfo)); sendRtpItem.setStreamId(ssrcInfo.getStream()); sendRtpItem.setSsrc(ssrc.equals(ssrcDefault) ? ssrcInfo.getSsrc() : ssrc); diff --git a/src/main/java/com/genersoft/iot/vmp/media/zlm/ZLMRESTfulUtils.java b/src/main/java/com/genersoft/iot/vmp/media/zlm/ZLMRESTfulUtils.java index 77758a3f5..13f324020 100644 --- a/src/main/java/com/genersoft/iot/vmp/media/zlm/ZLMRESTfulUtils.java +++ b/src/main/java/com/genersoft/iot/vmp/media/zlm/ZLMRESTfulUtils.java @@ -346,4 +346,12 @@ public class ZLMRESTfulUtils { param.put("stream_id", streamId); return sendPost(mediaServerItem, "resumeRtpCheck",param, null); } + + public JSONObject connectRtpServer(MediaServerItem mediaServerItem, String dst_url, int dst_port, String stream_id) { + Map param = new HashMap<>(1); + param.put("dst_url", dst_url); + param.put("dst_port", dst_port); + param.put("stream_id", stream_id); + return sendPost(mediaServerItem, "connectRtpServer",param, null); + } } diff --git a/src/main/java/com/genersoft/iot/vmp/media/zlm/ZLMRTPServerFactory.java b/src/main/java/com/genersoft/iot/vmp/media/zlm/ZLMRTPServerFactory.java index ce38b10de..99576c4ab 100644 --- a/src/main/java/com/genersoft/iot/vmp/media/zlm/ZLMRTPServerFactory.java +++ b/src/main/java/com/genersoft/iot/vmp/media/zlm/ZLMRTPServerFactory.java @@ -91,7 +91,17 @@ public class ZLMRTPServerFactory { return result; } - public int createRTPServer(MediaServerItem mediaServerItem, String streamId, int ssrc, Integer port) { + /** + * 开启rtpServer + * @param mediaServerItem zlm服务实例 + * @param streamId 流Id + * @param ssrc ssrc + * @param port 端口, 0/null为使用随机 + * @param reUsePort 是否重用端口 + * @param tcpMode 0/null udp 模式,1 tcp 被动模式, 2 tcp 主动模式。 + * @return + */ + public int createRTPServer(MediaServerItem mediaServerItem, String streamId, int ssrc, Integer port, Boolean reUsePort, Integer tcpMode) { int result = -1; // 查询此rtp server 是否已经存在 JSONObject rtpInfo = zlmresTfulUtils.getRtpInfo(mediaServerItem, streamId); @@ -107,7 +117,7 @@ public class ZLMRTPServerFactory { JSONObject jsonObject = zlmresTfulUtils.closeRtpServer(mediaServerItem, param); if (jsonObject != null ) { if (jsonObject.getInteger("code") == 0) { - return createRTPServer(mediaServerItem, streamId, ssrc, port); + return createRTPServer(mediaServerItem, streamId, ssrc, port, reUsePort, tcpMode); }else { logger.warn("[开启rtpServer], 重启RtpServer错误"); } @@ -121,8 +131,14 @@ public class ZLMRTPServerFactory { Map param = new HashMap<>(); - param.put("enable_tcp", 1); + if (tcpMode == null) { + tcpMode = 0; + } + param.put("tcp_mode", tcpMode); param.put("stream_id", streamId); + if (reUsePort != null) { + param.put("re_use_port", reUsePort?"1":"0"); + } // 推流端口设置0则使用随机端口 if (port == null) { param.put("port", 0); diff --git a/src/main/java/com/genersoft/iot/vmp/service/IMediaServerService.java b/src/main/java/com/genersoft/iot/vmp/service/IMediaServerService.java index a5b9034db..495b009e4 100644 --- a/src/main/java/com/genersoft/iot/vmp/service/IMediaServerService.java +++ b/src/main/java/com/genersoft/iot/vmp/service/IMediaServerService.java @@ -44,11 +44,8 @@ public interface IMediaServerService { void updateVmServer(List mediaServerItemList); - SSRCInfo openRTPServer(MediaServerItem mediaServerItem, String streamId, boolean ssrcCheck, boolean isPlayback); - - SSRCInfo openRTPServer(MediaServerItem mediaServerItem, String streamId, String ssrc, boolean ssrcCheck, boolean isPlayback); - - SSRCInfo openRTPServer(MediaServerItem mediaServerItem, String streamId, String ssrc, boolean ssrcCheck, boolean isPlayback, Integer port); + SSRCInfo openRTPServer(MediaServerItem mediaServerItem, String streamId, String ssrc, boolean ssrcCheck, + boolean isPlayback, Integer port, Boolean reUsePort, Integer tcpMode); void closeRTPServer(MediaServerItem mediaServerItem, String streamId); diff --git a/src/main/java/com/genersoft/iot/vmp/service/impl/MediaServerServiceImpl.java b/src/main/java/com/genersoft/iot/vmp/service/impl/MediaServerServiceImpl.java index 29eb6e974..94ed200fc 100644 --- a/src/main/java/com/genersoft/iot/vmp/service/impl/MediaServerServiceImpl.java +++ b/src/main/java/com/genersoft/iot/vmp/service/impl/MediaServerServiceImpl.java @@ -125,13 +125,10 @@ public class MediaServerServiceImpl implements IMediaServerService { } } - @Override - public SSRCInfo openRTPServer(MediaServerItem mediaServerItem, String streamId, boolean ssrcCheck, boolean isPlayback) { - return openRTPServer(mediaServerItem, streamId, null, ssrcCheck,isPlayback); - } @Override - public SSRCInfo openRTPServer(MediaServerItem mediaServerItem, String streamId, String presetSsrc, boolean ssrcCheck, boolean isPlayback, Integer port) { + public SSRCInfo openRTPServer(MediaServerItem mediaServerItem, String streamId, String presetSsrc, boolean ssrcCheck, + boolean isPlayback, Integer port, Boolean reUsePort, Integer tcpMode) { if (mediaServerItem == null || mediaServerItem.getId() == null) { logger.info("[openRTPServer] 失败, mediaServerItem == null || mediaServerItem.getId() == null"); return null; @@ -153,18 +150,13 @@ public class MediaServerServiceImpl implements IMediaServerService { } int rtpServerPort; if (mediaServerItem.isRtpEnable()) { - rtpServerPort = zlmrtpServerFactory.createRTPServer(mediaServerItem, streamId, ssrcCheck?Integer.parseInt(ssrc):0, port); + rtpServerPort = zlmrtpServerFactory.createRTPServer(mediaServerItem, streamId, ssrcCheck?Integer.parseInt(ssrc):0, port, reUsePort, tcpMode); } else { rtpServerPort = mediaServerItem.getRtpProxyPort(); } return new SSRCInfo(rtpServerPort, ssrc, streamId); } - @Override - public SSRCInfo openRTPServer(MediaServerItem mediaServerItem, String streamId, String ssrc, boolean ssrcCheck, boolean isPlayback) { - return openRTPServer(mediaServerItem, streamId, ssrc, ssrcCheck, isPlayback, null); - } - @Override public void closeRTPServer(MediaServerItem mediaServerItem, String streamId) { if (mediaServerItem == null) { diff --git a/src/main/java/com/genersoft/iot/vmp/service/impl/PlayServiceImpl.java b/src/main/java/com/genersoft/iot/vmp/service/impl/PlayServiceImpl.java index 6c2ee7ee6..a18d8ba10 100644 --- a/src/main/java/com/genersoft/iot/vmp/service/impl/PlayServiceImpl.java +++ b/src/main/java/com/genersoft/iot/vmp/service/impl/PlayServiceImpl.java @@ -43,6 +43,7 @@ import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Service; import org.springframework.util.ObjectUtils; +import javax.sdp.*; import javax.sip.InvalidArgumentException; import javax.sip.ResponseEvent; import javax.sip.SipException; @@ -51,6 +52,7 @@ import java.math.RoundingMode; import java.text.ParseException; import java.util.List; import java.util.UUID; +import java.util.Vector; @SuppressWarnings(value = {"rawtypes", "unchecked"}) @Service @@ -180,7 +182,7 @@ public class PlayServiceImpl implements IPlayService { if (mediaServerItem.isRtpEnable()) { streamId = String.format("%s_%s", device.getDeviceId(), channelId); } - SSRCInfo ssrcInfo = mediaServerService.openRTPServer(mediaServerItem, streamId, device.isSsrcCheck(), false); + SSRCInfo ssrcInfo = mediaServerService.openRTPServer(mediaServerItem, streamId, null, device.isSsrcCheck(), false, 0, false, device.getStreamModeForParam()); if (ssrcInfo == null) { WVPResult wvpResult = new WVPResult(); wvpResult.setCode(ErrorCode.ERROR100.getCode()); @@ -296,6 +298,29 @@ public class PlayServiceImpl implements IPlayService { String ssrcInResponse = contentString.substring(ssrcIndex + 2, ssrcIndex + 12).trim(); // 查询到ssrc不一致且开启了ssrc校验则需要针对处理 if (ssrcInfo.getSsrc().equals(ssrcInResponse)) { + if (device.getStreamMode().equalsIgnoreCase("TCP-ACTIVE")) { + String substring = contentString.substring(0, contentString.indexOf("y=")); + try { + SessionDescription sdp = SdpFactory.getInstance().createSessionDescription(substring); + int port = -1; + Vector mediaDescriptions = sdp.getMediaDescriptions(true); + for (Object description : mediaDescriptions) { + MediaDescription mediaDescription = (MediaDescription) description; + Media media = mediaDescription.getMedia(); + + Vector mediaFormats = media.getMediaFormats(false); + if (mediaFormats.contains("96")) { + port = media.getMediaPort(); + break; + } + } + logger.info("[点播-TCP主动连接对方] deviceId: {}, channelId: {}, 连接对方的地址:{}:{}, 收流模式:{}, SSRC: {}, SSRC校验:{}", device.getDeviceId(), channelId, sdp.getConnection().getAddress(), port, device.getStreamMode(), ssrcInfo.getSsrc(), device.isSsrcCheck()); + JSONObject jsonObject = zlmresTfulUtils.connectRtpServer(mediaServerItem, sdp.getConnection().getAddress(), port, ssrcInfo.getStream()); + logger.info("[点播-TCP主动连接对方] 结果: {}", jsonObject); + } catch (SdpException e) { + logger.error("[点播-TCP主动连接对方] deviceId: {}, channelId: {}, 解析200OK的SDP信息失败", device.getDeviceId(), channelId, e); + } + } return; } logger.info("[点播消息] 收到invite 200, 发现下级自定义了ssrc: {}", ssrcInResponse); @@ -331,7 +356,7 @@ public class PlayServiceImpl implements IPlayService { mediaServerService.closeRTPServer(mediaServerItem, ssrcInfo.getStream(), result->{ if (result) { // 重新开启ssrc server - mediaServerService.openRTPServer(mediaServerItem, ssrcInfo.getStream(), ssrcInResponse, device.isSsrcCheck(), false, ssrcInfo.getPort()); + mediaServerService.openRTPServer(mediaServerItem, ssrcInfo.getStream(), ssrcInResponse, device.isSsrcCheck(), false, ssrcInfo.getPort(), true, device.getStreamModeForParam()); }else { try { logger.warn("[停止点播] {}/{}", device.getDeviceId(), channelId); @@ -475,8 +500,7 @@ public class PlayServiceImpl implements IPlayService { return; } MediaServerItem newMediaServerItem = getNewMediaServerItem(device); - SSRCInfo ssrcInfo = mediaServerService.openRTPServer(newMediaServerItem, null, device.isSsrcCheck(), true); - + SSRCInfo ssrcInfo = mediaServerService.openRTPServer(newMediaServerItem, null, null, device.isSsrcCheck(), true, 0, false, device.getStreamModeForParam()); playBack(newMediaServerItem, ssrcInfo, deviceId, channelId, startTime, endTime, inviteStreamCallback, callback); } @@ -595,7 +619,7 @@ public class PlayServiceImpl implements IPlayService { mediaServerService.closeRTPServer(mediaServerItem, ssrcInfo.getStream(), result->{ if (result) { // 重新开启ssrc server - mediaServerService.openRTPServer(mediaServerItem, ssrcInfo.getStream(), ssrcInResponse, device.isSsrcCheck(), true, ssrcInfo.getPort()); + mediaServerService.openRTPServer(mediaServerItem, ssrcInfo.getStream(), ssrcInResponse, device.isSsrcCheck(), true, ssrcInfo.getPort(), true, device.getStreamModeForParam()); }else { try { logger.warn("[回放消息]停止 {}/{}", device.getDeviceId(), channelId); @@ -645,8 +669,7 @@ public class PlayServiceImpl implements IPlayService { playBackCallback.call(downloadResult); return; } - SSRCInfo ssrcInfo = mediaServerService.openRTPServer(newMediaServerItem, null, device.isSsrcCheck(), true); - + SSRCInfo ssrcInfo = mediaServerService.openRTPServer(newMediaServerItem, null, null, device.isSsrcCheck(), true, 0, false, device.getStreamModeForParam()); download(newMediaServerItem, ssrcInfo, deviceId, channelId, startTime, endTime, downloadSpeed, infoCallBack, playBackCallback); } @@ -755,7 +778,7 @@ public class PlayServiceImpl implements IPlayService { mediaServerService.closeRTPServer(mediaServerItem, ssrcInfo.getStream(), result->{ if (result) { // 重新开启ssrc server - mediaServerService.openRTPServer(mediaServerItem, ssrcInfo.getStream(), ssrcInResponse, device.isSsrcCheck(), true, ssrcInfo.getPort()); + mediaServerService.openRTPServer(mediaServerItem, ssrcInfo.getStream(), ssrcInResponse, device.isSsrcCheck(), true, ssrcInfo.getPort(), true, device.getStreamModeForParam()); }else { try { logger.warn("[录像下载] 停止{}/{}", device.getDeviceId(), channelId); diff --git a/web_src/src/components/DeviceList.vue b/web_src/src/components/DeviceList.vue index 29e049daf..bff0d1dd1 100644 --- a/web_src/src/components/DeviceList.vue +++ b/web_src/src/components/DeviceList.vue @@ -25,11 +25,13 @@ + + From 0f509049928733e49cd530d91f6463317e0c00ee Mon Sep 17 00:00:00 2001 From: 648540858 <648540858@qq.com> Date: Thu, 20 Apr 2023 10:26:42 +0800 Subject: [PATCH 22/48] =?UTF-8?q?=E6=94=AF=E6=8C=81=E8=AE=BE=E5=A4=87/?= =?UTF-8?q?=E9=80=9A=E9=81=93=E7=8A=B6=E6=80=81=E5=8F=98=E5=8C=96=E6=97=B6?= =?UTF-8?q?=E5=8F=91=E9=80=81redis=E9=80=9A=E7=9F=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../iot/vmp/common/VideoManagerConstants.java | 1 + .../com/genersoft/iot/vmp/conf/UserSetting.java | 10 ++++++++++ .../impl/NotifyRequestForCatalogProcessor.java | 17 +++++++++++++++++ .../iot/vmp/service/impl/DeviceServiceImpl.java | 10 ++++++++++ .../iot/vmp/storager/IRedisCatchStorage.java | 2 ++ .../storager/impl/RedisCatchStorageImpl.java | 14 ++++++++++++++ src/main/resources/all-application.yml | 2 ++ 7 files changed, 56 insertions(+) diff --git a/src/main/java/com/genersoft/iot/vmp/common/VideoManagerConstants.java b/src/main/java/com/genersoft/iot/vmp/common/VideoManagerConstants.java index f143fad10..ccfe77ec3 100644 --- a/src/main/java/com/genersoft/iot/vmp/common/VideoManagerConstants.java +++ b/src/main/java/com/genersoft/iot/vmp/common/VideoManagerConstants.java @@ -122,6 +122,7 @@ public class VideoManagerConstants { */ public static final String VM_MSG_SUBSCRIBE_ALARM = "alarm"; + /** * 报警通知的发送 (收到redis发出的通知,转发给其他平台) */ diff --git a/src/main/java/com/genersoft/iot/vmp/conf/UserSetting.java b/src/main/java/com/genersoft/iot/vmp/conf/UserSetting.java index 31fe7a4f1..539198f24 100644 --- a/src/main/java/com/genersoft/iot/vmp/conf/UserSetting.java +++ b/src/main/java/com/genersoft/iot/vmp/conf/UserSetting.java @@ -52,6 +52,8 @@ public class UserSetting { private Boolean refuseChannelStatusChannelFormNotify = Boolean.FALSE; + private Boolean deviceStatusNotify = Boolean.FALSE; + private String serverId = "000000"; private String recordPath = null; @@ -267,4 +269,12 @@ public class UserSetting { public void setMaxNotifyCountQueue(int maxNotifyCountQueue) { this.maxNotifyCountQueue = maxNotifyCountQueue; } + + public Boolean getDeviceStatusNotify() { + return deviceStatusNotify; + } + + public void setDeviceStatusNotify(Boolean deviceStatusNotify) { + this.deviceStatusNotify = deviceStatusNotify; + } } diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/NotifyRequestForCatalogProcessor.java b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/NotifyRequestForCatalogProcessor.java index 56fa187ee..f9f8fc24d 100644 --- a/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/NotifyRequestForCatalogProcessor.java +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/NotifyRequestForCatalogProcessor.java @@ -108,6 +108,11 @@ public class NotifyRequestForCatalogProcessor extends SIPRequestProcessorParent if (updateChannelOnlineList.size() > 300) { executeSaveForOnline(); } + if (userSetting.getDeviceStatusNotify()) { + // 发送redis消息 + redisCatchStorage.sendDeviceOrChannelStatus(device.getDeviceId(), channel.getChannelId(), true); + } + break; case CatalogEvent.OFF : // 离线 @@ -117,6 +122,10 @@ public class NotifyRequestForCatalogProcessor extends SIPRequestProcessorParent if (updateChannelOfflineList.size() > 300) { executeSaveForOffline(); } + if (userSetting.getDeviceStatusNotify()) { + // 发送redis消息 + redisCatchStorage.sendDeviceOrChannelStatus(device.getDeviceId(), channel.getChannelId(), false); + } }else { logger.info("[收到通道离线通知] 但是平台已配置拒绝此消息,来自设备: {}, 通道 {}", device.getDeviceId(), channel.getChannelId()); } @@ -129,6 +138,10 @@ public class NotifyRequestForCatalogProcessor extends SIPRequestProcessorParent if (updateChannelOfflineList.size() > 300) { executeSaveForOffline(); } + if (userSetting.getDeviceStatusNotify()) { + // 发送redis消息 + redisCatchStorage.sendDeviceOrChannelStatus(device.getDeviceId(), channel.getChannelId(), false); + } }else { logger.info("[收到通道视频丢失通知] 但是平台已配置拒绝此消息,来自设备: {}, 通道 {}", device.getDeviceId(), channel.getChannelId()); } @@ -141,6 +154,10 @@ public class NotifyRequestForCatalogProcessor extends SIPRequestProcessorParent if (updateChannelOfflineList.size() > 300) { executeSaveForOffline(); } + if (userSetting.getDeviceStatusNotify()) { + // 发送redis消息 + redisCatchStorage.sendDeviceOrChannelStatus(device.getDeviceId(), channel.getChannelId(), false); + } }else { logger.info("[收到通道视频故障通知] 但是平台已配置拒绝此消息,来自设备: {}, 通道 {}", device.getDeviceId(), channel.getChannelId()); } diff --git a/src/main/java/com/genersoft/iot/vmp/service/impl/DeviceServiceImpl.java b/src/main/java/com/genersoft/iot/vmp/service/impl/DeviceServiceImpl.java index 48de5d2bd..106caecd3 100644 --- a/src/main/java/com/genersoft/iot/vmp/service/impl/DeviceServiceImpl.java +++ b/src/main/java/com/genersoft/iot/vmp/service/impl/DeviceServiceImpl.java @@ -165,6 +165,11 @@ public class DeviceServiceImpl implements IDeviceService { String registerExpireTaskKey = VideoManagerConstants.REGISTER_EXPIRE_TASK_KEY_PREFIX + device.getDeviceId(); // 如果第一次注册那么必须在60 * 3时间内收到一个心跳,否则设备离线 dynamicTask.startDelay(registerExpireTaskKey, ()-> offline(device.getDeviceId(), "首次注册后未能收到心跳"), device.getKeepaliveIntervalTime() * 1000 * 3); + if (userSetting.getDeviceStatusNotify()) { + // 发送redis消息 + redisCatchStorage.sendDeviceOrChannelStatus(device.getDeviceId(), null, true); + } + } @Override @@ -193,6 +198,11 @@ public class DeviceServiceImpl implements IDeviceService { // 移除订阅 removeCatalogSubscribe(device); removeMobilePositionSubscribe(device); + if (userSetting.getDeviceStatusNotify()) { + // 发送redis消息 + redisCatchStorage.sendDeviceOrChannelStatus(device.getDeviceId(), null, false); + } + } @Override diff --git a/src/main/java/com/genersoft/iot/vmp/storager/IRedisCatchStorage.java b/src/main/java/com/genersoft/iot/vmp/storager/IRedisCatchStorage.java index 1e10469fb..42708f7f9 100644 --- a/src/main/java/com/genersoft/iot/vmp/storager/IRedisCatchStorage.java +++ b/src/main/java/com/genersoft/iot/vmp/storager/IRedisCatchStorage.java @@ -261,4 +261,6 @@ public interface IRedisCatchStorage { List getAllDevices(); void removeAllDevice(); + + void sendDeviceOrChannelStatus(String deviceId, String channelId, boolean online); } diff --git a/src/main/java/com/genersoft/iot/vmp/storager/impl/RedisCatchStorageImpl.java b/src/main/java/com/genersoft/iot/vmp/storager/impl/RedisCatchStorageImpl.java index 67f2e7ef5..facd54ca8 100644 --- a/src/main/java/com/genersoft/iot/vmp/storager/impl/RedisCatchStorageImpl.java +++ b/src/main/java/com/genersoft/iot/vmp/storager/impl/RedisCatchStorageImpl.java @@ -902,4 +902,18 @@ public class RedisCatchStorageImpl implements IRedisCatchStorage { + userSetting.getServerId() + "_*_" + id + "_*"; return RedisUtil.scan(redisTemplate, key).size(); } + + @Override + public void sendDeviceOrChannelStatus(String deviceId, String channelId, boolean online) { + String key = VideoManagerConstants.VM_MSG_SUBSCRIBE_DEVICE_STATUS; + logger.info("[redis通知] 推送设备/通道状态, {}/{}-{}", deviceId, channelId, online); + StringBuilder msg = new StringBuilder(); + msg.append(deviceId); + if (channelId != null) { + msg.append(":").append(channelId); + } + msg.append(" ").append(online? "ON":"OFF"); + + redisTemplate.convertAndSend(key, msg.toString()); + } } diff --git a/src/main/resources/all-application.yml b/src/main/resources/all-application.yml index bae990453..7f8b27823 100644 --- a/src/main/resources/all-application.yml +++ b/src/main/resources/all-application.yml @@ -180,6 +180,8 @@ user-settings: refuse-channel-status-channel-form-notify: false # 设置notify缓存队列最大长度,超过此长度的数据将返回486 BUSY_HERE,消息丢弃, 默认10000 max-notify-count-queue: 10000 + # 设备/通道状态变化时发送消息 + device-status-notify: false # 跨域配置,配置你访问前端页面的地址即可, 可以配置多个 allowed-origins: - http://localhost:8008 From 25b400cbe3f602b85239485d0e21f7ca6ce8fe50 Mon Sep 17 00:00:00 2001 From: 648540858 <648540858@qq.com> Date: Thu, 20 Apr 2023 14:38:22 +0800 Subject: [PATCH 23/48] =?UTF-8?q?=E6=9B=B4=E6=96=B0readme?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index d3ad3ac47..639596cfb 100644 --- a/README.md +++ b/README.md @@ -119,6 +119,9 @@ QQ群不再接受新成员直接进入,希望大家多多参考文档,用户 # 技术支持 建议加入[知识星球](https://t.zsxq.com/0drbw002x)可以获取更多的教程以及更加及时的回复。 +目前已经更新的内容: +- [使用入门系列一:WVP-PRO能做什么](https://t.zsxq.com/0dLguVoSp) + 如果项目需要一对一的技术支持,或者棘手的问题需要解决,请发送邮件到648540858@qq.com # 致谢 From d6262acf6ab2a5083e62ea98299b378f61cd5421 Mon Sep 17 00:00:00 2001 From: 648540858 <648540858@qq.com> Date: Thu, 20 Apr 2023 16:42:44 +0800 Subject: [PATCH 24/48] =?UTF-8?q?=E6=9B=B4=E6=96=B0readme?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 639596cfb..e231d51c3 100644 --- a/README.md +++ b/README.md @@ -107,18 +107,18 @@ https://gitee.com/pan648540858/wvp-GB28181-pro.git 1. 查看文档网站,仔细的阅读可以帮你避免几乎所有的问题 2. 搜索issues,这里有大部分的答案 3. 你可以把遇到问题的设备寄给我,可以更容易的兼容设备和解决问题。 -4. 欢迎加入[知识星球](https://t.zsxq.com/0drbw002x)支持本项目,同时可以得到更加快速的解答。 +4. 欢迎加入[知识星球](https://t.zsxq.com/0d8VAD3Dm)支持本项目,同时可以得到更加快速的解答。 # 使用帮助 ZLM使用文档[https://github.com/ZLMediaKit/ZLMediaKit](https://github.com/ZLMediaKit/ZLMediaKit) wvp官方文档[doc.wvp-pro.cn](https://doc.wvp-pro.cn) -QQ群不再接受新成员直接进入,希望大家多多参考文档,用户可加入[知识星球](https://t.zsxq.com/0drbw002x)提问以支持本项目,欢迎star和提交pr。 +QQ群不再接受新成员直接进入,希望大家多多参考文档,用户可加入[知识星球](https://t.zsxq.com/0d8VAD3Dm)提问以支持本项目,欢迎star和提交pr。 # 授权协议 本项目自有代码使用宽松的MIT协议,在保留版权信息的情况下可以自由应用于各自商用、非商业的项目。 但是本项目也零碎的使用了一些其他的开源代码,在商用的情况下请自行替代或剔除; 由于使用本项目而产生的商业纠纷或侵权行为一概与本项目及开发者无关,请自行承担法律风险。 在使用本项目代码时,也应该在授权协议中同时表明本项目依赖的第三方库的协议 # 技术支持 -建议加入[知识星球](https://t.zsxq.com/0drbw002x)可以获取更多的教程以及更加及时的回复。 +建议加入[知识星球](https://t.zsxq.com/0d8VAD3Dm)可以获取更多的教程以及更加及时的回复。 目前已经更新的内容: - [使用入门系列一:WVP-PRO能做什么](https://t.zsxq.com/0dLguVoSp) From 5c3c3e6a4c144f77eb832fa9f736967d0bffa220 Mon Sep 17 00:00:00 2001 From: 648540858 <648540858@qq.com> Date: Fri, 21 Apr 2023 14:18:52 +0800 Subject: [PATCH 25/48] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E6=B6=88=E6=81=AF?= =?UTF-8?q?=E5=8F=91=E9=80=81=E5=A4=B1=E8=B4=A5=E5=AF=BC=E8=87=B4=E5=90=8E?= =?UTF-8?q?=E7=BB=AD=E6=AC=A1=E9=80=9A=E9=81=93=E6=97=A0=E6=B3=95=E7=82=B9?= =?UTF-8?q?=E6=92=AD=E7=9A=84bug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/genersoft/iot/vmp/gb28181/event/SipSubscribe.java | 7 ++++++- .../com/genersoft/iot/vmp/gb28181/transmit/SIPSender.java | 6 +----- .../java/com/genersoft/iot/vmp/gb28181/utils/SipUtils.java | 4 ++-- .../genersoft/iot/vmp/service/impl/PlayServiceImpl.java | 4 +++- 4 files changed, 12 insertions(+), 9 deletions(-) diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/event/SipSubscribe.java b/src/main/java/com/genersoft/iot/vmp/gb28181/event/SipSubscribe.java index efa4d4246..75751ad2e 100644 --- a/src/main/java/com/genersoft/iot/vmp/gb28181/event/SipSubscribe.java +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/event/SipSubscribe.java @@ -76,7 +76,9 @@ public class SipSubscribe { // 会话已结束 dialogTerminated, // 设备未找到 - deviceNotFoundEvent + deviceNotFoundEvent, + // 设备未找到 + cmdSendFailEvent } public static class EventResult{ @@ -86,6 +88,9 @@ public class SipSubscribe { public String callId; public EventObject event; + public EventResult() { + } + public EventResult(EventObject event) { this.event = event; if (event instanceof ResponseEvent) { diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/SIPSender.java b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/SIPSender.java index 89e6e32f2..e8066b755 100644 --- a/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/SIPSender.java +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/SIPSender.java @@ -46,8 +46,7 @@ public class SIPSender { transmitRequest(ip, message, errorEvent, null); } - public void transmitRequest(String ip, Message message, SipSubscribe.Event errorEvent, SipSubscribe.Event okEvent) throws SipException, ParseException { - try { + public void transmitRequest(String ip, Message message, SipSubscribe.Event errorEvent, SipSubscribe.Event okEvent) throws SipException { ViaHeader viaHeader = (ViaHeader)message.getHeader(ViaHeader.NAME); String transport = "UDP"; if (viaHeader == null) { @@ -104,9 +103,6 @@ public class SIPSender { sipProvider.sendResponse((Response)message); } } - } finally { -// logger.info("[SEND]:SUCCESS:{}", message); - } } public CallIdHeader getNewCallIdHeader(String ip, String transport){ diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/utils/SipUtils.java b/src/main/java/com/genersoft/iot/vmp/gb28181/utils/SipUtils.java index f98315001..1f4632ea4 100644 --- a/src/main/java/com/genersoft/iot/vmp/gb28181/utils/SipUtils.java +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/utils/SipUtils.java @@ -142,8 +142,8 @@ public class SipUtils { remotePort = request.getTopmostViaHeader().getRPort(); // 解析本地地址替代 if (ObjectUtils.isEmpty(remoteAddress) || remotePort == -1) { - remoteAddress = request.getTopmostViaHeader().getHost(); - remotePort = request.getTopmostViaHeader().getPort(); + remoteAddress = request.getRemoteAddress().getHostAddress(); + remotePort = request.getRemotePort(); } } diff --git a/src/main/java/com/genersoft/iot/vmp/service/impl/PlayServiceImpl.java b/src/main/java/com/genersoft/iot/vmp/service/impl/PlayServiceImpl.java index a18d8ba10..aead66128 100644 --- a/src/main/java/com/genersoft/iot/vmp/service/impl/PlayServiceImpl.java +++ b/src/main/java/com/genersoft/iot/vmp/service/impl/PlayServiceImpl.java @@ -398,7 +398,9 @@ public class PlayServiceImpl implements IPlayService { mediaServerService.releaseSsrc(mediaServerItem.getId(), ssrcInfo.getSsrc()); streamSession.remove(device.getDeviceId(), channelId, ssrcInfo.getStream()); - SipSubscribe.EventResult eventResult = new SipSubscribe.EventResult(new CmdSendFailEvent(null)); + SipSubscribe.EventResult eventResult = new SipSubscribe.EventResult(); + eventResult.type = SipSubscribe.EventResultType.cmdSendFailEvent; + eventResult.statusCode = -1; eventResult.msg = "命令发送失败"; errorEvent.response(eventResult); } From 22deb206ba0c07949523d2c355b23a05a2d51007 Mon Sep 17 00:00:00 2001 From: 648540858 <648540858@qq.com> Date: Fri, 21 Apr 2023 14:42:23 +0800 Subject: [PATCH 26/48] =?UTF-8?q?=E8=A1=A5=E5=85=85=E4=B8=8A=E4=B8=80?= =?UTF-8?q?=E4=B8=AA=E4=BF=AE=E5=A4=8D=E7=9A=84=E5=86=85=E5=AE=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../vmp/gb28181/bean/CmdSendFailEvent.java | 27 ------------------- .../iot/vmp/service/impl/PlayServiceImpl.java | 8 ++++-- 2 files changed, 6 insertions(+), 29 deletions(-) delete mode 100644 src/main/java/com/genersoft/iot/vmp/gb28181/bean/CmdSendFailEvent.java diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/bean/CmdSendFailEvent.java b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/CmdSendFailEvent.java deleted file mode 100644 index 0cd4086e3..000000000 --- a/src/main/java/com/genersoft/iot/vmp/gb28181/bean/CmdSendFailEvent.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.genersoft.iot.vmp.gb28181.bean; - -import javax.sip.Dialog; -import java.util.EventObject; - -public class CmdSendFailEvent extends EventObject { - - private String callId; - - /** - * Constructs a prototypical Event. - * - * @param dialog - * @throws IllegalArgumentException if source is null. - */ - public CmdSendFailEvent(Dialog dialog) { - super(dialog); - } - - public String getCallId() { - return callId; - } - - public void setCallId(String callId) { - this.callId = callId; - } -} diff --git a/src/main/java/com/genersoft/iot/vmp/service/impl/PlayServiceImpl.java b/src/main/java/com/genersoft/iot/vmp/service/impl/PlayServiceImpl.java index aead66128..51574392b 100644 --- a/src/main/java/com/genersoft/iot/vmp/service/impl/PlayServiceImpl.java +++ b/src/main/java/com/genersoft/iot/vmp/service/impl/PlayServiceImpl.java @@ -649,7 +649,9 @@ public class PlayServiceImpl implements IPlayService { } catch (InvalidArgumentException | SipException | ParseException e) { logger.error("[命令发送失败] 回放: {}", e.getMessage()); - SipSubscribe.EventResult eventResult = new SipSubscribe.EventResult(new CmdSendFailEvent(null)); + SipSubscribe.EventResult eventResult = new SipSubscribe.EventResult(); + eventResult.type = SipSubscribe.EventResultType.cmdSendFailEvent; + eventResult.statusCode = -1; eventResult.msg = "命令发送失败"; errorEvent.response(eventResult); } @@ -807,7 +809,9 @@ public class PlayServiceImpl implements IPlayService { } catch (InvalidArgumentException | SipException | ParseException e) { logger.error("[命令发送失败] 录像下载: {}", e.getMessage()); - SipSubscribe.EventResult eventResult = new SipSubscribe.EventResult(new CmdSendFailEvent(null)); + SipSubscribe.EventResult eventResult = new SipSubscribe.EventResult(); + eventResult.type = SipSubscribe.EventResultType.cmdSendFailEvent; + eventResult.statusCode = -1; eventResult.msg = "命令发送失败"; errorEvent.response(eventResult); } From 269ad8cedbb07ca207a6f33af23085894dab4aa6 Mon Sep 17 00:00:00 2001 From: 648540858 <648540858@qq.com> Date: Sun, 23 Apr 2023 14:36:13 +0800 Subject: [PATCH 27/48] =?UTF-8?q?=E4=BF=AE=E8=BA=AB=E7=9B=AE=E5=BD=95?= =?UTF-8?q?=E5=88=B7=E6=96=B0=EF=BC=8C=E4=BC=98=E5=8C=96=E5=85=AC=E7=BD=91?= =?UTF-8?q?=E4=B8=8B=E8=BF=9C=E7=A8=8BIP=E7=AB=AF=E5=8F=A3=E7=9A=84?= =?UTF-8?q?=E8=8E=B7=E5=8F=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../vmp/gb28181/conf/ServerLoggerImpl.java | 8 ++- .../impl/RegisterRequestProcessor.java | 2 +- .../cmd/KeepaliveNotifyMessageHandler.java | 1 + .../iot/vmp/gb28181/utils/SipUtils.java | 9 ++-- .../vmp/storager/dao/DeviceChannelMapper.java | 54 +++++++++++++++++++ .../impl/VideoManagerStorageImpl.java | 8 +-- 6 files changed, 68 insertions(+), 14 deletions(-) diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/conf/ServerLoggerImpl.java b/src/main/java/com/genersoft/iot/vmp/gb28181/conf/ServerLoggerImpl.java index 3fc1d374e..19e1906c3 100644 --- a/src/main/java/com/genersoft/iot/vmp/gb28181/conf/ServerLoggerImpl.java +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/conf/ServerLoggerImpl.java @@ -27,7 +27,7 @@ public class ServerLoggerImpl implements ServerLogger { return; } StringBuilder stringBuilder = new StringBuilder(); - stringBuilder.append(!sender? "发送:目标--->" + from:"接收:来自--->" + to) + stringBuilder.append(sender? "发送:目标--->" + from:"接收:来自--->" + to) .append("\r\n") .append(message); this.stackLogger.logInfo(stringBuilder.toString()); @@ -40,7 +40,7 @@ public class ServerLoggerImpl implements ServerLogger { return; } StringBuilder stringBuilder = new StringBuilder(); - stringBuilder.append(!sender? "发送: 目标->" + from :"接收:来自->" + to) + stringBuilder.append(sender? "发送: 目标->" + from :"接收:来自->" + to) .append("\r\n") .append(message); this.stackLogger.logInfo(stringBuilder.toString()); @@ -52,7 +52,7 @@ public class ServerLoggerImpl implements ServerLogger { return; } StringBuilder stringBuilder = new StringBuilder(); - stringBuilder.append(!sender? "发送: 目标->" + from :"接收:来自->" + to) + stringBuilder.append(sender? "发送: 目标->" + from :"接收:来自->" + to) .append("\r\n") .append(message); this.stackLogger.logInfo(stringBuilder.toString()); @@ -87,6 +87,4 @@ public class ServerLoggerImpl implements ServerLogger { this.stackLogger = this.sipStack.getStackLogger(); } } - - } diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/RegisterRequestProcessor.java b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/RegisterRequestProcessor.java index 9f69950f1..81e7fa651 100644 --- a/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/RegisterRequestProcessor.java +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/RegisterRequestProcessor.java @@ -96,7 +96,7 @@ public class RegisterRequestProcessor extends SIPRequestProcessorParent implemen RemoteAddressInfo remoteAddressInfo = SipUtils.getRemoteAddressFromRequest(request, userSetting.getSipUseSourceIpAsRemoteAddress()); - + logger.info("[注册请求] 设备:{}, 远程地址为: {}:{}", deviceId, remoteAddressInfo.getIp(), remoteAddressInfo.getPort()); if (device != null && device.getSipTransactionInfo() != null && request.getCallIdHeader().getCallId().equals(device.getSipTransactionInfo().getCallId())) { diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/notify/cmd/KeepaliveNotifyMessageHandler.java b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/notify/cmd/KeepaliveNotifyMessageHandler.java index 865b6623c..4d1a58e2b 100644 --- a/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/notify/cmd/KeepaliveNotifyMessageHandler.java +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/notify/cmd/KeepaliveNotifyMessageHandler.java @@ -69,6 +69,7 @@ public class KeepaliveNotifyMessageHandler extends SIPRequestProcessorParent imp RemoteAddressInfo remoteAddressInfo = SipUtils.getRemoteAddressFromRequest(request, userSetting.getSipUseSourceIpAsRemoteAddress()); if (!device.getIp().equalsIgnoreCase(remoteAddressInfo.getIp()) || device.getPort() != remoteAddressInfo.getPort()) { + logger.info("[心跳] 设备{}地址变化, 远程地址为: {}:{}", device.getDeviceId(), remoteAddressInfo.getIp(), remoteAddressInfo.getPort()); device.setPort(remoteAddressInfo.getPort()); device.setHostAddress(remoteAddressInfo.getIp().concat(":").concat(String.valueOf(remoteAddressInfo.getPort()))); device.setIp(remoteAddressInfo.getIp()); diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/utils/SipUtils.java b/src/main/java/com/genersoft/iot/vmp/gb28181/utils/SipUtils.java index 1f4632ea4..d6037a113 100644 --- a/src/main/java/com/genersoft/iot/vmp/gb28181/utils/SipUtils.java +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/utils/SipUtils.java @@ -133,8 +133,9 @@ public class SipUtils { String remoteAddress; int remotePort; if (sipUseSourceIpAsRemoteAddress) { - remoteAddress = request.getRemoteAddress().getHostAddress(); - remotePort = request.getRemotePort(); + remoteAddress = request.getPeerPacketSourceAddress().getHostAddress(); + remotePort = request.getPeerPacketSourcePort(); + }else { // 判断RPort是否改变,改变则说明路由nat信息变化,修改设备信息 // 获取到通信地址等信息 @@ -142,8 +143,8 @@ public class SipUtils { remotePort = request.getTopmostViaHeader().getRPort(); // 解析本地地址替代 if (ObjectUtils.isEmpty(remoteAddress) || remotePort == -1) { - remoteAddress = request.getRemoteAddress().getHostAddress(); - remotePort = request.getRemotePort(); + remoteAddress = request.getPeerPacketSourceAddress().getHostAddress(); + remotePort = request.getPeerPacketSourcePort(); } } diff --git a/src/main/java/com/genersoft/iot/vmp/storager/dao/DeviceChannelMapper.java b/src/main/java/com/genersoft/iot/vmp/storager/dao/DeviceChannelMapper.java index 3f4d804c4..f81d1f87c 100644 --- a/src/main/java/com/genersoft/iot/vmp/storager/dao/DeviceChannelMapper.java +++ b/src/main/java/com/genersoft/iot/vmp/storager/dao/DeviceChannelMapper.java @@ -271,6 +271,60 @@ public interface DeviceChannelMapper { "") int batchAdd(List addChannels); + + @Insert("") + int batchAddOrUpdate(List addChannels); + @Update(value = {"UPDATE device_channel SET status=1 WHERE deviceId=#{deviceId} AND channelId=#{channelId}"}) void online(String deviceId, String channelId); diff --git a/src/main/java/com/genersoft/iot/vmp/storager/impl/VideoManagerStorageImpl.java b/src/main/java/com/genersoft/iot/vmp/storager/impl/VideoManagerStorageImpl.java index 206456d93..cee613dbe 100644 --- a/src/main/java/com/genersoft/iot/vmp/storager/impl/VideoManagerStorageImpl.java +++ b/src/main/java/com/genersoft/iot/vmp/storager/impl/VideoManagerStorageImpl.java @@ -184,10 +184,10 @@ public class VideoManagerStorageImpl implements IVideoManagerStorage { if (i + limitCount > channels.size()) { toIndex = channels.size(); } - result = result || deviceChannelMapper.batchAdd(channels.subList(i, toIndex)) < 0; + result = result || deviceChannelMapper.batchAddOrUpdate(channels.subList(i, toIndex)) < 0; } }else { - result = result || deviceChannelMapper.batchAdd(channels) < 0; + result = result || deviceChannelMapper.batchAddOrUpdate(channels) < 0; } } if (result) { @@ -285,10 +285,10 @@ public class VideoManagerStorageImpl implements IVideoManagerStorage { if (i + limitCount > addChannels.size()) { toIndex = addChannels.size(); } - result = result || deviceChannelMapper.batchAdd(addChannels.subList(i, toIndex)) < 0; + result = result || deviceChannelMapper.batchAddOrUpdate(addChannels.subList(i, toIndex)) < 0; } }else { - result = result || deviceChannelMapper.batchAdd(addChannels) < 0; + result = result || deviceChannelMapper.batchAddOrUpdate(addChannels) < 0; } } if (updateChannels.size() > 0) { From b944f8867c78dbe6dd4704115b48beb9f6dc12d9 Mon Sep 17 00:00:00 2001 From: 648540858 <648540858@qq.com> Date: Sun, 23 Apr 2023 15:54:34 +0800 Subject: [PATCH 28/48] =?UTF-8?q?=E6=94=AF=E6=8C=81=E6=8E=A8=E6=B5=81?= =?UTF-8?q?=E5=92=8C=E6=8B=89=E6=B5=81=E4=BB=A3=E7=90=86=E9=80=9A=E9=81=93?= =?UTF-8?q?=E7=8A=B6=E6=80=81=E5=8F=98=E5=8C=96=E5=8F=91=E9=80=81=E9=80=9A?= =?UTF-8?q?=E7=9F=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../subscribe/catalog/CatalogEventLister.java | 17 ++- .../NotifyRequestForCatalogProcessor.java | 12 +- .../vmp/media/zlm/ZLMHttpHookListener.java | 120 ++++++++++-------- .../vmp/media/zlm/dto/hook/HookResult.java | 4 + .../storager/impl/RedisCatchStorageImpl.java | 8 +- 5 files changed, 93 insertions(+), 68 deletions(-) diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/event/subscribe/catalog/CatalogEventLister.java b/src/main/java/com/genersoft/iot/vmp/gb28181/event/subscribe/catalog/CatalogEventLister.java index be73ebd98..89ecb1867 100644 --- a/src/main/java/com/genersoft/iot/vmp/gb28181/event/subscribe/catalog/CatalogEventLister.java +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/event/subscribe/catalog/CatalogEventLister.java @@ -1,14 +1,9 @@ package com.genersoft.iot.vmp.gb28181.event.subscribe.catalog; -import com.genersoft.iot.vmp.common.VideoManagerConstants; -import com.genersoft.iot.vmp.conf.SipConfig; import com.genersoft.iot.vmp.conf.UserSetting; import com.genersoft.iot.vmp.gb28181.bean.*; import com.genersoft.iot.vmp.gb28181.transmit.cmd.impl.SIPCommanderFroPlatform; -import com.genersoft.iot.vmp.media.zlm.ZLMRTPServerFactory; import com.genersoft.iot.vmp.service.IGbStreamService; -import com.genersoft.iot.vmp.service.IMediaServerService; -import com.genersoft.iot.vmp.storager.IRedisCatchStorage; import com.genersoft.iot.vmp.storager.IVideoManagerStorage; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -16,12 +11,14 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationListener; import org.springframework.stereotype.Component; import org.springframework.util.ObjectUtils; -import org.springframework.util.StringUtils; import javax.sip.InvalidArgumentException; import javax.sip.SipException; import java.text.ParseException; -import java.util.*; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; /** * catalog事件 @@ -43,6 +40,9 @@ public class CatalogEventLister implements ApplicationListener { @Autowired private SubscribeHolder subscribeHolder; + @Autowired + private UserSetting userSetting; + @Override public void onApplicationEvent(CatalogEvent event) { SubscribeInfo subscribe = null; @@ -93,6 +93,9 @@ public class CatalogEventLister implements ApplicationListener { } if (event.getGbStreams() != null && event.getGbStreams().size() > 0){ for (GbStream gbStream : event.getGbStreams()) { + if (gbStream.getStreamType().equals("push") && !userSetting.isUsePushingAsStatus()) { + continue; + } DeviceChannel deviceChannelByStream = gbStreamService.getDeviceChannelListByStream(gbStream, gbStream.getCatalogId(), parentPlatform); deviceChannelList.add(deviceChannelByStream); } diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/NotifyRequestForCatalogProcessor.java b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/NotifyRequestForCatalogProcessor.java index f9f8fc24d..b9a41a571 100644 --- a/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/NotifyRequestForCatalogProcessor.java +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/NotifyRequestForCatalogProcessor.java @@ -118,6 +118,8 @@ public class NotifyRequestForCatalogProcessor extends SIPRequestProcessorParent // 离线 logger.info("[收到通道离线通知] 来自设备: {}, 通道 {}", device.getDeviceId(), channel.getChannelId()); if (userSetting.getRefuseChannelStatusChannelFormNotify()) { + logger.info("[收到通道离线通知] 但是平台已配置拒绝此消息,来自设备: {}, 通道 {}", device.getDeviceId(), channel.getChannelId()); + }else { updateChannelOfflineList.add(channel); if (updateChannelOfflineList.size() > 300) { executeSaveForOffline(); @@ -126,14 +128,14 @@ public class NotifyRequestForCatalogProcessor extends SIPRequestProcessorParent // 发送redis消息 redisCatchStorage.sendDeviceOrChannelStatus(device.getDeviceId(), channel.getChannelId(), false); } - }else { - logger.info("[收到通道离线通知] 但是平台已配置拒绝此消息,来自设备: {}, 通道 {}", device.getDeviceId(), channel.getChannelId()); } break; case CatalogEvent.VLOST: // 视频丢失 logger.info("[收到通道视频丢失通知] 来自设备: {}, 通道 {}", device.getDeviceId(), channel.getChannelId()); if (userSetting.getRefuseChannelStatusChannelFormNotify()) { + logger.info("[收到通道视频丢失通知] 但是平台已配置拒绝此消息,来自设备: {}, 通道 {}", device.getDeviceId(), channel.getChannelId()); + }else { updateChannelOfflineList.add(channel); if (updateChannelOfflineList.size() > 300) { executeSaveForOffline(); @@ -142,14 +144,14 @@ public class NotifyRequestForCatalogProcessor extends SIPRequestProcessorParent // 发送redis消息 redisCatchStorage.sendDeviceOrChannelStatus(device.getDeviceId(), channel.getChannelId(), false); } - }else { - logger.info("[收到通道视频丢失通知] 但是平台已配置拒绝此消息,来自设备: {}, 通道 {}", device.getDeviceId(), channel.getChannelId()); } break; case CatalogEvent.DEFECT: // 故障 logger.info("[收到通道视频故障通知] 来自设备: {}, 通道 {}", device.getDeviceId(), channel.getChannelId()); if (userSetting.getRefuseChannelStatusChannelFormNotify()) { + logger.info("[收到通道视频故障通知] 但是平台已配置拒绝此消息,来自设备: {}, 通道 {}", device.getDeviceId(), channel.getChannelId()); + }else { updateChannelOfflineList.add(channel); if (updateChannelOfflineList.size() > 300) { executeSaveForOffline(); @@ -158,8 +160,6 @@ public class NotifyRequestForCatalogProcessor extends SIPRequestProcessorParent // 发送redis消息 redisCatchStorage.sendDeviceOrChannelStatus(device.getDeviceId(), channel.getChannelId(), false); } - }else { - logger.info("[收到通道视频故障通知] 但是平台已配置拒绝此消息,来自设备: {}, 通道 {}", device.getDeviceId(), channel.getChannelId()); } break; case CatalogEvent.ADD: diff --git a/src/main/java/com/genersoft/iot/vmp/media/zlm/ZLMHttpHookListener.java b/src/main/java/com/genersoft/iot/vmp/media/zlm/ZLMHttpHookListener.java index 523b10fac..405fdd00f 100644 --- a/src/main/java/com/genersoft/iot/vmp/media/zlm/ZLMHttpHookListener.java +++ b/src/main/java/com/genersoft/iot/vmp/media/zlm/ZLMHttpHookListener.java @@ -7,6 +7,7 @@ import com.genersoft.iot.vmp.conf.UserSetting; import com.genersoft.iot.vmp.conf.exception.SsrcTransactionNotFoundException; import com.genersoft.iot.vmp.gb28181.bean.*; import com.genersoft.iot.vmp.gb28181.event.EventPublisher; +import com.genersoft.iot.vmp.gb28181.event.subscribe.catalog.CatalogEvent; import com.genersoft.iot.vmp.gb28181.session.VideoStreamSessionManager; import com.genersoft.iot.vmp.gb28181.transmit.callback.DeferredResultHolder; import com.genersoft.iot.vmp.gb28181.transmit.callback.RequestMessage; @@ -292,22 +293,24 @@ public class ZLMHttpHookListener { JSONObject json = (JSONObject) JSON.toJSON(param); taskExecutor.execute(() -> { ZlmHttpHookSubscribe.Event subscribe = this.subscribe.sendNotify(HookType.on_stream_changed, json); + MediaServerItem mediaInfo = mediaServerService.getOne(param.getMediaServerId()); + if (mediaInfo == null) { + logger.info("[ZLM HOOK] 流变化未找到ZLM, {}", param.getMediaServerId()); + return; + } if (subscribe != null) { - MediaServerItem mediaInfo = mediaServerService.getOne(param.getMediaServerId()); - if (mediaInfo != null) { - subscribe.response(mediaInfo, json); - } + subscribe.response(mediaInfo, json); } List tracks = param.getTracks(); // TODO 重构此处逻辑 - + boolean isPush = false; if (param.isRegist()) { // 处理流注册的鉴权信息 if (param.getOriginType() == OriginType.RTMP_PUSH.ordinal() || param.getOriginType() == OriginType.RTSP_PUSH.ordinal() || param.getOriginType() == OriginType.RTC_PUSH.ordinal()) { - + isPush = true; StreamAuthorityInfo streamAuthorityInfo = redisCatchStorage.getStreamAuthorityInfo(param.getApp(), param.getStream()); if (streamAuthorityInfo == null) { streamAuthorityInfo = StreamAuthorityInfo.getInstanceByHook(param); @@ -329,7 +332,10 @@ public class ZLMHttpHookListener { mediaServerService.removeCount(param.getMediaServerId()); } // 设置拉流代理上线/离线 - streamProxyService.updateStatus(param.isRegist(), param.getApp(), param.getStream()); + int updateStatusResult = streamProxyService.updateStatus(param.isRegist(), param.getApp(), param.getStream()); + if (updateStatusResult > 0) { + + } if ("rtp".equals(param.getApp()) && !param.isRegist()) { StreamInfo streamInfo = redisCatchStorage.queryPlayByStreamId(param.getStream()); @@ -337,7 +343,8 @@ public class ZLMHttpHookListener { redisCatchStorage.stopPlay(streamInfo); storager.stopPlay(streamInfo.getDeviceID(), streamInfo.getChannelId()); } else { - streamInfo = redisCatchStorage.queryPlayback(null, null, param.getStream(), null); + streamInfo = redisCatchStorage.queryPlayback(null, null, + param.getStream(), null); if (streamInfo != null) { redisCatchStorage.stopPlayback(streamInfo.getDeviceID(), streamInfo.getChannelId(), streamInfo.getStream(), null); @@ -346,48 +353,50 @@ public class ZLMHttpHookListener { } else { if (!"rtp".equals(param.getApp())) { String type = OriginType.values()[param.getOriginType()].getType(); - MediaServerItem mediaServerItem = mediaServerService.getOne(param.getMediaServerId()); - - if (mediaServerItem != null) { - if (param.isRegist()) { - StreamAuthorityInfo streamAuthorityInfo = redisCatchStorage.getStreamAuthorityInfo(param.getApp(), param.getStream()); - String callId = null; - if (streamAuthorityInfo != null) { - callId = streamAuthorityInfo.getCallId(); - } - StreamInfo streamInfoByAppAndStream = mediaService.getStreamInfoByAppAndStream(mediaServerItem, - param.getApp(), param.getStream(), tracks, callId); - param.setStreamInfo(new StreamContent(streamInfoByAppAndStream)); - redisCatchStorage.addStream(mediaServerItem, type, param.getApp(), param.getStream(), param); - if (param.getOriginType() == OriginType.RTSP_PUSH.ordinal() - || param.getOriginType() == OriginType.RTMP_PUSH.ordinal() - || param.getOriginType() == OriginType.RTC_PUSH.ordinal()) { - param.setSeverId(userSetting.getServerId()); - zlmMediaListManager.addPush(param); - } - } else { - // 兼容流注销时类型从redis记录获取 - OnStreamChangedHookParam onStreamChangedHookParam = redisCatchStorage.getStreamInfo(param.getApp(), param.getStream(), param.getMediaServerId()); - if (onStreamChangedHookParam != null) { - type = OriginType.values()[onStreamChangedHookParam.getOriginType()].getType(); - redisCatchStorage.removeStream(mediaServerItem.getId(), type, param.getApp(), param.getStream()); - } - GbStream gbStream = storager.getGbStream(param.getApp(), param.getStream()); - if (gbStream != null) { + if (param.isRegist()) { + StreamAuthorityInfo streamAuthorityInfo = redisCatchStorage.getStreamAuthorityInfo( + param.getApp(), param.getStream()); + String callId = null; + if (streamAuthorityInfo != null) { + callId = streamAuthorityInfo.getCallId(); + } + StreamInfo streamInfoByAppAndStream = mediaService.getStreamInfoByAppAndStream(mediaInfo, + param.getApp(), param.getStream(), tracks, callId); + param.setStreamInfo(new StreamContent(streamInfoByAppAndStream)); + redisCatchStorage.addStream(mediaInfo, type, param.getApp(), param.getStream(), param); + if (param.getOriginType() == OriginType.RTSP_PUSH.ordinal() + || param.getOriginType() == OriginType.RTMP_PUSH.ordinal() + || param.getOriginType() == OriginType.RTC_PUSH.ordinal()) { + param.setSeverId(userSetting.getServerId()); + zlmMediaListManager.addPush(param); + } + } else { + // 兼容流注销时类型从redis记录获取 + OnStreamChangedHookParam onStreamChangedHookParam = redisCatchStorage.getStreamInfo( + param.getApp(), param.getStream(), param.getMediaServerId()); + if (onStreamChangedHookParam != null) { + type = OriginType.values()[onStreamChangedHookParam.getOriginType()].getType(); + redisCatchStorage.removeStream(mediaInfo.getId(), type, param.getApp(), param.getStream()); + } + GbStream gbStream = storager.getGbStream(param.getApp(), param.getStream()); + if (gbStream != null) { // eventPublisher.catalogEventPublishForStream(null, gbStream, CatalogEvent.OFF); - } - zlmMediaListManager.removeMedia(param.getApp(), param.getStream()); - } - if (type != null) { - // 发送流变化redis消息 - JSONObject jsonObject = new JSONObject(); - jsonObject.put("serverId", userSetting.getServerId()); - jsonObject.put("app", param.getApp()); - jsonObject.put("stream", param.getStream()); - jsonObject.put("register", param.isRegist()); - jsonObject.put("mediaServerId", param.getMediaServerId()); - redisCatchStorage.sendStreamChangeMsg(type, jsonObject); } + zlmMediaListManager.removeMedia(param.getApp(), param.getStream()); + } + GbStream gbStream = storager.getGbStream(param.getApp(), param.getStream()); + if (gbStream != null) { + eventPublisher.catalogEventPublishForStream(null, gbStream, param.isRegist()?CatalogEvent.ON:CatalogEvent.OFF); + } + if (type != null) { + // 发送流变化redis消息 + JSONObject jsonObject = new JSONObject(); + jsonObject.put("serverId", userSetting.getServerId()); + jsonObject.put("app", param.getApp()); + jsonObject.put("stream", param.getStream()); + jsonObject.put("register", param.isRegist()); + jsonObject.put("mediaServerId", param.getMediaServerId()); + redisCatchStorage.sendStreamChangeMsg(type, jsonObject); } } } @@ -403,7 +412,8 @@ public class ZLMHttpHookListener { try { if (platform != null) { commanderFroPlatform.streamByeCmd(platform, sendRtpItem); - redisCatchStorage.deleteSendRTPServer(platformId, sendRtpItem.getChannelId(), sendRtpItem.getCallId(), sendRtpItem.getStreamId()); + redisCatchStorage.deleteSendRTPServer(platformId, sendRtpItem.getChannelId(), + sendRtpItem.getCallId(), sendRtpItem.getStreamId()); } else { cmder.streamByeCmd(device, sendRtpItem.getChannelId(), param.getStream(), sendRtpItem.getCallId()); } @@ -428,7 +438,8 @@ public class ZLMHttpHookListener { @PostMapping(value = "/on_stream_none_reader", produces = "application/json;charset=UTF-8") public JSONObject onStreamNoneReader(@RequestBody OnStreamNoneReaderHookParam param) { - logger.info("[ZLM HOOK]流无人观看:{]->{}->{}/{}" + param.getMediaServerId(), param.getSchema(), param.getApp(), param.getStream()); + logger.info("[ZLM HOOK]流无人观看:{]->{}->{}/{}" + param.getMediaServerId(), param.getSchema(), + param.getApp(), param.getStream()); JSONObject ret = new JSONObject(); ret.put("code", 0); // 国标类型的流 @@ -440,7 +451,8 @@ public class ZLMHttpHookListener { if (streamInfoForPlayCatch != null) { // 收到无人观看说明流也没有在往上级推送 if (redisCatchStorage.isChannelSendingRTP(streamInfoForPlayCatch.getChannelId())) { - List sendRtpItems = redisCatchStorage.querySendRTPServerByChnnelId(streamInfoForPlayCatch.getChannelId()); + List sendRtpItems = redisCatchStorage.querySendRTPServerByChnnelId( + streamInfoForPlayCatch.getChannelId()); if (sendRtpItems.size() > 0) { for (SendRtpItem sendRtpItem : sendRtpItems) { ParentPlatform parentPlatform = storager.queryParentPlatByServerGBId(sendRtpItem.getPlatformId()); @@ -470,7 +482,8 @@ public class ZLMHttpHookListener { return ret; } // 录像回放 - StreamInfo streamInfoForPlayBackCatch = redisCatchStorage.queryPlayback(null, null, param.getStream(), null); + StreamInfo streamInfoForPlayBackCatch = redisCatchStorage.queryPlayback(null, null, + param.getStream(), null); if (streamInfoForPlayBackCatch != null) { if (streamInfoForPlayBackCatch.isPause()) { ret.put("close", false); @@ -491,7 +504,8 @@ public class ZLMHttpHookListener { return ret; } // 录像下载 - StreamInfo streamInfoForDownload = redisCatchStorage.queryDownload(null, null, param.getStream(), null); + StreamInfo streamInfoForDownload = redisCatchStorage.queryDownload(null, null, + param.getStream(), null); // 进行录像下载时无人观看不断流 if (streamInfoForDownload != null) { ret.put("close", false); diff --git a/src/main/java/com/genersoft/iot/vmp/media/zlm/dto/hook/HookResult.java b/src/main/java/com/genersoft/iot/vmp/media/zlm/dto/hook/HookResult.java index a2da561d8..b327f13a4 100644 --- a/src/main/java/com/genersoft/iot/vmp/media/zlm/dto/hook/HookResult.java +++ b/src/main/java/com/genersoft/iot/vmp/media/zlm/dto/hook/HookResult.java @@ -18,6 +18,10 @@ public class HookResult { return new HookResult(0, "success"); } + public static HookResult Fail(){ + return new HookResult(-1, "fail"); + } + public int getCode() { return code; } diff --git a/src/main/java/com/genersoft/iot/vmp/storager/impl/RedisCatchStorageImpl.java b/src/main/java/com/genersoft/iot/vmp/storager/impl/RedisCatchStorageImpl.java index facd54ca8..4c681e888 100644 --- a/src/main/java/com/genersoft/iot/vmp/storager/impl/RedisCatchStorageImpl.java +++ b/src/main/java/com/genersoft/iot/vmp/storager/impl/RedisCatchStorageImpl.java @@ -24,6 +24,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.stereotype.Component; import java.util.*; @@ -43,6 +44,9 @@ public class RedisCatchStorageImpl implements IRedisCatchStorage { @Autowired private RedisTemplate redisTemplate; + @Autowired + private StringRedisTemplate stringRedisTemplate; + @Override public Long getCSEQ() { String key = VideoManagerConstants.SIP_CSEQ_PREFIX + userSetting.getServerId(); @@ -913,7 +917,7 @@ public class RedisCatchStorageImpl implements IRedisCatchStorage { msg.append(":").append(channelId); } msg.append(" ").append(online? "ON":"OFF"); - - redisTemplate.convertAndSend(key, msg.toString()); + // 使用 RedisTemplate 发送字符串消息会导致发送的消息多带了双引号 + stringRedisTemplate.convertAndSend(key, msg.toString()); } } From ea3f899593f47c1c6fe708495af0a091a3c73d5f Mon Sep 17 00:00:00 2001 From: Kairlec Date: Sun, 23 Apr 2023 17:08:53 +0800 Subject: [PATCH 29/48] fix `Notification is not defined` on Android Webview or under ios16.4 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 如果没有这一行import,那么使用的会是Notification Web API,这在部分场景下是不支持的,具体见[Notification浏览器兼容性](https://developer.mozilla.org/zh-CN/docs/Web/API/Notification#%E6%B5%8F%E8%A7%88%E5%99%A8%E5%85%BC%E5%AE%B9%E6%80%A7) 比如在一些使用Android Webview的浏览器或部分ios系统上会导致报错而白屏 在此处应该为使用`element-ui`的Notification组件(下面有用到注册为element-ui的Notification全局属性$notify) --- web_src/src/layout/UiHeader.vue | 1 + 1 file changed, 1 insertion(+) diff --git a/web_src/src/layout/UiHeader.vue b/web_src/src/layout/UiHeader.vue index 3e9cca03d..2cdca02c0 100644 --- a/web_src/src/layout/UiHeader.vue +++ b/web_src/src/layout/UiHeader.vue @@ -40,6 +40,7 @@ import changePasswordDialog from '../components/dialog/changePassword.vue' import userService from '../components/service/UserService' +import {Notification} from 'element-ui'; export default { name: "UiHeader", From 94da74901c854fa4135d76d5c1a070aa1f8f973c Mon Sep 17 00:00:00 2001 From: 648540858 <648540858@qq.com> Date: Sun, 23 Apr 2023 17:33:28 +0800 Subject: [PATCH 30/48] =?UTF-8?q?=E4=BC=98=E5=8C=96Sip.ip=E7=9A=84?= =?UTF-8?q?=E6=B3=A8=E9=87=8A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/resources/all-application.yml | 7 +++++-- src/main/resources/application-dev.yml | 5 ++++- src/main/resources/application-docker.yml | 5 ++++- 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/src/main/resources/all-application.yml b/src/main/resources/all-application.yml index 7f8b27823..0fba9a92f 100644 --- a/src/main/resources/all-application.yml +++ b/src/main/resources/all-application.yml @@ -65,8 +65,11 @@ server: # 作为28181服务器的配置 sip: - # [必须修改] 本机的IP - ip: 192.168.0.100 + # [必须修改] 本机的IP,对应你的网卡,监听什么ip就是使用什么网卡, + # 如果要监听多张网卡,可以使用逗号分隔多个IP, 例如: 192.168.1.4,10.0.0.4 + # 如果不明白,就使用0.0.0.0,大部分情况都是可以的 + # 请不要使用127.0.0.1,任何包括localhost在内的域名都是不可以的。 + ip: 0.0.0.0 # [可选] 没有任何业务需求,仅仅是在前端展示的时候用 show-ip: 192.168.0.100 # [可选] 28181服务监听的端口 diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml index d02cdaf75..04f7742df 100644 --- a/src/main/resources/application-dev.yml +++ b/src/main/resources/application-dev.yml @@ -38,7 +38,10 @@ server: # 作为28181服务器的配置 sip: - # [必须修改] 本机的IP + # [必须修改] 本机的IP,对应你的网卡,监听什么ip就是使用什么网卡, + # 如果要监听多张网卡,可以使用逗号分隔多个IP, 例如: 192.168.1.4,10.0.0.4 + # 如果不明白,就使用0.0.0.0,大部分情况都是可以的 + # 请不要使用127.0.0.1,任何包括localhost在内的域名都是不可以的。 ip: 192.168.41.16 # [可选] 28181服务监听的端口 port: 5060 diff --git a/src/main/resources/application-docker.yml b/src/main/resources/application-docker.yml index f5c3d0f41..0e0c0ad35 100644 --- a/src/main/resources/application-docker.yml +++ b/src/main/resources/application-docker.yml @@ -32,7 +32,10 @@ server: # 作为28181服务器的配置 sip: - # [必须修改] 本机的IP + # [必须修改] 本机的IP,对应你的网卡,监听什么ip就是使用什么网卡, + # 如果要监听多张网卡,可以使用逗号分隔多个IP, 例如: 192.168.1.4,10.0.0.4 + # 如果不明白,就使用0.0.0.0,大部分情况都是可以的 + # 请不要使用127.0.0.1,任何包括localhost在内的域名都是不可以的。 ip: ${WVP_HOST:127.0.0.1} # [可选] 28181服务监听的端口 port: ${WVP_PORT:5060} From 8a68bae0bb02127cc62c07bf8727af4b88df0981 Mon Sep 17 00:00:00 2001 From: 648540858 <648540858@qq.com> Date: Thu, 27 Apr 2023 09:23:00 +0800 Subject: [PATCH 31/48] =?UTF-8?q?=E4=BF=AE=E5=A4=8Dredis=E6=95=B0=E6=8D=AE?= =?UTF-8?q?=E5=86=99=E5=85=A5=E6=97=B6=E8=B6=85=E6=97=B6=E6=97=B6=E9=97=B4?= =?UTF-8?q?=E8=AE=BE=E7=BD=AE=E9=94=99=E8=AF=AF=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../vmp/storager/impl/RedisCatchStorageImpl.java | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/genersoft/iot/vmp/storager/impl/RedisCatchStorageImpl.java b/src/main/java/com/genersoft/iot/vmp/storager/impl/RedisCatchStorageImpl.java index 4c681e888..d4abcb45f 100644 --- a/src/main/java/com/genersoft/iot/vmp/storager/impl/RedisCatchStorageImpl.java +++ b/src/main/java/com/genersoft/iot/vmp/storager/impl/RedisCatchStorageImpl.java @@ -27,6 +27,7 @@ import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.stereotype.Component; +import java.time.Duration; import java.util.*; @SuppressWarnings("rawtypes") @@ -189,7 +190,8 @@ public class RedisCatchStorageImpl implements IRedisCatchStorage { redisTemplate.opsForValue().set(key, stream); }else { logger.debug("添加下载缓存==未完成下载=》{}",key); - redisTemplate.opsForValue().set(key, stream, 60*60); + Duration duration = Duration.ofSeconds(60*60L); + redisTemplate.opsForValue().set(key, stream, duration); } return true; } @@ -355,7 +357,8 @@ public class RedisCatchStorageImpl implements IRedisCatchStorage { @Override public void updatePlatformRegisterInfo(String callId, PlatformRegisterInfo platformRegisterInfo) { String key = VideoManagerConstants.PLATFORM_REGISTER_INFO_PREFIX + userSetting.getServerId() + "_" + callId; - redisTemplate.opsForValue().set(key, platformRegisterInfo, 30); + Duration duration = Duration.ofSeconds(30L); + redisTemplate.opsForValue().set(key, platformRegisterInfo, duration); } @@ -566,7 +569,8 @@ public class RedisCatchStorageImpl implements IRedisCatchStorage { @Override public void updateWVPInfo(JSONObject jsonObject, int time) { String key = VideoManagerConstants.WVP_SERVER_PREFIX + userSetting.getServerId(); - redisTemplate.opsForValue().set(key, jsonObject, time); + Duration duration = Duration.ofSeconds(time); + redisTemplate.opsForValue().set(key, jsonObject, duration); } @Override @@ -698,7 +702,8 @@ public class RedisCatchStorageImpl implements IRedisCatchStorage { @Override public void updateGpsMsgInfo(GPSMsgInfo gpsMsgInfo) { String key = VideoManagerConstants.WVP_STREAM_GPS_MSG_PREFIX + userSetting.getServerId() + "_" + gpsMsgInfo.getId(); - redisTemplate.opsForValue().set(key, gpsMsgInfo, 60); // 默认GPS消息保存1分钟 + Duration duration = Duration.ofSeconds(60L); + redisTemplate.opsForValue().set(key, gpsMsgInfo, duration); // 默认GPS消息保存1分钟 } @Override From c561dda269a3c62453b56a46e4deee8154b17d8b Mon Sep 17 00:00:00 2001 From: wangjunyi Date: Thu, 27 Apr 2023 16:26:29 +0800 Subject: [PATCH 32/48] =?UTF-8?q?=E5=88=9B=E5=BB=BA=E5=9B=9E=E8=B0=83?= =?UTF-8?q?=E5=87=BD=E6=95=B0=E6=97=B6=EF=BC=8C=E4=B8=8D=E5=8F=AF=E5=B8=A6?= =?UTF-8?q?=E4=B8=8A=E6=8B=AC=E5=8F=B7=EF=BC=8C=E5=90=A6=E5=88=99=E4=BC=9A?= =?UTF-8?q?=E5=9C=A8=E8=B0=83=E7=94=A8=E6=AD=A4=E8=AF=AD=E5=8F=A5=E6=97=B6?= =?UTF-8?q?=EF=BC=8C=E7=AB=8B=E5=8D=B3=E6=89=A7=E8=A1=8C=E4=B8=80=E6=AC=A1?= =?UTF-8?q?=E5=9B=9E=E8=B0=83=E5=87=BD=E6=95=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- web_src/src/components/dialog/recordDownload.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web_src/src/components/dialog/recordDownload.vue b/web_src/src/components/dialog/recordDownload.vue index b3f46c879..33b0f8629 100644 --- a/web_src/src/components/dialog/recordDownload.vue +++ b/web_src/src/components/dialog/recordDownload.vue @@ -161,7 +161,7 @@ export default { } setTimeout( ()=>{ if (!this.showDialog) return; - this.getProgressForFile(this.getProgressForFileTimer()) + this.getProgressForFile(this.getProgressForFileTimer) }, 1000) }, getProgressForFile: function (callback){ From 8ef5e2618d6fed0bbfea1aca99ca010b1e041718 Mon Sep 17 00:00:00 2001 From: wangjunyi Date: Thu, 27 Apr 2023 17:39:45 +0800 Subject: [PATCH 33/48] =?UTF-8?q?=E8=A7=A3=E5=86=B3=E5=9B=BD=E6=A0=87?= =?UTF-8?q?=E8=AE=BE=E5=A4=87-=E9=80=9A=E9=81=93-=E8=AE=BE=E5=A4=87?= =?UTF-8?q?=E5=BD=95=E5=83=8F=E5=A4=84=E7=9A=84=E8=A7=86=E9=A2=91=E4=B8=8B?= =?UTF-8?q?=E8=BD=BD=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- web_src/src/components/dialog/recordDownload.vue | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/web_src/src/components/dialog/recordDownload.vue b/web_src/src/components/dialog/recordDownload.vue index 33b0f8629..c90cf1382 100644 --- a/web_src/src/components/dialog/recordDownload.vue +++ b/web_src/src/components/dialog/recordDownload.vue @@ -179,9 +179,12 @@ export default { if (res.data.code === 0) { if (res.data.data.length === 0){ this.percentage = 0 + // 往往在多次请求后(实验五分钟的视频是三次请求),才会返回数据,第一次请求通常是返回空数组 + if (callback)callback() return } - this.percentage = parseFloat(res.data.data.percentage)*100 + // res.data.data应是数组类型 + this.percentage = parseFloat(res.data.data[0].percentage)*100 if (res.data.data[0].percentage === '1') { this.getProgressForFileRun = false; window.open(res.data.data[0].downloadFile) From 97b673d6ad6c2266a6e26eddf38dae23c00bb855 Mon Sep 17 00:00:00 2001 From: QingObject <1120359293@qq.com> Date: Fri, 28 Apr 2023 10:10:06 +0800 Subject: [PATCH 34/48] =?UTF-8?q?=E6=96=B0=E5=A2=9EJT1078=20Template?= =?UTF-8?q?=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../iot/vmp/jt1078/annotation/MsgId.java | 15 ++ .../iot/vmp/jt1078/cmd/JT1078Template.java | 56 +++++++ .../vmp/jt1078/codec/decode/Jt808Decoder.java | 146 +++++++++++++++++ .../vmp/jt1078/codec/encode/Jt808Encoder.java | 33 ++++ .../jt1078/codec/encode/Jt808EncoderCmd.java | 151 ++++++++++++++++++ .../vmp/jt1078/codec/netty/Jt808Handler.java | 72 +++++++++ .../iot/vmp/jt1078/codec/netty/TcpServer.java | 112 +++++++++++++ .../vmp/jt1078/config/JT1078Controller.java | 46 ++++++ .../jt1078/config/TcpAutoConfiguration.java | 30 ++++ .../genersoft/iot/vmp/jt1078/proc/Header.java | 76 +++++++++ .../iot/vmp/jt1078/proc/entity/Cmd.java | 105 ++++++++++++ .../vmp/jt1078/proc/factory/CodecFactory.java | 44 +++++ .../iot/vmp/jt1078/proc/request/J0001.java | 50 ++++++ .../iot/vmp/jt1078/proc/request/J0002.java | 32 ++++ .../iot/vmp/jt1078/proc/request/J0004.java | 27 ++++ .../iot/vmp/jt1078/proc/request/J0100.java | 56 +++++++ .../iot/vmp/jt1078/proc/request/J0102.java | 36 +++++ .../iot/vmp/jt1078/proc/request/J0200.java | 32 ++++ .../iot/vmp/jt1078/proc/request/Re.java | 40 +++++ .../iot/vmp/jt1078/proc/response/J8001.java | 43 +++++ .../iot/vmp/jt1078/proc/response/J8100.java | 41 +++++ .../iot/vmp/jt1078/proc/response/J9101.java | 110 +++++++++++++ .../iot/vmp/jt1078/proc/response/J9102.java | 85 ++++++++++ .../iot/vmp/jt1078/proc/response/Rs.java | 27 ++++ .../iot/vmp/jt1078/session/Session.java | 114 +++++++++++++ .../vmp/jt1078/session/SessionManager.java | 127 +++++++++++++++ .../genersoft/iot/vmp/jt1078/util/Bin.java | 41 +++++ .../iot/vmp/jt1078/util/ClassUtil.java | 112 +++++++++++++ src/main/resources/all-application.yml | 9 ++ 29 files changed, 1868 insertions(+) create mode 100644 src/main/java/com/genersoft/iot/vmp/jt1078/annotation/MsgId.java create mode 100644 src/main/java/com/genersoft/iot/vmp/jt1078/cmd/JT1078Template.java create mode 100644 src/main/java/com/genersoft/iot/vmp/jt1078/codec/decode/Jt808Decoder.java create mode 100644 src/main/java/com/genersoft/iot/vmp/jt1078/codec/encode/Jt808Encoder.java create mode 100644 src/main/java/com/genersoft/iot/vmp/jt1078/codec/encode/Jt808EncoderCmd.java create mode 100644 src/main/java/com/genersoft/iot/vmp/jt1078/codec/netty/Jt808Handler.java create mode 100644 src/main/java/com/genersoft/iot/vmp/jt1078/codec/netty/TcpServer.java create mode 100644 src/main/java/com/genersoft/iot/vmp/jt1078/config/JT1078Controller.java create mode 100644 src/main/java/com/genersoft/iot/vmp/jt1078/config/TcpAutoConfiguration.java create mode 100644 src/main/java/com/genersoft/iot/vmp/jt1078/proc/Header.java create mode 100644 src/main/java/com/genersoft/iot/vmp/jt1078/proc/entity/Cmd.java create mode 100644 src/main/java/com/genersoft/iot/vmp/jt1078/proc/factory/CodecFactory.java create mode 100644 src/main/java/com/genersoft/iot/vmp/jt1078/proc/request/J0001.java create mode 100644 src/main/java/com/genersoft/iot/vmp/jt1078/proc/request/J0002.java create mode 100644 src/main/java/com/genersoft/iot/vmp/jt1078/proc/request/J0004.java create mode 100644 src/main/java/com/genersoft/iot/vmp/jt1078/proc/request/J0100.java create mode 100644 src/main/java/com/genersoft/iot/vmp/jt1078/proc/request/J0102.java create mode 100644 src/main/java/com/genersoft/iot/vmp/jt1078/proc/request/J0200.java create mode 100644 src/main/java/com/genersoft/iot/vmp/jt1078/proc/request/Re.java create mode 100644 src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J8001.java create mode 100644 src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J8100.java create mode 100644 src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J9101.java create mode 100644 src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J9102.java create mode 100644 src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/Rs.java create mode 100644 src/main/java/com/genersoft/iot/vmp/jt1078/session/Session.java create mode 100644 src/main/java/com/genersoft/iot/vmp/jt1078/session/SessionManager.java create mode 100644 src/main/java/com/genersoft/iot/vmp/jt1078/util/Bin.java create mode 100644 src/main/java/com/genersoft/iot/vmp/jt1078/util/ClassUtil.java diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/annotation/MsgId.java b/src/main/java/com/genersoft/iot/vmp/jt1078/annotation/MsgId.java new file mode 100644 index 000000000..d5c2de46b --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/annotation/MsgId.java @@ -0,0 +1,15 @@ +package com.genersoft.iot.vmp.jt1078.annotation; + +import java.lang.annotation.*; + +/** + * @author QingtaiJiang + * @date 2023/4/27 18:31 + * @email qingtaij@163.com + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface MsgId { + String id(); +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/cmd/JT1078Template.java b/src/main/java/com/genersoft/iot/vmp/jt1078/cmd/JT1078Template.java new file mode 100644 index 000000000..ad3ab0063 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/cmd/JT1078Template.java @@ -0,0 +1,56 @@ +package com.genersoft.iot.vmp.jt1078.cmd; + +import com.genersoft.iot.vmp.jt1078.proc.entity.Cmd; +import com.genersoft.iot.vmp.jt1078.proc.response.J9101; +import com.genersoft.iot.vmp.jt1078.proc.response.J9102; +import com.genersoft.iot.vmp.jt1078.session.SessionManager; + +import java.util.Random; + +/** + * @author QingtaiJiang + * @date 2023/4/27 18:58 + * @email qingtaij@163.com + */ +public class JT1078Template { + + private final Random random = new Random(); + + /** + * 开启直播视频 + * + * @param devId 设备号 + * @param j9101 开启视频参数 + */ + public String startLive(String devId, J9101 j9101, Integer timeOut) { + Cmd cmd = new Cmd.Builder() + .setDevId(devId) + .setPackageNo(randomInt()) + .setMsgId("9101") + .setRespId("0001") + .setRs(j9101) + .build(); + return SessionManager.INSTANCE.request(cmd, timeOut); + } + + /** + * 关闭直播视频 + * + * @param devId 设备号 + * @param j9102 关闭视频参数 + */ + public String stopLive(String devId, J9102 j9102, Integer timeOut) { + Cmd cmd = new Cmd.Builder() + .setDevId(devId) + .setPackageNo(randomInt()) + .setMsgId("9102") + .setRespId("0001") + .setRs(j9102) + .build(); + return SessionManager.INSTANCE.request(cmd, timeOut); + } + + private Long randomInt() { + return (long) random.nextInt(1000) + 1; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/codec/decode/Jt808Decoder.java b/src/main/java/com/genersoft/iot/vmp/jt1078/codec/decode/Jt808Decoder.java new file mode 100644 index 000000000..4817c665f --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/codec/decode/Jt808Decoder.java @@ -0,0 +1,146 @@ +package com.genersoft.iot.vmp.jt1078.codec.decode; + +import com.genersoft.iot.vmp.jt1078.proc.Header; +import com.genersoft.iot.vmp.jt1078.proc.factory.CodecFactory; +import com.genersoft.iot.vmp.jt1078.proc.request.Re; +import com.genersoft.iot.vmp.jt1078.proc.response.Rs; +import com.genersoft.iot.vmp.jt1078.session.Session; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufUtil; +import io.netty.buffer.CompositeByteBuf; +import io.netty.buffer.UnpooledByteBufAllocator; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.ByteToMessageDecoder; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.List; + +/** + * @author QingtaiJiang + * @date 2023/4/27 18:10 + * @email qingtaij@163.com + */ +public class Jt808Decoder extends ByteToMessageDecoder { + private final static Logger log = LoggerFactory.getLogger(Jt808Decoder.class); + + @Override + protected void decode(ChannelHandlerContext ctx, ByteBuf in, List out) throws Exception { + Session session = ctx.channel().attr(Session.KEY).get(); + log.info("> {} hex:{}", session, ByteBufUtil.hexDump(in)); + + try { + ByteBuf buf = unEscapeAndCheck(in); + + Header header = new Header(); + header.setMsgId(ByteBufUtil.hexDump(buf.readSlice(2))); + header.setMsgPro(buf.readUnsignedShort()); + if (header.is2019Version()) { + header.setVersion(buf.readUnsignedByte()); + String devId = ByteBufUtil.hexDump(buf.readSlice(10)); + header.setDevId(devId.replaceFirst("^0*", "")); + } else { + header.setDevId(ByteBufUtil.hexDump(buf.readSlice(6)).replaceFirst("^0*", "")); + } + header.setSn(buf.readUnsignedShort()); + + Re handler = CodecFactory.getHandler(header.getMsgId()); + if (handler == null) { + log.error("get msgId is null {}", header.getMsgId()); + return; + } + Rs decode = handler.decode(buf, header, session); + if (decode != null) { + out.add(decode); + } + } finally { + in.skipBytes(in.readableBytes()); + } + + + } + + + /** + * 转义与验证校验码 + * + * @param byteBuf 转义Buf + * @return 转义好的数据 + */ + public ByteBuf unEscapeAndCheck(ByteBuf byteBuf) throws Exception { + int low = byteBuf.readerIndex(); + int high = byteBuf.writerIndex(); + byte checkSum = 0; + int calculationCheckSum = 0; + + byte aByte = byteBuf.getByte(high - 2); + byte protocolEscapeFlag7d = 0x7d; + //0x7d转义 + byte protocolEscapeFlag01 = 0x01; + //0x7e转义 + byte protocolEscapeFlag02 = 0x02; + if (aByte == protocolEscapeFlag7d) { + byte b2 = byteBuf.getByte(high - 1); + if (b2 == protocolEscapeFlag01) { + checkSum = protocolEscapeFlag7d; + } else if (b2 == protocolEscapeFlag02) { + checkSum = 0x7e; + } else { + log.error("转义1异常:{}", ByteBufUtil.hexDump(byteBuf)); + throw new Exception("转义错误"); + } + high = high - 2; + } else { + high = high - 1; + checkSum = byteBuf.getByte(high); + } + List bufList = new ArrayList<>(); + int index = low; + while (index < high) { + byte b = byteBuf.getByte(index); + if (b == protocolEscapeFlag7d) { + byte c = byteBuf.getByte(index + 1); + if (c == protocolEscapeFlag01) { + ByteBuf slice = slice0x01(byteBuf, low, index); + bufList.add(slice); + b = protocolEscapeFlag7d; + } else if (c == protocolEscapeFlag02) { + ByteBuf slice = slice0x02(byteBuf, low, index); + bufList.add(slice); + b = 0x7e; + } else { + log.error("转义2异常:{}", ByteBufUtil.hexDump(byteBuf)); + throw new Exception("转义错误"); + } + index += 2; + low = index; + } else { + index += 1; + } + calculationCheckSum = calculationCheckSum ^ b; + } + + if (calculationCheckSum == checkSum) { + if (bufList.size() == 0) { + return byteBuf.slice(low, high); + } else { + bufList.add(byteBuf.slice(low, high - low)); + return new CompositeByteBuf(UnpooledByteBufAllocator.DEFAULT, false, bufList.size(), bufList); + } + } else { + log.info("{} 解析校验码:{}--计算校验码:{}", ByteBufUtil.hexDump(byteBuf), checkSum, calculationCheckSum); + throw new Exception("校验码错误!"); + } + } + + + private ByteBuf slice0x01(ByteBuf buf, int low, int sign) { + return buf.slice(low, sign - low + 1); + } + + private ByteBuf slice0x02(ByteBuf buf, int low, int sign) { + buf.setByte(sign, 0x7e); + return buf.slice(low, sign - low + 1); + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/codec/encode/Jt808Encoder.java b/src/main/java/com/genersoft/iot/vmp/jt1078/codec/encode/Jt808Encoder.java new file mode 100644 index 000000000..afb1a7973 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/codec/encode/Jt808Encoder.java @@ -0,0 +1,33 @@ +package com.genersoft.iot.vmp.jt1078.codec.encode; + + +import com.genersoft.iot.vmp.jt1078.proc.response.Rs; +import com.genersoft.iot.vmp.jt1078.session.Session; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufUtil; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.MessageToByteEncoder; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * @author QingtaiJiang + * @date 2023/4/27 18:10 + * @email qingtaij@163.com + */ +public class Jt808Encoder extends MessageToByteEncoder { + private final static Logger log = LoggerFactory.getLogger(Jt808Encoder.class); + + @Override + protected void encode(ChannelHandlerContext ctx, Rs msg, ByteBuf out) throws Exception { + Session session = ctx.channel().attr(Session.KEY).get(); + + ByteBuf encode = Jt808EncoderCmd.encode(msg, session, session.nextSerialNo()); + if(encode!=null){ + log.info("< {} hex:{}", session, ByteBufUtil.hexDump(encode)); + out.writeBytes(encode); + } + } + + +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/codec/encode/Jt808EncoderCmd.java b/src/main/java/com/genersoft/iot/vmp/jt1078/codec/encode/Jt808EncoderCmd.java new file mode 100644 index 000000000..0e9e11f6f --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/codec/encode/Jt808EncoderCmd.java @@ -0,0 +1,151 @@ +package com.genersoft.iot.vmp.jt1078.codec.encode; + +import com.genersoft.iot.vmp.jt1078.annotation.MsgId; +import com.genersoft.iot.vmp.jt1078.proc.Header; +import com.genersoft.iot.vmp.jt1078.proc.entity.Cmd; +import com.genersoft.iot.vmp.jt1078.proc.response.Rs; +import com.genersoft.iot.vmp.jt1078.session.Session; +import com.genersoft.iot.vmp.jt1078.util.Bin; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufUtil; +import io.netty.buffer.CompositeByteBuf; +import io.netty.buffer.Unpooled; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.MessageToByteEncoder; +import io.netty.util.ByteProcessor; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.util.StringUtils; + +import java.util.LinkedList; + +/** + * @author QingtaiJiang + * @date 2023/4/27 18:25 + * @email qingtaij@163.com + */ +public class Jt808EncoderCmd extends MessageToByteEncoder { + private final static Logger log = LoggerFactory.getLogger(Jt808EncoderCmd.class); + + @Override + protected void encode(ChannelHandlerContext ctx, Cmd cmd, ByteBuf out) throws Exception { + Session session = ctx.channel().attr(Session.KEY).get(); + Rs msg = cmd.getRs(); + ByteBuf encode = encode(msg, session, cmd.getPackageNo().intValue()); + if (encode != null) { + log.info("< {} hex:{}", session, ByteBufUtil.hexDump(encode)); + out.writeBytes(encode); + } + } + + + public static ByteBuf encode(Rs msg, Session session, Integer packageNo) { + String id = msg.getClass().getAnnotation(MsgId.class).id(); + if (!StringUtils.hasLength(id)) { + log.error("Not find msgId"); + return null; + } + + ByteBuf byteBuf = Unpooled.buffer(); + + byteBuf.writeBytes(ByteBufUtil.decodeHexDump(id)); + + ByteBuf encode = msg.encode(); + + Header header = msg.getHeader(); + if (header == null) { + header = session.getHeader(); + } + + if (header.is2019Version()) { + // 消息体属性 + byteBuf.writeShort(encode.readableBytes() | 1 << 14); + + // 版本号 + byteBuf.writeByte(header.getVersion()); + + // 终端手机号 + byteBuf.writeBytes(ByteBufUtil.decodeHexDump(Bin.strHexPaddingLeft(header.getDevId(), 20))); + } else { + // 消息体属性 + byteBuf.writeShort(encode.readableBytes()); + + byteBuf.writeBytes(ByteBufUtil.decodeHexDump(Bin.strHexPaddingLeft(header.getDevId(), 12))); + } + + // 消息体流水号 + byteBuf.writeShort(packageNo); + + // 写入消息体 + byteBuf.writeBytes(encode); + + // 计算校验码,并反转义 + byteBuf = escapeAndCheck0(byteBuf); + return byteBuf; + } + + + private static final ByteProcessor searcher = value -> !(value == 0x7d || value == 0x7e); + + //转义与校验 + public static ByteBuf escapeAndCheck0(ByteBuf source) { + + sign(source); + + int low = source.readerIndex(); + int high = source.writerIndex(); + + LinkedList bufList = new LinkedList<>(); + int mark, len; + while ((mark = source.forEachByte(low, high - low, searcher)) > 0) { + + len = mark + 1 - low; + ByteBuf[] slice = slice(source, low, len); + bufList.add(slice[0]); + bufList.add(slice[1]); + low += len; + } + + if (bufList.size() > 0) { + bufList.add(source.slice(low, high - low)); + } else { + bufList.add(source); + } + + ByteBuf delimiter = Unpooled.buffer(1, 1).writeByte(0x7e).retain(); + bufList.addFirst(delimiter); + bufList.addLast(delimiter); + + CompositeByteBuf byteBufLs = Unpooled.compositeBuffer(bufList.size()); + byteBufLs.addComponents(true, bufList); + return byteBufLs; + } + + public static void sign(ByteBuf buf) { + byte checkCode = bcc(buf); + buf.writeByte(checkCode); + } + + public static byte bcc(ByteBuf byteBuf) { + byte cs = 0; + while (byteBuf.isReadable()) + cs ^= byteBuf.readByte(); + byteBuf.resetReaderIndex(); + return cs; + } + + protected static ByteBuf[] slice(ByteBuf byteBuf, int index, int length) { + byte first = byteBuf.getByte(index + length - 1); + + ByteBuf[] byteBufList = new ByteBuf[2]; + byteBufList[0] = byteBuf.retainedSlice(index, length); + + if (first == 0x7d) { + byteBufList[1] = Unpooled.buffer(1, 1).writeByte(0x01); + } else { + byteBuf.setByte(index + length - 1, 0x7d); + byteBufList[1] = Unpooled.buffer(1, 1).writeByte(0x02); + } + return byteBufList; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/codec/netty/Jt808Handler.java b/src/main/java/com/genersoft/iot/vmp/jt1078/codec/netty/Jt808Handler.java new file mode 100644 index 000000000..fd5030272 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/codec/netty/Jt808Handler.java @@ -0,0 +1,72 @@ +package com.genersoft.iot.vmp.jt1078.codec.netty; + +import com.genersoft.iot.vmp.jt1078.proc.response.Rs; +import com.genersoft.iot.vmp.jt1078.session.Session; +import com.genersoft.iot.vmp.jt1078.session.SessionManager; +import io.netty.channel.Channel; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandlerAdapter; +import io.netty.handler.timeout.IdleState; +import io.netty.handler.timeout.IdleStateEvent; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * @author QingtaiJiang + * @date 2023/4/27 18:14 + * @email qingtaij@163.com + */ +public class Jt808Handler extends ChannelInboundHandlerAdapter { + + private final static Logger log = LoggerFactory.getLogger(Jt808Handler.class); + + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { + if (msg instanceof Rs) { + ctx.writeAndFlush(msg); + } else { + ctx.fireChannelRead(msg); + } + } + + @Override + public void channelActive(ChannelHandlerContext ctx) { + Channel channel = ctx.channel(); + Session session = SessionManager.INSTANCE.newSession(channel); + channel.attr(Session.KEY).set(session); + log.info("> Tcp connect {}", session); + } + + @Override + public void channelInactive(ChannelHandlerContext ctx) { + Session session = ctx.channel().attr(Session.KEY).get(); + log.info("< Tcp disconnect {}", session); + ctx.close(); + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable e) { + Session session = ctx.channel().attr(Session.KEY).get(); + String message = e.getMessage(); + if (message.toLowerCase().contains("Connection reset by peer".toLowerCase())) { + log.info("< exception{} {}", session, e.getMessage()); + } else { + log.info("< exception{} {}", session, e.getMessage(), e); + } + + } + + @Override + public void userEventTriggered(ChannelHandlerContext ctx, Object evt) { + if (evt instanceof IdleStateEvent) { + IdleStateEvent event = (IdleStateEvent) evt; + IdleState state = event.state(); + if (state == IdleState.READER_IDLE || state == IdleState.WRITER_IDLE) { + Session session = ctx.channel().attr(Session.KEY).get(); + log.warn("< Proactively disconnect{}", session); + ctx.close(); + } + } + } + +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/codec/netty/TcpServer.java b/src/main/java/com/genersoft/iot/vmp/jt1078/codec/netty/TcpServer.java new file mode 100644 index 000000000..a7e4df826 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/codec/netty/TcpServer.java @@ -0,0 +1,112 @@ +package com.genersoft.iot.vmp.jt1078.codec.netty; + +import com.genersoft.iot.vmp.jt1078.codec.decode.Jt808Decoder; +import com.genersoft.iot.vmp.jt1078.codec.encode.Jt808Encoder; +import com.genersoft.iot.vmp.jt1078.codec.encode.Jt808EncoderCmd; +import com.genersoft.iot.vmp.jt1078.proc.factory.CodecFactory; +import io.netty.bootstrap.ServerBootstrap; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelInitializer; +import io.netty.channel.EventLoopGroup; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.nio.NioChannelOption; +import io.netty.channel.socket.nio.NioServerSocketChannel; +import io.netty.channel.socket.nio.NioSocketChannel; +import io.netty.handler.codec.DelimiterBasedFrameDecoder; +import io.netty.handler.timeout.IdleStateHandler; +import io.netty.util.concurrent.Future; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.concurrent.TimeUnit; + +/** + * @author QingtaiJiang + * @date 2023/4/27 18:01 + * @email qingtaij@163.com + */ + +public class TcpServer { + private final static Logger log = LoggerFactory.getLogger(TcpServer.class); + + private final Integer port; + private boolean isRunning = false; + private EventLoopGroup bossGroup = null; + private EventLoopGroup workerGroup = null; + + private final ByteBuf DECODER_JT808 = Unpooled.wrappedBuffer(new byte[]{0x7e}); + + public TcpServer(Integer port) { + this.port = port; + } + + private void startTcpServer() { + try { + CodecFactory.init(); + this.bossGroup = new NioEventLoopGroup(); + this.workerGroup = new NioEventLoopGroup(); + ServerBootstrap bootstrap = new ServerBootstrap(); + bootstrap.channel(NioServerSocketChannel.class); + bootstrap.group(bossGroup, workerGroup); + + bootstrap.option(NioChannelOption.SO_BACKLOG, 1024) + .option(NioChannelOption.SO_REUSEADDR, true) + .childOption(NioChannelOption.TCP_NODELAY, true) + .childHandler(new ChannelInitializer() { + @Override + public void initChannel(NioSocketChannel channel) { + channel.pipeline() + .addLast(new IdleStateHandler(10, 0, 0, TimeUnit.MINUTES)) + .addLast(new DelimiterBasedFrameDecoder(1024 * 2, DECODER_JT808)) + .addLast(new Jt808Decoder()) + .addLast(new Jt808Encoder()) + .addLast(new Jt808EncoderCmd()) + .addLast(new Jt808Handler()); + } + }); + ChannelFuture channelFuture = bootstrap.bind(port).sync(); + // 监听设备TCP端口是否启动成功 + channelFuture.addListener(future -> { + if (!future.isSuccess()) { + log.error("Binding port:{} fail! cause: {}", port, future.cause().getCause(), future.cause()); + } + }); + log.info("服务:JT808 Server 启动成功, port:{}", port); + channelFuture.channel().closeFuture().sync(); + } catch (Exception e) { + log.warn("服务:JT808 Server 启动异常, port:{},{}", port, e.getMessage(), e); + } finally { + stop(); + } + } + + /** + * 开启一个新的线程,拉起来Netty + */ + public synchronized void start() { + if (this.isRunning) { + log.warn("服务:JT808 Server 已经启动, port:{}", port); + return; + } + this.isRunning = true; + new Thread(this::startTcpServer).start(); + } + + public synchronized void stop() { + if (!this.isRunning) { + log.warn("服务:JT808 Server 已经停止, port:{}", port); + } + this.isRunning = false; + Future future = this.bossGroup.shutdownGracefully(); + if (!future.isSuccess()) { + log.warn("bossGroup 无法正常停止", future.cause()); + } + future = this.workerGroup.shutdownGracefully(); + if (!future.isSuccess()) { + log.warn("workerGroup 无法正常停止", future.cause()); + } + log.warn("服务:JT808 Server 已经停止, port:{}", port); + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/config/JT1078Controller.java b/src/main/java/com/genersoft/iot/vmp/jt1078/config/JT1078Controller.java new file mode 100644 index 000000000..cffb147d2 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/config/JT1078Controller.java @@ -0,0 +1,46 @@ +package com.genersoft.iot.vmp.jt1078.config; + +import com.genersoft.iot.vmp.jt1078.cmd.JT1078Template; +import com.genersoft.iot.vmp.jt1078.proc.response.J9101; +import com.genersoft.iot.vmp.vmanager.bean.WVPResult; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import javax.annotation.Resource; + +/** + * curl http://localhost:18080/api/jt1078/start/live/18864197066/1 + * + * @author QingtaiJiang + * @date 2023/4/27 18:12 + * @email qingtaij@163.com + */ +@ConditionalOnProperty(value = "jt1078.enable", havingValue = "true") +@RestController +@RequestMapping("/api/jt1078") +public class JT1078Controller { + + @Resource + JT1078Template jt1078Template; + + @GetMapping("/start/live/{deviceId}/{channelId}") + public WVPResult startLive(@PathVariable String deviceId, @PathVariable String channelId) { + J9101 j9101 = new J9101(); + j9101.setChannel(Integer.valueOf(channelId)); + j9101.setIp("192.168.1.1"); + j9101.setRate(1); + j9101.setTcpPort(7618); + j9101.setUdpPort(7618); + j9101.setType(0); + + String s = jt1078Template.startLive(deviceId, j9101, 6); + WVPResult wvpResult = new WVPResult<>(); + wvpResult.setCode(200); + wvpResult.setData(String.format("http://192.168.1.1/rtp/%s_%s.live.mp4", deviceId, channelId)); + return wvpResult; + } +} + diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/config/TcpAutoConfiguration.java b/src/main/java/com/genersoft/iot/vmp/jt1078/config/TcpAutoConfiguration.java new file mode 100644 index 000000000..0b07bb430 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/config/TcpAutoConfiguration.java @@ -0,0 +1,30 @@ +package com.genersoft.iot.vmp.jt1078.config; + +import com.genersoft.iot.vmp.jt1078.cmd.JT1078Template; +import com.genersoft.iot.vmp.jt1078.codec.netty.TcpServer; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.annotation.Order; + +/** + * @author QingtaiJiang + * @date 2023/4/27 19:35 + * @email qingtaij@163.com + */ +@Order(Integer.MIN_VALUE) +@Configuration +@ConditionalOnProperty(value = "jt1078.enable", havingValue = "true") +public class TcpAutoConfiguration { + + @Bean(initMethod = "start", destroyMethod = "stop") + public TcpServer jt1078Server(@Value("${jt1078.port}") Integer port) { + return new TcpServer(port); + } + + @Bean + public JT1078Template jt1078Template() { + return new JT1078Template(); + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/proc/Header.java b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/Header.java new file mode 100644 index 000000000..86c5fff26 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/Header.java @@ -0,0 +1,76 @@ +package com.genersoft.iot.vmp.jt1078.proc; + +import com.genersoft.iot.vmp.jt1078.util.Bin; + +/** + * @author QingtaiJiang + * @date 2023/4/27 18:22 + * @email qingtaij@163.com + */ +public class Header { + // 消息ID + String msgId; + + // 消息体属性 + Integer msgPro; + + // 标识 + String devId; + + // 消息体流水号 + Integer sn; + + // 协议版本号 + Short version = -1; + + + public String getMsgId() { + return msgId; + } + + public void setMsgId(String msgId) { + this.msgId = msgId; + } + + public Integer getMsgPro() { + return msgPro; + } + + public void setMsgPro(Integer msgPro) { + this.msgPro = msgPro; + } + + public String getDevId() { + return devId; + } + + public void setDevId(String devId) { + this.devId = devId; + } + + public Integer getSn() { + return sn; + } + + public void setSn(Integer sn) { + this.sn = sn; + } + + public Short getVersion() { + return version; + } + + public void setVersion(Short version) { + this.version = version; + } + + /** + * 判断是否是2019的版本 + * + * @return true 2019后的版本。false 2013 + */ + public boolean is2019Version() { + return Bin.get(msgPro, 14); + } + +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/proc/entity/Cmd.java b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/entity/Cmd.java new file mode 100644 index 000000000..19d6d8f1d --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/entity/Cmd.java @@ -0,0 +1,105 @@ +package com.genersoft.iot.vmp.jt1078.proc.entity; + +import com.genersoft.iot.vmp.jt1078.proc.response.Rs; + +/** + * @author QingtaiJiang + * @date 2023/4/27 18:23 + * @email qingtaij@163.com + */ +public class Cmd { + String devId; + Long packageNo; + String msgId; + String respId; + Rs rs; + + public Cmd() { + } + + public Cmd(Builder builder) { + this.devId = builder.devId; + this.packageNo = builder.packageNo; + this.msgId = builder.msgId; + this.respId = builder.respId; + this.rs = builder.rs; + } + + public String getDevId() { + return devId; + } + + public void setDevId(String devId) { + this.devId = devId; + } + + public Long getPackageNo() { + return packageNo; + } + + public void setPackageNo(Long packageNo) { + this.packageNo = packageNo; + } + + public String getMsgId() { + return msgId; + } + + public void setMsgId(String msgId) { + this.msgId = msgId; + } + + public String getRespId() { + return respId; + } + + public void setRespId(String respId) { + this.respId = respId; + } + + public Rs getRs() { + return rs; + } + + public void setRs(Rs rs) { + this.rs = rs; + } + + public static class Builder { + String devId; + Long packageNo; + String msgId; + String respId; + Rs rs; + + public Builder setDevId(String devId) { + this.devId = devId.replaceFirst("^0*", ""); + return this; + } + + public Builder setPackageNo(Long packageNo) { + this.packageNo = packageNo; + return this; + } + + public Builder setMsgId(String msgId) { + this.msgId = msgId; + return this; + } + + public Builder setRespId(String respId) { + this.respId = respId; + return this; + } + + public Builder setRs(Rs re) { + this.rs = re; + return this; + } + + public Cmd build() { + return new Cmd(this); + } + } + +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/proc/factory/CodecFactory.java b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/factory/CodecFactory.java new file mode 100644 index 000000000..45d5fc71f --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/factory/CodecFactory.java @@ -0,0 +1,44 @@ +package com.genersoft.iot.vmp.jt1078.proc.factory; + +import com.genersoft.iot.vmp.jt1078.annotation.MsgId; +import com.genersoft.iot.vmp.jt1078.proc.request.Re; +import com.genersoft.iot.vmp.jt1078.util.ClassUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * @author QingtaiJiang + * @date 2023/4/27 18:29 + * @email qingtaij@163.com + */ + +public class CodecFactory { + private final static Logger log = LoggerFactory.getLogger(CodecFactory.class); + + private static Map> protocolHash; + + public static void init() { + protocolHash = new HashMap<>(); + List> classList = ClassUtil.getClassList("com.genersoft.iot.vmp.jt1078.proc", MsgId.class); + for (Class handlerClass : classList) { + String id = handlerClass.getAnnotation(MsgId.class).id(); + protocolHash.put(id, handlerClass); + } + if (log.isDebugEnabled()) { + log.debug("消息ID缓存表 protocolHash:{}", protocolHash); + } + } + + public static Re getHandler(String msgId) { + Class aClass = protocolHash.get(msgId); + Object bean = ClassUtil.getBean(aClass); + if (bean instanceof Re) { + return (Re) bean; + } + return null; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/proc/request/J0001.java b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/request/J0001.java new file mode 100644 index 000000000..1d7f85db0 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/request/J0001.java @@ -0,0 +1,50 @@ +package com.genersoft.iot.vmp.jt1078.proc.request; + +import com.alibaba.fastjson2.JSON; +import com.genersoft.iot.vmp.jt1078.annotation.MsgId; +import com.genersoft.iot.vmp.jt1078.proc.Header; +import com.genersoft.iot.vmp.jt1078.proc.response.Rs; +import com.genersoft.iot.vmp.jt1078.session.Session; +import com.genersoft.iot.vmp.jt1078.session.SessionManager; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufUtil; + +/** + * 终端通用应答 + * + * @author QingtaiJiang + * @date 2023/4/27 18:04 + * @email qingtaij@163.com + */ +@MsgId(id = "0001") +public class J0001 extends Re { + int respNo; + String respId; + int result; + + @Override + protected Rs decode0(ByteBuf buf, Header header, Session session) { + respNo = buf.readUnsignedShort(); + respId = ByteBufUtil.hexDump(buf.readSlice(2)); + result = buf.readUnsignedByte(); + return null; + } + + @Override + protected Rs handler(Header header, Session session) { + SessionManager.INSTANCE.response(header.getDevId(), "0001", (long) respNo, JSON.toJSONString(this)); + return null; + } + + public int getRespNo() { + return respNo; + } + + public String getRespId() { + return respId; + } + + public int getResult() { + return result; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/proc/request/J0002.java b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/request/J0002.java new file mode 100644 index 000000000..f52303a6f --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/request/J0002.java @@ -0,0 +1,32 @@ +package com.genersoft.iot.vmp.jt1078.proc.request; + +import com.genersoft.iot.vmp.jt1078.annotation.MsgId; +import com.genersoft.iot.vmp.jt1078.proc.Header; +import com.genersoft.iot.vmp.jt1078.proc.response.J8001; +import com.genersoft.iot.vmp.jt1078.proc.response.Rs; +import com.genersoft.iot.vmp.jt1078.session.Session; +import io.netty.buffer.ByteBuf; + +/** + * 终端心跳 + * + * @author QingtaiJiang + * @date 2023/4/27 18:04 + * @email qingtaij@163.com + */ +@MsgId(id = "0002") +public class J0002 extends Re { + @Override + protected Rs decode0(ByteBuf buf, Header header, Session session) { + return null; + } + + @Override + protected Rs handler(Header header, Session session) { + J8001 j8001 = new J8001(); + j8001.setRespNo(header.getSn()); + j8001.setRespId(header.getMsgId()); + j8001.setResult(J8001.SUCCESS); + return j8001; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/proc/request/J0004.java b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/request/J0004.java new file mode 100644 index 000000000..0f00a8013 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/request/J0004.java @@ -0,0 +1,27 @@ +package com.genersoft.iot.vmp.jt1078.proc.request; + +import com.genersoft.iot.vmp.jt1078.annotation.MsgId; +import com.genersoft.iot.vmp.jt1078.proc.Header; +import com.genersoft.iot.vmp.jt1078.proc.response.Rs; +import com.genersoft.iot.vmp.jt1078.session.Session; +import io.netty.buffer.ByteBuf; + +/** + * 查询服务器时间 + * + * @author QingtaiJiang + * @date 2023/4/27 18:06 + * @email qingtaij@163.com + */ +@MsgId(id = "0004") +public class J0004 extends Re { + @Override + protected Rs decode0(ByteBuf buf, Header header, Session session) { + return null; + } + + @Override + protected Rs handler(Header header, Session session) { + return null; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/proc/request/J0100.java b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/request/J0100.java new file mode 100644 index 000000000..a731dda63 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/request/J0100.java @@ -0,0 +1,56 @@ +package com.genersoft.iot.vmp.jt1078.proc.request; + +import com.genersoft.iot.vmp.jt1078.annotation.MsgId; +import com.genersoft.iot.vmp.jt1078.proc.Header; +import com.genersoft.iot.vmp.jt1078.proc.response.J8100; +import com.genersoft.iot.vmp.jt1078.proc.response.Rs; +import com.genersoft.iot.vmp.jt1078.session.Session; +import io.netty.buffer.ByteBuf; + +/** + * 终端注册 + * + * @author QingtaiJiang + * @date 2023/4/27 18:06 + * @email qingtaij@163.com + */ +@MsgId(id = "0100") +public class J0100 extends Re { + + private int provinceId; + + private int cityId; + + private String makerId; + + private String deviceModel; + + private String deviceId; + + private int plateColor; + + private String plateNo; + + @Override + protected Rs decode0(ByteBuf buf, Header header, Session session) { + Short version = header.getVersion(); + provinceId = buf.readUnsignedShort(); + if (version > 1) { + cityId = buf.readUnsignedShort(); + // decode as 2019 + } else { + int i = buf.readUnsignedShort(); + // decode as 2013 + } + return null; + } + + @Override + protected Rs handler(Header header, Session session) { + J8100 j8100 = new J8100(); + j8100.setRespNo(header.getSn()); + j8100.setResult(J8100.SUCCESS); + j8100.setCode("WVP_YYDS"); + return j8100; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/proc/request/J0102.java b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/request/J0102.java new file mode 100644 index 000000000..8e531ae41 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/request/J0102.java @@ -0,0 +1,36 @@ +package com.genersoft.iot.vmp.jt1078.proc.request; + +import com.genersoft.iot.vmp.jt1078.annotation.MsgId; +import com.genersoft.iot.vmp.jt1078.proc.Header; +import com.genersoft.iot.vmp.jt1078.proc.response.J8001; +import com.genersoft.iot.vmp.jt1078.proc.response.Rs; +import com.genersoft.iot.vmp.jt1078.session.Session; +import io.netty.buffer.ByteBuf; + +/** + * 终端鉴权 + * + * @author QingtaiJiang + * @date 2023/4/27 18:06 + * @email qingtaij@163.com + */ +@MsgId(id = "0102") +public class J0102 extends Re { + @Override + protected Rs decode0(ByteBuf buf, Header header, Session session) { + int lenCode = buf.readUnsignedByte(); +// String code = buf.readCharSequence(lenCode, CharsetUtil.UTF_8).toString(); + // if 2019 to decode next + return null; + } + + @Override + protected Rs handler(Header header, Session session) { + J8001 j8001 = new J8001(); + j8001.setRespNo(header.getSn()); + j8001.setRespId(header.getMsgId()); + j8001.setResult(J8001.SUCCESS); + return j8001; + } + +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/proc/request/J0200.java b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/request/J0200.java new file mode 100644 index 000000000..d027dd2e0 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/request/J0200.java @@ -0,0 +1,32 @@ +package com.genersoft.iot.vmp.jt1078.proc.request; + +import com.genersoft.iot.vmp.jt1078.annotation.MsgId; +import com.genersoft.iot.vmp.jt1078.proc.Header; +import com.genersoft.iot.vmp.jt1078.proc.response.J8001; +import com.genersoft.iot.vmp.jt1078.proc.response.Rs; +import com.genersoft.iot.vmp.jt1078.session.Session; +import io.netty.buffer.ByteBuf; + +/** + * 实时消息上报 + * + * @author QingtaiJiang + * @date 2023/4/27 18:06 + * @email qingtaij@163.com + */ +@MsgId(id = "0200") +public class J0200 extends Re { + @Override + protected Rs decode0(ByteBuf buf, Header header, Session session) { + return null; + } + + @Override + protected Rs handler(Header header, Session session) { + J8001 j8001 = new J8001(); + j8001.setRespNo(header.getSn()); + j8001.setRespId(header.getMsgId()); + j8001.setResult(J8001.SUCCESS); + return j8001; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/proc/request/Re.java b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/request/Re.java new file mode 100644 index 000000000..0a24ad274 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/request/Re.java @@ -0,0 +1,40 @@ +package com.genersoft.iot.vmp.jt1078.proc.request; + +import com.genersoft.iot.vmp.jt1078.proc.Header; +import com.genersoft.iot.vmp.jt1078.proc.response.Rs; +import com.genersoft.iot.vmp.jt1078.session.Session; +import io.netty.buffer.ByteBuf; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.util.StringUtils; + +/** + * @author QingtaiJiang + * @date 2023/4/27 18:50 + * @email qingtaij@163.com + */ +public abstract class Re { + private final static Logger log = LoggerFactory.getLogger(Re.class); + + protected abstract Rs decode0(ByteBuf buf, Header header, Session session); + + protected abstract Rs handler(Header header, Session session); + + public Rs decode(ByteBuf buf, Header header, Session session) { + if (session != null && !StringUtils.hasLength(session.getDevId())) { + session.register(header.getDevId(), (int) header.getVersion(), header); + } + Rs rs = decode0(buf, header, session); + Rs rsHand = handler(header, session); + if (rs == null && rsHand != null) { + rs = rsHand; + } else if (rs != null && rsHand != null) { + log.warn("decode0:{} 与 handler:{} 返回值冲突,采用decode0返回值", rs, rsHand); + } + if (rs != null) { + rs.setHeader(header); + } + + return rs; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J8001.java b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J8001.java new file mode 100644 index 000000000..ec9e31f19 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J8001.java @@ -0,0 +1,43 @@ +package com.genersoft.iot.vmp.jt1078.proc.response; + +import com.genersoft.iot.vmp.jt1078.annotation.MsgId; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufUtil; +import io.netty.buffer.Unpooled; + +/** + * @author QingtaiJiang + * @date 2023/4/27 18:48 + * @email qingtaij@163.com + */ +@MsgId(id = "8001") +public class J8001 extends Rs { + public static final Integer SUCCESS = 0; + + Integer respNo; + String respId; + Integer result; + + @Override + public ByteBuf encode() { + ByteBuf buffer = Unpooled.buffer(); + buffer.writeShort(respNo); + buffer.writeBytes(ByteBufUtil.decodeHexDump(respId)); + buffer.writeByte(result); + + return buffer; + } + + + public void setRespNo(Integer respNo) { + this.respNo = respNo; + } + + public void setRespId(String respId) { + this.respId = respId; + } + + public void setResult(Integer result) { + this.result = result; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J8100.java b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J8100.java new file mode 100644 index 000000000..48a9c95e6 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J8100.java @@ -0,0 +1,41 @@ +package com.genersoft.iot.vmp.jt1078.proc.response; + +import com.genersoft.iot.vmp.jt1078.annotation.MsgId; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.netty.util.CharsetUtil; + +/** + * @author QingtaiJiang + * @date 2023/4/27 18:40 + * @email qingtaij@163.com + */ +@MsgId(id = "8100") +public class J8100 extends Rs { + public static final Integer SUCCESS = 0; + + Integer respNo; + Integer result; + String code; + + @Override + public ByteBuf encode() { + ByteBuf buffer = Unpooled.buffer(); + buffer.writeShort(respNo); + buffer.writeByte(result); + buffer.writeCharSequence(code, CharsetUtil.UTF_8); + return buffer; + } + + public void setRespNo(Integer respNo) { + this.respNo = respNo; + } + + public void setResult(Integer result) { + this.result = result; + } + + public void setCode(String code) { + this.code = code; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J9101.java b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J9101.java new file mode 100644 index 000000000..d6713723b --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J9101.java @@ -0,0 +1,110 @@ +package com.genersoft.iot.vmp.jt1078.proc.response; + +import com.genersoft.iot.vmp.jt1078.annotation.MsgId; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.netty.util.CharsetUtil; + +/** + * @author QingtaiJiang + * @date 2023/4/27 18:25 + * @email qingtaij@163.com + */ +@MsgId(id = "9101") +public class J9101 extends Rs { + String ip; + + // TCP端口 + Integer tcpPort; + + // UDP端口 + Integer udpPort; + + // 逻辑通道号 + Integer channel; + + // 数据类型 + /** + * 0:音视频,1:视频,2:双向对讲,3:监听,4:中心广播,5:透传 + */ + Integer type; + + // 码流类型 + /** + * 0:主码流,1:子码流 + */ + Integer rate; + + @Override + public ByteBuf encode() { + ByteBuf buffer = Unpooled.buffer(); + buffer.writeByte(ip.getBytes().length); + buffer.writeCharSequence(ip, CharsetUtil.UTF_8); + buffer.writeShort(tcpPort); + buffer.writeShort(udpPort); + buffer.writeByte(channel); + buffer.writeByte(type); + buffer.writeByte(rate); + return buffer; + } + + public String getIp() { + return ip; + } + + public void setIp(String ip) { + this.ip = ip; + } + + public Integer getTcpPort() { + return tcpPort; + } + + public void setTcpPort(Integer tcpPort) { + this.tcpPort = tcpPort; + } + + public Integer getUdpPort() { + return udpPort; + } + + public void setUdpPort(Integer udpPort) { + this.udpPort = udpPort; + } + + public Integer getChannel() { + return channel; + } + + public void setChannel(Integer channel) { + this.channel = channel; + } + + public Integer getType() { + return type; + } + + public void setType(Integer type) { + this.type = type; + } + + public Integer getRate() { + return rate; + } + + public void setRate(Integer rate) { + this.rate = rate; + } + + @Override + public String toString() { + return "J9101{" + + "ip='" + ip + '\'' + + ", tcpPort=" + tcpPort + + ", udpPort=" + udpPort + + ", channel=" + channel + + ", type=" + type + + ", rate=" + rate + + '}'; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J9102.java b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J9102.java new file mode 100644 index 000000000..f92fe8e7c --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J9102.java @@ -0,0 +1,85 @@ +package com.genersoft.iot.vmp.jt1078.proc.response; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; + +/** + * @author QingtaiJiang + * @date 2023/4/27 18:49 + * @email qingtaij@163.com + */ +public class J9102 extends Rs { + + // 通道号 + Integer channel; + + // 控制指令 + /** + * 0:关闭音视频传输指令; + * 1:切换码流(增加暂停和继续); + * 2:暂停该通道所有流的发送; + * 3:恢复暂停前流的发送,与暂停前的流类型一致; + * 4:关闭双向对讲 + */ + Integer command; + + // 数据类型 + /** + * 0:关闭该通道有关的音视频数据; + * 1:只关闭该通道有关的音频,保留该通道 + * 有关的视频; + * 2:只关闭该通道有关的视频,保留该通道 + * 有关的音频 + */ + Integer closeType; + + // 数据类型 + /** + * 0:主码流; + * 1:子码流 + */ + Integer streamType; + + @Override + public ByteBuf encode() { + ByteBuf buffer = Unpooled.buffer(); + buffer.writeByte(channel); + buffer.writeByte(command); + buffer.writeByte(closeType); + buffer.writeByte(streamType); + return null; + } + + + public Integer getChannel() { + return channel; + } + + public void setChannel(Integer channel) { + this.channel = channel; + } + + public Integer getCommand() { + return command; + } + + public void setCommand(Integer command) { + this.command = command; + } + + public Integer getCloseType() { + return closeType; + } + + public void setCloseType(Integer closeType) { + this.closeType = closeType; + } + + public Integer getStreamType() { + return streamType; + } + + public void setStreamType(Integer streamType) { + this.streamType = streamType; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/Rs.java b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/Rs.java new file mode 100644 index 000000000..243cd9424 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/Rs.java @@ -0,0 +1,27 @@ +package com.genersoft.iot.vmp.jt1078.proc.response; + + +import com.genersoft.iot.vmp.jt1078.proc.Header; +import io.netty.buffer.ByteBuf; + + +/** + * @author QingtaiJiang + * @date 2021/8/30 18:54 + * @email qingtaij@163.com + */ + +public abstract class Rs { + private Header header; + + public abstract ByteBuf encode(); + + + public Header getHeader() { + return header; + } + + public void setHeader(Header header) { + this.header = header; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/session/Session.java b/src/main/java/com/genersoft/iot/vmp/jt1078/session/Session.java new file mode 100644 index 000000000..f7df8de0d --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/session/Session.java @@ -0,0 +1,114 @@ +package com.genersoft.iot.vmp.jt1078.session; + +import com.genersoft.iot.vmp.jt1078.proc.Header; +import io.netty.channel.Channel; +import io.netty.util.AttributeKey; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.concurrent.atomic.AtomicInteger; + +/** + * @author QingtaiJiang + * @date 2023/4/27 18:54 + * @email qingtaij@163.com + */ +public class Session { + private final static Logger log = LoggerFactory.getLogger(Session.class); + + public static final AttributeKey KEY = AttributeKey.newInstance(Session.class.getName()); + + // Netty的channel + protected final Channel channel; + + // 原子类的自增ID + private final AtomicInteger serialNo = new AtomicInteger(0); + + // 是否注册成功 + private boolean registered = false; + + // 设备ID + private String devId; + + // 创建时间 + private final long creationTime; + + // 协议版本号 + private Integer protocolVersion; + + private Header header; + + protected Session(Channel channel) { + this.channel = channel; + this.creationTime = System.currentTimeMillis(); + } + + public void writeObject(Object message) { + log.info("<<<<<<<<<< cmd{},{}", this, message); + channel.writeAndFlush(message); + } + + /** + * 获得下一个流水号 + * + * @return 流水号 + */ + public int nextSerialNo() { + int current; + int next; + do { + current = serialNo.get(); + next = current > 0xffff ? 0 : current; + } while (!serialNo.compareAndSet(current, next + 1)); + return next; + } + + /** + * 注册session + * + * @param devId 设备ID + */ + public void register(String devId, Integer version, Header header) { + this.devId = devId; + this.registered = true; + this.protocolVersion = version; + this.header = header; + SessionManager.INSTANCE.put(devId, this); + } + + /** + * 获取设备号 + * + * @return 设备号 + */ + public String getDevId() { + return devId; + } + + + public boolean isRegistered() { + return registered; + } + + public long getCreationTime() { + return creationTime; + } + + public Integer getProtocolVersion() { + return protocolVersion; + } + + public Header getHeader() { + return header; + } + + @Override + public String toString() { + return "[" + + "devId=" + devId + + ", reg=" + registered + + ", version=" + protocolVersion + + ",ip=" + channel.remoteAddress() + + ']'; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/session/SessionManager.java b/src/main/java/com/genersoft/iot/vmp/jt1078/session/SessionManager.java new file mode 100644 index 000000000..9347249e6 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/session/SessionManager.java @@ -0,0 +1,127 @@ +package com.genersoft.iot.vmp.jt1078.session; + +import com.genersoft.iot.vmp.jt1078.proc.entity.Cmd; +import io.netty.channel.Channel; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.SynchronousQueue; +import java.util.concurrent.TimeUnit; + + +/** + * @author QingtaiJiang + * @date 2023/4/27 19:54 + * @email qingtaij@163.com + */ +public enum SessionManager { + INSTANCE; + private final static Logger log = LoggerFactory.getLogger(SessionManager.class); + + // 用与消息的缓存 + private final Map> topicSubscribers = new ConcurrentHashMap<>(); + + // session的缓存 + private final Map sessionMap; + + SessionManager() { + this.sessionMap = new ConcurrentHashMap<>(); + } + + /** + * 创建新的Session + * + * @param channel netty通道 + * @return 创建的session对象 + */ + public Session newSession(Channel channel) { + return new Session(channel); + } + + + /** + * 获取指定设备的Session + * + * @param clientId 设备Id + * @return Session + */ + public Session get(Object clientId) { + return sessionMap.get(clientId); + } + + /** + * 放入新设备连接的session + * + * @param clientId 设备ID + * @param newSession session + */ + protected void put(Object clientId, Session newSession) { + sessionMap.put(clientId, newSession); + } + + + /** + * 发送同步消息,接收响应 + * 默认超时时间6秒 + */ + public String request(Cmd cmd) { + // 默认6秒 + int timeOut = 6000; + return request(cmd, timeOut); + } + + public String request(Cmd cmd, Integer timeOut) { + Session session = this.get(cmd.getDevId()); + if (session == null) { + log.error("DevId: {} not online!", cmd.getDevId()); + return "-1"; + } + String requestKey = requestKey(cmd.getDevId(), cmd.getRespId(), cmd.getPackageNo()); + SynchronousQueue subscribe = subscribe(requestKey); + if (subscribe == null) { + log.error("DevId: {} key:{} send repaid", cmd.getDevId(), requestKey); + return "-1"; + } + session.writeObject(cmd); + try { + return subscribe.poll(timeOut, TimeUnit.SECONDS); + } catch (InterruptedException e) { + log.warn("<<<<<<<<<< timeout" + session, e); + } finally { + this.unsubscribe(requestKey); + } + return null; + } + + public Boolean response(String devId, String respId, Long responseNo, String data) { + String requestKey = requestKey(devId, respId, responseNo); + SynchronousQueue queue = topicSubscribers.get(requestKey); + if (queue != null) { + try { + return queue.offer(data, 2, TimeUnit.SECONDS); + } catch (InterruptedException e) { + log.error("{}", e.getMessage(), e); + } + } + log.warn("未找到对应回复指令,key:{} 消息:{} ", requestKey, data); + return false; + } + + private void unsubscribe(String key) { + topicSubscribers.remove(key); + } + + private SynchronousQueue subscribe(String key) { + SynchronousQueue queue = null; + if (!topicSubscribers.containsKey(key)) + topicSubscribers.put(key, queue = new SynchronousQueue()); + return queue; + } + + private String requestKey(String devId, String respId, Long requestNo) { + return String.join("_", devId.replaceFirst("^0*", ""), respId, requestNo.toString()); + } + +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/util/Bin.java b/src/main/java/com/genersoft/iot/vmp/jt1078/util/Bin.java new file mode 100644 index 000000000..31f8b9301 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/util/Bin.java @@ -0,0 +1,41 @@ +package com.genersoft.iot.vmp.jt1078.util; + +/** + * 32位整型的二进制读写 + */ +public class Bin { + + private static final int[] bits = new int[32]; + + static { + bits[0] = 1; + for (int i = 1; i < bits.length; i++) { + bits[i] = bits[i - 1] << 1; + } + } + + /** + * 读取n的第i位 + * + * @param n int32 + * @param i 取值范围0-31 + */ + public static boolean get(int n, int i) { + return (n & bits[i]) == bits[i]; + } + + /** + * 不足位数从左边加0 + */ + public static String strHexPaddingLeft(String data, int length) { + int dataLength = data.length(); + if (dataLength < length) { + StringBuilder dataBuilder = new StringBuilder(data); + for (int i = dataLength; i < length; i++) { + dataBuilder.insert(0, "0"); + } + data = dataBuilder.toString(); + } + return data; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/util/ClassUtil.java b/src/main/java/com/genersoft/iot/vmp/jt1078/util/ClassUtil.java new file mode 100644 index 000000000..3dcb1b860 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/util/ClassUtil.java @@ -0,0 +1,112 @@ +package com.genersoft.iot.vmp.jt1078.util; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.core.io.Resource; +import org.springframework.core.io.support.PathMatchingResourcePatternResolver; +import org.springframework.core.io.support.ResourcePatternResolver; + +import java.lang.annotation.Annotation; +import java.util.LinkedList; +import java.util.List; + +public class ClassUtil { + + private static final Logger logger = LoggerFactory.getLogger(ClassUtil.class); + + + public static Object getBean(Class clazz) { + if (clazz != null) { + try { + return clazz.getDeclaredConstructor().newInstance(); + } catch (Exception ex) { + logger.error("ClassUtil:找不到指定的类", ex); + } + } + return null; + } + + + public static Object getBean(String className) { + Class clazz = null; + try { + clazz = Class.forName(className); + } catch (Exception ex) { + logger.error("ClassUtil:找不到指定的类"); + } + if (clazz != null) { + try { + //获取声明的构造器--》创建实例 + return clazz.getDeclaredConstructor().newInstance(); + } catch (Exception ex) { + logger.error("ClassUtil:找不到指定的类", ex); + } + } + return null; + } + + + /** + * 获取包下所有带注解的class + * + * @param packageName 包名 + * @param annotationClass 注解类型 + * @return list + */ + public static List> getClassList(String packageName, Class annotationClass) { + List> classList = getClassList(packageName); + classList.removeIf(next -> !next.isAnnotationPresent(annotationClass)); + return classList; + } + + public static List> getClassList(String... packageName) { + List> classList = new LinkedList<>(); + for (String s : packageName) { + List> c = getClassList(s); + classList.addAll(c); + } + return classList; + } + + public static List> getClassList(String packageName) { + List> classList = new LinkedList<>(); + try { + ResourcePatternResolver resourcePatternResolver = new PathMatchingResourcePatternResolver(); + Resource[] resources = resourcePatternResolver.getResources(packageName.replace(".", "/") + "/**/*.class"); + for (Resource resource : resources) { + String url = resource.getURL().toString(); + + String[] split = url.split(packageName.replace(".", "/")); + String s = split[split.length - 1]; + String className = s.replace("/", "."); + className = className.substring(0, className.lastIndexOf(".")); + doAddClass(classList, packageName + className); + } + + } catch (Exception e) { + throw new RuntimeException(e); + } + return classList; + } + + private static void doAddClass(List> classList, String className) { + Class cls = loadClass(className, false); + classList.add(cls); + } + + public static Class loadClass(String className, boolean isInitialized) { + Class cls; + try { + cls = Class.forName(className, isInitialized, getClassLoader()); + } catch (ClassNotFoundException e) { + throw new RuntimeException(e); + } + return cls; + } + + + public static ClassLoader getClassLoader() { + return Thread.currentThread().getContextClassLoader(); + } + +} diff --git a/src/main/resources/all-application.yml b/src/main/resources/all-application.yml index 0fba9a92f..cc2145a46 100644 --- a/src/main/resources/all-application.yml +++ b/src/main/resources/all-application.yml @@ -92,6 +92,15 @@ sip: # 是否存储alarm信息 alarm: false +# 做为JT1078服务器的配置 +jt1078: + #[必须修改] 是否开启1078的服务 + enable: true + #[必修修改] 1708设备接入的端口 + port: 21078 + #[可选] 设备鉴权的密码 + password: admin123 + #zlm 默认服务器配置 media: # [必须修改] zlm服务器唯一id,用于触发hook时区别是哪台服务器,general.mediaServerId From b1fb1c4616a1924496465c6cff60acf328d7ab05 Mon Sep 17 00:00:00 2001 From: QingObject <1120359293@qq.com> Date: Fri, 28 Apr 2023 14:39:50 +0800 Subject: [PATCH 35/48] =?UTF-8?q?1=E3=80=81=E6=96=B0=E5=A2=9E=E8=A7=86?= =?UTF-8?q?=E9=A2=91=E5=9B=9E=E6=94=BE=E3=80=81=E8=A7=86=E9=A2=91=E5=9B=9E?= =?UTF-8?q?=E6=94=BE=E6=8E=A7=E5=88=B6Template=202=E3=80=81=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D=E7=9B=B4=E6=92=AD=E6=8E=A7=E5=88=B6=E6=8C=87=E4=BB=A4?= =?UTF-8?q?BUG?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../iot/vmp/jt1078/cmd/JT1078Template.java | 71 ++++++- ...tion.java => JT1078AutoConfiguration.java} | 2 +- .../vmp/jt1078/config/JT1078Controller.java | 9 +- .../iot/vmp/jt1078/proc/entity/Cmd.java | 11 + .../iot/vmp/jt1078/proc/request/J1205.java | 190 ++++++++++++++++++ .../iot/vmp/jt1078/proc/response/J9101.java | 2 + .../iot/vmp/jt1078/proc/response/J9102.java | 16 +- .../iot/vmp/jt1078/proc/response/J9201.java | 173 ++++++++++++++++ .../iot/vmp/jt1078/proc/response/J9202.java | 80 ++++++++ .../iot/vmp/jt1078/proc/response/J9205.java | 94 +++++++++ .../vmp/jt1078/session/SessionManager.java | 6 +- .../iot/vmp/jt1078/JT1078ServerTest.java | 103 ++++++++++ 12 files changed, 744 insertions(+), 13 deletions(-) rename src/main/java/com/genersoft/iot/vmp/jt1078/config/{TcpAutoConfiguration.java => JT1078AutoConfiguration.java} (95%) create mode 100644 src/main/java/com/genersoft/iot/vmp/jt1078/proc/request/J1205.java create mode 100644 src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J9201.java create mode 100644 src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J9202.java create mode 100644 src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J9205.java create mode 100644 src/test/java/com/genersoft/iot/vmp/jt1078/JT1078ServerTest.java diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/cmd/JT1078Template.java b/src/main/java/com/genersoft/iot/vmp/jt1078/cmd/JT1078Template.java index ad3ab0063..c55c62760 100644 --- a/src/main/java/com/genersoft/iot/vmp/jt1078/cmd/JT1078Template.java +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/cmd/JT1078Template.java @@ -1,8 +1,7 @@ package com.genersoft.iot.vmp.jt1078.cmd; import com.genersoft.iot.vmp.jt1078.proc.entity.Cmd; -import com.genersoft.iot.vmp.jt1078.proc.response.J9101; -import com.genersoft.iot.vmp.jt1078.proc.response.J9102; +import com.genersoft.iot.vmp.jt1078.proc.response.*; import com.genersoft.iot.vmp.jt1078.session.SessionManager; import java.util.Random; @@ -16,6 +15,15 @@ public class JT1078Template { private final Random random = new Random(); + private static final String H9101 = "9101"; + private static final String H9102 = "9102"; + private static final String H9201 = "9201"; + private static final String H9202 = "9202"; + private static final String H9205 = "9205"; + + private static final String H0001 = "0001"; + private static final String H1205 = "1205"; + /** * 开启直播视频 * @@ -26,8 +34,8 @@ public class JT1078Template { Cmd cmd = new Cmd.Builder() .setDevId(devId) .setPackageNo(randomInt()) - .setMsgId("9101") - .setRespId("0001") + .setMsgId(H9101) + .setRespId(H0001) .setRs(j9101) .build(); return SessionManager.INSTANCE.request(cmd, timeOut); @@ -43,13 +51,64 @@ public class JT1078Template { Cmd cmd = new Cmd.Builder() .setDevId(devId) .setPackageNo(randomInt()) - .setMsgId("9102") - .setRespId("0001") + .setMsgId(H9102) + .setRespId(H0001) .setRs(j9102) .build(); return SessionManager.INSTANCE.request(cmd, timeOut); } + /** + * 查询音视频列表 + * + * @param devId 设备号 + * @param j9205 查询音视频列表 + */ + public String queryBackTime(String devId, J9205 j9205, Integer timeOut) { + Cmd cmd = new Cmd.Builder() + .setDevId(devId) + .setPackageNo(randomInt()) + .setMsgId(H9205) + .setRespId(H1205) + .setRs(j9205) + .build(); + return SessionManager.INSTANCE.request(cmd, timeOut); + } + + /** + * 开启视频回放 + * + * @param devId 设备号 + * @param j9201 视频回放参数 + */ + public String startBackLive(String devId, J9201 j9201, Integer timeOut) { + Cmd cmd = new Cmd.Builder() + .setDevId(devId) + .setPackageNo(randomInt()) + .setMsgId(H9201) + .setRespId(H1205) + .setRs(j9201) + .build(); + return SessionManager.INSTANCE.request(cmd, timeOut); + } + + /** + * 视频回放控制 + * + * @param devId 设备号 + * @param j9202 控制视频回放参数 + */ + public String controlBackLive(String devId, J9202 j9202, Integer timeOut) { + Cmd cmd = new Cmd.Builder() + .setDevId(devId) + .setPackageNo(randomInt()) + .setMsgId(H9202) + .setRespId(H0001) + .setRs(j9202) + .build(); + return SessionManager.INSTANCE.request(cmd, timeOut); + } + private Long randomInt() { return (long) random.nextInt(1000) + 1; } diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/config/TcpAutoConfiguration.java b/src/main/java/com/genersoft/iot/vmp/jt1078/config/JT1078AutoConfiguration.java similarity index 95% rename from src/main/java/com/genersoft/iot/vmp/jt1078/config/TcpAutoConfiguration.java rename to src/main/java/com/genersoft/iot/vmp/jt1078/config/JT1078AutoConfiguration.java index 0b07bb430..6cac30cec 100644 --- a/src/main/java/com/genersoft/iot/vmp/jt1078/config/TcpAutoConfiguration.java +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/config/JT1078AutoConfiguration.java @@ -16,7 +16,7 @@ import org.springframework.core.annotation.Order; @Order(Integer.MIN_VALUE) @Configuration @ConditionalOnProperty(value = "jt1078.enable", havingValue = "true") -public class TcpAutoConfiguration { +public class JT1078AutoConfiguration { @Bean(initMethod = "start", destroyMethod = "stop") public TcpServer jt1078Server(@Value("${jt1078.port}") Integer port) { diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/config/JT1078Controller.java b/src/main/java/com/genersoft/iot/vmp/jt1078/config/JT1078Controller.java index cffb147d2..0c71d26a9 100644 --- a/src/main/java/com/genersoft/iot/vmp/jt1078/config/JT1078Controller.java +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/config/JT1078Controller.java @@ -1,7 +1,7 @@ package com.genersoft.iot.vmp.jt1078.config; import com.genersoft.iot.vmp.jt1078.cmd.JT1078Template; -import com.genersoft.iot.vmp.jt1078.proc.response.J9101; +import com.genersoft.iot.vmp.jt1078.proc.response.*; import com.genersoft.iot.vmp.vmanager.bean.WVPResult; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.web.bind.annotation.GetMapping; @@ -26,6 +26,9 @@ public class JT1078Controller { @Resource JT1078Template jt1078Template; + /** + * jt1078Template 调用示例 + */ @GetMapping("/start/live/{deviceId}/{channelId}") public WVPResult startLive(@PathVariable String deviceId, @PathVariable String channelId) { J9101 j9101 = new J9101(); @@ -35,12 +38,14 @@ public class JT1078Controller { j9101.setTcpPort(7618); j9101.setUdpPort(7618); j9101.setType(0); - + // TODO 分配ZLM,获取IP、端口 String s = jt1078Template.startLive(deviceId, j9101, 6); + // TODO 设备响应成功后,封装拉流结果集 WVPResult wvpResult = new WVPResult<>(); wvpResult.setCode(200); wvpResult.setData(String.format("http://192.168.1.1/rtp/%s_%s.live.mp4", deviceId, channelId)); return wvpResult; } + } diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/proc/entity/Cmd.java b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/entity/Cmd.java index 19d6d8f1d..28726e83b 100644 --- a/src/main/java/com/genersoft/iot/vmp/jt1078/proc/entity/Cmd.java +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/entity/Cmd.java @@ -102,4 +102,15 @@ public class Cmd { } } + + @Override + public String toString() { + return "Cmd{" + + "devId='" + devId + '\'' + + ", packageNo=" + packageNo + + ", msgId='" + msgId + '\'' + + ", respId='" + respId + '\'' + + ", rs=" + rs + + '}'; + } } diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/proc/request/J1205.java b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/request/J1205.java new file mode 100644 index 000000000..da0b89ec3 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/request/J1205.java @@ -0,0 +1,190 @@ +package com.genersoft.iot.vmp.jt1078.proc.request; + +import com.alibaba.fastjson2.JSON; +import com.genersoft.iot.vmp.jt1078.annotation.MsgId; +import com.genersoft.iot.vmp.jt1078.proc.Header; +import com.genersoft.iot.vmp.jt1078.proc.response.J8001; +import com.genersoft.iot.vmp.jt1078.proc.response.Rs; +import com.genersoft.iot.vmp.jt1078.session.Session; +import com.genersoft.iot.vmp.jt1078.session.SessionManager; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufUtil; + +import java.util.ArrayList; +import java.util.List; + +/** + * 终端上传音视频资源列表 + * + * @author QingtaiJiang + * @date 2023/4/28 10:36 + * @email qingtaij@163.com + */ +@MsgId(id = "1205") +public class J1205 extends Re { + Integer respNo; + + private List recordList = new ArrayList(); + + @Override + protected Rs decode0(ByteBuf buf, Header header, Session session) { + respNo = buf.readUnsignedShort(); + long size = buf.readUnsignedInt(); + + for (int i = 0; i < size; i++) { + JRecordItem item = new JRecordItem(); + item.setChannelId(buf.readUnsignedByte()); + item.setStartTime(ByteBufUtil.hexDump(buf.readSlice(6))); + item.setEndTime(ByteBufUtil.hexDump(buf.readSlice(6))); + item.setWarn(buf.readLong()); + item.setMediaType(buf.readUnsignedByte()); + item.setStreamType(buf.readUnsignedByte()); + item.setStorageType(buf.readUnsignedByte()); + item.setSize(buf.readUnsignedInt()); + recordList.add(item); + } + + return null; + } + + @Override + protected Rs handler(Header header, Session session) { + SessionManager.INSTANCE.response(header.getDevId(), "1205", (long) respNo, JSON.toJSONString(this)); + + J8001 j8001 = new J8001(); + j8001.setRespNo(header.getSn()); + j8001.setRespId(header.getMsgId()); + j8001.setResult(J8001.SUCCESS); + return j8001; + } + + + public Integer getRespNo() { + return respNo; + } + + public void setRespNo(Integer respNo) { + this.respNo = respNo; + } + + public List getRecordList() { + return recordList; + } + + public void setRecordList(List recordList) { + this.recordList = recordList; + } + + public static class JRecordItem { + + // 逻辑通道号 + private int channelId; + + // 开始时间 + private String startTime; + + // 结束时间 + private String endTime; + + // 报警标志 + private long warn; + + // 音视频资源类型 + private int mediaType; + + // 码流类型 + private int streamType = 1; + + // 存储器类型 + private int storageType; + + // 文件大小 + private long size; + + public int getChannelId() { + return channelId; + } + + public void setChannelId(int channelId) { + this.channelId = channelId; + } + + public String getStartTime() { + return startTime; + } + + public void setStartTime(String startTime) { + this.startTime = startTime; + } + + public String getEndTime() { + return endTime; + } + + public void setEndTime(String endTime) { + this.endTime = endTime; + } + + public long getWarn() { + return warn; + } + + public void setWarn(long warn) { + this.warn = warn; + } + + public int getMediaType() { + return mediaType; + } + + public void setMediaType(int mediaType) { + this.mediaType = mediaType; + } + + public int getStreamType() { + return streamType; + } + + public void setStreamType(int streamType) { + this.streamType = streamType; + } + + public int getStorageType() { + return storageType; + } + + public void setStorageType(int storageType) { + this.storageType = storageType; + } + + public long getSize() { + return size; + } + + public void setSize(long size) { + this.size = size; + } + + @Override + public String toString() { + return "JRecordItem{" + + "channelId=" + channelId + + ", startTime='" + startTime + '\'' + + ", endTime='" + endTime + '\'' + + ", warn=" + warn + + ", mediaType=" + mediaType + + ", streamType=" + streamType + + ", storageType=" + storageType + + ", size=" + size + + '}'; + } + } + + @Override + public String toString() { + return "J1205{" + + "respNo=" + respNo + + ", recordList=" + recordList + + '}'; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J9101.java b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J9101.java index d6713723b..77e90b726 100644 --- a/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J9101.java +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J9101.java @@ -6,6 +6,8 @@ import io.netty.buffer.Unpooled; import io.netty.util.CharsetUtil; /** + * 实时音视频传输请求 + * * @author QingtaiJiang * @date 2023/4/27 18:25 * @email qingtaij@163.com diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J9102.java b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J9102.java index f92fe8e7c..8d560b20c 100644 --- a/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J9102.java +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J9102.java @@ -1,13 +1,17 @@ package com.genersoft.iot.vmp.jt1078.proc.response; +import com.genersoft.iot.vmp.jt1078.annotation.MsgId; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; /** + * 音视频实时传输控制 + * * @author QingtaiJiang * @date 2023/4/27 18:49 * @email qingtaij@163.com */ +@MsgId(id = "9102") public class J9102 extends Rs { // 通道号 @@ -47,7 +51,7 @@ public class J9102 extends Rs { buffer.writeByte(command); buffer.writeByte(closeType); buffer.writeByte(streamType); - return null; + return buffer; } @@ -82,4 +86,14 @@ public class J9102 extends Rs { public void setStreamType(Integer streamType) { this.streamType = streamType; } + + @Override + public String toString() { + return "J9102{" + + "channel=" + channel + + ", command=" + command + + ", closeType=" + closeType + + ", streamType=" + streamType + + '}'; + } } diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J9201.java b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J9201.java new file mode 100644 index 000000000..8a66f3544 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J9201.java @@ -0,0 +1,173 @@ +package com.genersoft.iot.vmp.jt1078.proc.response; + +import com.genersoft.iot.vmp.jt1078.annotation.MsgId; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufUtil; +import io.netty.buffer.Unpooled; +import io.netty.util.CharsetUtil; + +/** + * 回放请求 + * + * @author QingtaiJiang + * @date 2023/4/28 10:37 + * @email qingtaij@163.com + */ +@MsgId(id = "9201") +public class J9201 extends Rs { + // 服务器IP地址 + private String ip; + + // 实时视频服务器TCP端口号 + private int tcpPort; + + // 实时视频服务器UDP端口号 + private int udpPort; + + // 逻辑通道号 + private int channel; + + // 音视频资源类型:0.音视频 1.音频 2.视频 3.视频或音视频 + private int type; + + // 码流类型:0.所有码流 1.主码流 2.子码流(如果此通道只传输音频,此字段置0) + private int rate; + + // 存储器类型:0.所有存储器 1.主存储器 2.灾备存储器" + private int storageType; + + // 回放方式:0.正常回放 1.快进回放 2.关键帧快退回放 3.关键帧播放 4.单帧上传 + private int playbackType; + + // 快进或快退倍数:0.无效 1.1倍 2.2倍 3.4倍 4.8倍 5.16倍 (回放控制为1和2时,此字段内容有效,否则置0) + private int playbackSpeed; + + // 开始时间YYMMDDHHMMSS,回放方式为4时,该字段表示单帧上传时间 + private String startTime; + + // 结束时间YYMMDDHHMMSS,回放方式为4时,该字段无效,为0表示一直回放 + private String endTime; + + @Override + public ByteBuf encode() { + ByteBuf buffer = Unpooled.buffer(); + buffer.writeByte(ip.getBytes().length); + buffer.writeCharSequence(ip, CharsetUtil.UTF_8); + buffer.writeShort(tcpPort); + buffer.writeShort(udpPort); + buffer.writeByte(channel); + buffer.writeByte(type); + buffer.writeByte(rate); + buffer.writeByte(storageType); + buffer.writeByte(playbackType); + buffer.writeByte(playbackSpeed); + buffer.writeBytes(ByteBufUtil.decodeHexDump(startTime)); + buffer.writeBytes(ByteBufUtil.decodeHexDump(endTime)); + return buffer; + } + + public String getIp() { + return ip; + } + + public void setIp(String ip) { + this.ip = ip; + } + + public int getTcpPort() { + return tcpPort; + } + + public void setTcpPort(int tcpPort) { + this.tcpPort = tcpPort; + } + + public int getUdpPort() { + return udpPort; + } + + public void setUdpPort(int udpPort) { + this.udpPort = udpPort; + } + + public int getChannel() { + return channel; + } + + public void setChannel(int channel) { + this.channel = channel; + } + + public int getType() { + return type; + } + + public void setType(int type) { + this.type = type; + } + + public int getRate() { + return rate; + } + + public void setRate(int rate) { + this.rate = rate; + } + + public int getStorageType() { + return storageType; + } + + public void setStorageType(int storageType) { + this.storageType = storageType; + } + + public int getPlaybackType() { + return playbackType; + } + + public void setPlaybackType(int playbackType) { + this.playbackType = playbackType; + } + + public int getPlaybackSpeed() { + return playbackSpeed; + } + + public void setPlaybackSpeed(int playbackSpeed) { + this.playbackSpeed = playbackSpeed; + } + + public String getStartTime() { + return startTime; + } + + public void setStartTime(String startTime) { + this.startTime = startTime; + } + + public String getEndTime() { + return endTime; + } + + public void setEndTime(String endTime) { + this.endTime = endTime; + } + + @Override + public String toString() { + return "J9201{" + + "ip='" + ip + '\'' + + ", tcpPort=" + tcpPort + + ", udpPort=" + udpPort + + ", channel=" + channel + + ", type=" + type + + ", rate=" + rate + + ", storageType=" + storageType + + ", playbackType=" + playbackType + + ", playbackSpeed=" + playbackSpeed + + ", startTime='" + startTime + '\'' + + ", endTime='" + endTime + '\'' + + '}'; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J9202.java b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J9202.java new file mode 100644 index 000000000..7cb4e53ef --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J9202.java @@ -0,0 +1,80 @@ +package com.genersoft.iot.vmp.jt1078.proc.response; + +import com.genersoft.iot.vmp.jt1078.annotation.MsgId; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufUtil; +import io.netty.buffer.Unpooled; + +/** + * 平台下发远程录像回放控制 + * + * @author QingtaiJiang + * @date 2023/4/28 10:37 + * @email qingtaij@163.com + */ +@MsgId(id = "9202") +public class J9202 extends Rs { + // 逻辑通道号 + private int channel; + + // 回放控制:0.开始回放 1.暂停回放 2.结束回放 3.快进回放 4.关键帧快退回放 5.拖动回放 6.关键帧播放 + private int playbackType; + + // 快进或快退倍数:0.无效 1.1倍 2.2倍 3.4倍 4.8倍 5.16倍 (回放控制为3和4时,此字段内容有效,否则置0) + private int playbackSpeed; + + // 拖动回放位置(YYMMDDHHMMSS,回放控制为5时,此字段有效) + private String playbackTime; + + @Override + public ByteBuf encode() { + ByteBuf buffer = Unpooled.buffer(); + buffer.writeByte(channel); + buffer.writeByte(playbackType); + buffer.writeByte(playbackSpeed); + buffer.writeBytes(ByteBufUtil.decodeHexDump(playbackTime)); + return buffer; + } + + public int getChannel() { + return channel; + } + + public void setChannel(int channel) { + this.channel = channel; + } + + public int getPlaybackType() { + return playbackType; + } + + public void setPlaybackType(int playbackType) { + this.playbackType = playbackType; + } + + public int getPlaybackSpeed() { + return playbackSpeed; + } + + public void setPlaybackSpeed(int playbackSpeed) { + this.playbackSpeed = playbackSpeed; + } + + public String getPlaybackTime() { + return playbackTime; + } + + public void setPlaybackTime(String playbackTime) { + this.playbackTime = playbackTime; + } + + @Override + public String toString() { + return "J9202{" + + "channel=" + channel + + ", playbackType=" + playbackType + + ", playbackSpeed=" + playbackSpeed + + ", playbackTime='" + playbackTime + '\'' + + '}'; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J9205.java b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J9205.java new file mode 100644 index 000000000..36b858ebc --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J9205.java @@ -0,0 +1,94 @@ +package com.genersoft.iot.vmp.jt1078.proc.response; + +import com.genersoft.iot.vmp.jt1078.annotation.MsgId; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufUtil; +import io.netty.buffer.Unpooled; + +/** + * 查询资源列表 + * + * @author QingtaiJiang + * @date 2023/4/28 10:36 + * @email qingtaij@163.com + */ +@MsgId(id = "9205") +public class J9205 extends Rs { + // 逻辑通道号 + private int channelId; + + // 开始时间YYMMDDHHMMSS,全0表示无起始时间 + private String startTime; + + // 结束时间YYMMDDHHMMSS,全0表示无终止时间 + private String endTime; + + // 报警标志 + private final int warnType = 0; + + // 音视频资源类型:0.音视频 1.音频 2.视频 3.视频或音视频 + private int mediaType; + + // 码流类型:0.所有码流 1.主码流 2.子码流 + private int streamType = 0; + + // 存储器类型:0.所有存储器 1.主存储器 2.灾备存储器 + private int storageType = 0; + + @Override + public ByteBuf encode() { + ByteBuf buffer = Unpooled.buffer(); + + buffer.writeByte(channelId); + buffer.writeBytes(ByteBufUtil.decodeHexDump(startTime)); + buffer.writeBytes(ByteBufUtil.decodeHexDump(endTime)); + buffer.writeLong(warnType); + buffer.writeByte(mediaType); + buffer.writeByte(streamType); + buffer.writeByte(storageType); + + return buffer; + } + + + public void setChannelId(int channelId) { + this.channelId = channelId; + } + + public void setStartTime(String startTime) { + this.startTime = startTime; + } + + public void setEndTime(String endTime) { + this.endTime = endTime; + } + + public void setMediaType(int mediaType) { + this.mediaType = mediaType; + } + + public void setStreamType(int streamType) { + this.streamType = streamType; + } + + public void setStorageType(int storageType) { + this.storageType = storageType; + } + + public int getWarnType() { + return warnType; + } + + @Override + public String toString() { + return "J9205{" + + "channelId=" + channelId + + ", startTime='" + startTime + '\'' + + ", endTime='" + endTime + '\'' + + ", warnType=" + warnType + + ", mediaType=" + mediaType + + ", streamType=" + streamType + + ", storageType=" + storageType + + '}'; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/session/SessionManager.java b/src/main/java/com/genersoft/iot/vmp/jt1078/session/SessionManager.java index 9347249e6..c2876e561 100644 --- a/src/main/java/com/genersoft/iot/vmp/jt1078/session/SessionManager.java +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/session/SessionManager.java @@ -76,13 +76,13 @@ public enum SessionManager { Session session = this.get(cmd.getDevId()); if (session == null) { log.error("DevId: {} not online!", cmd.getDevId()); - return "-1"; + return null; } String requestKey = requestKey(cmd.getDevId(), cmd.getRespId(), cmd.getPackageNo()); SynchronousQueue subscribe = subscribe(requestKey); if (subscribe == null) { log.error("DevId: {} key:{} send repaid", cmd.getDevId(), requestKey); - return "-1"; + return null; } session.writeObject(cmd); try { @@ -105,7 +105,7 @@ public enum SessionManager { log.error("{}", e.getMessage(), e); } } - log.warn("未找到对应回复指令,key:{} 消息:{} ", requestKey, data); + log.warn("Not find response,key:{} data:{} ", requestKey, data); return false; } diff --git a/src/test/java/com/genersoft/iot/vmp/jt1078/JT1078ServerTest.java b/src/test/java/com/genersoft/iot/vmp/jt1078/JT1078ServerTest.java new file mode 100644 index 000000000..8986f91e6 --- /dev/null +++ b/src/test/java/com/genersoft/iot/vmp/jt1078/JT1078ServerTest.java @@ -0,0 +1,103 @@ +package com.genersoft.iot.vmp.jt1078; + +import com.genersoft.iot.vmp.jt1078.cmd.JT1078Template; +import com.genersoft.iot.vmp.jt1078.codec.netty.TcpServer; +import com.genersoft.iot.vmp.jt1078.proc.response.J9102; +import com.genersoft.iot.vmp.jt1078.proc.response.J9201; +import com.genersoft.iot.vmp.jt1078.proc.response.J9202; +import com.genersoft.iot.vmp.jt1078.proc.response.J9205; + +import java.util.Scanner; + +/** + * @author QingtaiJiang + * @date 2023/4/28 14:22 + * @email qingtaij@163.com + */ +public class JT1078ServerTest { + + private static final JT1078Template jt1078Template = new JT1078Template(); + + public static void main(String[] args) { + System.out.println("Starting jt1078 server..."); + TcpServer tcpServer = new TcpServer(21078); + tcpServer.start(); + System.out.println("Start jt1078 server success!"); + + + Scanner s = new Scanner(System.in); + while (true) { + String code = s.nextLine(); + switch (code) { + case "1": + test9102(); + break; + case "2": + test9201(); + break; + case "3": + test9202(); + break; + case "4": + test9205(); + break; + default: + break; + } + } + } + + private static void test9102() { + J9102 j9102 = new J9102(); + j9102.setChannel(1); + j9102.setCommand(0); + j9102.setCloseType(0); + j9102.setStreamType(0); + + String s = jt1078Template.stopLive("18864197066", j9102, 6); + System.out.println(s); + } + + private static void test9201() { + J9201 j9201 = new J9201(); + j9201.setIp("192.168.1.1"); + j9201.setChannel(1); + j9201.setTcpPort(7618); + j9201.setUdpPort(7618); + j9201.setType(0); + j9201.setRate(0); + j9201.setStorageType(0); + j9201.setPlaybackType(0); + j9201.setPlaybackSpeed(0); + j9201.setStartTime("230428134100"); + j9201.setEndTime("230428134200"); + + String s = jt1078Template.startBackLive("18864197066", j9201, 6); + System.out.println(s); + } + + private static void test9202() { + J9202 j9202 = new J9202(); + + j9202.setChannel(1); + j9202.setPlaybackType(2); + j9202.setPlaybackSpeed(0); + j9202.setPlaybackTime("230428134100"); + + String s = jt1078Template.controlBackLive("18864197066", j9202, 6); + System.out.println(s); + } + + private static void test9205() { + J9205 j9205 = new J9205(); + j9205.setChannelId(1); + j9205.setStartTime("230428134100"); + j9205.setEndTime("230428134100"); + j9205.setMediaType(0); + j9205.setStreamType(0); + j9205.setStorageType(0); + + String s = jt1078Template.queryBackTime("18864197066", j9205, 6); + System.out.println(s); + } +} From 3fe47021b9aefb11b1e659383ac2f3d0edd2aa42 Mon Sep 17 00:00:00 2001 From: 648540858 <648540858@qq.com> Date: Thu, 4 May 2023 14:21:58 +0800 Subject: [PATCH 36/48] =?UTF-8?q?=E4=BC=98=E5=8C=96=E5=9B=BD=E6=A0=87?= =?UTF-8?q?=E8=A7=84=E8=8C=83=EF=BC=8C=E5=8F=82=E8=80=83=E5=9B=BD=E6=A0=87?= =?UTF-8?q?=E6=96=87=E6=A1=A3=E4=B8=AD-=E7=82=B9=E6=92=AD=E5=A4=96?= =?UTF-8?q?=E5=9F=9F=E8=AE=BE=E5=A4=87=E5=AA=92=E4=BD=93=E6=B5=81SSRC?= =?UTF-8?q?=E5=A4=84=E7=90=86=E6=96=B9=E5=BC=8F=EF=BC=8C=E4=B8=8A=E7=BA=A7?= =?UTF-8?q?=E7=82=B9=E6=92=AD=E6=97=B6=E8=87=AA=E5=AE=9A=E4=B9=89ssrc?= =?UTF-8?q?=EF=BC=8C=E4=B8=8D=E9=80=82=E7=94=A8=E4=B8=8A=E7=BA=A7=E6=90=BA?= =?UTF-8?q?=E5=B8=A6=E7=9A=84ssrc=EF=BC=8C=E4=B9=9F=E9=81=BF=E5=85=8D?= =?UTF-8?q?=E4=B8=8A=E7=BA=A7=E5=85=BC=E5=AE=B9=E6=80=A7=E5=B7=AE=EF=BC=8C?= =?UTF-8?q?=E4=B8=8D=E6=90=BA=E5=B8=A6ssrc=E7=9A=84=E9=97=AE=E9=A2=98?= =?UTF-8?q?=EF=BC=8C=E5=8F=AF=E9=80=9A=E8=BF=87=E9=85=8D=E7=BD=AE=E5=85=B3?= =?UTF-8?q?=E9=97=AD=E6=AD=A4=E7=89=B9=E6=80=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../genersoft/iot/vmp/conf/UserSetting.java | 9 ++++ .../iot/vmp/gb28181/task/SipRunner.java | 5 +++ .../request/impl/ByeRequestProcessor.java | 5 +++ .../request/impl/InviteRequestProcessor.java | 44 ++++++++++--------- .../vmp/media/zlm/ZLMHttpHookListener.java | 5 +++ .../vmp/service/impl/PlatformServiceImpl.java | 5 +++ .../storager/impl/RedisCatchStorageImpl.java | 7 ++- src/main/resources/all-application.yml | 2 + 8 files changed, 61 insertions(+), 21 deletions(-) diff --git a/src/main/java/com/genersoft/iot/vmp/conf/UserSetting.java b/src/main/java/com/genersoft/iot/vmp/conf/UserSetting.java index 539198f24..5ae9236bb 100644 --- a/src/main/java/com/genersoft/iot/vmp/conf/UserSetting.java +++ b/src/main/java/com/genersoft/iot/vmp/conf/UserSetting.java @@ -53,6 +53,7 @@ public class UserSetting { private Boolean refuseChannelStatusChannelFormNotify = Boolean.FALSE; private Boolean deviceStatusNotify = Boolean.FALSE; + private Boolean useCustomSsrcForParentInvite = Boolean.TRUE; private String serverId = "000000"; @@ -277,4 +278,12 @@ public class UserSetting { public void setDeviceStatusNotify(Boolean deviceStatusNotify) { this.deviceStatusNotify = deviceStatusNotify; } + + public Boolean getUseCustomSsrcForParentInvite() { + return useCustomSsrcForParentInvite; + } + + public void setUseCustomSsrcForParentInvite(Boolean useCustomSsrcForParentInvite) { + this.useCustomSsrcForParentInvite = useCustomSsrcForParentInvite; + } } diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/task/SipRunner.java b/src/main/java/com/genersoft/iot/vmp/gb28181/task/SipRunner.java index 3352838f9..17c23a00d 100644 --- a/src/main/java/com/genersoft/iot/vmp/gb28181/task/SipRunner.java +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/task/SipRunner.java @@ -5,6 +5,7 @@ import com.genersoft.iot.vmp.conf.UserSetting; import com.genersoft.iot.vmp.gb28181.bean.Device; import com.genersoft.iot.vmp.gb28181.bean.ParentPlatform; import com.genersoft.iot.vmp.gb28181.bean.SendRtpItem; +import com.genersoft.iot.vmp.gb28181.session.SSRCFactory; import com.genersoft.iot.vmp.gb28181.transmit.cmd.ISIPCommanderForPlatform; import com.genersoft.iot.vmp.media.zlm.ZLMRESTfulUtils; import com.genersoft.iot.vmp.media.zlm.dto.MediaServerItem; @@ -37,6 +38,9 @@ public class SipRunner implements CommandLineRunner { @Autowired private IRedisCatchStorage redisCatchStorage; + @Autowired + private SSRCFactory ssrcFactory; + @Autowired private UserSetting userSetting; @@ -96,6 +100,7 @@ public class SipRunner implements CommandLineRunner { MediaServerItem mediaServerItem = mediaServerService.getOne(sendRtpItem.getMediaServerId()); redisCatchStorage.deleteSendRTPServer(sendRtpItem.getPlatformId(),sendRtpItem.getChannelId(), sendRtpItem.getCallId(),sendRtpItem.getStreamId()); if (mediaServerItem != null) { + ssrcFactory.releaseSsrc(sendRtpItem.getMediaServerId(), sendRtpItem.getSsrc()); Map param = new HashMap<>(); param.put("vhost","__defaultVhost__"); param.put("app",sendRtpItem.getApp()); diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/ByeRequestProcessor.java b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/ByeRequestProcessor.java index addd6336d..cc3051fb0 100644 --- a/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/ByeRequestProcessor.java +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/ByeRequestProcessor.java @@ -6,6 +6,7 @@ import com.genersoft.iot.vmp.gb28181.bean.Device; import com.genersoft.iot.vmp.gb28181.bean.InviteStreamType; import com.genersoft.iot.vmp.gb28181.bean.SendRtpItem; import com.genersoft.iot.vmp.gb28181.bean.SsrcTransaction; +import com.genersoft.iot.vmp.gb28181.session.SSRCFactory; import com.genersoft.iot.vmp.gb28181.session.VideoStreamSessionManager; import com.genersoft.iot.vmp.gb28181.transmit.SIPProcessorObserver; import com.genersoft.iot.vmp.gb28181.transmit.cmd.ISIPCommander; @@ -60,6 +61,9 @@ public class ByeRequestProcessor extends SIPRequestProcessorParent implements In @Autowired private ZLMRTPServerFactory zlmrtpServerFactory; + @Autowired + private SSRCFactory ssrcFactory; + @Autowired private IMediaServerService mediaServerService; @@ -102,6 +106,7 @@ public class ByeRequestProcessor extends SIPRequestProcessorParent implements In logger.info("[收到bye] 停止向上级推流:{}", streamId); MediaServerItem mediaInfo = mediaServerService.getOne(sendRtpItem.getMediaServerId()); redisCatchStorage.deleteSendRTPServer(platformGbId, channelId, callIdHeader.getCallId(), null); + ssrcFactory.releaseSsrc(sendRtpItem.getMediaServerId(), sendRtpItem.getSsrc()); zlmrtpServerFactory.stopSendRtpStream(mediaInfo, param); int totalReaderCount = zlmrtpServerFactory.totalReaderCount(mediaInfo, sendRtpItem.getApp(), streamId); if (totalReaderCount <= 0) { diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/InviteRequestProcessor.java b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/InviteRequestProcessor.java index a4367b4a8..c23532061 100644 --- a/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/InviteRequestProcessor.java +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/InviteRequestProcessor.java @@ -245,18 +245,15 @@ public class InviteRequestProcessor extends SIPRequestProcessorParent implements String contentString = new String(request.getRawContent()); // jainSip不支持y=字段, 移除以解析。 - int ssrcIndex = contentString.indexOf("y="); // 检查是否有y字段 - String ssrcDefault = "0000000000"; - String ssrc; + int ssrcIndex = contentString.indexOf("y="); + SessionDescription sdp; if (ssrcIndex >= 0) { //ssrc规定长度为10个字节,不取余下长度以避免后续还有“f=”字段 - ssrc = contentString.substring(ssrcIndex + 2, ssrcIndex + 12); - String substring = contentString.substring(0, contentString.indexOf("y=")); + String substring = contentString.substring(0, ssrcIndex); sdp = SdpFactory.getInstance().createSessionDescription(substring); } else { - ssrc = ssrcDefault; sdp = SdpFactory.getInstance().createSessionDescription(contentString); } String sessionName = sdp.getSessionName().getValue(); @@ -320,7 +317,7 @@ public class InviteRequestProcessor extends SIPRequestProcessorParent implements String username = sdp.getOrigin().getUsername(); String addressStr = sdp.getConnection().getAddress(); - logger.info("[上级点播]用户:{}, 通道:{}, 地址:{}:{}, ssrc:{}", username, channelId, addressStr, port, ssrc); + Device device = null; // 通过 channel 和 gbStream 是否为null 值判断来源是直播流合适国标 if (channel != null) { @@ -344,6 +341,15 @@ public class InviteRequestProcessor extends SIPRequestProcessorParent implements } return; } + + String ssrc; + if (userSetting.getUseCustomSsrcForParentInvite() || ssrcIndex < 0) { + // 上级平台点播时不使用上级平台指定的ssrc,使用自定义的ssrc,参考国标文档-点播外域设备媒体流SSRC处理方式 + ssrc = "Play".equalsIgnoreCase(sessionName) ? ssrcFactory.getPlaySsrc(mediaServerItem.getId()) : ssrcFactory.getPlayBackSsrc(mediaServerItem.getId()); + }else { + ssrc = contentString.substring(ssrcIndex + 2, ssrcIndex + 12); + } + logger.info("[上级点播] 用户:{}, 通道:{}, 地址:{}:{}, ssrc:{}", username, channelId, addressStr, port, ssrc); SendRtpItem sendRtpItem = zlmrtpServerFactory.createSendRtpItem(mediaServerItem, addressStr, port, ssrc, requesterId, device.getDeviceId(), channelId, mediaTransmissionTCP, platform.isRtcp()); @@ -465,29 +471,23 @@ public class InviteRequestProcessor extends SIPRequestProcessorParent implements } } if (playTransaction == null) { + // 被点播的通道目前未被点播,则开始点播 String streamId = null; if (mediaServerItem.isRtpEnable()) { streamId = String.format("%s_%s", device.getDeviceId(), channelId); } - SSRCInfo ssrcInfo = mediaServerService.openRTPServer(mediaServerItem, streamId, null, device.isSsrcCheck(), false, 0, false, device.getStreamModeForParam()); + SSRCInfo ssrcInfo = mediaServerService.openRTPServer(mediaServerItem, streamId, ssrc, device.isSsrcCheck(), false, 0, false, device.getStreamModeForParam()); logger.info(JSONObject.toJSONString(ssrcInfo)); sendRtpItem.setStreamId(ssrcInfo.getStream()); - sendRtpItem.setSsrc(ssrc.equals(ssrcDefault) ? ssrcInfo.getSsrc() : ssrc); +// sendRtpItem.setSsrc(ssrcInfo.getSsrc()); // 写入redis, 超时时回复 redisCatchStorage.updateSendRTPSever(sendRtpItem); - MediaServerItem finalMediaServerItem = mediaServerItem; playService.play(mediaServerItem, ssrcInfo, device, channelId, hookEvent, errorEvent, (code, msg) -> { logger.info("[上级点播]超时, 用户:{}, 通道:{}", username, channelId); redisCatchStorage.deleteSendRTPServer(platform.getServerGBId(), channelId, callIdHeader.getCallId(), null); }); } else { - // 当前系统作为下级平台使用,当上级平台点播时不携带ssrc时,并且设备在当前系统中已经点播了。这个时候需要重新给生成一个ssrc,不使用默认的"0000000000"。 - if (ssrc.equals(ssrcDefault)) { - ssrc = ssrcFactory.getPlaySsrc(mediaServerItem.getId()); - ssrcFactory.releaseSsrc(mediaServerItem.getId(), ssrc); - sendRtpItem.setSsrc(ssrc); - } sendRtpItem.setStreamId(playTransaction.getStream()); // 写入redis, 超时时回复 @@ -499,11 +499,15 @@ public class InviteRequestProcessor extends SIPRequestProcessorParent implements } } } else if (gbStream != null) { - if(ssrc.equals(ssrcDefault)) - { - ssrc = ssrcFactory.getPlaySsrc(mediaServerItem.getId()); - ssrcFactory.releaseSsrc(mediaServerItem.getId(), ssrc); + + String ssrc; + if (userSetting.getUseCustomSsrcForParentInvite() || ssrcIndex < 0) { + // 上级平台点播时不使用上级平台指定的ssrc,使用自定义的ssrc,参考国标文档-点播外域设备媒体流SSRC处理方式 + ssrc = "Play".equalsIgnoreCase(sessionName) ? ssrcFactory.getPlaySsrc(mediaServerItem.getId()) : ssrcFactory.getPlayBackSsrc(mediaServerItem.getId()); + }else { + ssrc = contentString.substring(ssrcIndex + 2, ssrcIndex + 12); } + if("push".equals(gbStream.getStreamType())) { if (streamPushItem != null && streamPushItem.isPushIng()) { // 推流状态 diff --git a/src/main/java/com/genersoft/iot/vmp/media/zlm/ZLMHttpHookListener.java b/src/main/java/com/genersoft/iot/vmp/media/zlm/ZLMHttpHookListener.java index 405fdd00f..4e9b57d2d 100644 --- a/src/main/java/com/genersoft/iot/vmp/media/zlm/ZLMHttpHookListener.java +++ b/src/main/java/com/genersoft/iot/vmp/media/zlm/ZLMHttpHookListener.java @@ -8,6 +8,7 @@ import com.genersoft.iot.vmp.conf.exception.SsrcTransactionNotFoundException; import com.genersoft.iot.vmp.gb28181.bean.*; import com.genersoft.iot.vmp.gb28181.event.EventPublisher; import com.genersoft.iot.vmp.gb28181.event.subscribe.catalog.CatalogEvent; +import com.genersoft.iot.vmp.gb28181.session.SSRCFactory; import com.genersoft.iot.vmp.gb28181.session.VideoStreamSessionManager; import com.genersoft.iot.vmp.gb28181.transmit.callback.DeferredResultHolder; import com.genersoft.iot.vmp.gb28181.transmit.callback.RequestMessage; @@ -105,6 +106,9 @@ public class ZLMHttpHookListener { @Autowired private AssistRESTfulUtils assistRESTfulUtils; + @Autowired + private SSRCFactory ssrcFactory; + @Qualifier("taskExecutor") @Autowired private ThreadPoolTaskExecutor taskExecutor; @@ -666,6 +670,7 @@ public class ZLMHttpHookListener { if (sendRtpItems.size() > 0) { for (SendRtpItem sendRtpItem : sendRtpItems) { ParentPlatform parentPlatform = storager.queryParentPlatByServerGBId(sendRtpItem.getPlatformId()); + ssrcFactory.releaseSsrc(sendRtpItem.getMediaServerId(), sendRtpItem.getSsrc()); try { commanderFroPlatform.streamByeCmd(parentPlatform, sendRtpItem.getCallId()); } catch (SipException | InvalidArgumentException | ParseException e) { diff --git a/src/main/java/com/genersoft/iot/vmp/service/impl/PlatformServiceImpl.java b/src/main/java/com/genersoft/iot/vmp/service/impl/PlatformServiceImpl.java index 9233ca9d9..63999b7c5 100644 --- a/src/main/java/com/genersoft/iot/vmp/service/impl/PlatformServiceImpl.java +++ b/src/main/java/com/genersoft/iot/vmp/service/impl/PlatformServiceImpl.java @@ -4,6 +4,7 @@ import com.genersoft.iot.vmp.conf.DynamicTask; import com.genersoft.iot.vmp.conf.UserSetting; import com.genersoft.iot.vmp.gb28181.bean.*; import com.genersoft.iot.vmp.gb28181.event.SipSubscribe; +import com.genersoft.iot.vmp.gb28181.session.SSRCFactory; import com.genersoft.iot.vmp.gb28181.transmit.cmd.impl.SIPCommanderFroPlatform; import com.genersoft.iot.vmp.media.zlm.ZLMRTPServerFactory; import com.genersoft.iot.vmp.media.zlm.dto.MediaServerItem; @@ -53,6 +54,9 @@ public class PlatformServiceImpl implements IPlatformService { @Autowired private IRedisCatchStorage redisCatchStorage; + @Autowired + private SSRCFactory ssrcFactory; + @Autowired private IMediaServerService mediaServerService; @@ -328,6 +332,7 @@ public class PlatformServiceImpl implements IPlatformService { List sendRtpItems = redisCatchStorage.querySendRTPServer(platformId); if (sendRtpItems != null && sendRtpItems.size() > 0) { for (SendRtpItem sendRtpItem : sendRtpItems) { + ssrcFactory.releaseSsrc(sendRtpItem.getMediaServerId(), sendRtpItem.getSsrc()); redisCatchStorage.deleteSendRTPServer(platformId, sendRtpItem.getChannelId(), null, null); MediaServerItem mediaInfo = mediaServerService.getOne(sendRtpItem.getMediaServerId()); Map param = new HashMap<>(3); diff --git a/src/main/java/com/genersoft/iot/vmp/storager/impl/RedisCatchStorageImpl.java b/src/main/java/com/genersoft/iot/vmp/storager/impl/RedisCatchStorageImpl.java index d4abcb45f..aed981191 100644 --- a/src/main/java/com/genersoft/iot/vmp/storager/impl/RedisCatchStorageImpl.java +++ b/src/main/java/com/genersoft/iot/vmp/storager/impl/RedisCatchStorageImpl.java @@ -915,7 +915,12 @@ public class RedisCatchStorageImpl implements IRedisCatchStorage { @Override public void sendDeviceOrChannelStatus(String deviceId, String channelId, boolean online) { String key = VideoManagerConstants.VM_MSG_SUBSCRIBE_DEVICE_STATUS; - logger.info("[redis通知] 推送设备/通道状态, {}/{}-{}", deviceId, channelId, online); + if (channelId == null) { + logger.info("[redis通知] 推送设备状态, {}-{}", deviceId, online); + }else { + logger.info("[redis通知] 推送通道状态, {}/{}-{}", deviceId, channelId, online); + } + StringBuilder msg = new StringBuilder(); msg.append(deviceId); if (channelId != null) { diff --git a/src/main/resources/all-application.yml b/src/main/resources/all-application.yml index cc2145a46..54d348dc4 100644 --- a/src/main/resources/all-application.yml +++ b/src/main/resources/all-application.yml @@ -194,6 +194,8 @@ user-settings: max-notify-count-queue: 10000 # 设备/通道状态变化时发送消息 device-status-notify: false + # 上级平台点播时不使用上级平台指定的ssrc,使用自定义的ssrc,参考国标文档-点播外域设备媒体流SSRC处理方式 + use-custom-ssrc-for-parent-invite: true # 跨域配置,配置你访问前端页面的地址即可, 可以配置多个 allowed-origins: - http://localhost:8008 From 2bc284222483d0fb76b861ead4f462421274706d Mon Sep 17 00:00:00 2001 From: 648540858 <648540858@qq.com> Date: Thu, 4 May 2023 15:36:04 +0800 Subject: [PATCH 37/48] =?UTF-8?q?=E4=BC=98=E5=8C=96=E5=9B=BD=E6=A0=87?= =?UTF-8?q?=E7=82=B9=E6=92=AD=E4=B8=8B=E7=BA=A7=E5=B9=B3=E5=8F=B0=EF=BC=8C?= =?UTF-8?q?ssrc=E6=9B=B4=E6=96=B0=E7=9A=84=E6=97=B6=E5=9B=A0=E4=B8=BA?= =?UTF-8?q?=E6=97=A7=E7=9A=84=E7=AB=AF=E5=8F=A3=E9=87=8A=E6=94=BE=E6=85=A2?= =?UTF-8?q?=E5=AF=BC=E8=87=B4=E7=82=B9=E6=92=AD=E5=A4=B1=E8=B4=A5=E7=9A=84?= =?UTF-8?q?=E9=97=AE=E9=A2=98=EF=BC=8C=E4=BD=BF=E7=94=A8=E6=96=B0=E7=9A=84?= =?UTF-8?q?=E6=8E=A5=E5=8F=A3=E7=9B=B4=E6=8E=A5=E6=9B=B4=E6=96=B0ssrc?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../iot/vmp/media/zlm/ZLMRESTfulUtils.java | 9 +++ .../vmp/media/zlm/ZLMRTPServerFactory.java | 15 ++++ .../iot/vmp/service/IMediaServerService.java | 1 + .../service/impl/MediaServerServiceImpl.java | 5 ++ .../iot/vmp/service/impl/PlayServiceImpl.java | 72 ++++++++++++------- 5 files changed, 76 insertions(+), 26 deletions(-) diff --git a/src/main/java/com/genersoft/iot/vmp/media/zlm/ZLMRESTfulUtils.java b/src/main/java/com/genersoft/iot/vmp/media/zlm/ZLMRESTfulUtils.java index 13f324020..a2fd1e376 100644 --- a/src/main/java/com/genersoft/iot/vmp/media/zlm/ZLMRESTfulUtils.java +++ b/src/main/java/com/genersoft/iot/vmp/media/zlm/ZLMRESTfulUtils.java @@ -25,6 +25,8 @@ public class ZLMRESTfulUtils { private OkHttpClient client; + + public interface RequestCallback{ void run(JSONObject response); } @@ -354,4 +356,11 @@ public class ZLMRESTfulUtils { param.put("stream_id", stream_id); return sendPost(mediaServerItem, "connectRtpServer",param, null); } + + public JSONObject updateRtpServerSSRC(MediaServerItem mediaServerItem, String streamId, String ssrc) { + Map param = new HashMap<>(1); + param.put("ssrc", ssrc); + param.put("stream_id", streamId); + return sendPost(mediaServerItem, "updateRtpServerSSRC",param, null); + } } diff --git a/src/main/java/com/genersoft/iot/vmp/media/zlm/ZLMRTPServerFactory.java b/src/main/java/com/genersoft/iot/vmp/media/zlm/ZLMRTPServerFactory.java index 99576c4ab..f3e4d4404 100644 --- a/src/main/java/com/genersoft/iot/vmp/media/zlm/ZLMRTPServerFactory.java +++ b/src/main/java/com/genersoft/iot/vmp/media/zlm/ZLMRTPServerFactory.java @@ -386,4 +386,19 @@ public class ZLMRTPServerFactory { public void closeAllSendRtpStream() { } + + public Boolean updateRtpServerSSRC(MediaServerItem mediaServerItem, String streamId, String ssrc) { + boolean result = false; + JSONObject jsonObject = zlmresTfulUtils.updateRtpServerSSRC(mediaServerItem, streamId, ssrc); + if (jsonObject == null) { + logger.error("[更新RTPServer] 失败: 请检查ZLM服务"); + } else if (jsonObject.getInteger("code") == 0) { + result= true; + logger.info("[更新RTPServer] 成功"); + } else { + logger.error("[更新RTPServer] 失败: {}, streamId:{},ssrc:{}->\r\n{}",jsonObject.getString("msg"), + streamId, ssrc, jsonObject); + } + return result; + } } diff --git a/src/main/java/com/genersoft/iot/vmp/service/IMediaServerService.java b/src/main/java/com/genersoft/iot/vmp/service/IMediaServerService.java index 495b009e4..530cd6dfd 100644 --- a/src/main/java/com/genersoft/iot/vmp/service/IMediaServerService.java +++ b/src/main/java/com/genersoft/iot/vmp/service/IMediaServerService.java @@ -50,6 +50,7 @@ public interface IMediaServerService { void closeRTPServer(MediaServerItem mediaServerItem, String streamId); void closeRTPServer(MediaServerItem mediaServerItem, String streamId, CommonCallback callback); + Boolean updateRtpServerSSRC(MediaServerItem mediaServerItem, String streamId, String ssrc); void closeRTPServer(String mediaServerId, String streamId); diff --git a/src/main/java/com/genersoft/iot/vmp/service/impl/MediaServerServiceImpl.java b/src/main/java/com/genersoft/iot/vmp/service/impl/MediaServerServiceImpl.java index 94ed200fc..c0d07650d 100644 --- a/src/main/java/com/genersoft/iot/vmp/service/impl/MediaServerServiceImpl.java +++ b/src/main/java/com/genersoft/iot/vmp/service/impl/MediaServerServiceImpl.java @@ -180,6 +180,11 @@ public class MediaServerServiceImpl implements IMediaServerService { closeRTPServer(mediaServerItem, streamId); } + @Override + public Boolean updateRtpServerSSRC(MediaServerItem mediaServerItem, String streamId, String ssrc) { + return zlmrtpServerFactory.updateRtpServerSSRC(mediaServerItem, streamId, ssrc); + } + @Override public void releaseSsrc(String mediaServerItemId, String ssrc) { MediaServerItem mediaServerItem = getOne(mediaServerItemId); diff --git a/src/main/java/com/genersoft/iot/vmp/service/impl/PlayServiceImpl.java b/src/main/java/com/genersoft/iot/vmp/service/impl/PlayServiceImpl.java index 51574392b..0347611c7 100644 --- a/src/main/java/com/genersoft/iot/vmp/service/impl/PlayServiceImpl.java +++ b/src/main/java/com/genersoft/iot/vmp/service/impl/PlayServiceImpl.java @@ -143,7 +143,7 @@ public class PlayServiceImpl implements IPlayService { if (rtpInfo.getBoolean("exist")) { int localPort = rtpInfo.getInteger("local_port"); if (localPort == 0) { - logger.warn("[点播],点播时发现rtpServerC存在,但是尚未开始推流"); + logger.warn("[点播],点播时发现rtpServer存在,但是尚未开始推流"); // 此时说明rtpServer已经创建但是流还没有推上来 WVPResult wvpResult = new WVPResult(); wvpResult.setCode(ErrorCode.ERROR100.getCode()); @@ -228,7 +228,7 @@ public class PlayServiceImpl implements IPlayService { ZlmHttpHookSubscribe.Event hookEvent, SipSubscribe.Event errorEvent, InviteTimeOutCallback timeoutCallback) { - logger.info("[点播开始] deviceId: {}, channelId: {},收流端口: {}, 收流模式:{}, SSRC: {}, SSRC校验:{}", device.getDeviceId(), channelId, ssrcInfo.getPort(), device.getStreamMode(), ssrcInfo.getSsrc(), device.isSsrcCheck()); + logger.info("[点播开始] deviceId: {}, channelId: {},收流端口:{}, 收流模式:{}, SSRC: {}, SSRC校验:{}", device.getDeviceId(), channelId, ssrcInfo.getPort(), device.getStreamMode(), ssrcInfo.getSsrc(), device.isSsrcCheck()); // 超时处理 String timeOutTaskKey = UUID.randomUUID().toString(); dynamicTask.startDelay(timeOutTaskKey, () -> { @@ -352,30 +352,50 @@ public class PlayServiceImpl implements IPlayService { hookEvent.response(mediaServerItemInUse, response); }); } - // 关闭rtp server - mediaServerService.closeRTPServer(mediaServerItem, ssrcInfo.getStream(), result->{ - if (result) { - // 重新开启ssrc server - mediaServerService.openRTPServer(mediaServerItem, ssrcInfo.getStream(), ssrcInResponse, device.isSsrcCheck(), false, ssrcInfo.getPort(), true, device.getStreamModeForParam()); - }else { - try { - logger.warn("[停止点播] {}/{}", device.getDeviceId(), channelId); - cmder.streamByeCmd(device, channelId, ssrcInfo.getStream(), null, null); - } catch (InvalidArgumentException | SipException | ParseException | SsrcTransactionNotFoundException e) { - logger.error("[命令发送失败] 停止点播, 发送BYE: {}", e.getMessage()); - throw new ControllerException(ErrorCode.ERROR100.getCode(), "命令发送失败: " + e.getMessage()); - } - dynamicTask.stop(timeOutTaskKey); - // 释放ssrc - mediaServerService.releaseSsrc(mediaServerItem.getId(), ssrcInfo.getSsrc()); - - streamSession.remove(device.getDeviceId(), channelId, ssrcInfo.getStream()); - event.msg = "下级自定义了ssrc,重新设置收流信息失败"; - event.statusCode = 500; - errorEvent.response(event); + Boolean result = mediaServerService.updateRtpServerSSRC(mediaServerItem, ssrcInfo.getStream(), ssrcInResponse); + if (!result) { + try { + logger.warn("[停止点播] {}/{}", device.getDeviceId(), channelId); + cmder.streamByeCmd(device, channelId, ssrcInfo.getStream(), null, null); + } catch (InvalidArgumentException | SipException | ParseException | SsrcTransactionNotFoundException e) { + logger.error("[命令发送失败] 停止点播, 发送BYE: {}", e.getMessage()); + throw new ControllerException(ErrorCode.ERROR100.getCode(), "命令发送失败: " + e.getMessage()); } - }); + + dynamicTask.stop(timeOutTaskKey); + // 释放ssrc + mediaServerService.releaseSsrc(mediaServerItem.getId(), ssrcInfo.getSsrc()); + + streamSession.remove(device.getDeviceId(), channelId, ssrcInfo.getStream()); + event.msg = "下级自定义了ssrc,重新设置收流信息失败"; + event.statusCode = 500; + errorEvent.response(event); + } +// // 关闭rtp server +// mediaServerService.closeRTPServer(mediaServerItem, ssrcInfo.getStream(), result->{ +// if (result) { +// // 重新开启ssrc server +// mediaServerService.openRTPServer(mediaServerItem, ssrcInfo.getStream(), ssrcInResponse, device.isSsrcCheck(), false, ssrcInfo.getPort(), true, device.getStreamModeForParam()); +// }else { +// try { +// logger.warn("[停止点播] {}/{}", device.getDeviceId(), channelId); +// cmder.streamByeCmd(device, channelId, ssrcInfo.getStream(), null, null); +// } catch (InvalidArgumentException | SipException | ParseException | SsrcTransactionNotFoundException e) { +// logger.error("[命令发送失败] 停止点播, 发送BYE: {}", e.getMessage()); +// throw new ControllerException(ErrorCode.ERROR100.getCode(), "命令发送失败: " + e.getMessage()); +// } +// +// dynamicTask.stop(timeOutTaskKey); +// // 释放ssrc +// mediaServerService.releaseSsrc(mediaServerItem.getId(), ssrcInfo.getSsrc()); +// +// streamSession.remove(device.getDeviceId(), channelId, ssrcInfo.getStream()); +// event.msg = "下级自定义了ssrc,重新设置收流信息失败"; +// event.statusCode = 500; +// errorEvent.response(event); +// } +// }); } @@ -519,7 +539,7 @@ public class PlayServiceImpl implements IPlayService { if (device == null) { throw new ControllerException(ErrorCode.ERROR100.getCode(), "设备: " + deviceId + "不存在"); } - logger.info("[回放消息] deviceId: {}, channelId: {},收流端口: {}, 收流模式:{}, SSRC: {}, SSRC校验:{}", device.getDeviceId(), channelId, ssrcInfo.getPort(), device.getStreamMode(), ssrcInfo.getSsrc(), device.isSsrcCheck()); + logger.info("[回放消息] deviceId: {}, channelId: {},收流端口:{}, 收流模式:{}, SSRC: {}, SSRC校验:{}", device.getDeviceId(), channelId, ssrcInfo.getPort(), device.getStreamMode(), ssrcInfo.getSsrc(), device.isSsrcCheck()); PlayBackResult playBackResult = new PlayBackResult<>(); String playBackTimeOutTaskKey = UUID.randomUUID().toString(); dynamicTask.startDelay(playBackTimeOutTaskKey, () -> { @@ -689,7 +709,7 @@ public class PlayServiceImpl implements IPlayService { throw new ControllerException(ErrorCode.ERROR400.getCode(), "设备:" + deviceId + "不存在"); } PlayBackResult downloadResult = new PlayBackResult<>(); - logger.info("[录像下载] deviceId: {}, channelId: {},收流端口: {}, 收流模式:{}, SSRC: {}, SSRC校验:{}", device.getDeviceId(), channelId, ssrcInfo.getPort(), device.getStreamMode(), ssrcInfo.getSsrc(), device.isSsrcCheck()); + logger.info("[录像下载] deviceId: {}, channelId: {},收流端口:{}, 收流模式:{}, SSRC: {}, SSRC校验:{}", device.getDeviceId(), channelId, ssrcInfo.getPort(), device.getStreamMode(), ssrcInfo.getSsrc(), device.isSsrcCheck()); String downLoadTimeOutTaskKey = UUID.randomUUID().toString(); dynamicTask.startDelay(downLoadTimeOutTaskKey, () -> { logger.warn(String.format("录像下载请求超时,deviceId:%s ,channelId:%s", deviceId, channelId)); From 381c3bdc2079ece5147cf4cee004e9071edadf7a Mon Sep 17 00:00:00 2001 From: 648540858 <648540858@qq.com> Date: Thu, 4 May 2023 16:04:44 +0800 Subject: [PATCH 38/48] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E5=9B=BD=E6=A0=87?= =?UTF-8?q?=E7=82=B9=E6=92=AD=E4=B8=8B=E7=BA=A7=E5=B9=B3=E5=8F=B0=EF=BC=8C?= =?UTF-8?q?ssrc=E6=9B=B4=E6=96=B0=E7=9A=84=E6=97=B6=E5=8D=95=E7=AB=AF?= =?UTF-8?q?=E5=8F=A3=E9=94=99=E8=AF=AF=E6=9B=B4=E6=96=B0rtpserver=E7=9A=84?= =?UTF-8?q?=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../request/impl/InviteRequestProcessor.java | 12 ++++++- .../iot/vmp/service/impl/PlayServiceImpl.java | 36 ++++--------------- 2 files changed, 18 insertions(+), 30 deletions(-) diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/InviteRequestProcessor.java b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/InviteRequestProcessor.java index c23532061..7034a0e57 100644 --- a/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/InviteRequestProcessor.java +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/InviteRequestProcessor.java @@ -349,7 +349,17 @@ public class InviteRequestProcessor extends SIPRequestProcessorParent implements }else { ssrc = contentString.substring(ssrcIndex + 2, ssrcIndex + 12); } - logger.info("[上级点播] 用户:{}, 通道:{}, 地址:{}:{}, ssrc:{}", username, channelId, addressStr, port, ssrc); + String streamTypeStr = null; + if (mediaTransmissionTCP) { + if (tcpActive) { + streamTypeStr = "TCP-ACTIVE"; + }else { + streamTypeStr = "TCP-PASSIVE"; + } + }else { + streamTypeStr = "UDP"; + } + logger.info("[上级点播] 平台:{}, 通道:{}, 收流地址:{}:{},收流方式:{}, ssrc:{}", username, channelId, addressStr, port, streamTypeStr, ssrc); SendRtpItem sendRtpItem = zlmrtpServerFactory.createSendRtpItem(mediaServerItem, addressStr, port, ssrc, requesterId, device.getDeviceId(), channelId, mediaTransmissionTCP, platform.isRtcp()); diff --git a/src/main/java/com/genersoft/iot/vmp/service/impl/PlayServiceImpl.java b/src/main/java/com/genersoft/iot/vmp/service/impl/PlayServiceImpl.java index 0347611c7..96e4098ad 100644 --- a/src/main/java/com/genersoft/iot/vmp/service/impl/PlayServiceImpl.java +++ b/src/main/java/com/genersoft/iot/vmp/service/impl/PlayServiceImpl.java @@ -326,9 +326,9 @@ public class PlayServiceImpl implements IPlayService { logger.info("[点播消息] 收到invite 200, 发现下级自定义了ssrc: {}", ssrcInResponse); if (!mediaServerItem.isRtpEnable() || device.isSsrcCheck()) { logger.info("[点播消息] SSRC修正 {}->{}", ssrcInfo.getSsrc(), ssrcInResponse); - if (!ssrcFactory.checkSsrc(mediaServerItem.getId(),ssrcInResponse)) { // ssrc 不可用 + logger.info("[点播消息] SSRC修正时发现ssrc不可使用 {}->{}", ssrcInfo.getSsrc(), ssrcInResponse); // 释放ssrc ssrcFactory.releaseSsrc(mediaServerItem.getId(), ssrcInfo.getSsrc()); streamSession.remove(device.getDeviceId(), channelId, ssrcInfo.getStream()); @@ -337,8 +337,7 @@ public class PlayServiceImpl implements IPlayService { errorEvent.response(event); return; } - - // 单端口模式streamId也有变化,需要重新设置监听 + // 单端口模式streamId也有变化,重新设置监听即可 if (!mediaServerItem.isRtpEnable()) { // 添加订阅 HookSubscribeForStreamChange hookSubscribe = HookSubscribeFactory.on_stream_changed("rtp", ssrcInfo.getStream(), true, "rtsp", mediaServerItem.getId()); @@ -351,8 +350,11 @@ public class PlayServiceImpl implements IPlayService { onPublishHandlerForPlay(mediaServerItemInUse, response, device.getDeviceId(), channelId); hookEvent.response(mediaServerItemInUse, response); }); + return; } + + // 更新ssrc Boolean result = mediaServerService.updateRtpServerSSRC(mediaServerItem, ssrcInfo.getStream(), ssrcInResponse); if (!result) { try { @@ -372,32 +374,8 @@ public class PlayServiceImpl implements IPlayService { event.statusCode = 500; errorEvent.response(event); } -// // 关闭rtp server -// mediaServerService.closeRTPServer(mediaServerItem, ssrcInfo.getStream(), result->{ -// if (result) { -// // 重新开启ssrc server -// mediaServerService.openRTPServer(mediaServerItem, ssrcInfo.getStream(), ssrcInResponse, device.isSsrcCheck(), false, ssrcInfo.getPort(), true, device.getStreamModeForParam()); -// }else { -// try { -// logger.warn("[停止点播] {}/{}", device.getDeviceId(), channelId); -// cmder.streamByeCmd(device, channelId, ssrcInfo.getStream(), null, null); -// } catch (InvalidArgumentException | SipException | ParseException | SsrcTransactionNotFoundException e) { -// logger.error("[命令发送失败] 停止点播, 发送BYE: {}", e.getMessage()); -// throw new ControllerException(ErrorCode.ERROR100.getCode(), "命令发送失败: " + e.getMessage()); -// } -// -// dynamicTask.stop(timeOutTaskKey); -// // 释放ssrc -// mediaServerService.releaseSsrc(mediaServerItem.getId(), ssrcInfo.getSsrc()); -// -// streamSession.remove(device.getDeviceId(), channelId, ssrcInfo.getStream()); -// event.msg = "下级自定义了ssrc,重新设置收流信息失败"; -// event.statusCode = 500; -// errorEvent.response(event); -// } -// }); - - + }else { + logger.info("[点播消息] 收到invite 200, 下级自定义了ssrc, 但是当前模式无需修正"); } } }, (event) -> { From e2f9ee8f7b2c8b210c75fcd328b2d42c37f9d737 Mon Sep 17 00:00:00 2001 From: 648540858 <648540858@qq.com> Date: Sat, 6 May 2023 17:40:57 +0800 Subject: [PATCH 39/48] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E5=9B=BD=E6=A0=87?= =?UTF-8?q?=E8=A7=86=E9=A2=91=E7=82=B9=E6=92=AD=E4=B8=89=E7=A7=8D=E7=82=B9?= =?UTF-8?q?=E6=92=AD=E6=96=B9=E5=BC=8F=EF=BC=88=E8=87=AA=E5=8A=A8=E7=82=B9?= =?UTF-8?q?=E6=92=AD=EF=BC=8C=E4=B8=8A=E7=BA=A7=E7=82=B9=E6=92=AD=EF=BC=8C?= =?UTF-8?q?=E6=8E=A5=E5=8F=A3=E7=82=B9=E6=92=AD=EF=BC=89=E5=B9=B6=E5=8F=91?= =?UTF-8?q?=E6=83=85=E5=86=B5=E4=B8=8B=E5=A4=B1=E8=B4=A5=E7=9A=84=E9=97=AE?= =?UTF-8?q?=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../genersoft/iot/vmp/common/InviteInfo.java | 126 +++++++ .../iot/vmp/common/InviteSessionStatus.java | 11 + .../iot/vmp/common/InviteSessionType.java | 7 + .../iot/vmp/common/VideoManagerConstants.java | 9 +- .../iot/vmp/conf/ProxyServletConfig.java | 11 +- .../iot/vmp/gb28181/bean/SsrcTransaction.java | 8 +- .../session/VideoStreamSessionManager.java | 9 +- .../transmit/cmd/impl/SIPCommander.java | 7 +- .../request/impl/ByeRequestProcessor.java | 32 +- .../request/impl/InviteRequestProcessor.java | 81 ++-- .../vmp/media/zlm/ZLMHttpHookListener.java | 67 ++-- .../vmp/media/zlm/ZLMMediaListManager.java | 3 +- .../vmp/media/zlm/ZLMRTPServerFactory.java | 9 +- .../vmp/media/zlm/ZlmHttpHookSubscribe.java | 5 +- .../iot/vmp/service/IInviteStreamService.java | 63 ++++ .../iot/vmp/service/IPlayService.java | 15 +- .../vmp/service/bean/InviteErrorCallback.java | 6 + .../iot/vmp/service/bean/InviteErrorCode.java | 34 ++ .../impl/DeviceChannelServiceImpl.java | 26 +- .../service/impl/InviteStreamServiceImpl.java | 178 +++++++++ .../iot/vmp/service/impl/PlayServiceImpl.java | 357 ++++++++++-------- .../redisMsg/RedisGbPlayMsgListener.java | 7 +- .../iot/vmp/storager/IRedisCatchStorage.java | 28 -- .../storager/impl/RedisCatchStorageImpl.java | 81 ---- .../vmanager/gb28181/play/PlayController.java | 100 ++--- .../vmp/web/gb28181/ApiStreamController.java | 153 +++++--- 26 files changed, 916 insertions(+), 517 deletions(-) create mode 100644 src/main/java/com/genersoft/iot/vmp/common/InviteInfo.java create mode 100644 src/main/java/com/genersoft/iot/vmp/common/InviteSessionStatus.java create mode 100644 src/main/java/com/genersoft/iot/vmp/common/InviteSessionType.java create mode 100644 src/main/java/com/genersoft/iot/vmp/service/IInviteStreamService.java create mode 100644 src/main/java/com/genersoft/iot/vmp/service/bean/InviteErrorCallback.java create mode 100644 src/main/java/com/genersoft/iot/vmp/service/bean/InviteErrorCode.java create mode 100644 src/main/java/com/genersoft/iot/vmp/service/impl/InviteStreamServiceImpl.java diff --git a/src/main/java/com/genersoft/iot/vmp/common/InviteInfo.java b/src/main/java/com/genersoft/iot/vmp/common/InviteInfo.java new file mode 100644 index 000000000..9fe43f742 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/common/InviteInfo.java @@ -0,0 +1,126 @@ +package com.genersoft.iot.vmp.common; + +import com.genersoft.iot.vmp.service.bean.SSRCInfo; + +/** + * 记录每次发送invite消息的状态 + */ +public class InviteInfo { + + private String deviceId; + + private String channelId; + + private String stream; + + private SSRCInfo ssrcInfo; + + private String receiveIp; + + private Integer receivePort; + + private String streamMode; + + private InviteSessionType type; + + private InviteSessionStatus status; + + private StreamInfo streamInfo; + + + public static InviteInfo getinviteInfo(String deviceId, String channelId, String stream, SSRCInfo ssrcInfo, + String receiveIp, Integer receivePort, String streamMode, + InviteSessionType type, InviteSessionStatus status) { + InviteInfo inviteInfo = new InviteInfo(); + inviteInfo.setDeviceId(deviceId); + inviteInfo.setChannelId(channelId); + inviteInfo.setStream(stream); + inviteInfo.setSsrcInfo(ssrcInfo); + inviteInfo.setReceiveIp(receiveIp); + inviteInfo.setReceivePort(receivePort); + inviteInfo.setStreamMode(streamMode); + inviteInfo.setType(type); + inviteInfo.setStatus(status); + return inviteInfo; + } + + 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 InviteSessionType getType() { + return type; + } + + public void setType(InviteSessionType type) { + this.type = type; + } + + public InviteSessionStatus getStatus() { + return status; + } + + public void setStatus(InviteSessionStatus status) { + this.status = status; + } + + public StreamInfo getStreamInfo() { + return streamInfo; + } + + public void setStreamInfo(StreamInfo streamInfo) { + this.streamInfo = streamInfo; + } + + public String getStream() { + return stream; + } + + public void setStream(String stream) { + this.stream = stream; + } + + public SSRCInfo getSsrcInfo() { + return ssrcInfo; + } + + public void setSsrcInfo(SSRCInfo ssrcInfo) { + this.ssrcInfo = ssrcInfo; + } + + public String getReceiveIp() { + return receiveIp; + } + + public void setReceiveIp(String receiveIp) { + this.receiveIp = receiveIp; + } + + public Integer getReceivePort() { + return receivePort; + } + + public void setReceivePort(Integer receivePort) { + this.receivePort = receivePort; + } + + public String getStreamMode() { + return streamMode; + } + + public void setStreamMode(String streamMode) { + this.streamMode = streamMode; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/common/InviteSessionStatus.java b/src/main/java/com/genersoft/iot/vmp/common/InviteSessionStatus.java new file mode 100644 index 000000000..04cc7c918 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/common/InviteSessionStatus.java @@ -0,0 +1,11 @@ +package com.genersoft.iot.vmp.common; + +/** + * 标识invite消息发出后的各个状态, + * 收到ok钱停止invite发送cancel, + * 收到200ok后发送BYE停止invite + */ +public enum InviteSessionStatus { + ready, + ok, +} diff --git a/src/main/java/com/genersoft/iot/vmp/common/InviteSessionType.java b/src/main/java/com/genersoft/iot/vmp/common/InviteSessionType.java new file mode 100644 index 000000000..5a6eb85b7 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/common/InviteSessionType.java @@ -0,0 +1,7 @@ +package com.genersoft.iot.vmp.common; + +public enum InviteSessionType { + PLAY, + PLAYBACK, + DOWNLOAD +} diff --git a/src/main/java/com/genersoft/iot/vmp/common/VideoManagerConstants.java b/src/main/java/com/genersoft/iot/vmp/common/VideoManagerConstants.java index ccfe77ec3..51c2ab10c 100644 --- a/src/main/java/com/genersoft/iot/vmp/common/VideoManagerConstants.java +++ b/src/main/java/com/genersoft/iot/vmp/common/VideoManagerConstants.java @@ -16,8 +16,6 @@ public class VideoManagerConstants { public static final String MEDIA_SERVERS_ONLINE_PREFIX = "VMP_MEDIA_ONLINE_SERVERS_"; - public static final String MEDIA_STREAM_PREFIX = "VMP_MEDIA_STREAM"; - public static final String DEVICE_PREFIX = "VMP_DEVICE_"; // 设备同步完成 @@ -28,9 +26,10 @@ public class VideoManagerConstants { public static final String KEEPLIVEKEY_PREFIX = "VMP_KEEPALIVE_"; // TODO 此处多了一个_,暂不修改 - public static final String PLAYER_PREFIX = "VMP_PLAYER_"; - public static final String PLAY_BLACK_PREFIX = "VMP_PLAYBACK_"; - public static final String DOWNLOAD_PREFIX = "VMP_DOWNLOAD_"; + public static final String INVITE_PREFIX = "VMP_INVITE"; + public static final String PLAYER_PREFIX = "VMP_INVITE_PLAY_"; + public static final String PLAY_BLACK_PREFIX = "VMP_INVITE_PLAYBACK_"; + public static final String DOWNLOAD_PREFIX = "VMP_INVITE_DOWNLOAD_"; public static final String PLATFORM_KEEPALIVE_PREFIX = "VMP_PLATFORM_KEEPALIVE_"; diff --git a/src/main/java/com/genersoft/iot/vmp/conf/ProxyServletConfig.java b/src/main/java/com/genersoft/iot/vmp/conf/ProxyServletConfig.java index 6cc3b415b..569b5e199 100644 --- a/src/main/java/com/genersoft/iot/vmp/conf/ProxyServletConfig.java +++ b/src/main/java/com/genersoft/iot/vmp/conf/ProxyServletConfig.java @@ -2,7 +2,6 @@ package com.genersoft.iot.vmp.conf; import com.genersoft.iot.vmp.media.zlm.dto.MediaServerItem; import com.genersoft.iot.vmp.service.IMediaServerService; -import org.apache.catalina.connector.ClientAbortException; import org.apache.http.HttpHost; import org.apache.http.HttpRequest; import org.apache.http.HttpResponse; @@ -194,11 +193,11 @@ public class ProxyServletConfig { } catch (IOException ioException) { if (ioException instanceof ConnectException) { logger.error("录像服务 连接失败"); - }else if (ioException instanceof ClientAbortException) { - /** - * TODO 使用这个代理库实现代理在遇到代理视频文件时,如果是206结果,会遇到报错蛋市目前功能正常, - * TODO 暂时去除异常处理。后续使用其他代理框架修改测试 - */ +// }else if (ioException instanceof ClientAbortException) { +// /** +// * TODO 使用这个代理库实现代理在遇到代理视频文件时,如果是206结果,会遇到报错蛋市目前功能正常, +// * TODO 暂时去除异常处理。后续使用其他代理框架修改测试 +// */ }else { logger.error("录像服务 代理失败: ", e); diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/bean/SsrcTransaction.java b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/SsrcTransaction.java index d27ce2628..6ed8d1441 100644 --- a/src/main/java/com/genersoft/iot/vmp/gb28181/bean/SsrcTransaction.java +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/SsrcTransaction.java @@ -1,6 +1,6 @@ package com.genersoft.iot.vmp.gb28181.bean; -import com.genersoft.iot.vmp.gb28181.session.VideoStreamSessionManager; +import com.genersoft.iot.vmp.common.InviteSessionType; public class SsrcTransaction { @@ -13,7 +13,7 @@ public class SsrcTransaction { private SipTransactionInfo sipTransactionInfo; - private VideoStreamSessionManager.SessionType type; + private InviteSessionType type; public String getDeviceId() { return deviceId; @@ -63,11 +63,11 @@ public class SsrcTransaction { this.ssrc = ssrc; } - public VideoStreamSessionManager.SessionType getType() { + public InviteSessionType getType() { return type; } - public void setType(VideoStreamSessionManager.SessionType type) { + public void setType(InviteSessionType type) { this.type = type; } diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/session/VideoStreamSessionManager.java b/src/main/java/com/genersoft/iot/vmp/gb28181/session/VideoStreamSessionManager.java index dabfdff4f..a5da0186b 100644 --- a/src/main/java/com/genersoft/iot/vmp/gb28181/session/VideoStreamSessionManager.java +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/session/VideoStreamSessionManager.java @@ -1,5 +1,6 @@ package com.genersoft.iot.vmp.gb28181.session; +import com.genersoft.iot.vmp.common.InviteSessionType; import com.genersoft.iot.vmp.common.VideoManagerConstants; import com.genersoft.iot.vmp.conf.UserSetting; import com.genersoft.iot.vmp.gb28181.bean.SipTransactionInfo; @@ -27,12 +28,6 @@ public class VideoStreamSessionManager { @Autowired private RedisTemplate redisTemplate; - public enum SessionType { - play, - playback, - download - } - /** * 添加一个点播/回放的事务信息 * 后续可以通过流Id/callID @@ -43,7 +38,7 @@ public class VideoStreamSessionManager { * @param mediaServerId 所使用的流媒体ID * @param response 回复 */ - public void put(String deviceId, String channelId, String callId, String stream, String ssrc, String mediaServerId, SIPResponse response, SessionType type){ + public void put(String deviceId, String channelId, String callId, String stream, String ssrc, String mediaServerId, SIPResponse response, InviteSessionType type){ SsrcTransaction ssrcTransaction = new SsrcTransaction(); ssrcTransaction.setDeviceId(deviceId); ssrcTransaction.setChannelId(channelId); diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/cmd/impl/SIPCommander.java b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/cmd/impl/SIPCommander.java index 4f0dc11a5..e26d6ed87 100644 --- a/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/cmd/impl/SIPCommander.java +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/cmd/impl/SIPCommander.java @@ -1,6 +1,7 @@ package com.genersoft.iot.vmp.gb28181.transmit.cmd.impl; import com.alibaba.fastjson2.JSONObject; +import com.genersoft.iot.vmp.common.InviteSessionType; import com.genersoft.iot.vmp.common.StreamInfo; import com.genersoft.iot.vmp.conf.SipConfig; import com.genersoft.iot.vmp.conf.UserSetting; @@ -350,7 +351,7 @@ public class SIPCommander implements ISIPCommander { // 这里为例避免一个通道的点播只有一个callID这个参数使用一个固定值 ResponseEvent responseEvent = (ResponseEvent) e.event; SIPResponse response = (SIPResponse) responseEvent.getResponse(); - streamSession.put(device.getDeviceId(), channelId, "play", stream, ssrcInfo.getSsrc(), mediaServerItem.getId(), response, VideoStreamSessionManager.SessionType.play); + streamSession.put(device.getDeviceId(), channelId, "play", stream, ssrcInfo.getSsrc(), mediaServerItem.getId(), response, InviteSessionType.PLAY); okEvent.response(e); }); } @@ -452,7 +453,7 @@ public class SIPCommander implements ISIPCommander { sipSender.transmitRequest(sipLayer.getLocalIp(device.getLocalIp()), request, errorEvent, event -> { ResponseEvent responseEvent = (ResponseEvent) event.event; SIPResponse response = (SIPResponse) responseEvent.getResponse(); - streamSession.put(device.getDeviceId(), channelId,sipSender.getNewCallIdHeader(sipLayer.getLocalIp(device.getLocalIp()),device.getTransport()).getCallId(), ssrcInfo.getStream(), ssrcInfo.getSsrc(), mediaServerItem.getId(), response, VideoStreamSessionManager.SessionType.playback); + streamSession.put(device.getDeviceId(), channelId,sipSender.getNewCallIdHeader(sipLayer.getLocalIp(device.getLocalIp()),device.getTransport()).getCallId(), ssrcInfo.getStream(), ssrcInfo.getSsrc(), mediaServerItem.getId(), response, InviteSessionType.PLAYBACK); okEvent.response(event); }); if (inviteStreamCallback != null) { @@ -580,7 +581,7 @@ public class SIPCommander implements ISIPCommander { if (ssrcIndex >= 0) { ssrc = contentString.substring(ssrcIndex + 2, ssrcIndex + 12); } - streamSession.put(device.getDeviceId(), channelId, response.getCallIdHeader().getCallId(), ssrcInfo.getStream(), ssrc, mediaServerItem.getId(), response, VideoStreamSessionManager.SessionType.download); + streamSession.put(device.getDeviceId(), channelId, response.getCallIdHeader().getCallId(), ssrcInfo.getStream(), ssrc, mediaServerItem.getId(), response, InviteSessionType.DOWNLOAD); okEvent.response(event); }); } diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/ByeRequestProcessor.java b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/ByeRequestProcessor.java index cc3051fb0..66a1ce056 100644 --- a/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/ByeRequestProcessor.java +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/ByeRequestProcessor.java @@ -1,6 +1,7 @@ package com.genersoft.iot.vmp.gb28181.transmit.event.request.impl; -import com.genersoft.iot.vmp.common.StreamInfo; +import com.genersoft.iot.vmp.common.InviteInfo; +import com.genersoft.iot.vmp.common.InviteSessionType; import com.genersoft.iot.vmp.conf.exception.SsrcTransactionNotFoundException; import com.genersoft.iot.vmp.gb28181.bean.Device; import com.genersoft.iot.vmp.gb28181.bean.InviteStreamType; @@ -15,6 +16,7 @@ import com.genersoft.iot.vmp.gb28181.transmit.event.request.SIPRequestProcessorP import com.genersoft.iot.vmp.media.zlm.ZLMRTPServerFactory; import com.genersoft.iot.vmp.media.zlm.dto.MediaServerItem; import com.genersoft.iot.vmp.service.IDeviceService; +import com.genersoft.iot.vmp.service.IInviteStreamService; import com.genersoft.iot.vmp.service.IMediaServerService; import com.genersoft.iot.vmp.service.bean.MessageForPushChannel; import com.genersoft.iot.vmp.storager.IRedisCatchStorage; @@ -26,7 +28,9 @@ import org.springframework.beans.factory.InitializingBean; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; -import javax.sip.*; +import javax.sip.InvalidArgumentException; +import javax.sip.RequestEvent; +import javax.sip.SipException; import javax.sip.address.SipURI; import javax.sip.header.CallIdHeader; import javax.sip.header.FromHeader; @@ -52,6 +56,9 @@ public class ByeRequestProcessor extends SIPRequestProcessorParent implements In @Autowired private IRedisCatchStorage redisCatchStorage; + @Autowired + private IInviteStreamService inviteStreamService; + @Autowired private IDeviceService deviceService; @@ -136,11 +143,6 @@ public class ByeRequestProcessor extends SIPRequestProcessorParent implements In Device device = storager.queryVideoDeviceByChannelId(platformGbId); if (device != null) { storager.stopPlay(device.getDeviceId(), channelId); - StreamInfo streamInfo = redisCatchStorage.queryPlayByDevice(device.getDeviceId(), channelId); - if (streamInfo != null) { - redisCatchStorage.stopPlay(streamInfo); - mediaServerService.closeRTPServer(streamInfo.getMediaServerId(), streamInfo.getStream()); - } SsrcTransaction ssrcTransactionForPlay = streamSession.getSsrcTransaction(device.getDeviceId(), channelId, "play", null); if (ssrcTransactionForPlay != null){ if (ssrcTransactionForPlay.getCallId().equals(callIdHeader.getCallId())){ @@ -151,6 +153,14 @@ public class ByeRequestProcessor extends SIPRequestProcessorParent implements In } streamSession.remove(device.getDeviceId(), channelId, ssrcTransactionForPlay.getStream()); } + InviteInfo inviteInfo = inviteStreamService.getInviteInfoByDeviceAndChannel(InviteSessionType.PLAY, device.getDeviceId(), channelId); + + if (inviteInfo != null) { + inviteStreamService.removeInviteInfo(inviteInfo); + if (inviteInfo.getStreamInfo() != null) { + mediaServerService.closeRTPServer(inviteInfo.getStreamInfo().getMediaServerId(), inviteInfo.getStream()); + } + } } SsrcTransaction ssrcTransactionForPlayBack = streamSession.getSsrcTransaction(device.getDeviceId(), channelId, callIdHeader.getCallId(), null); if (ssrcTransactionForPlayBack != null) { @@ -160,6 +170,14 @@ public class ByeRequestProcessor extends SIPRequestProcessorParent implements In mediaServerService.releaseSsrc(mediaServerItem.getId(), ssrcTransactionForPlayBack.getSsrc()); } streamSession.remove(device.getDeviceId(), channelId, ssrcTransactionForPlayBack.getStream()); + InviteInfo inviteInfo = inviteStreamService.getInviteInfoByDeviceAndChannel(InviteSessionType.PLAYBACK, device.getDeviceId(), channelId); + + if (inviteInfo != null) { + inviteStreamService.removeInviteInfo(inviteInfo); + if (inviteInfo.getStreamInfo() != null) { + mediaServerService.closeRTPServer(inviteInfo.getStreamInfo().getMediaServerId(), inviteInfo.getStream()); + } + } } } diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/InviteRequestProcessor.java b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/InviteRequestProcessor.java index 7034a0e57..2acf402c6 100644 --- a/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/InviteRequestProcessor.java +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/InviteRequestProcessor.java @@ -1,12 +1,10 @@ package com.genersoft.iot.vmp.gb28181.transmit.event.request.impl; -import com.alibaba.fastjson2.JSONObject; +import com.genersoft.iot.vmp.common.StreamInfo; import com.genersoft.iot.vmp.conf.DynamicTask; import com.genersoft.iot.vmp.conf.UserSetting; import com.genersoft.iot.vmp.gb28181.bean.*; -import com.genersoft.iot.vmp.gb28181.event.SipSubscribe; import com.genersoft.iot.vmp.gb28181.session.SSRCFactory; -import com.genersoft.iot.vmp.gb28181.session.VideoStreamSessionManager; import com.genersoft.iot.vmp.gb28181.transmit.SIPProcessorObserver; import com.genersoft.iot.vmp.gb28181.transmit.SIPSender; import com.genersoft.iot.vmp.gb28181.transmit.cmd.impl.SIPCommanderFroPlatform; @@ -21,6 +19,8 @@ import com.genersoft.iot.vmp.service.IMediaServerService; import com.genersoft.iot.vmp.service.IPlayService; import com.genersoft.iot.vmp.service.IStreamProxyService; import com.genersoft.iot.vmp.service.IStreamPushService; +import com.genersoft.iot.vmp.service.bean.InviteErrorCallback; +import com.genersoft.iot.vmp.service.bean.InviteErrorCode; import com.genersoft.iot.vmp.service.bean.MessageForPushChannel; import com.genersoft.iot.vmp.service.bean.SSRCInfo; import com.genersoft.iot.vmp.service.redisMsg.RedisGbPlayMsgListener; @@ -101,9 +101,6 @@ public class InviteRequestProcessor extends SIPRequestProcessorParent implements @Autowired private SIPProcessorObserver sipProcessorObserver; - @Autowired - private VideoStreamSessionManager sessionManager; - @Autowired private UserSetting userSetting; @@ -380,10 +377,10 @@ public class InviteRequestProcessor extends SIPRequestProcessorParent implements Long finalStartTime = startTime; Long finalStopTime = stopTime; - ZlmHttpHookSubscribe.Event hookEvent = (mediaServerItemInUSe, responseJSON) -> { - String app = responseJSON.getString("app"); - String stream = responseJSON.getString("stream"); - logger.info("[上级点播]下级已经开始推流。 回复200OK(SDP), {}/{}", app, stream); + InviteErrorCallback hookEvent = (code, msg, data) -> { + StreamInfo streamInfo = (StreamInfo)data; + MediaServerItem mediaServerItemInUSe = mediaServerService.getOne(streamInfo.getMediaServerId()); + logger.info("[上级点播]下级已经开始推流。 回复200OK(SDP), {}/{}", streamInfo.getApp(), streamInfo.getStream()); // * 0 等待设备推流上来 // * 1 下级已经推流,等待上级平台回复ack // * 2 推流中 @@ -429,10 +426,10 @@ public class InviteRequestProcessor extends SIPRequestProcessorParent implements logger.error("[命令发送失败] 国标级联 回复SdpAck", e); } }; - SipSubscribe.Event errorEvent = ((event) -> { + InviteErrorCallback errorEvent = ((statusCode, msg, data) -> { // 未知错误。直接转发设备点播的错误 try { - Response response = getMessageFactory().createResponse(event.statusCode, evt.getRequest()); + Response response = getMessageFactory().createResponse(statusCode, evt.getRequest()); sipSender.transmitRequest(request.getLocalAddress().getHostAddress(), response); } catch (ParseException | SipException e) { logger.error("未处理的异常 ", e); @@ -450,7 +447,7 @@ public class InviteRequestProcessor extends SIPRequestProcessorParent implements if (result.getCode() != 0) { logger.warn("录像回放失败"); if (result.getEvent() != null) { - errorEvent.response(result.getEvent()); +// errorEvent.response(result.getEvent()); } redisCatchStorage.deleteSendRTPServer(platform.getServerGBId(), channelId, callIdHeader.getCallId(), null); try { @@ -460,53 +457,31 @@ public class InviteRequestProcessor extends SIPRequestProcessorParent implements } } else { if (result.getMediaServerItem() != null) { - hookEvent.response(result.getMediaServerItem(), result.getResponse()); +// hookEvent.response(result.getMediaServerItem(), result.getResponse()); } } }); } else { sendRtpItem.setPlayType(InviteStreamType.PLAY); - SsrcTransaction playTransaction = sessionManager.getSsrcTransaction(device.getDeviceId(), channelId, "play", null); - if (playTransaction != null) { - Boolean streamReady = zlmrtpServerFactory.isStreamReady(mediaServerItem, "rtp", playTransaction.getStream()); - if (!streamReady) { - boolean hasRtpServer = mediaServerService.checkRtpServer(mediaServerItem, "rtp", playTransaction.getStream()); - if (hasRtpServer) { - logger.info("[上级点播]已经开启rtpServer但是尚未收到流,开启监听流的到来"); - HookSubscribeForStreamChange hookSubscribe = HookSubscribeFactory.on_stream_changed("rtp", playTransaction.getStream(), true, "rtsp", mediaServerItem.getId()); - zlmHttpHookSubscribe.addSubscribe(hookSubscribe, hookEvent); - }else { - playTransaction = null; - } - } + String streamId = null; + if (mediaServerItem.isRtpEnable()) { + streamId = String.format("%s_%s", device.getDeviceId(), channelId); + }else { + streamId = String.format("%08x", Integer.parseInt(ssrc)).toUpperCase(); } - if (playTransaction == null) { - // 被点播的通道目前未被点播,则开始点播 - String streamId = null; - if (mediaServerItem.isRtpEnable()) { - streamId = String.format("%s_%s", device.getDeviceId(), channelId); - } - SSRCInfo ssrcInfo = mediaServerService.openRTPServer(mediaServerItem, streamId, ssrc, device.isSsrcCheck(), false, 0, false, device.getStreamModeForParam()); - logger.info(JSONObject.toJSONString(ssrcInfo)); - sendRtpItem.setStreamId(ssrcInfo.getStream()); -// sendRtpItem.setSsrc(ssrcInfo.getSsrc()); - - // 写入redis, 超时时回复 - redisCatchStorage.updateSendRTPSever(sendRtpItem); - playService.play(mediaServerItem, ssrcInfo, device, channelId, hookEvent, errorEvent, (code, msg) -> { + sendRtpItem.setStreamId(streamId); + redisCatchStorage.updateSendRTPSever(sendRtpItem); + playService.play(mediaServerItem, device.getDeviceId(), channelId, ((code, msg, data) -> { + if (code == InviteErrorCode.SUCCESS.getCode()){ + hookEvent.run(code, msg, data); + }else if (code == InviteErrorCode.ERROR_FOR_SIGNALLING_TIMEOUT.getCode() || code == InviteErrorCode.ERROR_FOR_STREAM_TIMEOUT.getCode()){ logger.info("[上级点播]超时, 用户:{}, 通道:{}", username, channelId); redisCatchStorage.deleteSendRTPServer(platform.getServerGBId(), channelId, callIdHeader.getCallId(), null); - }); - } else { + }else { + errorEvent.run(code, msg, data); + } + })); - sendRtpItem.setStreamId(playTransaction.getStream()); - // 写入redis, 超时时回复 - redisCatchStorage.updateSendRTPSever(sendRtpItem); - JSONObject jsonObject = new JSONObject(); - jsonObject.put("app", sendRtpItem.getApp()); - jsonObject.put("stream", sendRtpItem.getStreamId()); - hookEvent.response(mediaServerItem, jsonObject); - } } } else if (gbStream != null) { @@ -559,7 +534,7 @@ public class InviteRequestProcessor extends SIPRequestProcessorParent implements int port, Boolean tcpActive, boolean mediaTransmissionTCP, String channelId, String addressStr, String ssrc, String requesterId) { Boolean streamReady = zlmrtpServerFactory.isStreamReady(mediaServerItem, gbStream.getApp(), gbStream.getStream()); - if (streamReady) { + if (streamReady != null && streamReady) { // 自平台内容 SendRtpItem sendRtpItem = zlmrtpServerFactory.createSendRtpItem(mediaServerItem, addressStr, port, ssrc, requesterId, gbStream.getApp(), gbStream.getStream(), channelId, mediaTransmissionTCP, platform.isRtcp()); @@ -598,7 +573,7 @@ public class InviteRequestProcessor extends SIPRequestProcessorParent implements // 推流 if (streamPushItem.isSelf()) { Boolean streamReady = zlmrtpServerFactory.isStreamReady(mediaServerItem, gbStream.getApp(), gbStream.getStream()); - if (streamReady) { + if (streamReady != null && streamReady) { // 自平台内容 SendRtpItem sendRtpItem = zlmrtpServerFactory.createSendRtpItem(mediaServerItem, addressStr, port, ssrc, requesterId, gbStream.getApp(), gbStream.getStream(), channelId, mediaTransmissionTCP, platform.isRtcp()); diff --git a/src/main/java/com/genersoft/iot/vmp/media/zlm/ZLMHttpHookListener.java b/src/main/java/com/genersoft/iot/vmp/media/zlm/ZLMHttpHookListener.java index 4e9b57d2d..168183568 100644 --- a/src/main/java/com/genersoft/iot/vmp/media/zlm/ZLMHttpHookListener.java +++ b/src/main/java/com/genersoft/iot/vmp/media/zlm/ZLMHttpHookListener.java @@ -2,6 +2,8 @@ package com.genersoft.iot.vmp.media.zlm; import com.alibaba.fastjson2.JSON; import com.alibaba.fastjson2.JSONObject; +import com.genersoft.iot.vmp.common.InviteInfo; +import com.genersoft.iot.vmp.common.InviteSessionType; import com.genersoft.iot.vmp.common.StreamInfo; import com.genersoft.iot.vmp.conf.UserSetting; import com.genersoft.iot.vmp.conf.exception.SsrcTransactionNotFoundException; @@ -22,10 +24,8 @@ import com.genersoft.iot.vmp.media.zlm.dto.hook.*; import com.genersoft.iot.vmp.service.*; import com.genersoft.iot.vmp.storager.IRedisCatchStorage; import com.genersoft.iot.vmp.storager.IVideoManagerStorage; -import com.genersoft.iot.vmp.vmanager.bean.DeferredResultEx; import com.genersoft.iot.vmp.vmanager.bean.ErrorCode; import com.genersoft.iot.vmp.vmanager.bean.StreamContent; -import com.genersoft.iot.vmp.vmanager.bean.WVPResult; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; @@ -70,6 +70,9 @@ public class ZLMHttpHookListener { @Autowired private IRedisCatchStorage redisCatchStorage; + @Autowired + private IInviteStreamService inviteStreamService; + @Autowired private IDeviceService deviceService; @@ -252,7 +255,7 @@ public class ZLMHttpHookListener { result.setEnable_audio(deviceChannel.isHasAudio()); } // 如果是录像下载就设置视频间隔十秒 - if (ssrcTransactionForAll.get(0).getType() == VideoStreamSessionManager.SessionType.download) { + if (ssrcTransactionForAll.get(0).getType() == InviteSessionType.DOWNLOAD) { result.setMp4_max_second(10); result.setEnable_audio(true); result.setEnable_mp4(true); @@ -342,17 +345,10 @@ public class ZLMHttpHookListener { } if ("rtp".equals(param.getApp()) && !param.isRegist()) { - StreamInfo streamInfo = redisCatchStorage.queryPlayByStreamId(param.getStream()); - if (streamInfo != null) { - redisCatchStorage.stopPlay(streamInfo); - storager.stopPlay(streamInfo.getDeviceID(), streamInfo.getChannelId()); - } else { - streamInfo = redisCatchStorage.queryPlayback(null, null, - param.getStream(), null); - if (streamInfo != null) { - redisCatchStorage.stopPlayback(streamInfo.getDeviceID(), streamInfo.getChannelId(), - streamInfo.getStream(), null); - } + InviteInfo inviteInfo = inviteStreamService.getInviteInfoByStream(null, param.getStream()); + if (inviteInfo != null && (inviteInfo.getType() == InviteSessionType.PLAY || inviteInfo.getType() == InviteSessionType.PLAYBACK)) { + inviteStreamService.removeInviteInfo(inviteInfo); + storager.stopPlay(inviteInfo.getDeviceId(), inviteInfo.getChannelId()); } } else { if (!"rtp".equals(param.getApp())) { @@ -450,13 +446,15 @@ public class ZLMHttpHookListener { if ("rtp".equals(param.getApp())) { ret.put("close", userSetting.getStreamOnDemand()); // 国标流, 点播/录像回放/录像下载 - StreamInfo streamInfoForPlayCatch = redisCatchStorage.queryPlayByStreamId(param.getStream()); +// StreamInfo streamInfoForPlayCatch = redisCatchStorage.queryPlayByStreamId(param.getStream()); + + InviteInfo inviteInfo = inviteStreamService.getInviteInfoByStream(null, param.getStream()); // 点播 - if (streamInfoForPlayCatch != null) { + if (inviteInfo != null) { // 收到无人观看说明流也没有在往上级推送 - if (redisCatchStorage.isChannelSendingRTP(streamInfoForPlayCatch.getChannelId())) { + if (redisCatchStorage.isChannelSendingRTP(inviteInfo.getChannelId())) { List sendRtpItems = redisCatchStorage.querySendRTPServerByChnnelId( - streamInfoForPlayCatch.getChannelId()); + inviteInfo.getChannelId()); if (sendRtpItems.size() > 0) { for (SendRtpItem sendRtpItem : sendRtpItems) { ParentPlatform parentPlatform = storager.queryParentPlatByServerGBId(sendRtpItem.getPlatformId()); @@ -470,19 +468,22 @@ public class ZLMHttpHookListener { } } } - Device device = deviceService.getDevice(streamInfoForPlayCatch.getDeviceID()); + Device device = deviceService.getDevice(inviteInfo.getDeviceId()); if (device != null) { try { - cmder.streamByeCmd(device, streamInfoForPlayCatch.getChannelId(), - streamInfoForPlayCatch.getStream(), null); + if (inviteStreamService.getInviteInfo(inviteInfo.getType(), inviteInfo.getDeviceId(), inviteInfo.getChannelId(), inviteInfo.getStream()) != null) { + cmder.streamByeCmd(device, inviteInfo.getChannelId(), + inviteInfo.getStream(), null); + } } catch (InvalidArgumentException | ParseException | SipException | SsrcTransactionNotFoundException e) { logger.error("[无人观看]点播, 发送BYE失败 {}", e.getMessage()); } } - redisCatchStorage.stopPlay(streamInfoForPlayCatch); - storager.stopPlay(streamInfoForPlayCatch.getDeviceID(), streamInfoForPlayCatch.getChannelId()); + inviteStreamService.removeInviteInfo(inviteInfo.getType(), inviteInfo.getDeviceId(), + inviteInfo.getChannelId(), inviteInfo.getStream()); + storager.stopPlay(inviteInfo.getDeviceId(), inviteInfo.getChannelId()); return ret; } // 录像回放 @@ -582,6 +583,7 @@ public class ZLMHttpHookListener { return defaultResult; } logger.info("[ZLM HOOK] 流未找到, 发起自动点播:{}->{}->{}/{}", param.getMediaServerId(), param.getSchema(), param.getApp(), param.getStream()); + RequestMessage msg = new RequestMessage(); String key = DeferredResultHolder.CALLBACK_CMD_PLAY + deviceId + channelId; boolean exist = resultHolder.exist(key, null); @@ -589,31 +591,22 @@ public class ZLMHttpHookListener { String uuid = UUID.randomUUID().toString(); msg.setId(uuid); DeferredResult result = new DeferredResult<>(userSetting.getPlayTimeout().longValue()); - DeferredResultEx deferredResultEx = new DeferredResultEx<>(result); result.onTimeout(() -> { - logger.info("点播接口等待超时"); + logger.info("[ZLM HOOK] 自动点播, 等待超时"); // 释放rtpserver msg.setData(new HookResult(ErrorCode.ERROR100.getCode(), "点播超时")); resultHolder.invokeResult(msg); }); - // TODO 在点播未成功的情况下在此调用接口点播会导致返回的流地址ip错误 - deferredResultEx.setFilter(result1 -> { - WVPResult wvpResult1 = (WVPResult) result1; - HookResult resultForEnd = new HookResult(); - resultForEnd.setCode(wvpResult1.getCode()); - resultForEnd.setMsg(wvpResult1.getMsg()); - return resultForEnd; - }); // 录像查询以channelId作为deviceId查询 - resultHolder.put(key, uuid, deferredResultEx); + resultHolder.put(key, uuid, result); if (!exist) { - playService.play(mediaInfo, deviceId, channelId, null, eventResult -> { - msg.setData(new HookResult(eventResult.statusCode, eventResult.msg)); + playService.play(mediaInfo, deviceId, channelId, (code, message, data) -> { + msg.setData(new HookResult(code, message)); resultHolder.invokeResult(msg); - }, null); + }); } return result; } else { diff --git a/src/main/java/com/genersoft/iot/vmp/media/zlm/ZLMMediaListManager.java b/src/main/java/com/genersoft/iot/vmp/media/zlm/ZLMMediaListManager.java index db2beb0c0..8e9b3d0b9 100644 --- a/src/main/java/com/genersoft/iot/vmp/media/zlm/ZLMMediaListManager.java +++ b/src/main/java/com/genersoft/iot/vmp/media/zlm/ZLMMediaListManager.java @@ -97,7 +97,8 @@ public class ZLMMediaListManager { public void sendStreamEvent(String app, String stream, String mediaServerId) { MediaServerItem mediaServerItem = mediaServerService.getOne(mediaServerId); // 查看推流状态 - if (zlmrtpServerFactory.isStreamReady(mediaServerItem, app, stream)) { + Boolean streamReady = zlmrtpServerFactory.isStreamReady(mediaServerItem, app, stream); + if (streamReady != null && streamReady) { ChannelOnlineEvent channelOnlineEventLister = getChannelOnlineEventLister(app, stream); if (channelOnlineEventLister != null) { try { diff --git a/src/main/java/com/genersoft/iot/vmp/media/zlm/ZLMRTPServerFactory.java b/src/main/java/com/genersoft/iot/vmp/media/zlm/ZLMRTPServerFactory.java index f3e4d4404..423777f6b 100644 --- a/src/main/java/com/genersoft/iot/vmp/media/zlm/ZLMRTPServerFactory.java +++ b/src/main/java/com/genersoft/iot/vmp/media/zlm/ZLMRTPServerFactory.java @@ -330,6 +330,9 @@ public class ZLMRTPServerFactory { */ public Boolean isRtpReady(MediaServerItem mediaServerItem, String streamId) { JSONObject mediaInfo = zlmresTfulUtils.getMediaInfo(mediaServerItem,"rtp", "rtsp", streamId); + if (mediaInfo.getInteger("code") == -2) { + return null; + } return (mediaInfo.getInteger("code") == 0 && mediaInfo.getBoolean("online")); } @@ -338,8 +341,10 @@ public class ZLMRTPServerFactory { */ public Boolean isStreamReady(MediaServerItem mediaServerItem, String app, String streamId) { JSONObject mediaInfo = zlmresTfulUtils.getMediaList(mediaServerItem, app, streamId); - return mediaInfo != null && (mediaInfo.getInteger("code") == 0 - + if (mediaInfo == null || (mediaInfo.getInteger("code") == -2)) { + return null; + } + return (mediaInfo.getInteger("code") == 0 && mediaInfo.getJSONArray("data") != null && mediaInfo.getJSONArray("data").size() > 0); } diff --git a/src/main/java/com/genersoft/iot/vmp/media/zlm/ZlmHttpHookSubscribe.java b/src/main/java/com/genersoft/iot/vmp/media/zlm/ZlmHttpHookSubscribe.java index cf33bb24a..4d0069b1d 100644 --- a/src/main/java/com/genersoft/iot/vmp/media/zlm/ZlmHttpHookSubscribe.java +++ b/src/main/java/com/genersoft/iot/vmp/media/zlm/ZlmHttpHookSubscribe.java @@ -134,9 +134,10 @@ public class ZlmHttpHookSubscribe { /** * 对订阅数据进行过期清理 */ - @Scheduled(cron="0 0/5 * * * ?") //每5分钟执行一次 +// @Scheduled(cron="0 0/5 * * * ?") //每5分钟执行一次 + @Scheduled(fixedRate = 2 * 1000) public void execute(){ - + System.out.println(allSubscribes.size()); Instant instant = Instant.now().minusMillis(TimeUnit.MINUTES.toMillis(5)); int total = 0; for (HookType hookType : allSubscribes.keySet()) { diff --git a/src/main/java/com/genersoft/iot/vmp/service/IInviteStreamService.java b/src/main/java/com/genersoft/iot/vmp/service/IInviteStreamService.java new file mode 100644 index 000000000..439cdded4 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/service/IInviteStreamService.java @@ -0,0 +1,63 @@ +package com.genersoft.iot.vmp.service; + +import com.genersoft.iot.vmp.common.InviteInfo; +import com.genersoft.iot.vmp.common.InviteSessionType; +import com.genersoft.iot.vmp.service.bean.InviteErrorCallback; + +/** + * 记录国标点播的状态,包括实时预览,下载,录像回放 + */ +public interface IInviteStreamService { + + /** + * 更新点播的状态信息 + */ + void updateInviteInfo(InviteInfo inviteInfo); + + /** + * 获取点播的状态信息 + */ + InviteInfo getInviteInfo(InviteSessionType type, + String deviceId, + String channelId, + String stream); + + /** + * 移除点播的状态信息 + */ + void removeInviteInfo(InviteSessionType type, + String deviceId, + String channelId, + String stream); + /** + * 移除点播的状态信息 + */ + void removeInviteInfo(InviteInfo inviteInfo); + /** + * 移除点播的状态信息 + */ + void removeInviteInfoByDeviceAndChannel(InviteSessionType inviteSessionType, String deviceId, String channelId); + + /** + * 获取点播的状态信息 + */ + InviteInfo getInviteInfoByDeviceAndChannel(InviteSessionType type, + String deviceId, + String channelId); + + /** + * 获取点播的状态信息 + */ + InviteInfo getInviteInfoByStream(InviteSessionType type, String stream); + + + /** + * 添加一个invite回调 + */ + void once(InviteSessionType type, String deviceId, String channelId, String stream, InviteErrorCallback callback); + + /** + * 调用一个invite回调 + */ + void call(InviteSessionType type, String deviceId, String channelId, String stream, int code, String msg, Object data); +} diff --git a/src/main/java/com/genersoft/iot/vmp/service/IPlayService.java b/src/main/java/com/genersoft/iot/vmp/service/IPlayService.java index ad59cb6eb..b2b030892 100644 --- a/src/main/java/com/genersoft/iot/vmp/service/IPlayService.java +++ b/src/main/java/com/genersoft/iot/vmp/service/IPlayService.java @@ -1,15 +1,11 @@ package com.genersoft.iot.vmp.service; -import com.alibaba.fastjson2.JSONObject; import com.genersoft.iot.vmp.common.StreamInfo; import com.genersoft.iot.vmp.conf.exception.ServiceException; import com.genersoft.iot.vmp.gb28181.bean.Device; import com.genersoft.iot.vmp.gb28181.bean.InviteStreamCallback; -import com.genersoft.iot.vmp.gb28181.bean.InviteStreamInfo; -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.InviteTimeOutCallback; +import com.genersoft.iot.vmp.service.bean.InviteErrorCallback; import com.genersoft.iot.vmp.service.bean.PlayBackCallback; import com.genersoft.iot.vmp.service.bean.SSRCInfo; @@ -22,12 +18,9 @@ import java.text.ParseException; */ public interface IPlayService { - void onPublishHandlerForPlay(MediaServerItem mediaServerItem, JSONObject resonse, String deviceId, String channelId); - void play(MediaServerItem mediaServerItem, SSRCInfo ssrcInfo, Device device, String channelId, - ZlmHttpHookSubscribe.Event hookEvent, SipSubscribe.Event errorEvent, - InviteTimeOutCallback timeoutCallback); - void play(MediaServerItem mediaServerItem, String deviceId, String channelId, ZlmHttpHookSubscribe.Event event, SipSubscribe.Event errorEvent, Runnable timeoutCallback); + InviteErrorCallback callback); + SSRCInfo play(MediaServerItem mediaServerItem, String deviceId, String channelId, InviteErrorCallback callback); MediaServerItem getNewMediaServerItem(Device device); @@ -36,8 +29,6 @@ public interface IPlayService { */ MediaServerItem getNewMediaServerItemHasAssist(Device device); - void onPublishHandlerForDownload(InviteStreamInfo inviteStreamInfo, String deviceId, String channelId, String toString); - void playBack(String deviceId, String channelId, String startTime, String endTime, InviteStreamCallback infoCallBack, PlayBackCallback playBackCallback); void playBack(MediaServerItem mediaServerItem, SSRCInfo ssrcInfo, String deviceId, String channelId, String startTime, String endTime, InviteStreamCallback infoCallBack, PlayBackCallback hookCallBack); diff --git a/src/main/java/com/genersoft/iot/vmp/service/bean/InviteErrorCallback.java b/src/main/java/com/genersoft/iot/vmp/service/bean/InviteErrorCallback.java new file mode 100644 index 000000000..974057e5c --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/service/bean/InviteErrorCallback.java @@ -0,0 +1,6 @@ +package com.genersoft.iot.vmp.service.bean; + +public interface InviteErrorCallback { + + void run(int code, String msg, T data); +} diff --git a/src/main/java/com/genersoft/iot/vmp/service/bean/InviteErrorCode.java b/src/main/java/com/genersoft/iot/vmp/service/bean/InviteErrorCode.java new file mode 100644 index 000000000..3f3c76b36 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/service/bean/InviteErrorCode.java @@ -0,0 +1,34 @@ +package com.genersoft.iot.vmp.service.bean; + +/** + * 全局错误码 + */ +public enum InviteErrorCode { + SUCCESS(0, "成功"), + ERROR_FOR_SIGNALLING_TIMEOUT(-1, "点播超时"), + ERROR_FOR_STREAM_TIMEOUT(-2, "收流超时"), + ERROR_FOR_RESOURCE_EXHAUSTION(-3, "资源耗尽"), + ERROR_FOR_CATCH_DATA(-4, "缓存数据异常"), + ERROR_FOR_SIGNALLING_ERROR(-5, "收到信令错误"), + ERROR_FOR_STREAM_PARSING_EXCEPTIONS(-6, "流地址解析错误"), + ERROR_FOR_SDP_PARSING_EXCEPTIONS(-7, "SDP信息解析失败"), + ERROR_FOR_SSRC_UNAVAILABLE(-8, "SSRC不可用"), + ERROR_FOR_RESET_SSRC(-9, "重新设置收流信息失败"), + ERROR_FOR_SIP_SENDING_FAILED(-10, "命令发送失败"); + + private final int code; + private final String msg; + + InviteErrorCode(int code, String msg) { + this.code = code; + this.msg = msg; + } + + public int getCode() { + return code; + } + + public String getMsg() { + return msg; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/service/impl/DeviceChannelServiceImpl.java b/src/main/java/com/genersoft/iot/vmp/service/impl/DeviceChannelServiceImpl.java index 229bc0d2c..73adf2e49 100644 --- a/src/main/java/com/genersoft/iot/vmp/service/impl/DeviceChannelServiceImpl.java +++ b/src/main/java/com/genersoft/iot/vmp/service/impl/DeviceChannelServiceImpl.java @@ -1,10 +1,12 @@ package com.genersoft.iot.vmp.service.impl; -import com.genersoft.iot.vmp.common.StreamInfo; +import com.genersoft.iot.vmp.common.InviteInfo; +import com.genersoft.iot.vmp.common.InviteSessionType; import com.genersoft.iot.vmp.gb28181.bean.Device; import com.genersoft.iot.vmp.gb28181.bean.DeviceChannel; import com.genersoft.iot.vmp.gb28181.utils.Coordtransform; import com.genersoft.iot.vmp.service.IDeviceChannelService; +import com.genersoft.iot.vmp.service.IInviteStreamService; import com.genersoft.iot.vmp.storager.IRedisCatchStorage; import com.genersoft.iot.vmp.storager.dao.DeviceChannelMapper; import com.genersoft.iot.vmp.storager.dao.DeviceMapper; @@ -32,6 +34,9 @@ public class DeviceChannelServiceImpl implements IDeviceChannelService { @Autowired private IRedisCatchStorage redisCatchStorage; + @Autowired + private IInviteStreamService inviteStreamService; + @Autowired private DeviceChannelMapper channelMapper; @@ -78,9 +83,10 @@ public class DeviceChannelServiceImpl implements IDeviceChannelService { public void updateChannel(String deviceId, DeviceChannel channel) { String channelId = channel.getChannelId(); channel.setDeviceId(deviceId); - StreamInfo streamInfo = redisCatchStorage.queryPlayByDevice(deviceId, channelId); - if (streamInfo != null) { - channel.setStreamId(streamInfo.getStream()); +// StreamInfo streamInfo = redisCatchStorage.queryPlayByDevice(deviceId, channelId); + InviteInfo inviteInfo = inviteStreamService.getInviteInfoByDeviceAndChannel(InviteSessionType.PLAY, deviceId, channelId); + if (inviteInfo != null && inviteInfo.getStreamInfo() != null) { + channel.setStreamId(inviteInfo.getStreamInfo().getStream()); } String now = DateUtil.getNow(); channel.setUpdateTime(now); @@ -106,9 +112,9 @@ public class DeviceChannelServiceImpl implements IDeviceChannelService { if (channelList.size() == 0) { for (DeviceChannel channel : channels) { channel.setDeviceId(deviceId); - StreamInfo streamInfo = redisCatchStorage.queryPlayByDevice(deviceId, channel.getChannelId()); - if (streamInfo != null) { - channel.setStreamId(streamInfo.getStream()); + InviteInfo inviteInfo = inviteStreamService.getInviteInfoByDeviceAndChannel(InviteSessionType.PLAY, deviceId, channel.getChannelId()); + if (inviteInfo != null && inviteInfo.getStreamInfo() != null) { + channel.setStreamId(inviteInfo.getStreamInfo().getStream()); } String now = DateUtil.getNow(); channel.setUpdateTime(now); @@ -122,9 +128,9 @@ public class DeviceChannelServiceImpl implements IDeviceChannelService { } for (DeviceChannel channel : channels) { channel.setDeviceId(deviceId); - StreamInfo streamInfo = redisCatchStorage.queryPlayByDevice(deviceId, channel.getChannelId()); - if (streamInfo != null) { - channel.setStreamId(streamInfo.getStream()); + InviteInfo inviteInfo = inviteStreamService.getInviteInfoByDeviceAndChannel(InviteSessionType.PLAY, deviceId, channel.getChannelId()); + if (inviteInfo != null && inviteInfo.getStreamInfo() != null) { + channel.setStreamId(inviteInfo.getStreamInfo().getStream()); } String now = DateUtil.getNow(); channel.setUpdateTime(now); diff --git a/src/main/java/com/genersoft/iot/vmp/service/impl/InviteStreamServiceImpl.java b/src/main/java/com/genersoft/iot/vmp/service/impl/InviteStreamServiceImpl.java new file mode 100644 index 000000000..8b8c839e4 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/service/impl/InviteStreamServiceImpl.java @@ -0,0 +1,178 @@ +package com.genersoft.iot.vmp.service.impl; + +import com.alibaba.fastjson2.JSON; +import com.genersoft.iot.vmp.common.InviteInfo; +import com.genersoft.iot.vmp.common.InviteSessionStatus; +import com.genersoft.iot.vmp.common.InviteSessionType; +import com.genersoft.iot.vmp.common.VideoManagerConstants; +import com.genersoft.iot.vmp.service.IInviteStreamService; +import com.genersoft.iot.vmp.service.bean.InviteErrorCallback; +import com.genersoft.iot.vmp.utils.redis.RedisUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CopyOnWriteArrayList; + +@Service +public class InviteStreamServiceImpl implements IInviteStreamService { + + private final Logger logger = LoggerFactory.getLogger(InviteStreamServiceImpl.class); + + private final Map>> inviteErrorCallbackMap = new ConcurrentHashMap<>(); + + @Autowired + private RedisTemplate redisTemplate; + + @Override + public void updateInviteInfo(InviteInfo inviteInfo) { + if (inviteInfo == null || (inviteInfo.getDeviceId() == null || inviteInfo.getChannelId() == null)) { + logger.warn("[更新Invite信息],参数不全: {}", JSON.toJSON(inviteInfo)); + return; + } + InviteInfo inviteInfoForUpdate = null; + + if (InviteSessionStatus.ready == inviteInfo.getStatus()) { + if (inviteInfo.getDeviceId() == null + || inviteInfo.getChannelId() == null + || inviteInfo.getType() == null + || inviteInfo.getStream() == null + ) { + return; + } + inviteInfoForUpdate = inviteInfo; + } else { + InviteInfo inviteInfoInRedis = getInviteInfo(inviteInfo.getType(), inviteInfo.getDeviceId(), + inviteInfo.getChannelId(), inviteInfo.getStream()); + if (inviteInfoInRedis == null) { + logger.warn("[更新Invite信息],未从缓存中读取到Invite信息: deviceId: {}, channel: {}, stream: {}", + inviteInfo.getDeviceId(), inviteInfo.getChannelId(), inviteInfo.getStream()); + return; + } + if (inviteInfo.getStreamInfo() != null) { + inviteInfoInRedis.setStreamInfo(inviteInfo.getStreamInfo()); + } + if (inviteInfo.getSsrcInfo() != null) { + inviteInfoInRedis.setSsrcInfo(inviteInfo.getSsrcInfo()); + } + if (inviteInfo.getStreamMode() != null) { + inviteInfoInRedis.setStreamMode(inviteInfo.getStreamMode()); + } + if (inviteInfo.getReceiveIp() != null) { + inviteInfoInRedis.setReceiveIp(inviteInfo.getReceiveIp()); + } + if (inviteInfo.getReceivePort() != null) { + inviteInfoInRedis.setReceivePort(inviteInfo.getReceivePort()); + } + if (inviteInfo.getStatus() != null) { + inviteInfoInRedis.setStatus(inviteInfo.getStatus()); + } + + inviteInfoForUpdate = inviteInfoInRedis; + + } + String key = VideoManagerConstants.INVITE_PREFIX + + "_" + inviteInfoForUpdate.getType() + + "_" + inviteInfoForUpdate.getDeviceId() + + "_" + inviteInfoForUpdate.getChannelId() + + "_" + inviteInfoForUpdate.getStream(); + redisTemplate.opsForValue().set(key, inviteInfoForUpdate); + } + + @Override + public InviteInfo getInviteInfo(InviteSessionType type, String deviceId, String channelId, String stream) { + String key = VideoManagerConstants.INVITE_PREFIX + + "_" + (type != null ? type : "*") + + "_" + (deviceId != null ? deviceId : "*") + + "_" + (channelId != null ? channelId : "*") + + "_" + (stream != null ? stream : "*"); + List scanResult = RedisUtil.scan(redisTemplate, key); + if (scanResult.size() != 1) { + return null; + } + + return (InviteInfo) redisTemplate.opsForValue().get(scanResult.get(0)); + } + + @Override + public InviteInfo getInviteInfoByDeviceAndChannel(InviteSessionType type, String deviceId, String channelId) { + return getInviteInfo(type, deviceId, channelId, null); + } + + @Override + public InviteInfo getInviteInfoByStream(InviteSessionType type, String stream) { + return getInviteInfo(type, null, null, stream); + } + + @Override + public void removeInviteInfo(InviteSessionType type, String deviceId, String channelId, String stream) { + String scanKey = VideoManagerConstants.INVITE_PREFIX + + "_" + (type != null ? type : "*") + + "_" + (deviceId != null ? deviceId : "*") + + "_" + (channelId != null ? channelId : "*") + + "_" + (stream != null ? stream : "*"); + List scanResult = RedisUtil.scan(redisTemplate, scanKey); + if (scanResult.size() > 0) { + for (Object keyObj : scanResult) { + String key = (String) keyObj; + InviteInfo inviteInfo = (InviteInfo) redisTemplate.opsForValue().get(key); + if (inviteInfo == null) { + continue; + } + redisTemplate.delete(key); + inviteErrorCallbackMap.remove(buildKey(type, deviceId, channelId, inviteInfo.getStream())); + } + } + } + + @Override + public void removeInviteInfoByDeviceAndChannel(InviteSessionType inviteSessionType, String deviceId, String channelId) { + removeInviteInfo(inviteSessionType, deviceId, channelId, null); + } + + @Override + public void removeInviteInfo(InviteInfo inviteInfo) { + removeInviteInfo(inviteInfo.getType(), inviteInfo.getDeviceId(), inviteInfo.getChannelId(), inviteInfo.getStream()); + } + + @Override + public void once(InviteSessionType type, String deviceId, String channelId, String stream, InviteErrorCallback callback) { + String key = buildKey(type, deviceId, channelId, stream); + List> callbacks = inviteErrorCallbackMap.get(key); + if (callbacks == null) { + callbacks = new CopyOnWriteArrayList<>(); + inviteErrorCallbackMap.put(key, callbacks); + } + callbacks.add(callback); + + } + + @Override + public void call(InviteSessionType type, String deviceId, String channelId, String stream, int code, String msg, Object data) { + String key = buildKey(type, deviceId, channelId, stream); + List> callbacks = inviteErrorCallbackMap.get(key); + if (callbacks == null) { + return; + } + for (InviteErrorCallback callback : callbacks) { + callback.run(code, msg, data); + } + inviteErrorCallbackMap.remove(key); + } + + private String buildKey(InviteSessionType type, String deviceId, String channelId, String stream) { + String key = type + "_" + deviceId + "_" + channelId; + // 如果ssrc未null那么可以实现一个通道只能一次操作,ssrc不为null则可以支持一个通道多次invite + if (stream != null) { + key += ("_" + stream); + } + return key; + } + + +} diff --git a/src/main/java/com/genersoft/iot/vmp/service/impl/PlayServiceImpl.java b/src/main/java/com/genersoft/iot/vmp/service/impl/PlayServiceImpl.java index 96e4098ad..1fcac3837 100644 --- a/src/main/java/com/genersoft/iot/vmp/service/impl/PlayServiceImpl.java +++ b/src/main/java/com/genersoft/iot/vmp/service/impl/PlayServiceImpl.java @@ -3,6 +3,9 @@ package com.genersoft.iot.vmp.service.impl; import com.alibaba.fastjson2.JSON; import com.alibaba.fastjson2.JSONArray; import com.alibaba.fastjson2.JSONObject; +import com.genersoft.iot.vmp.common.InviteInfo; +import com.genersoft.iot.vmp.common.InviteSessionStatus; +import com.genersoft.iot.vmp.common.InviteSessionType; import com.genersoft.iot.vmp.common.StreamInfo; import com.genersoft.iot.vmp.conf.DynamicTask; import com.genersoft.iot.vmp.conf.UserSetting; @@ -19,18 +22,13 @@ import com.genersoft.iot.vmp.gb28181.transmit.cmd.impl.SIPCommander; import com.genersoft.iot.vmp.gb28181.transmit.cmd.impl.SIPCommanderFroPlatform; import com.genersoft.iot.vmp.media.zlm.AssistRESTfulUtils; import com.genersoft.iot.vmp.media.zlm.ZLMRESTfulUtils; +import com.genersoft.iot.vmp.media.zlm.ZLMRTPServerFactory; import com.genersoft.iot.vmp.media.zlm.ZlmHttpHookSubscribe; import com.genersoft.iot.vmp.media.zlm.dto.HookSubscribeFactory; import com.genersoft.iot.vmp.media.zlm.dto.HookSubscribeForStreamChange; 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.IMediaService; -import com.genersoft.iot.vmp.service.IPlayService; -import com.genersoft.iot.vmp.service.bean.InviteTimeOutCallback; -import com.genersoft.iot.vmp.service.bean.PlayBackCallback; -import com.genersoft.iot.vmp.service.bean.PlayBackResult; -import com.genersoft.iot.vmp.service.bean.SSRCInfo; +import com.genersoft.iot.vmp.service.*; +import com.genersoft.iot.vmp.service.bean.*; import com.genersoft.iot.vmp.storager.IRedisCatchStorage; import com.genersoft.iot.vmp.storager.IVideoManagerStorage; import com.genersoft.iot.vmp.utils.DateUtil; @@ -72,12 +70,18 @@ public class PlayServiceImpl implements IPlayService { @Autowired private IRedisCatchStorage redisCatchStorage; + @Autowired + private IInviteStreamService inviteStreamService; + @Autowired private DeferredResultHolder resultHolder; @Autowired private ZLMRESTfulUtils zlmresTfulUtils; + @Autowired + private ZLMRTPServerFactory zlmrtpServerFactory; + @Autowired private AssistRESTfulUtils assistRESTfulUtils; @@ -111,137 +115,122 @@ public class PlayServiceImpl implements IPlayService { @Override - public void play(MediaServerItem mediaServerItem, String deviceId, String channelId, - ZlmHttpHookSubscribe.Event hookEvent, SipSubscribe.Event errorEvent, - Runnable timeoutCallback) { + public SSRCInfo play(MediaServerItem mediaServerItem, String deviceId, String channelId, InviteErrorCallback callback) { if (mediaServerItem == null) { throw new ControllerException(ErrorCode.ERROR100.getCode(), "未找到可用的zlm"); } - String key = DeferredResultHolder.CALLBACK_CMD_PLAY + deviceId + channelId; - - RequestMessage msg = new RequestMessage(); - msg.setKey(key); Device device = redisCatchStorage.getDevice(deviceId); - StreamInfo streamInfo = redisCatchStorage.queryPlayByDevice(deviceId, channelId); + InviteInfo inviteInfo = inviteStreamService.getInviteInfoByDeviceAndChannel(InviteSessionType.PLAY, deviceId, channelId); - if (streamInfo != null) { - String streamId = streamInfo.getStream(); - if (streamId == null) { - WVPResult wvpResult = new WVPResult(); - wvpResult.setCode(ErrorCode.ERROR100.getCode()); - wvpResult.setMsg("点播失败, redis缓存streamId等于null"); - msg.setData(wvpResult); - resultHolder.invokeAllResult(msg); - return; - } - String mediaServerId = streamInfo.getMediaServerId(); - MediaServerItem mediaInfo = mediaServerService.getOne(mediaServerId); + if (inviteInfo != null ) { + System.out.println("inviteInfo 已存在"); + if (inviteInfo.getStreamInfo() == null) { + System.out.println("inviteInfo 已存在, StreamInfo 不存在,添加回调等待"); + // 点播发起了但是尚未成功, 仅注册回调等待结果即可 + inviteStreamService.once(InviteSessionType.PLAY, deviceId, channelId, null, callback); + return inviteInfo.getSsrcInfo(); + }else { + StreamInfo streamInfo = inviteInfo.getStreamInfo(); + String streamId = streamInfo.getStream(); + if (streamId == null) { + callback.run(InviteErrorCode.ERROR_FOR_CATCH_DATA.getCode(), "点播失败, redis缓存streamId等于null", null); + inviteStreamService.call(InviteSessionType.PLAY, device.getDeviceId(), channelId, null, + InviteErrorCode.ERROR_FOR_CATCH_DATA.getCode(), + "点播失败, redis缓存streamId等于null", + null); + return inviteInfo.getSsrcInfo(); + } + String mediaServerId = streamInfo.getMediaServerId(); + MediaServerItem mediaInfo = mediaServerService.getOne(mediaServerId); - JSONObject rtpInfo = zlmresTfulUtils.getRtpInfo(mediaInfo, streamId); - if (rtpInfo.getInteger("code") == 0) { - if (rtpInfo.getBoolean("exist")) { - int localPort = rtpInfo.getInteger("local_port"); - if (localPort == 0) { - logger.warn("[点播],点播时发现rtpServer存在,但是尚未开始推流"); - // 此时说明rtpServer已经创建但是流还没有推上来 - WVPResult wvpResult = new WVPResult(); - wvpResult.setCode(ErrorCode.ERROR100.getCode()); - wvpResult.setMsg("点播已经在进行中,请稍候重试"); - msg.setData(wvpResult); - - resultHolder.invokeAllResult(msg); - return; - } else { - WVPResult wvpResult = new WVPResult(); - wvpResult.setCode(ErrorCode.SUCCESS.getCode()); - wvpResult.setMsg(ErrorCode.SUCCESS.getMsg()); - wvpResult.setData(streamInfo); - msg.setData(wvpResult); - resultHolder.invokeAllResult(msg); - if (hookEvent != null) { - hookEvent.response(mediaServerItem, JSON.parseObject(JSON.toJSONString(streamInfo))); - } - } - - } else { - redisCatchStorage.stopPlay(streamInfo); + Boolean ready = zlmrtpServerFactory.isStreamReady(mediaInfo, "rtp", streamId); + if (ready != null && ready) { + callback.run(InviteErrorCode.SUCCESS.getCode(), InviteErrorCode.SUCCESS.getMsg(), streamInfo); + inviteStreamService.call(InviteSessionType.PLAY, device.getDeviceId(), channelId, null, + InviteErrorCode.SUCCESS.getCode(), + InviteErrorCode.SUCCESS.getMsg(), + streamInfo); + return inviteInfo.getSsrcInfo(); + }else { + // 点播发起了但是尚未成功, 仅注册回调等待结果即可 + inviteStreamService.once(InviteSessionType.PLAY, deviceId, channelId, null, callback); storager.stopPlay(streamInfo.getDeviceID(), streamInfo.getChannelId()); - streamInfo = null; + inviteStreamService.removeInviteInfoByDeviceAndChannel(InviteSessionType.PLAY, deviceId, channelId); } - } else { - //zlm连接失败 - redisCatchStorage.stopPlay(streamInfo); - storager.stopPlay(streamInfo.getDeviceID(), streamInfo.getChannelId()); - streamInfo = null; - } } - if (streamInfo == null) { - String streamId = null; - if (mediaServerItem.isRtpEnable()) { - streamId = String.format("%s_%s", device.getDeviceId(), channelId); - } - SSRCInfo ssrcInfo = mediaServerService.openRTPServer(mediaServerItem, streamId, null, device.isSsrcCheck(), false, 0, false, device.getStreamModeForParam()); - if (ssrcInfo == null) { - WVPResult wvpResult = new WVPResult(); - wvpResult.setCode(ErrorCode.ERROR100.getCode()); - wvpResult.setMsg("开启收流失败"); - msg.setData(wvpResult); - resultHolder.invokeAllResult(msg); - return; - } - play(mediaServerItem, ssrcInfo, device, channelId, (mediaServerItemInUse, response) -> { - if (hookEvent != null) { - hookEvent.response(mediaServerItem, response); - } - }, event -> { - // sip error错误 - WVPResult wvpResult = new WVPResult(); - wvpResult.setCode(ErrorCode.ERROR100.getCode()); - wvpResult.setMsg(String.format("点播失败, 错误码: %s, %s", event.statusCode, event.msg)); - msg.setData(wvpResult); - resultHolder.invokeAllResult(msg); - if (errorEvent != null) { - errorEvent.response(event); - } - }, (code, msgStr) -> { - // invite点播超时 - WVPResult wvpResult = new WVPResult(); - wvpResult.setCode(ErrorCode.ERROR100.getCode()); - if (code == 0) { - wvpResult.setMsg("点播超时,请稍候重试"); - } else if (code == 1) { - wvpResult.setMsg("收流超时,请稍候重试"); - } - msg.setData(wvpResult); - // 回复之前所有的点播请求 - resultHolder.invokeAllResult(msg); - }); + String streamId = null; + if (mediaServerItem.isRtpEnable()) { + streamId = String.format("%s_%s", device.getDeviceId(), channelId); } + SSRCInfo ssrcInfo = mediaServerService.openRTPServer(mediaServerItem, streamId, null, device.isSsrcCheck(), false, 0, false, device.getStreamModeForParam()); + if (ssrcInfo == null) { + callback.run(InviteErrorCode.ERROR_FOR_RESOURCE_EXHAUSTION.getCode(), InviteErrorCode.ERROR_FOR_RESOURCE_EXHAUSTION.getMsg(), null); + inviteStreamService.call(InviteSessionType.PLAY, device.getDeviceId(), channelId, null, + InviteErrorCode.ERROR_FOR_RESOURCE_EXHAUSTION.getCode(), + InviteErrorCode.ERROR_FOR_RESOURCE_EXHAUSTION.getMsg(), + null); + return null; + } + // TODO 记录点播的状态 + play(mediaServerItem, ssrcInfo, device, channelId, callback); + return ssrcInfo; } @Override public void play(MediaServerItem mediaServerItem, SSRCInfo ssrcInfo, Device device, String channelId, - ZlmHttpHookSubscribe.Event hookEvent, SipSubscribe.Event errorEvent, - InviteTimeOutCallback timeoutCallback) { + InviteErrorCallback callback) { logger.info("[点播开始] deviceId: {}, channelId: {},收流端口:{}, 收流模式:{}, SSRC: {}, SSRC校验:{}", device.getDeviceId(), channelId, ssrcInfo.getPort(), device.getStreamMode(), ssrcInfo.getSsrc(), device.isSsrcCheck()); + + //端口获取失败的ssrcInfo 没有必要发送点播指令 + if (ssrcInfo.getPort() <= 0) { + logger.info("[点播端口分配异常],deviceId={},channelId={},ssrcInfo={}", device.getDeviceId(), channelId, ssrcInfo); + // 释放ssrc + mediaServerService.releaseSsrc(mediaServerItem.getId(), ssrcInfo.getSsrc()); + streamSession.remove(device.getDeviceId(), channelId, ssrcInfo.getStream()); + + callback.run(InviteErrorCode.ERROR_FOR_RESOURCE_EXHAUSTION.getCode(), "点播端口分配异常", null); + inviteStreamService.call(InviteSessionType.PLAY, device.getDeviceId(), channelId, null, + InviteErrorCode.ERROR_FOR_RESOURCE_EXHAUSTION.getCode(), "点播端口分配异常", null); + return; + } + + // 初始化redis中的invite消息状态 + InviteInfo inviteInfo = InviteInfo.getinviteInfo(device.getDeviceId(), channelId, ssrcInfo.getStream(), ssrcInfo, + mediaServerItem.getSdpIp(), ssrcInfo.getPort(), device.getStreamMode(), InviteSessionType.PLAY, + InviteSessionStatus.ready); + inviteStreamService.updateInviteInfo(inviteInfo); // 超时处理 String timeOutTaskKey = UUID.randomUUID().toString(); dynamicTask.startDelay(timeOutTaskKey, () -> { // 执行超时任务时查询是否已经成功,成功了则不执行超时任务,防止超时任务取消失败的情况 - if (redisCatchStorage.queryPlayByDevice(device.getDeviceId(), channelId) == null) { + InviteInfo inviteInfoForTimeOut = inviteStreamService.getInviteInfoByDeviceAndChannel(InviteSessionType.PLAY, device.getDeviceId(), channelId); + if (inviteInfoForTimeOut == null || inviteInfoForTimeOut.getStreamInfo() == null) { logger.info("[点播超时] 收流超时 deviceId: {}, channelId: {},端口:{}, SSRC: {}", device.getDeviceId(), channelId, ssrcInfo.getPort(), ssrcInfo.getSsrc()); // 点播超时回复BYE 同时释放ssrc以及此次点播的资源 +// InviteInfo inviteInfoForTimeout = inviteStreamService.getInviteInfoByDeviceAndChannel(InviteSessionType.play, device.getDeviceId(), channelId); +// if (inviteInfoForTimeout == null) { +// return; +// } +// if (InviteSessionStatus.ok == inviteInfoForTimeout.getStatus() ) { +// // TODO 发送bye +// }else { +// // TODO 发送cancel +// } + callback.run(InviteErrorCode.ERROR_FOR_STREAM_TIMEOUT.getCode(), InviteErrorCode.ERROR_FOR_STREAM_TIMEOUT.getMsg(), null); + inviteStreamService.call(InviteSessionType.PLAY, device.getDeviceId(), channelId, null, + InviteErrorCode.ERROR_FOR_STREAM_TIMEOUT.getCode(), InviteErrorCode.ERROR_FOR_STREAM_TIMEOUT.getMsg(), null); + + inviteStreamService.removeInviteInfoByDeviceAndChannel(InviteSessionType.PLAY, device.getDeviceId(), channelId); try { cmder.streamByeCmd(device, channelId, ssrcInfo.getStream(), null); } catch (InvalidArgumentException | ParseException | SipException | SsrcTransactionNotFoundException e) { logger.error("[点播超时], 发送BYE失败 {}", e.getMessage()); } finally { - timeoutCallback.run(1, "收流超时"); mediaServerService.releaseSsrc(mediaServerItem.getId(), ssrcInfo.getSsrc()); mediaServerService.closeRTPServer(mediaServerItem, ssrcInfo.getStream()); streamSession.remove(device.getDeviceId(), channelId, ssrcInfo.getStream()); @@ -252,28 +241,26 @@ public class PlayServiceImpl implements IPlayService { } } }, userSetting.getPlayTimeout()); - //端口获取失败的ssrcInfo 没有必要发送点播指令 - if (ssrcInfo.getPort() <= 0) { - logger.info("[点播端口分配异常],deviceId={},channelId={},ssrcInfo={}", device.getDeviceId(), channelId, ssrcInfo); - dynamicTask.stop(timeOutTaskKey); - // 释放ssrc - mediaServerService.releaseSsrc(mediaServerItem.getId(), ssrcInfo.getSsrc()); - streamSession.remove(device.getDeviceId(), channelId, ssrcInfo.getStream()); - RequestMessage msg = new RequestMessage(); - msg.setKey(DeferredResultHolder.CALLBACK_CMD_PLAY + device.getDeviceId() + channelId); - msg.setData(WVPResult.fail(ErrorCode.ERROR100.getCode(), "点播端口分配异常")); - resultHolder.invokeAllResult(msg); - return; - } try { cmder.playStreamCmd(mediaServerItem, ssrcInfo, device, channelId, (MediaServerItem mediaServerItemInuse, JSONObject response) -> { logger.info("收到订阅消息: " + response.toJSONString()); dynamicTask.stop(timeOutTaskKey); - // hook响应 - onPublishHandlerForPlay(mediaServerItemInuse, response, device.getDeviceId(), channelId); - hookEvent.response(mediaServerItemInuse, response); + StreamInfo streamInfo = onPublishHandlerForPlay(mediaServerItemInuse, response, device.getDeviceId(), channelId); + if (streamInfo == null){ + callback.run(InviteErrorCode.ERROR_FOR_STREAM_PARSING_EXCEPTIONS.getCode(), + InviteErrorCode.ERROR_FOR_STREAM_PARSING_EXCEPTIONS.getMsg(), null); + inviteStreamService.call(InviteSessionType.PLAY, device.getDeviceId(), channelId, null, + InviteErrorCode.ERROR_FOR_STREAM_PARSING_EXCEPTIONS.getCode(), + InviteErrorCode.ERROR_FOR_STREAM_PARSING_EXCEPTIONS.getMsg(), null); + return; + } + callback.run(InviteErrorCode.SUCCESS.getCode(), InviteErrorCode.SUCCESS.getMsg(), streamInfo); + inviteStreamService.call(InviteSessionType.PLAY, device.getDeviceId(), channelId, null, + InviteErrorCode.SUCCESS.getCode(), + InviteErrorCode.SUCCESS.getMsg(), + streamInfo); logger.info("[点播成功] deviceId: {}, channelId: {}", device.getDeviceId(), channelId); String streamUrl; if (mediaServerItemInuse.getRtspPort() != 0) { @@ -288,6 +275,8 @@ public class PlayServiceImpl implements IPlayService { zlmresTfulUtils.getSnap(mediaServerItemInuse, streamUrl, 15, 1, path, fileName); }, (event) -> { + inviteInfo.setStatus(InviteSessionStatus.ok); + ResponseEvent responseEvent = (ResponseEvent) event.event; String contentString = new String(responseEvent.getResponse().getRawContent()); // 获取ssrc @@ -319,6 +308,18 @@ public class PlayServiceImpl implements IPlayService { logger.info("[点播-TCP主动连接对方] 结果: {}", jsonObject); } catch (SdpException e) { logger.error("[点播-TCP主动连接对方] deviceId: {}, channelId: {}, 解析200OK的SDP信息失败", device.getDeviceId(), channelId, e); + dynamicTask.stop(timeOutTaskKey); + mediaServerService.closeRTPServer(mediaServerItem, ssrcInfo.getStream()); + // 释放ssrc + mediaServerService.releaseSsrc(mediaServerItem.getId(), ssrcInfo.getSsrc()); + + streamSession.remove(device.getDeviceId(), channelId, ssrcInfo.getStream()); + + callback.run(InviteErrorCode.ERROR_FOR_SDP_PARSING_EXCEPTIONS.getCode(), + InviteErrorCode.ERROR_FOR_SDP_PARSING_EXCEPTIONS.getMsg(), null); + inviteStreamService.call(InviteSessionType.PLAY, device.getDeviceId(), channelId, null, + InviteErrorCode.ERROR_FOR_SDP_PARSING_EXCEPTIONS.getCode(), + InviteErrorCode.ERROR_FOR_SDP_PARSING_EXCEPTIONS.getMsg(), null); } } return; @@ -332,9 +333,13 @@ public class PlayServiceImpl implements IPlayService { // 释放ssrc ssrcFactory.releaseSsrc(mediaServerItem.getId(), ssrcInfo.getSsrc()); streamSession.remove(device.getDeviceId(), channelId, ssrcInfo.getStream()); - event.msg = "下级自定义了ssrc,但是此ssrc不可用"; - event.statusCode = 400; - errorEvent.response(event); + + callback.run(InviteErrorCode.ERROR_FOR_SSRC_UNAVAILABLE.getCode(), + InviteErrorCode.ERROR_FOR_SSRC_UNAVAILABLE.getMsg(), null); + inviteStreamService.call(InviteSessionType.PLAY, device.getDeviceId(), channelId, null, + InviteErrorCode.ERROR_FOR_SSRC_UNAVAILABLE.getCode(), + InviteErrorCode.ERROR_FOR_SSRC_UNAVAILABLE.getMsg(), null); + return; } // 单端口模式streamId也有变化,重新设置监听即可 @@ -342,18 +347,32 @@ public class PlayServiceImpl implements IPlayService { // 添加订阅 HookSubscribeForStreamChange hookSubscribe = HookSubscribeFactory.on_stream_changed("rtp", ssrcInfo.getStream(), true, "rtsp", mediaServerItem.getId()); subscribe.removeSubscribe(hookSubscribe); - hookSubscribe.getContent().put("stream", String.format("%08x", Integer.parseInt(ssrcInResponse)).toUpperCase()); + String stream = String.format("%08x", Integer.parseInt(ssrcInResponse)).toUpperCase(); + hookSubscribe.getContent().put("stream", stream); + inviteInfo.setStream(stream); subscribe.addSubscribe(hookSubscribe, (MediaServerItem mediaServerItemInUse, JSONObject response) -> { logger.info("[ZLM HOOK] ssrc修正后收到订阅消息: " + response.toJSONString()); dynamicTask.stop(timeOutTaskKey); // hook响应 - onPublishHandlerForPlay(mediaServerItemInUse, response, device.getDeviceId(), channelId); - hookEvent.response(mediaServerItemInUse, response); + StreamInfo streamInfo = onPublishHandlerForPlay(mediaServerItemInUse, response, device.getDeviceId(), channelId); + if (streamInfo == null){ + callback.run(InviteErrorCode.ERROR_FOR_STREAM_PARSING_EXCEPTIONS.getCode(), + InviteErrorCode.ERROR_FOR_STREAM_PARSING_EXCEPTIONS.getMsg(), null); + inviteStreamService.call(InviteSessionType.PLAY, device.getDeviceId(), channelId, null, + InviteErrorCode.ERROR_FOR_STREAM_PARSING_EXCEPTIONS.getCode(), + InviteErrorCode.ERROR_FOR_STREAM_PARSING_EXCEPTIONS.getMsg(), null); + return; + } + callback.run(InviteErrorCode.SUCCESS.getCode(), + InviteErrorCode.SUCCESS.getMsg(), null); + inviteStreamService.call(InviteSessionType.PLAY, device.getDeviceId(), channelId, null, + InviteErrorCode.SUCCESS.getCode(), + InviteErrorCode.SUCCESS.getMsg(), + streamInfo); }); return; } - // 更新ssrc Boolean result = mediaServerService.updateRtpServerSSRC(mediaServerItem, ssrcInfo.getStream(), ssrcInResponse); if (!result) { @@ -370,14 +389,23 @@ public class PlayServiceImpl implements IPlayService { mediaServerService.releaseSsrc(mediaServerItem.getId(), ssrcInfo.getSsrc()); streamSession.remove(device.getDeviceId(), channelId, ssrcInfo.getStream()); - event.msg = "下级自定义了ssrc,重新设置收流信息失败"; - event.statusCode = 500; - errorEvent.response(event); + + callback.run(InviteErrorCode.ERROR_FOR_RESET_SSRC.getCode(), + "下级自定义了ssrc,重新设置收流信息失败", null); + inviteStreamService.call(InviteSessionType.PLAY, device.getDeviceId(), channelId, null, + InviteErrorCode.ERROR_FOR_RESET_SSRC.getCode(), + "下级自定义了ssrc,重新设置收流信息失败", null); + + }else { + ssrcInfo.setSsrc(ssrcInResponse); + inviteInfo.setSsrcInfo(ssrcInfo); + inviteInfo.setStream(ssrcInfo.getStream()); } }else { logger.info("[点播消息] 收到invite 200, 下级自定义了ssrc, 但是当前模式无需修正"); } } + inviteStreamService.updateInviteInfo(inviteInfo); }, (event) -> { dynamicTask.stop(timeOutTaskKey); mediaServerService.closeRTPServer(mediaServerItem, ssrcInfo.getStream()); @@ -385,7 +413,14 @@ public class PlayServiceImpl implements IPlayService { mediaServerService.releaseSsrc(mediaServerItem.getId(), ssrcInfo.getSsrc()); streamSession.remove(device.getDeviceId(), channelId, ssrcInfo.getStream()); - errorEvent.response(event); + + callback.run(InviteErrorCode.ERROR_FOR_SIGNALLING_ERROR.getCode(), + String.format("点播失败, 错误码: %s, %s", event.statusCode, event.msg), null); + inviteStreamService.call(InviteSessionType.PLAY, device.getDeviceId(), channelId, null, + InviteErrorCode.ERROR_FOR_RESET_SSRC.getCode(), + String.format("点播失败, 错误码: %s, %s", event.statusCode, event.msg), null); + + inviteStreamService.removeInviteInfoByDeviceAndChannel(InviteSessionType.PLAY, device.getDeviceId(), channelId); }); } catch (InvalidArgumentException | SipException | ParseException e) { @@ -396,40 +431,34 @@ public class PlayServiceImpl implements IPlayService { mediaServerService.releaseSsrc(mediaServerItem.getId(), ssrcInfo.getSsrc()); streamSession.remove(device.getDeviceId(), channelId, ssrcInfo.getStream()); - SipSubscribe.EventResult eventResult = new SipSubscribe.EventResult(); - eventResult.type = SipSubscribe.EventResultType.cmdSendFailEvent; - eventResult.statusCode = -1; - eventResult.msg = "命令发送失败"; - errorEvent.response(eventResult); + + callback.run(InviteErrorCode.ERROR_FOR_SIP_SENDING_FAILED.getCode(), + InviteErrorCode.ERROR_FOR_SIP_SENDING_FAILED.getMsg(), null); + inviteStreamService.call(InviteSessionType.PLAY, device.getDeviceId(), channelId, null, + InviteErrorCode.ERROR_FOR_SIP_SENDING_FAILED.getCode(), + InviteErrorCode.ERROR_FOR_SIP_SENDING_FAILED.getMsg(), null); + + inviteStreamService.removeInviteInfoByDeviceAndChannel(InviteSessionType.PLAY, device.getDeviceId(), channelId); } } - @Override - public void onPublishHandlerForPlay(MediaServerItem mediaServerItem, JSONObject response, String deviceId, String channelId) { + private StreamInfo onPublishHandlerForPlay(MediaServerItem mediaServerItem, JSONObject response, String deviceId, String channelId) { StreamInfo streamInfo = onPublishHandler(mediaServerItem, response, deviceId, channelId); - RequestMessage msg = new RequestMessage(); - msg.setKey(DeferredResultHolder.CALLBACK_CMD_PLAY + deviceId + channelId); if (streamInfo != null) { DeviceChannel deviceChannel = storager.queryChannel(deviceId, channelId); if (deviceChannel != null) { deviceChannel.setStreamId(streamInfo.getStream()); storager.startPlay(deviceId, channelId, streamInfo.getStream()); } - redisCatchStorage.startPlay(streamInfo); - - WVPResult wvpResult = new WVPResult(); - wvpResult.setCode(ErrorCode.SUCCESS.getCode()); - wvpResult.setMsg(ErrorCode.SUCCESS.getMsg()); - wvpResult.setData(streamInfo); - - msg.setData(wvpResult); - resultHolder.invokeAllResult(msg); - - } else { - logger.warn("设备预览API调用失败!"); - msg.setData(WVPResult.fail(ErrorCode.ERROR100.getCode(), "设备预览API调用失败!")); - resultHolder.invokeAllResult(msg); + InviteInfo inviteInfo = inviteStreamService.getInviteInfoByDeviceAndChannel(InviteSessionType.PLAY, deviceId, channelId); + if (inviteInfo != null) { + inviteInfo.setStatus(InviteSessionStatus.ok); + inviteInfo.setStreamInfo(streamInfo); + inviteStreamService.updateInviteInfo(inviteInfo); + } } + return streamInfo; + } private void onPublishHandlerForPlayback(MediaServerItem mediaServerItem, JSONObject response, String deviceId, String channelId, PlayBackCallback playBackCallback) { @@ -442,8 +471,12 @@ public class PlayServiceImpl implements IPlayService { deviceChannel.setStreamId(streamInfo.getStream()); storager.startPlay(deviceId, channelId, streamInfo.getStream()); } - redisCatchStorage.startPlay(streamInfo); - + InviteInfo inviteInfo = inviteStreamService.getInviteInfoByDeviceAndChannel(InviteSessionType.PLAYBACK, deviceId, channelId); + if (inviteInfo != null) { + inviteInfo.setStatus(InviteSessionStatus.ok); + inviteInfo.setStreamInfo(streamInfo); + inviteStreamService.updateInviteInfo(inviteInfo); + } playBackResult.setCode(ErrorCode.SUCCESS.getCode()); playBackResult.setMsg(ErrorCode.SUCCESS.getMsg()); @@ -560,6 +593,7 @@ public class PlayServiceImpl implements IPlayService { return; } redisCatchStorage.startPlayback(streamInfo, inviteStreamInfo.getCallId()); + playBackResult.setCode(ErrorCode.SUCCESS.getCode()); playBackResult.setMsg(ErrorCode.SUCCESS.getMsg()); playBackResult.setData(streamInfo); @@ -858,8 +892,7 @@ public class PlayServiceImpl implements IPlayService { return streamInfo; } - @Override - public void onPublishHandlerForDownload(InviteStreamInfo inviteStreamInfo, String deviceId, String channelId, String uuid) { + private void onPublishHandlerForDownload(InviteStreamInfo inviteStreamInfo, String deviceId, String channelId, String uuid) { RequestMessage msg = new RequestMessage(); msg.setKey(DeferredResultHolder.CALLBACK_CMD_DOWNLOAD + deviceId + channelId); msg.setId(uuid); diff --git a/src/main/java/com/genersoft/iot/vmp/service/redisMsg/RedisGbPlayMsgListener.java b/src/main/java/com/genersoft/iot/vmp/service/redisMsg/RedisGbPlayMsgListener.java index 868e861d8..7399b2a77 100644 --- a/src/main/java/com/genersoft/iot/vmp/service/redisMsg/RedisGbPlayMsgListener.java +++ b/src/main/java/com/genersoft/iot/vmp/service/redisMsg/RedisGbPlayMsgListener.java @@ -264,8 +264,8 @@ public class RedisGbPlayMsgListener implements MessageListener { return; } // 确定流是否在线 - boolean streamReady = zlmrtpServerFactory.isStreamReady(mediaServerItem, content.getApp(), content.getStream()); - if (streamReady) { + Boolean streamReady = zlmrtpServerFactory.isStreamReady(mediaServerItem, content.getApp(), content.getStream()); + if (streamReady != null && streamReady) { logger.info("[回复推流信息] {}/{}", content.getApp(), content.getStream()); responseSendItem(mediaServerItem, content, toId, serial); }else { @@ -301,9 +301,6 @@ public class RedisGbPlayMsgListener implements MessageListener { String key = VideoManagerConstants.VM_MSG_STREAM_PUSH_REQUESTED; logger.info("[redis发送通知] 推流被请求 {}: {}/{}", key, messageForPushChannel.getApp(), messageForPushChannel.getStream()); redisTemplate.convertAndSend(key, JSON.toJSON(messageForPushChannel)); - -// redisCatchStorage.sendStreamPushRequestedMsg(messageForPushChannel); - } } diff --git a/src/main/java/com/genersoft/iot/vmp/storager/IRedisCatchStorage.java b/src/main/java/com/genersoft/iot/vmp/storager/IRedisCatchStorage.java index 42708f7f9..b6cfab4f0 100644 --- a/src/main/java/com/genersoft/iot/vmp/storager/IRedisCatchStorage.java +++ b/src/main/java/com/genersoft/iot/vmp/storager/IRedisCatchStorage.java @@ -23,34 +23,6 @@ public interface IRedisCatchStorage { */ Long getCSEQ(); - /** - * 开始播放时将流存入 - * - * @param stream 流信息 - * @return - */ - boolean startPlay(StreamInfo stream); - - - /** - * 停止播放时删除 - * - * @return - */ - boolean stopPlay(StreamInfo streamInfo); - - /** - * 查询播放列表 - * @return - */ - StreamInfo queryPlay(StreamInfo streamInfo); - - StreamInfo queryPlayByStreamId(String steamId); - - StreamInfo queryPlayByDevice(String deviceId, String channelId); - - Map queryPlayByDeviceId(String deviceId); - boolean startPlayback(StreamInfo stream, String callId); boolean stopPlayback(String deviceId, String channelId, String stream, String callId); diff --git a/src/main/java/com/genersoft/iot/vmp/storager/impl/RedisCatchStorageImpl.java b/src/main/java/com/genersoft/iot/vmp/storager/impl/RedisCatchStorageImpl.java index aed981191..dabe9f828 100644 --- a/src/main/java/com/genersoft/iot/vmp/storager/impl/RedisCatchStorageImpl.java +++ b/src/main/java/com/genersoft/iot/vmp/storager/impl/RedisCatchStorageImpl.java @@ -92,87 +92,6 @@ public class RedisCatchStorageImpl implements IRedisCatchStorage { } } - /** - * 开始播放时将流存入redis - */ - @Override - public boolean startPlay(StreamInfo stream) { - - redisTemplate.opsForValue().set(String.format("%S_%s_%s_%s_%s_%s", VideoManagerConstants.PLAYER_PREFIX, userSetting.getServerId(), - stream.getMediaServerId(), stream.getStream(), stream.getDeviceID(), stream.getChannelId()), - stream); - return true; - } - - /** - * 停止播放时从redis删除 - */ - @Override - public boolean stopPlay(StreamInfo streamInfo) { - if (streamInfo == null) { - return false; - } - Boolean result = redisTemplate.delete(String.format("%S_%s_%s_%s_%s_%s", VideoManagerConstants.PLAYER_PREFIX, - userSetting.getServerId(), - streamInfo.getMediaServerId(), - streamInfo.getStream(), - streamInfo.getDeviceID(), - streamInfo.getChannelId())); - return result != null && result; - } - - /** - * 查询播放列表 - */ - @Override - public StreamInfo queryPlay(StreamInfo streamInfo) { - return (StreamInfo)redisTemplate.opsForValue().get(String.format("%S_%s_%s_%s_%s_%s", - VideoManagerConstants.PLAYER_PREFIX, - userSetting.getServerId(), - streamInfo.getMediaServerId(), - streamInfo.getStream(), - streamInfo.getDeviceID(), - streamInfo.getChannelId())); - } - @Override - public StreamInfo queryPlayByStreamId(String streamId) { - List playLeys = RedisUtil.scan(redisTemplate, String.format("%S_%s_*_%s_*", VideoManagerConstants.PLAYER_PREFIX, userSetting.getServerId(), streamId)); - if (playLeys.size() == 0) { - return null; - } - return (StreamInfo)redisTemplate.opsForValue().get(playLeys.get(0).toString()); - } - - @Override - public StreamInfo queryPlayByDevice(String deviceId, String channelId) { - List playLeys = RedisUtil.scan(redisTemplate, String.format("%S_%s_*_*_%s_%s", VideoManagerConstants.PLAYER_PREFIX, - userSetting.getServerId(), - deviceId, - channelId)); - if (playLeys.size() == 0) { - return null; - } - return (StreamInfo)redisTemplate.opsForValue().get(playLeys.get(0).toString()); - } - - @Override - public Map queryPlayByDeviceId(String deviceId) { - Map streamInfos = new HashMap<>(); - List players = RedisUtil.scan(redisTemplate, String.format("%S_%s_*_*_%s_*", VideoManagerConstants.PLAYER_PREFIX, userSetting.getServerId(),deviceId)); - if (players.size() == 0) { - return streamInfos; - } - for (Object player : players) { - String key = (String) player; - StreamInfo streamInfo = JsonUtil.redisJsonToObject(redisTemplate, key, StreamInfo.class); - if (Objects.isNull(streamInfo)) { - continue; - } - streamInfos.put(streamInfo.getDeviceID() + "_" + streamInfo.getChannelId(), streamInfo); - } - return streamInfos; - } - @Override public boolean startPlayback(StreamInfo stream, String callId) { diff --git a/src/main/java/com/genersoft/iot/vmp/vmanager/gb28181/play/PlayController.java b/src/main/java/com/genersoft/iot/vmp/vmanager/gb28181/play/PlayController.java index 958cc6895..eb828a89c 100644 --- a/src/main/java/com/genersoft/iot/vmp/vmanager/gb28181/play/PlayController.java +++ b/src/main/java/com/genersoft/iot/vmp/vmanager/gb28181/play/PlayController.java @@ -1,7 +1,11 @@ package com.genersoft.iot.vmp.vmanager.gb28181.play; +import com.alibaba.fastjson2.JSON; import com.alibaba.fastjson2.JSONArray; import com.alibaba.fastjson2.JSONObject; +import com.genersoft.iot.vmp.common.InviteInfo; +import com.genersoft.iot.vmp.common.InviteSessionStatus; +import com.genersoft.iot.vmp.common.InviteSessionType; import com.genersoft.iot.vmp.common.StreamInfo; import com.genersoft.iot.vmp.conf.UserSetting; import com.genersoft.iot.vmp.conf.exception.ControllerException; @@ -14,12 +18,13 @@ import com.genersoft.iot.vmp.gb28181.transmit.callback.RequestMessage; import com.genersoft.iot.vmp.gb28181.transmit.cmd.impl.SIPCommander; import com.genersoft.iot.vmp.media.zlm.ZLMRESTfulUtils; import com.genersoft.iot.vmp.media.zlm.dto.MediaServerItem; +import com.genersoft.iot.vmp.service.IInviteStreamService; import com.genersoft.iot.vmp.service.IMediaServerService; import com.genersoft.iot.vmp.service.IMediaService; import com.genersoft.iot.vmp.service.IPlayService; +import com.genersoft.iot.vmp.service.bean.InviteErrorCode; import com.genersoft.iot.vmp.storager.IRedisCatchStorage; import com.genersoft.iot.vmp.storager.IVideoManagerStorage; -import com.genersoft.iot.vmp.vmanager.bean.DeferredResultEx; import com.genersoft.iot.vmp.vmanager.bean.ErrorCode; import com.genersoft.iot.vmp.vmanager.bean.StreamContent; import com.genersoft.iot.vmp.vmanager.bean.WVPResult; @@ -59,6 +64,9 @@ public class PlayController { @Autowired private IRedisCatchStorage redisCatchStorage; + @Autowired + private IInviteStreamService inviteStreamService; + @Autowired private ZLMRESTfulUtils zlmresTfulUtils; @@ -88,14 +96,12 @@ public class PlayController { Device device = storager.queryVideoDevice(deviceId); MediaServerItem newMediaServerItem = playService.getNewMediaServerItem(device); - RequestMessage msg = new RequestMessage(); + RequestMessage requestMessage = new RequestMessage(); String key = DeferredResultHolder.CALLBACK_CMD_PLAY + deviceId + channelId; - boolean exist = resultHolder.exist(key, null); - msg.setKey(key); + requestMessage.setKey(key); String uuid = UUID.randomUUID().toString(); - msg.setId(uuid); + requestMessage.setId(uuid); DeferredResult> result = new DeferredResult<>(userSetting.getPlayTimeout().longValue()); - DeferredResultEx> deferredResultEx = new DeferredResultEx<>(result); result.onTimeout(()->{ logger.info("点播接口等待超时"); @@ -103,32 +109,36 @@ public class PlayController { WVPResult wvpResult = new WVPResult<>(); wvpResult.setCode(ErrorCode.ERROR100.getCode()); wvpResult.setMsg("点播超时"); - msg.setData(wvpResult); - resultHolder.invokeResult(msg); + requestMessage.setData(wvpResult); + resultHolder.invokeResult(requestMessage); }); - // TODO 在点播未成功的情况下在此调用接口点播会导致返回的流地址ip错误 - deferredResultEx.setFilter(result1 -> { - WVPResult wvpResult1 = (WVPResult)result1; - WVPResult resultStream = new WVPResult<>(); - resultStream.setCode(wvpResult1.getCode()); - resultStream.setMsg(wvpResult1.getMsg()); - if (wvpResult1.getCode() == ErrorCode.SUCCESS.getCode()) { - StreamInfo data = wvpResult1.getData().clone(); - if (userSetting.getUseSourceIpAsStreamIp()) { - data.channgeStreamIp(request.getLocalName()); - } - resultStream.setData(new StreamContent(wvpResult1.getData())); - } - return resultStream; - }); - // 录像查询以channelId作为deviceId查询 - resultHolder.put(key, uuid, deferredResultEx); + resultHolder.put(key, uuid, result); - if (!exist) { - playService.play(newMediaServerItem, deviceId, channelId, null, null, null); - } + playService.play(newMediaServerItem, deviceId, channelId, ((code, msg, data) -> { + System.out.println("controller收到回调"); + System.out.println(JSON.toJSONString(data)); + WVPResult wvpResult = new WVPResult<>(); + if (code == InviteErrorCode.SUCCESS.getCode()) { + wvpResult.setCode(ErrorCode.SUCCESS.getCode()); + wvpResult.setMsg(ErrorCode.SUCCESS.getMsg()); + + if (data != null) { + StreamInfo streamInfo = (StreamInfo)data; + if (userSetting.getUseSourceIpAsStreamIp()) { + streamInfo.channgeStreamIp(request.getLocalName()); + } + wvpResult.setData(new StreamContent(streamInfo)); + } + }else { + wvpResult.setCode(code); + wvpResult.setMsg(msg); + } + System.out.println(JSON.toJSONString(wvpResult)); + requestMessage.setData(wvpResult); + resultHolder.invokeResult(requestMessage); + })); return result; } @@ -149,21 +159,22 @@ public class PlayController { throw new ControllerException(ErrorCode.ERROR100.getCode(), "设备[" + deviceId + "]不存在"); } - StreamInfo streamInfo = redisCatchStorage.queryPlayByDevice(deviceId, channelId); - if (streamInfo == null) { + InviteInfo inviteInfo = inviteStreamService.getInviteInfoByDeviceAndChannel(InviteSessionType.PLAY, deviceId, channelId); + if (inviteInfo == null) { throw new ControllerException(ErrorCode.ERROR100.getCode(), "点播未找到"); } - - try { - logger.warn("[停止点播] {}/{}", device.getDeviceId(), channelId); - cmder.streamByeCmd(device, channelId, streamInfo.getStream(), null, null); - } catch (InvalidArgumentException | SipException | ParseException | SsrcTransactionNotFoundException e) { - logger.error("[命令发送失败] 停止点播, 发送BYE: {}", e.getMessage()); - throw new ControllerException(ErrorCode.ERROR100.getCode(), "命令发送失败: " + e.getMessage()); + if (InviteSessionStatus.ok == inviteInfo.getStatus()) { + try { + logger.warn("[停止点播] {}/{}", device.getDeviceId(), channelId); + cmder.streamByeCmd(device, channelId, inviteInfo.getStream(), null, null); + } catch (InvalidArgumentException | SipException | ParseException | SsrcTransactionNotFoundException e) { + logger.error("[命令发送失败] 停止点播, 发送BYE: {}", e.getMessage()); + throw new ControllerException(ErrorCode.ERROR100.getCode(), "命令发送失败: " + e.getMessage()); + } } - redisCatchStorage.stopPlay(streamInfo); + inviteStreamService.removeInviteInfoByDeviceAndChannel(InviteSessionType.PLAY, deviceId, channelId); - storager.stopPlay(streamInfo.getDeviceID(), streamInfo.getChannelId()); + storager.stopPlay(deviceId, channelId); JSONObject json = new JSONObject(); json.put("deviceId", deviceId); json.put("channelId", channelId); @@ -178,15 +189,14 @@ public class PlayController { @Parameter(name = "streamId", description = "视频流ID", required = true) @PostMapping("/convert/{streamId}") public JSONObject playConvert(@PathVariable String streamId) { - StreamInfo streamInfo = redisCatchStorage.queryPlayByStreamId(streamId); - if (streamInfo == null) { - streamInfo = redisCatchStorage.queryPlayback(null, null, streamId, null); - } - if (streamInfo == null) { +// StreamInfo streamInfo = redisCatchStorage.queryPlayByStreamId(streamId); + + InviteInfo inviteInfo = inviteStreamService.getInviteInfoByStream(null, streamId); + if (inviteInfo == null || inviteInfo.getStreamInfo() == null) { logger.warn("视频转码API调用失败!, 视频流已经停止!"); throw new ControllerException(ErrorCode.ERROR100.getCode(), "未找到视频流信息, 视频流可能已经停止"); } - MediaServerItem mediaInfo = mediaServerService.getOne(streamInfo.getMediaServerId()); + MediaServerItem mediaInfo = mediaServerService.getOne(inviteInfo.getStreamInfo().getMediaServerId()); JSONObject rtpInfo = zlmresTfulUtils.getRtpInfo(mediaInfo, streamId); if (!rtpInfo.getBoolean("exist")) { logger.warn("视频转码API调用失败!, 视频流已停止推流!"); diff --git a/src/main/java/com/genersoft/iot/vmp/web/gb28181/ApiStreamController.java b/src/main/java/com/genersoft/iot/vmp/web/gb28181/ApiStreamController.java index 72a1b5da8..5d04f1078 100644 --- a/src/main/java/com/genersoft/iot/vmp/web/gb28181/ApiStreamController.java +++ b/src/main/java/com/genersoft/iot/vmp/web/gb28181/ApiStreamController.java @@ -1,7 +1,8 @@ package com.genersoft.iot.vmp.web.gb28181; import com.alibaba.fastjson2.JSONObject; -import com.genersoft.iot.vmp.common.StreamInfo; +import com.genersoft.iot.vmp.common.InviteInfo; +import com.genersoft.iot.vmp.common.InviteSessionType; import com.genersoft.iot.vmp.conf.UserSetting; import com.genersoft.iot.vmp.conf.exception.SsrcTransactionNotFoundException; import com.genersoft.iot.vmp.gb28181.bean.Device; @@ -9,13 +10,18 @@ import com.genersoft.iot.vmp.gb28181.bean.DeviceChannel; import com.genersoft.iot.vmp.gb28181.transmit.cmd.impl.SIPCommander; import com.genersoft.iot.vmp.media.zlm.dto.MediaServerItem; import com.genersoft.iot.vmp.service.IDeviceService; +import com.genersoft.iot.vmp.service.IInviteStreamService; import com.genersoft.iot.vmp.service.IPlayService; +import com.genersoft.iot.vmp.service.bean.InviteErrorCode; 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.web.bind.annotation.*; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.bind.annotation.RestController; import org.springframework.web.context.request.async.DeferredResult; import javax.sip.InvalidArgumentException; @@ -45,6 +51,9 @@ public class ApiStreamController { @Autowired private IRedisCatchStorage redisCatchStorage; + @Autowired + private IInviteStreamService inviteStreamService; + @Autowired private IDeviceService deviceService; @@ -111,46 +120,96 @@ public class ApiStreamController { return resultDeferredResult; } MediaServerItem newMediaServerItem = playService.getNewMediaServerItem(device); - playService.play(newMediaServerItem, serial, code, (mediaServerItem, response)->{ - StreamInfo streamInfo = redisCatchStorage.queryPlayByDevice(serial, code); - JSONObject result = new JSONObject(); - result.put("StreamID", streamInfo.getStream()); - result.put("DeviceID", device.getDeviceId()); - result.put("ChannelID", code); - result.put("ChannelName", deviceChannel.getName()); - result.put("ChannelCustomName", ""); - result.put("FLV", streamInfo.getFlv().getUrl()); - result.put("WS_FLV", streamInfo.getWs_flv().getUrl()); - result.put("RTMP", streamInfo.getRtmp().getUrl()); - result.put("HLS", streamInfo.getHls().getUrl()); - result.put("RTSP", streamInfo.getRtsp().getUrl()); - result.put("WEBRTC", streamInfo.getRtc().getUrl()); - result.put("CDN", ""); - result.put("SnapURL", ""); - result.put("Transport", device.getTransport()); - result.put("StartAt", ""); - result.put("Duration", ""); - result.put("SourceVideoCodecName", ""); - result.put("SourceVideoWidth", ""); - result.put("SourceVideoHeight", ""); - result.put("SourceVideoFrameRate", ""); - result.put("SourceAudioCodecName", ""); - result.put("SourceAudioSampleRate", ""); - result.put("AudioEnable", ""); - result.put("Ondemand", ""); - result.put("InBytes", ""); - result.put("InBitRate", ""); - result.put("OutBytes", ""); - result.put("NumOutputs", ""); - result.put("CascadeSize", ""); - result.put("RelaySize", ""); - result.put("ChannelPTZType", "0"); - resultDeferredResult.setResult(result); - }, (eventResult) -> { - JSONObject result = new JSONObject(); - result.put("error", "channel[ " + code + " ] " + eventResult.msg); - resultDeferredResult.setResult(result); - }, null); +// playService.play(newMediaServerItem, serial, code, (mediaServerItem, response)->{ +// InviteInfo inviteInfo = inviteStreamService.getInviteInfoByDeviceAndChannel(InviteSessionType.PLAY, serial, code); +// if (inviteInfo != null && inviteInfo.getStreamInfo() != null) { +// JSONObject result = new JSONObject(); +// result.put("StreamID", inviteInfo.getStreamInfo().getStream()); +// result.put("DeviceID", device.getDeviceId()); +// result.put("ChannelID", code); +// result.put("ChannelName", deviceChannel.getName()); +// result.put("ChannelCustomName", ""); +// result.put("FLV", inviteInfo.getStreamInfo().getFlv().getUrl()); +// result.put("WS_FLV", inviteInfo.getStreamInfo().getWs_flv().getUrl()); +// result.put("RTMP", inviteInfo.getStreamInfo().getRtmp().getUrl()); +// result.put("HLS", inviteInfo.getStreamInfo().getHls().getUrl()); +// result.put("RTSP", inviteInfo.getStreamInfo().getRtsp().getUrl()); +// result.put("WEBRTC", inviteInfo.getStreamInfo().getRtc().getUrl()); +// result.put("CDN", ""); +// result.put("SnapURL", ""); +// result.put("Transport", device.getTransport()); +// result.put("StartAt", ""); +// result.put("Duration", ""); +// result.put("SourceVideoCodecName", ""); +// result.put("SourceVideoWidth", ""); +// result.put("SourceVideoHeight", ""); +// result.put("SourceVideoFrameRate", ""); +// result.put("SourceAudioCodecName", ""); +// result.put("SourceAudioSampleRate", ""); +// result.put("AudioEnable", ""); +// result.put("Ondemand", ""); +// result.put("InBytes", ""); +// result.put("InBitRate", ""); +// result.put("OutBytes", ""); +// result.put("NumOutputs", ""); +// result.put("CascadeSize", ""); +// result.put("RelaySize", ""); +// result.put("ChannelPTZType", "0"); +// resultDeferredResult.setResult(result); +// } +// +// }, (eventResult) -> { +// JSONObject result = new JSONObject(); +// result.put("error", "channel[ " + code + " ] " + eventResult.msg); +// resultDeferredResult.setResult(result); +// }, null); + + + playService.play(newMediaServerItem, serial, code, (errorCode, msg, data) -> { + if (errorCode == InviteErrorCode.SUCCESS.getCode()) { + InviteInfo inviteInfo = inviteStreamService.getInviteInfoByDeviceAndChannel(InviteSessionType.PLAY, serial, code); + if (inviteInfo != null && inviteInfo.getStreamInfo() != null) { + JSONObject result = new JSONObject(); + result.put("StreamID", inviteInfo.getStreamInfo().getStream()); + result.put("DeviceID", device.getDeviceId()); + result.put("ChannelID", code); + result.put("ChannelName", deviceChannel.getName()); + result.put("ChannelCustomName", ""); + result.put("FLV", inviteInfo.getStreamInfo().getFlv().getUrl()); + result.put("WS_FLV", inviteInfo.getStreamInfo().getWs_flv().getUrl()); + result.put("RTMP", inviteInfo.getStreamInfo().getRtmp().getUrl()); + result.put("HLS", inviteInfo.getStreamInfo().getHls().getUrl()); + result.put("RTSP", inviteInfo.getStreamInfo().getRtsp().getUrl()); + result.put("WEBRTC", inviteInfo.getStreamInfo().getRtc().getUrl()); + result.put("CDN", ""); + result.put("SnapURL", ""); + result.put("Transport", device.getTransport()); + result.put("StartAt", ""); + result.put("Duration", ""); + result.put("SourceVideoCodecName", ""); + result.put("SourceVideoWidth", ""); + result.put("SourceVideoHeight", ""); + result.put("SourceVideoFrameRate", ""); + result.put("SourceAudioCodecName", ""); + result.put("SourceAudioSampleRate", ""); + result.put("AudioEnable", ""); + result.put("Ondemand", ""); + result.put("InBytes", ""); + result.put("InBitRate", ""); + result.put("OutBytes", ""); + result.put("NumOutputs", ""); + result.put("CascadeSize", ""); + result.put("RelaySize", ""); + result.put("ChannelPTZType", "0"); + resultDeferredResult.setResult(result); + } + }else { + JSONObject result = new JSONObject(); + result.put("error", "channel[ " + code + " ] " + msg); + resultDeferredResult.setResult(result); + } + }); + return resultDeferredResult; } @@ -171,8 +230,8 @@ public class ApiStreamController { ){ - StreamInfo streamInfo = redisCatchStorage.queryPlayByDevice(serial, code); - if (streamInfo == null) { + InviteInfo inviteInfo = inviteStreamService.getInviteInfoByDeviceAndChannel(InviteSessionType.PLAY, serial, code); + if (inviteInfo == null) { JSONObject result = new JSONObject(); result.put("error","未找到流信息"); return result; @@ -184,14 +243,14 @@ public class ApiStreamController { return result; } try { - cmder.streamByeCmd(device, code, streamInfo.getStream(), null); + cmder.streamByeCmd(device, code, inviteInfo.getStream(), null); } catch (InvalidArgumentException | ParseException | SipException | SsrcTransactionNotFoundException e) { JSONObject result = new JSONObject(); result.put("error","发送BYE失败:" + e.getMessage()); return result; } - redisCatchStorage.stopPlay(streamInfo); - storager.stopPlay(streamInfo.getDeviceID(), streamInfo.getChannelId()); + inviteStreamService.removeInviteInfo(inviteInfo); + storager.stopPlay(inviteInfo.getDeviceId(), inviteInfo.getChannelId()); return null; } From 490c55381f75e4c43c050de593eb1a418d9a83ed Mon Sep 17 00:00:00 2001 From: 648540858 <648540858@qq.com> Date: Sat, 6 May 2023 17:59:12 +0800 Subject: [PATCH 40/48] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E5=9B=BD=E6=A0=87?= =?UTF-8?q?=E8=A7=86=E9=A2=91=E7=82=B9=E6=92=AD=E4=B8=89=E7=A7=8D=E7=82=B9?= =?UTF-8?q?=E6=92=AD=E6=96=B9=E5=BC=8F=EF=BC=88=E8=87=AA=E5=8A=A8=E7=82=B9?= =?UTF-8?q?=E6=92=AD=EF=BC=8C=E4=B8=8A=E7=BA=A7=E7=82=B9=E6=92=AD=EF=BC=8C?= =?UTF-8?q?=E6=8E=A5=E5=8F=A3=E7=82=B9=E6=92=AD=EF=BC=89=E5=B9=B6=E5=8F=91?= =?UTF-8?q?=E6=83=85=E5=86=B5=E4=B8=8B=E5=A4=B1=E8=B4=A5=E7=9A=84=E9=97=AE?= =?UTF-8?q?=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../iot/vmp/service/impl/PlayServiceImpl.java | 2 - .../vmanager/gb28181/play/PlayController.java | 2 - .../vmp/web/gb28181/ApiStreamController.java | 43 ------------------- 3 files changed, 47 deletions(-) diff --git a/src/main/java/com/genersoft/iot/vmp/service/impl/PlayServiceImpl.java b/src/main/java/com/genersoft/iot/vmp/service/impl/PlayServiceImpl.java index 1fcac3837..e43f8db0f 100644 --- a/src/main/java/com/genersoft/iot/vmp/service/impl/PlayServiceImpl.java +++ b/src/main/java/com/genersoft/iot/vmp/service/impl/PlayServiceImpl.java @@ -124,9 +124,7 @@ public class PlayServiceImpl implements IPlayService { InviteInfo inviteInfo = inviteStreamService.getInviteInfoByDeviceAndChannel(InviteSessionType.PLAY, deviceId, channelId); if (inviteInfo != null ) { - System.out.println("inviteInfo 已存在"); if (inviteInfo.getStreamInfo() == null) { - System.out.println("inviteInfo 已存在, StreamInfo 不存在,添加回调等待"); // 点播发起了但是尚未成功, 仅注册回调等待结果即可 inviteStreamService.once(InviteSessionType.PLAY, deviceId, channelId, null, callback); return inviteInfo.getSsrcInfo(); diff --git a/src/main/java/com/genersoft/iot/vmp/vmanager/gb28181/play/PlayController.java b/src/main/java/com/genersoft/iot/vmp/vmanager/gb28181/play/PlayController.java index eb828a89c..f6f42b38e 100644 --- a/src/main/java/com/genersoft/iot/vmp/vmanager/gb28181/play/PlayController.java +++ b/src/main/java/com/genersoft/iot/vmp/vmanager/gb28181/play/PlayController.java @@ -117,8 +117,6 @@ public class PlayController { resultHolder.put(key, uuid, result); playService.play(newMediaServerItem, deviceId, channelId, ((code, msg, data) -> { - System.out.println("controller收到回调"); - System.out.println(JSON.toJSONString(data)); WVPResult wvpResult = new WVPResult<>(); if (code == InviteErrorCode.SUCCESS.getCode()) { wvpResult.setCode(ErrorCode.SUCCESS.getCode()); diff --git a/src/main/java/com/genersoft/iot/vmp/web/gb28181/ApiStreamController.java b/src/main/java/com/genersoft/iot/vmp/web/gb28181/ApiStreamController.java index 5d04f1078..8e35d04f0 100644 --- a/src/main/java/com/genersoft/iot/vmp/web/gb28181/ApiStreamController.java +++ b/src/main/java/com/genersoft/iot/vmp/web/gb28181/ApiStreamController.java @@ -120,49 +120,6 @@ public class ApiStreamController { return resultDeferredResult; } MediaServerItem newMediaServerItem = playService.getNewMediaServerItem(device); -// playService.play(newMediaServerItem, serial, code, (mediaServerItem, response)->{ -// InviteInfo inviteInfo = inviteStreamService.getInviteInfoByDeviceAndChannel(InviteSessionType.PLAY, serial, code); -// if (inviteInfo != null && inviteInfo.getStreamInfo() != null) { -// JSONObject result = new JSONObject(); -// result.put("StreamID", inviteInfo.getStreamInfo().getStream()); -// result.put("DeviceID", device.getDeviceId()); -// result.put("ChannelID", code); -// result.put("ChannelName", deviceChannel.getName()); -// result.put("ChannelCustomName", ""); -// result.put("FLV", inviteInfo.getStreamInfo().getFlv().getUrl()); -// result.put("WS_FLV", inviteInfo.getStreamInfo().getWs_flv().getUrl()); -// result.put("RTMP", inviteInfo.getStreamInfo().getRtmp().getUrl()); -// result.put("HLS", inviteInfo.getStreamInfo().getHls().getUrl()); -// result.put("RTSP", inviteInfo.getStreamInfo().getRtsp().getUrl()); -// result.put("WEBRTC", inviteInfo.getStreamInfo().getRtc().getUrl()); -// result.put("CDN", ""); -// result.put("SnapURL", ""); -// result.put("Transport", device.getTransport()); -// result.put("StartAt", ""); -// result.put("Duration", ""); -// result.put("SourceVideoCodecName", ""); -// result.put("SourceVideoWidth", ""); -// result.put("SourceVideoHeight", ""); -// result.put("SourceVideoFrameRate", ""); -// result.put("SourceAudioCodecName", ""); -// result.put("SourceAudioSampleRate", ""); -// result.put("AudioEnable", ""); -// result.put("Ondemand", ""); -// result.put("InBytes", ""); -// result.put("InBitRate", ""); -// result.put("OutBytes", ""); -// result.put("NumOutputs", ""); -// result.put("CascadeSize", ""); -// result.put("RelaySize", ""); -// result.put("ChannelPTZType", "0"); -// resultDeferredResult.setResult(result); -// } -// -// }, (eventResult) -> { -// JSONObject result = new JSONObject(); -// result.put("error", "channel[ " + code + " ] " + eventResult.msg); -// resultDeferredResult.setResult(result); -// }, null); playService.play(newMediaServerItem, serial, code, (errorCode, msg, data) -> { From 98bd8913e7a4c7c8e08f7d49a1a2b54a28274e44 Mon Sep 17 00:00:00 2001 From: xiaoQQya Date: Mon, 8 May 2023 16:15:50 +0800 Subject: [PATCH 41/48] =?UTF-8?q?fix(=E9=80=9A=E9=81=93=E5=BF=AB=E7=85=A7)?= =?UTF-8?q?:=20=E4=BF=AE=E5=A4=8D=E5=89=8D=E5=90=8E=E7=AB=AF=E5=88=86?= =?UTF-8?q?=E7=A6=BB=E9=83=A8=E7=BD=B2=E5=90=8E=E9=80=9A=E9=81=93=E5=BF=AB?= =?UTF-8?q?=E7=85=A7=E4=B8=8D=E6=98=BE=E7=A4=BA=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- web_src/src/components/channelList.vue | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/web_src/src/components/channelList.vue b/web_src/src/components/channelList.vue index 563f43f3d..938638654 100644 --- a/web_src/src/components/channelList.vue +++ b/web_src/src/components/channelList.vue @@ -123,7 +123,6 @@ diff --git a/web_src/src/components/dialog/recordDownload.vue b/web_src/src/components/dialog/recordDownload.vue index c90cf1382..ea44353a0 100644 --- a/web_src/src/components/dialog/recordDownload.vue +++ b/web_src/src/components/dialog/recordDownload.vue @@ -21,7 +21,7 @@ import moment from "moment"; export default { name: 'recordDownload', created() { - + window.addEventListener('beforeunload', this.stopDownloadRecord) }, data() { @@ -197,6 +197,9 @@ export default { console.log(error); }); } + }, + destroyed() { + window.removeEventListener('beforeunload', this.stopDownloadRecord) } }; From 21258d6ba3c0e87ee860f9c3819efbf0ce319d79 Mon Sep 17 00:00:00 2001 From: 648540858 <648540858@qq.com> Date: Tue, 9 May 2023 17:54:36 +0800 Subject: [PATCH 45/48] =?UTF-8?q?=E4=BC=98=E5=8C=96=E5=BD=95=E5=83=8F?= =?UTF-8?q?=E4=B8=8B=E8=BD=BD=E9=A1=B5=E9=9D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- web_src/src/components/dialog/recordDownload.vue | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/web_src/src/components/dialog/recordDownload.vue b/web_src/src/components/dialog/recordDownload.vue index ea44353a0..7a945400d 100644 --- a/web_src/src/components/dialog/recordDownload.vue +++ b/web_src/src/components/dialog/recordDownload.vue @@ -7,6 +7,7 @@ 停止缓存并下载 + 点击下载 @@ -39,7 +40,8 @@ export default { taskId: null, getProgressRun: false, getProgressForFileRun: false, - timer: null + timer: null, + downloadFile: null, }; }, @@ -187,8 +189,9 @@ export default { this.percentage = parseFloat(res.data.data[0].percentage)*100 if (res.data.data[0].percentage === '1') { this.getProgressForFileRun = false; - window.open(res.data.data[0].downloadFile) - this.close(); + this.downloadFile = res.data.data[0].downloadFile + this.title = "文件处理完成,点击按扭下载" + // window.open(res.data.data[0].downloadFile) }else { if (callback)callback() } @@ -196,7 +199,10 @@ export default { }).catch(function (error) { console.log(error); }); - } + }, + downloadFileClientEvent: function (){ + window.open(this.downloadFile ) + } }, destroyed() { window.removeEventListener('beforeunload', this.stopDownloadRecord) From b726dc97538b7d4ba71fd06c14161017dd67c2b7 Mon Sep 17 00:00:00 2001 From: xubinbin <1323875150@qq.com> Date: Wed, 10 May 2023 15:38:22 +0800 Subject: [PATCH 46/48] =?UTF-8?q?"@schedule"=E6=98=AFSpring=E6=A1=86?= =?UTF-8?q?=E6=9E=B6=E6=8F=90=E4=BE=9B=E7=9A=84=E4=B8=80=E7=A7=8D=E5=AE=9A?= =?UTF-8?q?=E6=97=B6=E4=BB=BB=E5=8A=A1=E6=89=A7=E8=A1=8C=E6=9C=BA=E5=88=B6?= =?UTF-8?q?=EF=BC=8C=E9=BB=98=E8=AE=A4=E6=83=85=E5=86=B5=E4=B8=8B=E5=AE=83?= =?UTF-8?q?=E6=98=AF=E5=8D=95=E7=BA=BF=E7=A8=8B=E6=89=A7=E8=A1=8C=EF=BC=8C?= =?UTF-8?q?=E9=A1=B9=E7=9B=AE=E4=B8=AD=E5=A4=9A=E6=AC=A1=E4=BD=BF=E7=94=A8?= =?UTF-8?q?fixedRate=E6=8C=89=E6=8C=87=E5=AE=9A=E9=A2=91=E7=8E=87=E6=89=A7?= =?UTF-8?q?=E8=A1=8C=E4=BB=BB=E5=8A=A1(=E4=B8=8D=E7=AE=A1=E5=89=8D?= =?UTF-8?q?=E9=9D=A2=E4=BB=BB=E5=8A=A1=E6=98=AF=E5=90=A6=E5=B7=B2=E7=BB=8F?= =?UTF-8?q?=E5=AE=8C=E6=88=90)=EF=BC=8C=E5=9C=A8=E5=90=8C=E6=97=B6?= =?UTF-8?q?=E6=89=A7=E8=A1=8C=E5=A4=9A=E4=B8=AA=E5=AE=9A=E6=97=B6=E4=BB=BB?= =?UTF-8?q?=E5=8A=A1=E6=97=B6=E5=8F=AF=E8=83=BD=E4=BC=9A=E5=87=BA=E7=8E=B0?= =?UTF-8?q?=E9=98=BB=E5=A1=9E=E5=92=8C=E6=80=A7=E8=83=BD=E9=97=AE=E9=A2=98?= =?UTF-8?q?=EF=BC=8C=E4=B8=BA=E4=BA=86=E8=A7=A3=E5=86=B3=E8=BF=99=E7=A7=8D?= =?UTF-8?q?=E5=8D=95=E7=BA=BF=E7=A8=8B=E7=93=B6=E9=A2=88=E9=97=AE=E9=A2=98?= =?UTF-8?q?=EF=BC=8C=E5=B0=86=E5=AE=9A=E6=97=B6=E4=BB=BB=E5=8A=A1=E7=9A=84?= =?UTF-8?q?=E6=89=A7=E8=A1=8C=E6=9C=BA=E5=88=B6=E6=94=B9=E4=B8=BA=E6=94=AF?= =?UTF-8?q?=E6=8C=81=E5=A4=9A=E7=BA=BF=E7=A8=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../iot/vmp/conf/ScheduleConfig.java | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 src/main/java/com/genersoft/iot/vmp/conf/ScheduleConfig.java diff --git a/src/main/java/com/genersoft/iot/vmp/conf/ScheduleConfig.java b/src/main/java/com/genersoft/iot/vmp/conf/ScheduleConfig.java new file mode 100644 index 000000000..432fafbb0 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/conf/ScheduleConfig.java @@ -0,0 +1,30 @@ +package com.genersoft.iot.vmp.conf; + +import org.apache.commons.lang3.concurrent.BasicThreadFactory; +import org.springframework.context.annotation.Configuration; +import org.springframework.scheduling.annotation.SchedulingConfigurer; +import org.springframework.scheduling.config.ScheduledTaskRegistrar; + +import java.util.concurrent.ScheduledThreadPoolExecutor; +import java.util.concurrent.ThreadPoolExecutor; + +/** + * "@Scheduled"是Spring框架提供的一种定时任务执行机制,默认情况下它是单线程的,在同时执行多个定时任务时可能会出现阻塞和性能问题。 + * 为了解决这种单线程瓶颈问题,可以将定时任务的执行机制改为支持多线程 + */ +@Configuration +public class ScheduleConfig implements SchedulingConfigurer { + + public static final int cpuNum = Runtime.getRuntime().availableProcessors(); + + private static final int corePoolSize = cpuNum; + + private static final String threadNamePrefix = "scheduled-task-pool-%d"; + + @Override + public void configureTasks(ScheduledTaskRegistrar taskRegistrar) { + taskRegistrar.setScheduler(new ScheduledThreadPoolExecutor(corePoolSize, + new BasicThreadFactory.Builder().namingPattern(threadNamePrefix).daemon(true).build(), + new ThreadPoolExecutor.CallerRunsPolicy())); + } +} From b498e2fcf21ee4f612dfaf0b45a945c52da37c60 Mon Sep 17 00:00:00 2001 From: 648540858 <648540858@qq.com> Date: Fri, 12 May 2023 12:36:38 +0800 Subject: [PATCH 47/48] =?UTF-8?q?=E6=9B=B4=E6=96=B0=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 39 ++++++++++++++------------------------- doc/README.md | 3 +++ doc/_content/qa/bug.md | 20 ++++++-------------- doc/_media/shequ.png | Bin 0 -> 36126 bytes 4 files changed, 23 insertions(+), 39 deletions(-) create mode 100644 doc/_media/shequ.png diff --git a/README.md b/README.md index e231d51c3..5b0c8e86e 100644 --- a/README.md +++ b/README.md @@ -15,17 +15,20 @@ WEB VIDEO PLATFORM是一个基于GB28181-2016标准实现的开箱即用的网 前端页面基于@Kyle MediaServerUI [https://gitee.com/kkkkk5G/MediaServerUI](https://gitee.com/kkkkk5G/MediaServerUI) 进行修改. # 应用场景: -支持浏览器无插件播放摄像头视频。 -支持摄像机、平台、NVR等设备接入。 +支持浏览器无插件播放摄像头视频。 +支持国标设备(摄像机、平台、NVR等)设备接入 +支持非国标(onvif, rtsp, rtmp,直播设备等等)设备接入,充分利旧。 支持国标级联。多平台级联。跨网视频预览。 -支持rtsp/rtmp等视频流转发到国标平台。 -支持rtsp/rtmp等推流转发到国标平台。 +支持跨网网闸平台互联。 -# 项目目标 -旨在打造一个易配置,易使用,便于维护的28181国标信令系统, 依托优秀的开源流媒体服务框架ZLMediaKit, 实现一个完整易用GB28181平台. -# 部署文档 -[doc.wvp-pro.cn](https://doc.wvp-pro.cn) +# 文档 +wvp使用文档 [https://doc.wvp-pro.cn](https://doc.wvp-pro.cn) +ZLM使用文档 [https://github.com/ZLMediaKit/ZLMediaKit](https://github.com/ZLMediaKit/ZLMediaKit) + +# 社群地址 +[![社群](doc/_media/shequ.png "shequ")](https://t.zsxq.com/0d8VAD3Dm) +> 收费是为了提供更好的服务,也是对作者更大的激励。加入星球的用户三天后可以私信我留下微信号,我会拉大家入群。加入三天内不满意可以直接退款,大家不需要有顾虑,来白嫖三天也不是不可以。 # gitee同步仓库 https://gitee.com/pan648540858/wvp-GB28181-pro.git @@ -100,29 +103,16 @@ https://gitee.com/pan648540858/wvp-GB28181-pro.git - [X] 云端录像,推流/代理/国标视频均可以录制在云端服务器,支持预览和下载 - [X] 支持打包可执行jar和war - [X] 支持跨域请求,支持前后端分离部署 - - -# 遇到问题如何解决 -国标最麻烦的地方在于设备的兼容性,所以需要大量的设备来测试,目前作者手里的设备有限,再加上作者水平有限,所以遇到问题在所难免; -1. 查看文档网站,仔细的阅读可以帮你避免几乎所有的问题 -2. 搜索issues,这里有大部分的答案 -3. 你可以把遇到问题的设备寄给我,可以更容易的兼容设备和解决问题。 -4. 欢迎加入[知识星球](https://t.zsxq.com/0d8VAD3Dm)支持本项目,同时可以得到更加快速的解答。 - -# 使用帮助 -ZLM使用文档[https://github.com/ZLMediaKit/ZLMediaKit](https://github.com/ZLMediaKit/ZLMediaKit) -wvp官方文档[doc.wvp-pro.cn](https://doc.wvp-pro.cn) -QQ群不再接受新成员直接进入,希望大家多多参考文档,用户可加入[知识星球](https://t.zsxq.com/0d8VAD3Dm)提问以支持本项目,欢迎star和提交pr。 # 授权协议 本项目自有代码使用宽松的MIT协议,在保留版权信息的情况下可以自由应用于各自商用、非商业的项目。 但是本项目也零碎的使用了一些其他的开源代码,在商用的情况下请自行替代或剔除; 由于使用本项目而产生的商业纠纷或侵权行为一概与本项目及开发者无关,请自行承担法律风险。 在使用本项目代码时,也应该在授权协议中同时表明本项目依赖的第三方库的协议 # 技术支持 -建议加入[知识星球](https://t.zsxq.com/0d8VAD3Dm)可以获取更多的教程以及更加及时的回复。 -目前已经更新的内容: + +[知识星球](https://t.zsxq.com/0d8VAD3Dm)专栏列表: - [使用入门系列一:WVP-PRO能做什么](https://t.zsxq.com/0dLguVoSp) -如果项目需要一对一的技术支持,或者棘手的问题需要解决,请发送邮件到648540858@qq.com +有偿技术支持,请发送邮件到648540858@qq.com # 致谢 感谢作者[夏楚](https://github.com/xia-chu) 提供这么棒的开源流媒体服务框架,并在开发过程中给予支持与帮助。 @@ -135,5 +125,4 @@ QQ群不再接受新成员直接进入,希望大家多多参考文档,用户 [ydpd](https://github.com/ydpd) [szy833](https://github.com/szy833) [ydwxb](https://github.com/ydwxb) [Albertzhu666](https://github.com/Albertzhu666) [mk1990](https://github.com/mk1990) [SaltFish001](https://github.com/SaltFish001) -ps: 刚增加了这个名单,肯定遗漏了一些大佬,欢迎大佬联系我添加。 diff --git a/doc/README.md b/doc/README.md index 0fb5b86d4..f652f12fe 100644 --- a/doc/README.md +++ b/doc/README.md @@ -13,6 +13,9 @@ - 前端完善,自带完整前端页面,无需二次开发可直接部署使用。 - 完全开源,且使用MIT许可协议。保留版权的情况下可以用于商业项目。 - 支持多流媒体节点负载均衡。 +# 社群 +[![社群](_media/shequ.png "shequ")](https://t.zsxq.com/0d8VAD3Dm) +> 收费是为了提供更好的服务,也是对作者更大的激励。加入星球的用户三天后可以私信我留下微信号,我会拉大家入群。加入三天内不满意可以直接退款,大家不需要有顾虑,来白嫖三天也不是不可以。 # 我们实现了哪些国标功能 **作为上级平台** diff --git a/doc/_content/qa/bug.md b/doc/_content/qa/bug.md index f452161a4..39b8dd303 100644 --- a/doc/_content/qa/bug.md +++ b/doc/_content/qa/bug.md @@ -2,18 +2,10 @@ # 反馈bug 代码是在不断的完善的,不断修改会修复旧的问题也有可能引入新的问题,所以遇到BUG是很正常的一件事。所以遇到问题不要烦燥,咱们就事论事就好了。 ## 如何反馈 -1. 更新代码,很可能你遇到问题别人已经更早的遇到了,或者是作者自己发现了,已经解决了,所以你可以更新代码再次进行测试; -2. 可以在github提ISSUE,我几乎每天都会去看issue,你的问题我会尽快给予答复; -3. 你可以来我的QQ群里,询问群友看看是否遇到了同样的问题; -4. 你可以私聊我的QQ,如果我有时间我会给你答复,但是除非你有明确的复现步骤或者修复方案,否则你可能等不到我的答复。 - -## 如何快速解决BUG -目前解决BUG有三种方式: -1. 作者验证以及修复; -2. 热心开发者提来的PR; -3. 使用运维手段屏蔽BUG的影响。 - -- 对于第一种:详细的复现步骤,完整的抓包文件,有条理的错误分析都可以帮助作者复现问题,进而解决问题。解决问题往往不是最难的,复现才是。 -- 对于第二种:如果你是开发者,你已经发现了造成BUG的原因以及知道如何正确的修复,那么我很希望你PR,SRS的大佬经常说的,开源不是一个人的事。所以你的参与就是最大的鼓励。 -- 对于第三种:如果你有一个有经验的运维伙伴,那么部分问题是可以通过运维的手段暂时屏蔽的,在等待修复的这段时间了以保证项目的运行。 +1. 在知识星球提问。 +2. 更新代码,很可能你遇到问题别人已经更早的遇到了,或者是作者自己发现了,已经解决了,所以你可以更新代码再次进行测试; +3. 可以在github提ISSUE,我几乎每天都会去看issue,你的问题我会尽快给予答复; +> 有偿支持可以给我发邮件, 648540858@qq.com +[![社群](../../_media/shequ.png "shequ")](https://t.zsxq.com/0d8VAD3Dm) +> 收费是为了提供更好的服务,也是对作者更大的激励。加入星球的用户三天后可以私信我留下微信号,我会拉大家入群。加入三天内不满意可以直接退款,大家不需要有顾虑,来白嫖三天也不是不可以。 \ No newline at end of file diff --git a/doc/_media/shequ.png b/doc/_media/shequ.png new file mode 100644 index 0000000000000000000000000000000000000000..c5aec9807af56f568fde80555f823c8cd70950e5 GIT binary patch literal 36126 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≪hQ)1F@&A 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 literal 0 HcmV?d00001 From cf48bd95ff853b481d2d372c847215eab02c96c0 Mon Sep 17 00:00:00 2001 From: 648540858 <648540858@qq.com> Date: Fri, 12 May 2023 12:40:22 +0800 Subject: [PATCH 48/48] =?UTF-8?q?=E6=9B=B4=E6=96=B0=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 1 + doc/README.md | 1 + doc/_content/qa/bug.md | 1 + 3 files changed, 3 insertions(+) diff --git a/README.md b/README.md index 5b0c8e86e..3aa99f86a 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,7 @@ WEB VIDEO PLATFORM是一个基于GB28181-2016标准实现的开箱即用的网 # 文档 wvp使用文档 [https://doc.wvp-pro.cn](https://doc.wvp-pro.cn) ZLM使用文档 [https://github.com/ZLMediaKit/ZLMediaKit](https://github.com/ZLMediaKit/ZLMediaKit) +> wvp文档由gitee提供服务,如果遇到打不开请多刷新几次。 # 社群地址 [![社群](doc/_media/shequ.png "shequ")](https://t.zsxq.com/0d8VAD3Dm) diff --git a/doc/README.md b/doc/README.md index f652f12fe..3d2f09f76 100644 --- a/doc/README.md +++ b/doc/README.md @@ -13,6 +13,7 @@ - 前端完善,自带完整前端页面,无需二次开发可直接部署使用。 - 完全开源,且使用MIT许可协议。保留版权的情况下可以用于商业项目。 - 支持多流媒体节点负载均衡。 + # 社群 [![社群](_media/shequ.png "shequ")](https://t.zsxq.com/0d8VAD3Dm) > 收费是为了提供更好的服务,也是对作者更大的激励。加入星球的用户三天后可以私信我留下微信号,我会拉大家入群。加入三天内不满意可以直接退款,大家不需要有顾虑,来白嫖三天也不是不可以。 diff --git a/doc/_content/qa/bug.md b/doc/_content/qa/bug.md index 39b8dd303..81267ffe2 100644 --- a/doc/_content/qa/bug.md +++ b/doc/_content/qa/bug.md @@ -7,5 +7,6 @@ 3. 可以在github提ISSUE,我几乎每天都会去看issue,你的问题我会尽快给予答复; > 有偿支持可以给我发邮件, 648540858@qq.com +## 社群 [![社群](../../_media/shequ.png "shequ")](https://t.zsxq.com/0d8VAD3Dm) > 收费是为了提供更好的服务,也是对作者更大的激励。加入星球的用户三天后可以私信我留下微信号,我会拉大家入群。加入三天内不满意可以直接退款,大家不需要有顾虑,来白嫖三天也不是不可以。 \ No newline at end of file