|
|
@@ -67,6 +67,10 @@ public class Gb28181DeviceSimulator implements SipListener {
|
|
|
private volatile Future<?> streamingTask;
|
|
|
private volatile ScheduledFuture<?> reRegisterTask;
|
|
|
private volatile String activeDeviceId;
|
|
|
+ /** Catalog 上报的通道 ID,IPC 默认与 deviceId 相同 */
|
|
|
+ private volatile String activeChannelId;
|
|
|
+ private volatile CountDownLatch registerLatch;
|
|
|
+ private volatile String lastRegisterError;
|
|
|
|
|
|
public Gb28181DeviceSimulator(SipClientProperties properties) {
|
|
|
this.properties = properties;
|
|
|
@@ -98,9 +102,21 @@ public class Gb28181DeviceSimulator implements SipListener {
|
|
|
* @param deviceId 20 位设备编码(动态传入)
|
|
|
*/
|
|
|
public synchronized Map<String, Object> register(String deviceId) {
|
|
|
+ return register(deviceId, null);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 注册到 GB28181 平台。
|
|
|
+ *
|
|
|
+ * @param deviceId 20 位设备编码
|
|
|
+ * @param channelId 可选通道编码;为空时与 deviceId 相同(单路 IPC)
|
|
|
+ */
|
|
|
+ public synchronized Map<String, Object> register(String deviceId, String channelId) {
|
|
|
validateDeviceId(deviceId);
|
|
|
- if (registered.get() && deviceId.equals(activeDeviceId)) {
|
|
|
- log.info("设备已注册: deviceId={}", activeDeviceId);
|
|
|
+ String resolvedChannelId = resolveChannelId(deviceId, channelId);
|
|
|
+ if (registered.get() && deviceId.equals(activeDeviceId)
|
|
|
+ && resolvedChannelId.equals(activeChannelId)) {
|
|
|
+ log.info("设备已注册: deviceId={}, channelId={}", activeDeviceId, activeChannelId);
|
|
|
return status();
|
|
|
}
|
|
|
if (sipStack != null) {
|
|
|
@@ -108,16 +124,22 @@ public class Gb28181DeviceSimulator implements SipListener {
|
|
|
}
|
|
|
try {
|
|
|
activeDeviceId = deviceId.trim();
|
|
|
+ activeChannelId = resolvedChannelId;
|
|
|
initStack();
|
|
|
registerAuthSent.set(false);
|
|
|
authChallenge.set(null);
|
|
|
+ lastRegisterError = null;
|
|
|
+ registerLatch = new CountDownLatch(1);
|
|
|
sendRegister(false, properties.getRegisterExpires());
|
|
|
scheduleReRegister();
|
|
|
- log.info("GB28181 注册请求已发送: deviceId={}, platform={}@{}:{}, transport={}",
|
|
|
- activeDeviceId, properties.getPlatformId(),
|
|
|
- properties.getServerHost(), properties.getServerPort(), sipTransport);
|
|
|
+ log.info("GB28181 注册请求已发送: deviceId={}, channelId={}, platform={}@{}:{}, transport={}, passwordLen={}",
|
|
|
+ activeDeviceId, activeChannelId, properties.getPlatformId(),
|
|
|
+ properties.getServerHost(), properties.getServerPort(), sipTransport,
|
|
|
+ passwordLength());
|
|
|
+ awaitRegisterResult();
|
|
|
} catch (Exception e) {
|
|
|
activeDeviceId = null;
|
|
|
+ activeChannelId = null;
|
|
|
shutdownStack();
|
|
|
sipStack = null;
|
|
|
sipProvider = null;
|
|
|
@@ -149,6 +171,7 @@ public class Gb28181DeviceSimulator implements SipListener {
|
|
|
}
|
|
|
registered.set(false);
|
|
|
activeDeviceId = null;
|
|
|
+ activeChannelId = null;
|
|
|
boundRtspUrl.set(null);
|
|
|
pendingStream.set(null);
|
|
|
shutdownStack();
|
|
|
@@ -197,6 +220,7 @@ public class Gb28181DeviceSimulator implements SipListener {
|
|
|
Map<String, Object> map = new HashMap<>();
|
|
|
map.put("registered", registered.get());
|
|
|
map.put("deviceId", activeDeviceId);
|
|
|
+ map.put("channelId", activeChannelId);
|
|
|
map.put("platformId", properties.getPlatformId());
|
|
|
map.put("serverHost", properties.getServerHost());
|
|
|
map.put("serverPort", properties.getServerPort());
|
|
|
@@ -205,9 +229,44 @@ public class Gb28181DeviceSimulator implements SipListener {
|
|
|
map.put("localSipAddress", localIp != null ? localIp + ":" + localSipPort : null);
|
|
|
map.put("boundRtspUrl", boundRtspUrl.get());
|
|
|
map.put("streaming", streamingTask != null && !streamingTask.isDone());
|
|
|
+ map.put("lastRegisterError", lastRegisterError);
|
|
|
+ map.put("passwordConfigured", StringUtils.hasText(properties.getPassword()));
|
|
|
return map;
|
|
|
}
|
|
|
|
|
|
+ private void awaitRegisterResult() {
|
|
|
+ CountDownLatch latch = registerLatch;
|
|
|
+ if (latch == null) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ try {
|
|
|
+ int timeout = Math.max(5, properties.getRegisterTimeout());
|
|
|
+ if (!latch.await(timeout, TimeUnit.SECONDS)) {
|
|
|
+ lastRegisterError = "注册超时(" + timeout + "s 内未收到平台最终响应)";
|
|
|
+ log.warn(lastRegisterError);
|
|
|
+ }
|
|
|
+ } catch (InterruptedException e) {
|
|
|
+ Thread.currentThread().interrupt();
|
|
|
+ lastRegisterError = "注册等待被中断";
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private void completeRegisterAttempt(int status, String error) {
|
|
|
+ if (status == Response.OK) {
|
|
|
+ lastRegisterError = null;
|
|
|
+ } else if (StringUtils.hasText(error)) {
|
|
|
+ lastRegisterError = error;
|
|
|
+ }
|
|
|
+ CountDownLatch latch = registerLatch;
|
|
|
+ if (latch != null) {
|
|
|
+ latch.countDown();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private int passwordLength() {
|
|
|
+ return properties.getPassword() != null ? properties.getPassword().length() : 0;
|
|
|
+ }
|
|
|
+
|
|
|
private void initStack() throws Exception {
|
|
|
SipFactory sipFactory = SipFactory.getInstance();
|
|
|
sipFactory.setPathName("gov.nist");
|
|
|
@@ -255,15 +314,15 @@ public class Gb28181DeviceSimulator implements SipListener {
|
|
|
|
|
|
private void sendRegister(boolean withAuth, int expires) throws Exception {
|
|
|
ensureActiveDeviceId();
|
|
|
- SipURI requestUri = createUri(activeDeviceId, properties.getDomain());
|
|
|
- requestUri.setPort(properties.getServerPort());
|
|
|
- requestUri.setTransportParam(sipTransport);
|
|
|
+ String uriUser = resolveRegisterUriUser();
|
|
|
+ SipURI requestUri = createUri(uriUser, properties.getDomain());
|
|
|
+ String digestUri = "sip:" + uriUser + "@" + properties.getDomain();
|
|
|
|
|
|
Request register = buildRequest(Request.REGISTER, requestUri, activeDeviceId, "reg-tag");
|
|
|
register.addHeader(headerFactory.createExpiresHeader(expires));
|
|
|
|
|
|
if (withAuth) {
|
|
|
- AuthorizationHeader auth = buildAuthorization(Request.REGISTER, requestUri.toString());
|
|
|
+ AuthorizationHeader auth = buildAuthorization(Request.REGISTER, digestUri);
|
|
|
if (auth != null) {
|
|
|
register.addHeader(auth);
|
|
|
}
|
|
|
@@ -271,7 +330,16 @@ public class Gb28181DeviceSimulator implements SipListener {
|
|
|
|
|
|
ClientTransaction ct = sipProvider.getNewClientTransaction(register);
|
|
|
ct.sendRequest();
|
|
|
- log.debug("发送 REGISTER{} -> {}:{}", withAuth ? "(鉴权)" : "", properties.getServerHost(), properties.getServerPort());
|
|
|
+ log.info("发送 REGISTER{} uri={} mode={} deviceId={} -> {}:{}",
|
|
|
+ withAuth ? "(鉴权)" : "", digestUri, properties.getRegisterUriUser(), activeDeviceId,
|
|
|
+ properties.getServerHost(), properties.getServerPort());
|
|
|
+ }
|
|
|
+
|
|
|
+ private String resolveRegisterUriUser() {
|
|
|
+ if ("device".equalsIgnoreCase(properties.getRegisterUriUser())) {
|
|
|
+ return activeDeviceId;
|
|
|
+ }
|
|
|
+ return properties.getPlatformId();
|
|
|
}
|
|
|
|
|
|
private void handleIncomingInvite(RequestEvent event) throws Exception {
|
|
|
@@ -414,6 +482,51 @@ public class Gb28181DeviceSimulator implements SipListener {
|
|
|
});
|
|
|
}
|
|
|
|
|
|
+ private void handleIncomingMessage(RequestEvent event) throws Exception {
|
|
|
+ Request request = event.getRequest();
|
|
|
+ String body = extractSdp(request);
|
|
|
+ if (!StringUtils.hasText(body)) {
|
|
|
+ sendResponse(event, Response.BAD_REQUEST);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ String cmdType = Gb28181CatalogBuilder.extractXmlValue(body, "CmdType");
|
|
|
+ String sn = Gb28181CatalogBuilder.extractXmlValue(body, "SN");
|
|
|
+ if (!StringUtils.hasText(sn)) {
|
|
|
+ sn = "1";
|
|
|
+ }
|
|
|
+
|
|
|
+ sendResponse(event, Response.OK);
|
|
|
+
|
|
|
+ if ("Catalog".equalsIgnoreCase(cmdType)) {
|
|
|
+ String catalogXml = Gb28181CatalogBuilder.buildCatalogResponse(
|
|
|
+ sn, activeDeviceId, activeChannelId, properties.getChannelName());
|
|
|
+ sendManscdpMessage(catalogXml);
|
|
|
+ log.info("已应答 Catalog 查询: deviceId={}, channelId={}", activeDeviceId, activeChannelId);
|
|
|
+ } else if ("DeviceInfo".equalsIgnoreCase(cmdType)) {
|
|
|
+ String infoXml = Gb28181CatalogBuilder.buildDeviceInfoResponse(sn, activeDeviceId);
|
|
|
+ sendManscdpMessage(infoXml);
|
|
|
+ log.info("已应答 DeviceInfo 查询: deviceId={}", activeDeviceId);
|
|
|
+ } else {
|
|
|
+ log.debug("忽略未处理的 MESSAGE CmdType={}", cmdType);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private void sendManscdpMessage(String xmlBody) throws Exception {
|
|
|
+ ensureActiveDeviceId();
|
|
|
+ SipURI requestUri = createUri(properties.getPlatformId(), properties.getDomain());
|
|
|
+ requestUri.setPort(properties.getServerPort());
|
|
|
+ requestUri.setTransportParam(sipTransport);
|
|
|
+
|
|
|
+ Request message = buildRequest(Request.MESSAGE, requestUri, activeDeviceId, "msg-tag");
|
|
|
+ message.setContent(xmlBody.getBytes(StandardCharsets.UTF_8),
|
|
|
+ headerFactory.createContentTypeHeader("Application", "MANSCDP+xml"));
|
|
|
+
|
|
|
+ ClientTransaction ct = sipProvider.getNewClientTransaction(message);
|
|
|
+ ct.sendRequest();
|
|
|
+ log.debug("发送 MESSAGE (MANSCDP+xml) -> {}:{}", properties.getServerHost(), properties.getServerPort());
|
|
|
+ }
|
|
|
+
|
|
|
private void handleIncomingBye(RequestEvent event) throws Exception {
|
|
|
log.info("收到平台 BYE");
|
|
|
if (streamingTask != null) {
|
|
|
@@ -467,9 +580,10 @@ public class Gb28181DeviceSimulator implements SipListener {
|
|
|
}
|
|
|
String cnonce = UUID.randomUUID().toString().replace("-", "").substring(0, 8);
|
|
|
String nc = "00000001";
|
|
|
+ String qop = resolveQop(challenge.getQop());
|
|
|
String response = SipDigestAuth.computeResponse(
|
|
|
activeDeviceId, properties.getPassword(), challenge.getRealm(),
|
|
|
- method, uri, challenge.getNonce(), nc, cnonce, challenge.getQop());
|
|
|
+ method, uri, challenge.getNonce(), nc, cnonce, qop);
|
|
|
|
|
|
AuthorizationHeader auth = headerFactory.createAuthorizationHeader("Digest");
|
|
|
auth.setUsername(activeDeviceId);
|
|
|
@@ -478,14 +592,22 @@ public class Gb28181DeviceSimulator implements SipListener {
|
|
|
auth.setURI(addressFactory.createURI(uri));
|
|
|
auth.setResponse(response);
|
|
|
auth.setAlgorithm("MD5");
|
|
|
- if (challenge.getQop() != null && !challenge.getQop().isEmpty()) {
|
|
|
- auth.setQop(challenge.getQop());
|
|
|
+ if (qop != null && !qop.isEmpty()) {
|
|
|
+ auth.setQop(qop);
|
|
|
auth.setCNonce(cnonce);
|
|
|
auth.setNonceCount(Integer.parseInt(nc, 16));
|
|
|
}
|
|
|
return auth;
|
|
|
}
|
|
|
|
|
|
+ private static String resolveQop(String qop) {
|
|
|
+ if (qop == null || qop.isEmpty()) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ int comma = qop.indexOf(',');
|
|
|
+ return comma > 0 ? qop.substring(0, comma).trim() : qop.trim();
|
|
|
+ }
|
|
|
+
|
|
|
private void sendResponse(RequestEvent event, int code) throws Exception {
|
|
|
Response response = messageFactory.createResponse(code, event.getRequest());
|
|
|
if (code >= 200) {
|
|
|
@@ -561,6 +683,14 @@ public class Gb28181DeviceSimulator implements SipListener {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ private static String resolveChannelId(String deviceId, String channelId) {
|
|
|
+ if (StringUtils.hasText(channelId)) {
|
|
|
+ validateDeviceId(channelId);
|
|
|
+ return channelId.trim();
|
|
|
+ }
|
|
|
+ return deviceId.trim();
|
|
|
+ }
|
|
|
+
|
|
|
private void shutdownStack() {
|
|
|
try {
|
|
|
if (sipProvider != null) {
|
|
|
@@ -589,6 +719,7 @@ public class Gb28181DeviceSimulator implements SipListener {
|
|
|
challenge = (WWWAuthenticateHeader) event.getResponse().getHeader(ProxyAuthenticateHeader.NAME);
|
|
|
}
|
|
|
authChallenge.set(challenge);
|
|
|
+ log.info("收到 REGISTER 401 挑战, realm={}", challenge != null ? challenge.getRealm() : "unknown");
|
|
|
if (!registerAuthSent.getAndSet(true)) {
|
|
|
try {
|
|
|
ExpiresHeader expires = (ExpiresHeader) ct.getRequest().getHeader(ExpiresHeader.NAME);
|
|
|
@@ -607,9 +738,24 @@ public class Gb28181DeviceSimulator implements SipListener {
|
|
|
registered.set(true);
|
|
|
log.info("GB28181 注册成功: deviceId={}(平台应可见设备在线)", activeDeviceId);
|
|
|
}
|
|
|
+ completeRegisterAttempt(status, null);
|
|
|
} else if (status >= 300) {
|
|
|
registered.set(false);
|
|
|
- log.warn("GB28181 注册失败: deviceId={}, status={}", activeDeviceId, status);
|
|
|
+ String error;
|
|
|
+ if (status == Response.FORBIDDEN) {
|
|
|
+ error = "平台拒绝注册(403):密码错误或设备未在平台添加,当前 passwordLen="
|
|
|
+ + passwordLength() + " registerUriUser=" + properties.getRegisterUriUser();
|
|
|
+ log.warn("GB28181 注册失败 403: deviceId={}, platformId={}, domain={}, passwordLen={}。"
|
|
|
+ + " 请核对:① sip.client.password 与平台该设备密码一致"
|
|
|
+ + " ② 平台已添加设备 {} ③ domain/platformId 与海康配置一致"
|
|
|
+ + " ④ 仍失败可试 sip.client.register-uri-user=device",
|
|
|
+ activeDeviceId, properties.getPlatformId(), properties.getDomain(),
|
|
|
+ passwordLength(), activeDeviceId);
|
|
|
+ } else {
|
|
|
+ error = "平台拒绝注册(" + status + ")";
|
|
|
+ log.warn("GB28181 注册失败: deviceId={}, status={}", activeDeviceId, status);
|
|
|
+ }
|
|
|
+ completeRegisterAttempt(status, error);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@@ -624,6 +770,8 @@ public class Gb28181DeviceSimulator implements SipListener {
|
|
|
handleIncomingAck(event);
|
|
|
} else if (Request.BYE.equals(method)) {
|
|
|
handleIncomingBye(event);
|
|
|
+ } else if (Request.MESSAGE.equals(method)) {
|
|
|
+ handleIncomingMessage(event);
|
|
|
}
|
|
|
} catch (Exception e) {
|
|
|
log.error("处理 {} 失败: {}", method, e.getMessage(), e);
|