Merge branch 'wvp-28181-2.0' into main-dev

# Conflicts:
#	src/main/java/com/genersoft/iot/vmp/common/VideoManagerConstants.java
#	src/main/java/com/genersoft/iot/vmp/gb28181/transmit/cmd/ISIPCommander.java
#	src/main/java/com/genersoft/iot/vmp/gb28181/transmit/cmd/SIPRequestHeaderProvider.java
#	src/main/java/com/genersoft/iot/vmp/gb28181/transmit/cmd/impl/SIPCommander.java
#	src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/info/InfoRequestProcessor.java
#	src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/MessageHandlerAbstract.java
#	src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/notify/cmd/KeepaliveNotifyMessageHandler.java
#	src/main/java/com/genersoft/iot/vmp/media/zlm/ZLMHttpHookListener.java
#	src/main/java/com/genersoft/iot/vmp/media/zlm/ZLMServerFactory.java
#	src/main/java/com/genersoft/iot/vmp/service/impl/MediaServerServiceImpl.java
This commit is contained in:
648540858 2023-09-11 11:21:50 +08:00
commit 88350873ee
365 changed files with 3884 additions and 2745 deletions

View File

@ -153,7 +153,7 @@ user-settings:
# 国标是否录制 # 国标是否录制
record-sip: true record-sip: true
# 是否将日志存储进数据库 # 是否将日志存储进数据库
logInDatebase: true logInDatabase: true
# 第三方匹配用于从stream钟获取有效信息 # 第三方匹配用于从stream钟获取有效信息
thirdPartyGBIdReg: [\s\S]* thirdPartyGBIdReg: [\s\S]*
``` ```

View File

@ -159,7 +159,9 @@ public class VideoManagerConstants {
public static final String WVP_STREAM_GB_ID_PREFIX = "memberNo_"; public static final String WVP_STREAM_GB_ID_PREFIX = "memberNo_";
public static final String WVP_STREAM_GPS_MSG_PREFIX = "WVP_STREAM_GPS_MSG_"; public static final String WVP_STREAM_GPS_MSG_PREFIX = "WVP_STREAM_GPS_MSG_";
public static final String WVP_OTHER_SEND_RTP_INFO = "VMP_OTHER_SEND_RTP_INFO_"; public static final String WVP_OTHER_SEND_RTP_INFO = "VMP_OTHER_SEND_RTP_INFO_";
public static final String WVP_OTHER_SEND_PS_INFO = "VMP_OTHER_SEND_PS_INFO_";
public static final String WVP_OTHER_RECEIVE_RTP_INFO = "VMP_OTHER_RECEIVE_RTP_INFO_"; public static final String WVP_OTHER_RECEIVE_RTP_INFO = "VMP_OTHER_RECEIVE_RTP_INFO_";
public static final String WVP_OTHER_RECEIVE_PS_INFO = "VMP_OTHER_RECEIVE_PS_INFO_";
/** /**
* Redis Const * Redis Const

View File

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

View File

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

View File

@ -111,7 +111,7 @@ public class DynamicTask {
} }
boolean result = false; boolean result = false;
if (!ObjectUtils.isEmpty(futureMap.get(key)) && !futureMap.get(key).isCancelled() && !futureMap.get(key).isDone()) { if (!ObjectUtils.isEmpty(futureMap.get(key)) && !futureMap.get(key).isCancelled() && !futureMap.get(key).isDone()) {
result = futureMap.get(key).cancel(true); result = futureMap.get(key).cancel(false);
futureMap.remove(key); futureMap.remove(key);
runnableMap.remove(key); runnableMap.remove(key);
} }
@ -143,7 +143,8 @@ public class DynamicTask {
public void execute(){ public void execute(){
if (futureMap.size() > 0) { if (futureMap.size() > 0) {
for (String key : futureMap.keySet()) { for (String key : futureMap.keySet()) {
if (futureMap.get(key).isDone() || futureMap.get(key).isCancelled()) { ScheduledFuture<?> future = futureMap.get(key);
if (future.isDone() || future.isCancelled()) {
futureMap.remove(key); futureMap.remove(key);
runnableMap.remove(key); runnableMap.remove(key);
} }

View File

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

View File

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

View File

@ -31,7 +31,7 @@ public class UserSetting {
private Boolean recordSip = Boolean.TRUE; private Boolean recordSip = Boolean.TRUE;
private Boolean logInDatebase = Boolean.TRUE; private Boolean logInDatabase = Boolean.TRUE;
private Boolean usePushingAsStatus = Boolean.FALSE; private Boolean usePushingAsStatus = Boolean.FALSE;
@ -134,12 +134,12 @@ public class UserSetting {
this.interfaceAuthenticationExcludes = interfaceAuthenticationExcludes; this.interfaceAuthenticationExcludes = interfaceAuthenticationExcludes;
} }
public Boolean getLogInDatebase() { public Boolean getLogInDatabase() {
return logInDatebase; return logInDatabase;
} }
public void setLogInDatebase(Boolean logInDatebase) { public void setLogInDatabase(Boolean logInDatabase) {
this.logInDatebase = logInDatebase; this.logInDatabase = logInDatabase;
} }
public String getServerId() { public String getServerId() {

View File

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

View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

@ -2,12 +2,9 @@ package com.genersoft.iot.vmp.gb28181.bean;
import com.genersoft.iot.vmp.common.VideoManagerConstants; import com.genersoft.iot.vmp.common.VideoManagerConstants;
import com.genersoft.iot.vmp.conf.DynamicTask; import com.genersoft.iot.vmp.conf.DynamicTask;
import com.genersoft.iot.vmp.conf.UserSetting;
import com.genersoft.iot.vmp.gb28181.task.ISubscribeTask; import com.genersoft.iot.vmp.gb28181.task.ISubscribeTask;
import com.genersoft.iot.vmp.gb28181.task.impl.MobilePositionSubscribeHandlerTask; import com.genersoft.iot.vmp.gb28181.task.impl.MobilePositionSubscribeHandlerTask;
import com.genersoft.iot.vmp.gb28181.transmit.cmd.ISIPCommanderForPlatform;
import com.genersoft.iot.vmp.service.IPlatformService;
import com.genersoft.iot.vmp.storager.IRedisCatchStorage;
import com.genersoft.iot.vmp.storager.IVideoManagerStorage;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
@ -24,6 +21,9 @@ public class SubscribeHolder {
@Autowired @Autowired
private DynamicTask dynamicTask; private DynamicTask dynamicTask;
@Autowired
private UserSetting userSetting;
private final String taskOverduePrefix = "subscribe_overdue_"; private final String taskOverduePrefix = "subscribe_overdue_";
private static ConcurrentHashMap<String, SubscribeInfo> catalogMap = new ConcurrentHashMap<>(); private static ConcurrentHashMap<String, SubscribeInfo> catalogMap = new ConcurrentHashMap<>();
@ -58,7 +58,7 @@ public class SubscribeHolder {
public void putMobilePositionSubscribe(String platformId, SubscribeInfo subscribeInfo) { public void putMobilePositionSubscribe(String platformId, SubscribeInfo subscribeInfo) {
mobilePositionMap.put(platformId, subscribeInfo); mobilePositionMap.put(platformId, subscribeInfo);
String key = VideoManagerConstants.SIP_SUBSCRIBE_PREFIX + "MobilePosition_" + platformId; String key = VideoManagerConstants.SIP_SUBSCRIBE_PREFIX + userSetting.getServerId() + "MobilePosition_" + platformId;
// 添加任务处理GPS定时推送 // 添加任务处理GPS定时推送
dynamicTask.startCron(key, new MobilePositionSubscribeHandlerTask(platformId), dynamicTask.startCron(key, new MobilePositionSubscribeHandlerTask(platformId),
subscribeInfo.getGpsInterval() * 1000); subscribeInfo.getGpsInterval() * 1000);
@ -76,7 +76,7 @@ public class SubscribeHolder {
public void removeMobilePositionSubscribe(String platformId) { public void removeMobilePositionSubscribe(String platformId) {
mobilePositionMap.remove(platformId); mobilePositionMap.remove(platformId);
String key = VideoManagerConstants.SIP_SUBSCRIBE_PREFIX + "MobilePosition_" + platformId; String key = VideoManagerConstants.SIP_SUBSCRIBE_PREFIX + userSetting.getServerId() + "MobilePosition_" + platformId;
// 结束任务处理GPS定时推送 // 结束任务处理GPS定时推送
dynamicTask.stop(key); dynamicTask.stop(key);
String taskOverdueKey = taskOverduePrefix + "MobilePosition_" + platformId; String taskOverdueKey = taskOverduePrefix + "MobilePosition_" + platformId;

View File

View File

View File

View File

View File

View File

View File

View File

View File

@ -12,13 +12,19 @@ import com.genersoft.iot.vmp.media.zlm.dto.MediaServerItem;
import com.genersoft.iot.vmp.service.IDeviceService; import com.genersoft.iot.vmp.service.IDeviceService;
import com.genersoft.iot.vmp.service.IMediaServerService; import com.genersoft.iot.vmp.service.IMediaServerService;
import com.genersoft.iot.vmp.service.IPlatformService; import com.genersoft.iot.vmp.service.IPlatformService;
import com.genersoft.iot.vmp.service.impl.PlatformServiceImpl;
import com.genersoft.iot.vmp.storager.IRedisCatchStorage; import com.genersoft.iot.vmp.storager.IRedisCatchStorage;
import com.genersoft.iot.vmp.storager.IVideoManagerStorage; 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.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner; import org.springframework.boot.CommandLineRunner;
import org.springframework.core.annotation.Order; import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import javax.sip.InvalidArgumentException;
import javax.sip.SipException;
import java.text.ParseException;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@ -59,6 +65,8 @@ public class SipRunner implements CommandLineRunner {
@Autowired @Autowired
private ISIPCommanderForPlatform commanderForPlatform; private ISIPCommanderForPlatform commanderForPlatform;
private final static Logger logger = LoggerFactory.getLogger(PlatformServiceImpl.class);
@Override @Override
public void run(String... args) throws Exception { public void run(String... args) throws Exception {
List<Device> deviceList = deviceService.getAllOnlineDevice(); List<Device> deviceList = deviceService.getAllOnlineDevice();
@ -110,7 +118,11 @@ public class SipRunner implements CommandLineRunner {
if (jsonObject != null && jsonObject.getInteger("code") == 0) { if (jsonObject != null && jsonObject.getInteger("code") == 0) {
ParentPlatform platform = platformService.queryPlatformByServerGBId(sendRtpItem.getPlatformId()); ParentPlatform platform = platformService.queryPlatformByServerGBId(sendRtpItem.getPlatformId());
if (platform != null) { if (platform != null) {
commanderForPlatform.streamByeCmd(platform, sendRtpItem.getCallId()); try {
commanderForPlatform.streamByeCmd(platform, sendRtpItem.getCallId());
} catch (InvalidArgumentException | ParseException | SipException e) {
logger.error("[命令发送失败] 国标级联 发送BYE: {}", e.getMessage());
}
} }
} }
} }

View File

@ -25,6 +25,8 @@ public interface ISIPCommanderForPlatform {
void register(ParentPlatform parentPlatform, SipSubscribe.Event errorEvent , SipSubscribe.Event okEvent) throws InvalidArgumentException, ParseException, SipException; void register(ParentPlatform parentPlatform, SipSubscribe.Event errorEvent , SipSubscribe.Event okEvent) throws InvalidArgumentException, ParseException, SipException;
void register(ParentPlatform parentPlatform, SipTransactionInfo sipTransactionInfo, SipSubscribe.Event errorEvent , SipSubscribe.Event okEvent) throws InvalidArgumentException, ParseException, SipException; void register(ParentPlatform parentPlatform, SipTransactionInfo sipTransactionInfo, SipSubscribe.Event errorEvent , SipSubscribe.Event okEvent) throws InvalidArgumentException, ParseException, SipException;
void register(ParentPlatform parentPlatform, SipTransactionInfo sipTransactionInfo, WWWAuthenticateHeader www, SipSubscribe.Event errorEvent , SipSubscribe.Event okEvent, boolean isRegister) throws SipException, InvalidArgumentException, ParseException; void register(ParentPlatform parentPlatform, SipTransactionInfo sipTransactionInfo, WWWAuthenticateHeader www, SipSubscribe.Event errorEvent , SipSubscribe.Event okEvent, boolean isRegister) throws SipException, InvalidArgumentException, ParseException;
/** /**

View File

@ -21,10 +21,7 @@ import com.genersoft.iot.vmp.media.zlm.ZLMServerFactory;
import com.genersoft.iot.vmp.media.zlm.ZlmHttpHookSubscribe; import com.genersoft.iot.vmp.media.zlm.ZlmHttpHookSubscribe;
import com.genersoft.iot.vmp.media.zlm.dto.*; import com.genersoft.iot.vmp.media.zlm.dto.*;
import com.genersoft.iot.vmp.media.zlm.dto.hook.OnStreamChangedHookParam; import com.genersoft.iot.vmp.media.zlm.dto.hook.OnStreamChangedHookParam;
import com.genersoft.iot.vmp.service.IMediaServerService; import com.genersoft.iot.vmp.service.*;
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.ErrorCallback; import com.genersoft.iot.vmp.service.bean.ErrorCallback;
import com.genersoft.iot.vmp.service.bean.InviteErrorCode; import com.genersoft.iot.vmp.service.bean.InviteErrorCode;
import com.genersoft.iot.vmp.service.bean.MessageForPushChannel; import com.genersoft.iot.vmp.service.bean.MessageForPushChannel;
@ -83,6 +80,9 @@ public class InviteRequestProcessor extends SIPRequestProcessorParent implements
@Autowired @Autowired
private IRedisCatchStorage redisCatchStorage; private IRedisCatchStorage redisCatchStorage;
@Autowired
private IInviteStreamService inviteStreamService;
@Autowired @Autowired
private SSRCFactory ssrcFactory; private SSRCFactory ssrcFactory;
@ -518,10 +518,10 @@ public class InviteRequestProcessor extends SIPRequestProcessorParent implements
errorEvent.run(code, msg, data); errorEvent.run(code, msg, data);
} }
}); });
}else { } else {
SSRCInfo ssrcInfo = playService.play(mediaServerItem, device.getDeviceId(), channelId, ssrc, ((code, msg, data) -> { SSRCInfo ssrcInfo = playService.play(mediaServerItem, device.getDeviceId(), channelId, ssrc, ((code, msg, data) -> {
if (code == InviteErrorCode.SUCCESS.getCode()){ if (code == InviteErrorCode.SUCCESS.getCode()) {
hookEvent.run(code, msg, data); hookEvent.run(code, msg, data);
} else if (code == InviteErrorCode.ERROR_FOR_SIGNALLING_TIMEOUT.getCode() || code == InviteErrorCode.ERROR_FOR_STREAM_TIMEOUT.getCode()) { } else if (code == InviteErrorCode.ERROR_FOR_SIGNALLING_TIMEOUT.getCode() || code == InviteErrorCode.ERROR_FOR_STREAM_TIMEOUT.getCode()) {
logger.info("[上级点播]超时, 用户:{} 通道:{}", username, channelId); logger.info("[上级点播]超时, 用户:{} 通道:{}", username, channelId);

View File

@ -132,7 +132,7 @@ public class NotifyRequestProcessor extends SIPRequestProcessorParent implements
if (CmdType.CATALOG.equals(cmd)) { if (CmdType.CATALOG.equals(cmd)) {
logger.info("接收到Catalog通知"); logger.info("接收到Catalog通知");
// processNotifyCatalogList(take.getEvt()); processNotifyCatalogList(take.getEvt());
notifyRequestForCatalogProcessor.process(take.getEvt()); notifyRequestForCatalogProcessor.process(take.getEvt());
} else if (CmdType.ALARM.equals(cmd)) { } else if (CmdType.ALARM.equals(cmd)) {
logger.info("接收到Alarm通知"); logger.info("接收到Alarm通知");
@ -319,6 +319,7 @@ public class NotifyRequestProcessor extends SIPRequestProcessorParent implements
logger.info("[收到Notify-Alarm]{}/{}", device.getDeviceId(), deviceAlarm.getChannelId()); logger.info("[收到Notify-Alarm]{}/{}", device.getDeviceId(), deviceAlarm.getChannelId());
if ("4".equals(deviceAlarm.getAlarmMethod())) { if ("4".equals(deviceAlarm.getAlarmMethod())) {
MobilePosition mobilePosition = new MobilePosition(); MobilePosition mobilePosition = new MobilePosition();
mobilePosition.setChannelId(channelId);
mobilePosition.setCreateTime(DateUtil.getNow()); mobilePosition.setCreateTime(DateUtil.getNow());
mobilePosition.setDeviceId(deviceAlarm.getDeviceId()); mobilePosition.setDeviceId(deviceAlarm.getDeviceId());
mobilePosition.setTime(deviceAlarm.getAlarmTime()); mobilePosition.setTime(deviceAlarm.getAlarmTime());

View File

@ -88,7 +88,11 @@ public class RegisterRequestProcessor extends SIPRequestProcessorParent implemen
Response response = null; Response response = null;
boolean passwordCorrect = false; boolean passwordCorrect = false;
// 注册标志 // 注册标志
boolean registerFlag; boolean registerFlag = true;
if (request.getExpires().getExpires() == 0) {
// 注销成功
registerFlag = false;
}
FromHeader fromHeader = (FromHeader) request.getHeader(FromHeader.NAME); FromHeader fromHeader = (FromHeader) request.getHeader(FromHeader.NAME);
AddressImpl address = (AddressImpl) fromHeader.getAddress(); AddressImpl address = (AddressImpl) fromHeader.getAddress();
SipUri uri = (SipUri) address.getURI(); SipUri uri = (SipUri) address.getURI();
@ -99,11 +103,12 @@ public class RegisterRequestProcessor extends SIPRequestProcessorParent implemen
RemoteAddressInfo remoteAddressInfo = SipUtils.getRemoteAddressFromRequest(request, RemoteAddressInfo remoteAddressInfo = SipUtils.getRemoteAddressFromRequest(request,
userSetting.getSipUseSourceIpAsRemoteAddress()); userSetting.getSipUseSourceIpAsRemoteAddress());
String requestAddress = remoteAddressInfo.getIp() + ":" + remoteAddressInfo.getPort(); String requestAddress = remoteAddressInfo.getIp() + ":" + remoteAddressInfo.getPort();
logger.info("[注册请求] 设备:{}, 开始处理: {}", deviceId, requestAddress); String title = registerFlag ? "[注册请求]": "[注销请求]";
logger.info(title + "设备:{}, 开始处理: {}", deviceId, requestAddress);
if (device != null && if (device != null &&
device.getSipTransactionInfo() != null && device.getSipTransactionInfo() != null &&
request.getCallIdHeader().getCallId().equals(device.getSipTransactionInfo().getCallId())) { request.getCallIdHeader().getCallId().equals(device.getSipTransactionInfo().getCallId())) {
logger.info("[注册请求] 设备:{}, 注册续订: {}",device.getDeviceId(), device.getDeviceId()); logger.info(title + "设备:{}, 注册续订: {}",device.getDeviceId(), device.getDeviceId());
device.setExpires(request.getExpires().getExpires()); device.setExpires(request.getExpires().getExpires());
device.setIp(remoteAddressInfo.getIp()); device.setIp(remoteAddressInfo.getIp());
device.setPort(remoteAddressInfo.getPort()); device.setPort(remoteAddressInfo.getPort());
@ -123,7 +128,7 @@ public class RegisterRequestProcessor extends SIPRequestProcessorParent implemen
String password = (device != null && !ObjectUtils.isEmpty(device.getPassword()))? device.getPassword() : sipConfig.getPassword(); String password = (device != null && !ObjectUtils.isEmpty(device.getPassword()))? device.getPassword() : sipConfig.getPassword();
AuthorizationHeader authHead = (AuthorizationHeader) request.getHeader(AuthorizationHeader.NAME); AuthorizationHeader authHead = (AuthorizationHeader) request.getHeader(AuthorizationHeader.NAME);
if (authHead == null && !ObjectUtils.isEmpty(password)) { if (authHead == null && !ObjectUtils.isEmpty(password)) {
logger.info("[注册请求] 设备:{}, 回复401: {}",deviceId, requestAddress); logger.info(title + " 设备:{}, 回复401: {}",deviceId, requestAddress);
response = getMessageFactory().createResponse(Response.UNAUTHORIZED, request); response = getMessageFactory().createResponse(Response.UNAUTHORIZED, request);
new DigestServerAuthenticationHelper().generateChallenge(getHeaderFactory(), response, sipConfig.getDomain()); new DigestServerAuthenticationHelper().generateChallenge(getHeaderFactory(), response, sipConfig.getDomain());
sipSender.transmitRequest(request.getLocalAddress().getHostAddress(), response); sipSender.transmitRequest(request.getLocalAddress().getHostAddress(), response);
@ -138,7 +143,7 @@ public class RegisterRequestProcessor extends SIPRequestProcessorParent implemen
// 注册失败 // 注册失败
response = getMessageFactory().createResponse(Response.FORBIDDEN, request); response = getMessageFactory().createResponse(Response.FORBIDDEN, request);
response.setReasonPhrase("wrong password"); response.setReasonPhrase("wrong password");
logger.info("[注册请求] 设备:{}, 密码/SIP服务器ID错误, 回复403: {}", deviceId, requestAddress); logger.info(title + " 设备:{}, 密码/SIP服务器ID错误, 回复403: {}", deviceId, requestAddress);
sipSender.transmitRequest(request.getLocalAddress().getHostAddress(), response); sipSender.transmitRequest(request.getLocalAddress().getHostAddress(), response);
return; return;
} }

View File

@ -3,7 +3,6 @@ package com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message;
import com.genersoft.iot.vmp.gb28181.bean.Device; import com.genersoft.iot.vmp.gb28181.bean.Device;
import com.genersoft.iot.vmp.gb28181.bean.ParentPlatform; import com.genersoft.iot.vmp.gb28181.bean.ParentPlatform;
import com.genersoft.iot.vmp.gb28181.transmit.event.request.SIPRequestProcessorParent; import com.genersoft.iot.vmp.gb28181.transmit.event.request.SIPRequestProcessorParent;
import gov.nist.javax.sip.message.SIPRequest;
import org.dom4j.Element; import org.dom4j.Element;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@ -24,6 +23,9 @@ public abstract class MessageHandlerAbstract extends SIPRequestProcessorParent i
public Map<String, IMessageHandler> messageHandlerMap = new ConcurrentHashMap<>(); public Map<String, IMessageHandler> messageHandlerMap = new ConcurrentHashMap<>();
@Autowired
private IVideoManagerStorage storage;
public void addHandler(String cmdType, IMessageHandler messageHandler) { public void addHandler(String cmdType, IMessageHandler messageHandler) {
messageHandlerMap.put(cmdType, messageHandler); messageHandlerMap.put(cmdType, messageHandler);
} }
@ -40,7 +42,15 @@ public abstract class MessageHandlerAbstract extends SIPRequestProcessorParent i
return; return;
} }
IMessageHandler messageHandler = messageHandlerMap.get(cmd); IMessageHandler messageHandler = messageHandlerMap.get(cmd);
if (messageHandler != null) { if (messageHandler != null) {
//两个国标平台互相级联时由于上一步判断导致本该在平台处理的消息 放到了设备的处理逻辑
//所以对目录查询单独做了校验
if(messageHandler instanceof CatalogQueryMessageHandler){
ParentPlatform parentPlatform = storage.queryParentPlatByServerGBId(device.getDeviceId());
messageHandler.handForPlatform(evt, parentPlatform, element);
return;
}
messageHandler.handForDevice(evt, device, element); messageHandler.handForDevice(evt, device, element);
} }
} }

View File

@ -137,6 +137,7 @@ public class AlarmNotifyMessageHandler extends SIPRequestProcessorParent impleme
MobilePosition mobilePosition = new MobilePosition(); MobilePosition mobilePosition = new MobilePosition();
mobilePosition.setCreateTime(DateUtil.getNow()); mobilePosition.setCreateTime(DateUtil.getNow());
mobilePosition.setDeviceId(deviceAlarm.getDeviceId()); mobilePosition.setDeviceId(deviceAlarm.getDeviceId());
mobilePosition.setChannelId(channelId);
mobilePosition.setTime(deviceAlarm.getAlarmTime()); mobilePosition.setTime(deviceAlarm.getAlarmTime());
mobilePosition.setLongitude(deviceAlarm.getLongitude()); mobilePosition.setLongitude(deviceAlarm.getLongitude());
mobilePosition.setLatitude(deviceAlarm.getLatitude()); mobilePosition.setLatitude(deviceAlarm.getLatitude());

View File

@ -13,6 +13,7 @@ import com.genersoft.iot.vmp.gb28181.utils.SipUtils;
import com.genersoft.iot.vmp.service.IDeviceService; import com.genersoft.iot.vmp.service.IDeviceService;
import com.genersoft.iot.vmp.utils.DateUtil; import com.genersoft.iot.vmp.utils.DateUtil;
import gov.nist.javax.sip.message.SIPRequest; import gov.nist.javax.sip.message.SIPRequest;
import org.apache.commons.lang3.ObjectUtils;
import org.dom4j.Element; import org.dom4j.Element;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@ -68,7 +69,7 @@ public class KeepaliveNotifyMessageHandler extends SIPRequestProcessorParent imp
} catch (SipException | InvalidArgumentException | ParseException e) { } catch (SipException | InvalidArgumentException | ParseException e) {
logger.error("[命令发送失败] 心跳回复: {}", e.getMessage()); logger.error("[命令发送失败] 心跳回复: {}", e.getMessage());
} }
if (device.getKeepaliveTime() != null && DateUtil.getDifferenceForNow(device.getKeepaliveTime()) <= 3000L){ if (!ObjectUtils.isEmpty(device.getKeepaliveTime()) && DateUtil.getDifferenceForNow(device.getKeepaliveTime()) <= 3000L) {
logger.info("[收到心跳] 心跳发送过于频繁,已忽略 device: {}, callId: {}", device.getDeviceId(), request.getCallIdHeader().getCallId()); logger.info("[收到心跳] 心跳发送过于频繁,已忽略 device: {}, callId: {}", device.getDeviceId(), request.getCallIdHeader().getCallId());
return; return;
} }
@ -109,7 +110,11 @@ public class KeepaliveNotifyMessageHandler extends SIPRequestProcessorParent imp
@Override @Override
public void handForPlatform(RequestEvent evt, ParentPlatform parentPlatform, Element element) { public void handForPlatform(RequestEvent evt, ParentPlatform parentPlatform, Element element) {
// 不会收到上级平台的心跳信息 // 个别平台保活不回复200OK会判定离线
try {
responseAck((SIPRequest) evt.getRequest(), Response.OK);
} catch (SipException | InvalidArgumentException | ParseException e) {
logger.error("[命令发送失败] 心跳回复: {}", e.getMessage());
}
} }
} }

View File

@ -30,6 +30,7 @@ import java.util.ArrayList;
import java.util.Iterator; import java.util.Iterator;
import java.util.List; import java.util.List;
import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.atomic.AtomicBoolean;
/** /**
* 目录查询的回复 * 目录查询的回复
@ -61,6 +62,7 @@ public class CatalogResponseMessageHandler extends SIPRequestProcessorParent imp
@Autowired @Autowired
private SipConfig sipConfig; private SipConfig sipConfig;
private AtomicBoolean processing = new AtomicBoolean(false);
@Override @Override
public void afterPropertiesSet() throws Exception { public void afterPropertiesSet() throws Exception {
@ -69,7 +71,6 @@ public class CatalogResponseMessageHandler extends SIPRequestProcessorParent imp
@Override @Override
public void handForDevice(RequestEvent evt, Device device, Element element) { public void handForDevice(RequestEvent evt, Device device, Element element) {
boolean isEmpty = taskQueue.isEmpty();
taskQueue.offer(new HandlerCatchData(evt, device, element)); taskQueue.offer(new HandlerCatchData(evt, device, element));
// 回复200 OK // 回复200 OK
try { try {
@ -77,8 +78,8 @@ public class CatalogResponseMessageHandler extends SIPRequestProcessorParent imp
} catch (SipException | InvalidArgumentException | ParseException e) { } catch (SipException | InvalidArgumentException | ParseException e) {
logger.error("[命令发送失败] 目录查询回复: {}", e.getMessage()); logger.error("[命令发送失败] 目录查询回复: {}", e.getMessage());
} }
// 如果不为空则说明已经开启消息处理 // 已经开启消息处理则跳过
if (isEmpty) { if (processing.compareAndSet(false, true)) {
taskExecutor.execute(() -> { taskExecutor.execute(() -> {
while (!taskQueue.isEmpty()) { while (!taskQueue.isEmpty()) {
// 全局异常捕获保证下一条可以得到处理 // 全局异常捕获保证下一条可以得到处理
@ -147,11 +148,12 @@ public class CatalogResponseMessageHandler extends SIPRequestProcessorParent imp
} }
} }
}catch (Exception e) { } catch (Exception e) {
logger.warn("[收到通道] 发现未处理的异常, \r\n{}", evt.getRequest()); logger.warn("[收到通道] 发现未处理的异常, \r\n{}", evt.getRequest());
logger.error("[收到通道] 异常内容: ", e); logger.error("[收到通道] 异常内容: ", e);
} }
} }
processing.set(false);
}); });
} }

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