Browse Source

/*B接口SIP协议开发代码初次提交*/

master
htjcAdmin 4 months ago
parent
commit
5ffa0cee7c
14 changed files with 1103 additions and 0 deletions
  1. +1
    -0
      .gitignore
  2. +25
    -0
      pom.xml
  3. +27
    -0
      src/main/java/com/inspect/tcpserver/sip/config/AccountManagerImpl.java
  4. +36
    -0
      src/main/java/com/inspect/tcpserver/sip/config/SipConfig.java
  5. +33
    -0
      src/main/java/com/inspect/tcpserver/sip/items/RequestHistoryAlarmItem.java
  6. +18
    -0
      src/main/java/com/inspect/tcpserver/sip/items/RequestResourceItem.java
  7. +18
    -0
      src/main/java/com/inspect/tcpserver/sip/items/SipEventRegistry.java
  8. +583
    -0
      src/main/java/com/inspect/tcpserver/sip/service/SipClientService.java
  9. +229
    -0
      src/main/java/com/inspect/tcpserver/sip/service/SipVideoClient.java
  10. +62
    -0
      src/main/java/com/inspect/tcpserver/sip/utils/DigestUtil.java
  11. +20
    -0
      src/main/java/com/inspect/tcpserver/sip/utils/SipXmlEnvelope.java
  12. +29
    -0
      src/main/java/com/inspect/tcpserver/sip/utils/SipXmlParser.java
  13. +11
    -0
      src/main/resources/application-dev.yml
  14. +11
    -0
      src/main/resources/application.yml

+ 1
- 0
.gitignore View File

@ -35,3 +35,4 @@ build/
/log/
/logs/
/logs/inspect-tcpserver/info.log
sipserverlog.txt

+ 25
- 0
pom.xml View File

@ -113,6 +113,31 @@
<artifactId>okhttp</artifactId>
<version>4.11.0</version>
</dependency>
<dependency>
<groupId>javax.sip</groupId>
<artifactId>jain-sip-ri</artifactId>
<version>1.2.279</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>log4j-over-slf4j</artifactId>
<version>1.7.36</version>
</dependency>
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-xml</artifactId>
</dependency>
</dependencies>
<dependencyManagement>


+ 27
- 0
src/main/java/com/inspect/tcpserver/sip/config/AccountManagerImpl.java View File

@ -0,0 +1,27 @@
package com.inspect.tcpserver.sip.config;
import gov.nist.javax.sip.clientauthutils.AccountManager;
import gov.nist.javax.sip.clientauthutils.UserCredentials;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import javax.sip.ClientTransaction;
@Component
public class AccountManagerImpl implements AccountManager {
@Value("${sip.username}")
private String username;
@Value("${sip.password}")
private String password;
@Value("${sip.domain}")
private String domain;
@Override
public UserCredentials getCredentials(ClientTransaction clientTransaction, String s) {
return null;
}
}

+ 36
- 0
src/main/java/com/inspect/tcpserver/sip/config/SipConfig.java View File

@ -0,0 +1,36 @@
package com.inspect.tcpserver.sip.config;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.sip.SipFactory;
import javax.sip.SipStack;
import java.util.Properties;
@Configuration
public class SipConfig {
@Value("${sip.local-ip}")
private String localIp;
@Value("${sip.transport}")
private String transport;
@Bean
public SipStack sipStack() throws Exception {
SipFactory sipFactory = SipFactory.getInstance();
sipFactory.setPathName("gov.nist");
Properties properties = new Properties();
properties.setProperty("javax.sip.STACK_NAME", "spring-boot-sip-stack");
properties.setProperty("gov.nist.javax.sip.IP_ADDRESS", localIp);
// 日志文件
properties.setProperty("gov.nist.javax.sip.DEBUG_LOG", "sipdebug.txt");
properties.setProperty("gov.nist.javax.sip.SERVER_LOG", "sipserverlog.txt");
// 延迟/线程池配置
properties.setProperty("gov.nist.javax.sip.THREAD_POOL_SIZE", "20");
return sipFactory.createSipStack(properties);
}
}

+ 33
- 0
src/main/java/com/inspect/tcpserver/sip/items/RequestHistoryAlarmItem.java View File

@ -0,0 +1,33 @@
package com.inspect.tcpserver.sip.items;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class RequestHistoryAlarmItem {
@JacksonXmlProperty(isAttribute = true, localName = "Code")
private String code;
@JacksonXmlProperty(isAttribute = true, localName = "UserCode")
private String userCode;
@JacksonXmlProperty(isAttribute = true, localName = "Type")
private String type;
@JacksonXmlProperty(isAttribute = true, localName = "BeginTime")
private String beginTime;
@JacksonXmlProperty(isAttribute = true, localName = "EndTime")
private String endTime;
@JacksonXmlProperty(isAttribute = true, localName = "Level")
private String level;
@JacksonXmlProperty(isAttribute = true, localName = "FromIndex")
private int fromIndex;
@JacksonXmlProperty(isAttribute = true, localName = "ToIndex")
private int toIndex;
}

+ 18
- 0
src/main/java/com/inspect/tcpserver/sip/items/RequestResourceItem.java View File

@ -0,0 +1,18 @@
package com.inspect.tcpserver.sip.items;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class RequestResourceItem {
@JacksonXmlProperty(isAttribute = true, localName = "Code")
private String code;
@JacksonXmlProperty(isAttribute = true, localName = "FromIndex")
private int fromIndex;
@JacksonXmlProperty(isAttribute = true, localName = "ToIndex")
private int toIndex;
}

+ 18
- 0
src/main/java/com/inspect/tcpserver/sip/items/SipEventRegistry.java View File

@ -0,0 +1,18 @@
package com.inspect.tcpserver.sip.items;
import java.util.HashMap;
import java.util.Map;
public class SipEventRegistry {
private static final Map<String, Class<?>> EVENT_MAP = new HashMap<>();
static {
EVENT_MAP.put("Request_Resource", RequestResourceItem.class);
EVENT_MAP.put("Request_History_Alarm", RequestHistoryAlarmItem.class);
}
public static Class<?> getItemClass(String eventType) {
return EVENT_MAP.get(eventType);
}
}

+ 583
- 0
src/main/java/com/inspect/tcpserver/sip/service/SipClientService.java View File

@ -0,0 +1,583 @@
package com.inspect.tcpserver.sip.service;
import com.inspect.tcpserver.sip.items.SipEventRegistry;
import com.inspect.tcpserver.sip.utils.DigestUtil;
import com.inspect.tcpserver.sip.utils.SipXmlEnvelope;
import com.inspect.tcpserver.sip.utils.SipXmlParser;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.stereotype.Service;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import javax.sip.*;
import javax.sip.address.Address;
import javax.sip.address.AddressFactory;
import javax.sip.address.SipURI;
import javax.sip.header.*;
import javax.sip.message.MessageFactory;
import javax.sip.message.Request;
import javax.sip.message.Response;
import java.io.UnsupportedEncodingException;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.*;
import java.util.concurrent.atomic.AtomicInteger;
@Slf4j
@Service
@ConditionalOnProperty(name = "sip.enable", havingValue = "true")
public class SipClientService implements SipListener {
private SipFactory sipFactory;
private SipStack sipStack;
private SipProvider sipProvider;
private MessageFactory messageFactory;
private HeaderFactory headerFactory;
private AddressFactory addressFactory;
// 保存上次请求信息重发用
private String lastDeviceCode;
private String lastServerIp;
private int lastServerPort;
private String lastXmlBody;
// 保存 WWW-Authenticate 参数
private String lastNonce;
private String lastRealm;
private String lastOpaque;
private String lastQop;
@Value("${sip.username}")
private String username;
@Value("${sip.password}")
private String password;
@Value("${sip.domain}")
private String domain;
@Value("${sip.port}")
private int port;
@Value("${sip.transport}")
private String transport;
@Value("${sip.local-ip}")
private String localIp;
@Value("${sip.local-port}")
private int localPort;
@Value("${sip.expires}")
private int expires;
private ClientTransaction lastRegisterTransaction;
private Timer refreshTimer = new Timer(true);
private String fromTag = Long.toHexString(System.currentTimeMillis());
private AtomicInteger cSeqCounter = new AtomicInteger(1);
String testXml = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" +
"<SIP_XML EventType=\"Push_Resource\">\n" +
" <Code>1234567890</Code>\n" +
" <SubList SubNum=\"50\">\n" +
" <Item Code=\"CAM001 \" Name=\"摄像头1 \" Status=\"1\" DecoderTag=\"H264\" Longitude=\"120.12\" Latitude=\"30.28\" SubNum=\"1 \"/>\n" +
" <Item Code=\"CAM002 \" Name=\"摄像头2 \" Status=\"1\" DecoderTag=\"H264\" Longitude=\"120.15\" Latitude=\"30.32\" SubNum=\"2 \"/>\n" +
" <Item Code=\"CAM003 \" Name=\"摄像头3 \" Status=\"1\" DecoderTag=\"H264\" Longitude=\"120.15\" Latitude=\"30.32\" SubNum=\"3 \"/>\n" +
" <Item Code=\"CAM004 \" Name=\"摄像头4 \" Status=\"1\" DecoderTag=\"H264\" Longitude=\"120.15\" Latitude=\"30.32\" SubNum=\"4 \"/>\n" +
" <Item Code=\"CAM005 \" Name=\"摄像头5 \" Status=\"1\" DecoderTag=\"H264\" Longitude=\"120.15\" Latitude=\"30.32\" SubNum=\"5 \"/>\n" +
" <Item Code=\"CAM006 \" Name=\"摄像头6 \" Status=\"1\" DecoderTag=\"H264\" Longitude=\"120.15\" Latitude=\"30.32\" SubNum=\"6 \"/>\n" +
" <Item Code=\"CAM007 \" Name=\"摄像头7 \" Status=\"1\" DecoderTag=\"H264\" Longitude=\"120.15\" Latitude=\"30.32\" SubNum=\"7 \"/>\n" +
" <Item Code=\"CAM008 \" Name=\"摄像头8 \" Status=\"1\" DecoderTag=\"H264\" Longitude=\"120.15\" Latitude=\"30.32\" SubNum=\"8 \"/>\n" +
" <Item Code=\"CAM009 \" Name=\"摄像头9 \" Status=\"1\" DecoderTag=\"H264\" Longitude=\"120.15\" Latitude=\"30.32\" SubNum=\"9 \"/>\n" +
" <Item Code=\"CAM0010\" Name=\"摄像头10\" Status=\"1\" DecoderTag=\"H264\" Longitude=\"120.15\" Latitude=\"30.32\" SubNum=\"10\"/>\n" +
" <Item Code=\"CAM0011\" Name=\"摄像头11\" Status=\"1\" DecoderTag=\"H264\" Longitude=\"120.15\" Latitude=\"30.32\" SubNum=\"11\"/>\n" +
" <Item Code=\"CAM0012\" Name=\"摄像头12\" Status=\"1\" DecoderTag=\"H264\" Longitude=\"120.15\" Latitude=\"30.32\" SubNum=\"12\"/>\n" +
" <Item Code=\"CAM0013\" Name=\"摄像头13\" Status=\"1\" DecoderTag=\"H264\" Longitude=\"120.15\" Latitude=\"30.32\" SubNum=\"13\"/>\n" +
" <Item Code=\"CAM0014\" Name=\"摄像头14\" Status=\"1\" DecoderTag=\"H264\" Longitude=\"120.15\" Latitude=\"30.32\" SubNum=\"14\"/>\n" +
" <Item Code=\"CAM0015\" Name=\"摄像头15\" Status=\"1\" DecoderTag=\"H264\" Longitude=\"120.15\" Latitude=\"30.32\" SubNum=\"15\"/>\n" +
" <Item Code=\"CAM0016\" Name=\"摄像头16\" Status=\"1\" DecoderTag=\"H264\" Longitude=\"120.15\" Latitude=\"30.32\" SubNum=\"16\"/>\n" +
" <Item Code=\"CAM0017\" Name=\"摄像头17\" Status=\"1\" DecoderTag=\"H264\" Longitude=\"120.15\" Latitude=\"30.32\" SubNum=\"17\"/>\n" +
" <Item Code=\"CAM0018\" Name=\"摄像头18\" Status=\"1\" DecoderTag=\"H264\" Longitude=\"120.15\" Latitude=\"30.32\" SubNum=\"18\"/>\n" +
" <Item Code=\"CAM0019\" Name=\"摄像头19\" Status=\"1\" DecoderTag=\"H264\" Longitude=\"120.15\" Latitude=\"30.32\" SubNum=\"19\"/>\n" +
" <Item Code=\"CAM0020\" Name=\"摄像头20\" Status=\"1\" DecoderTag=\"H264\" Longitude=\"120.15\" Latitude=\"30.32\" SubNum=\"20\"/>\n" +
" <Item Code=\"CAM0021\" Name=\"摄像头21\" Status=\"1\" DecoderTag=\"H264\" Longitude=\"120.15\" Latitude=\"30.32\" SubNum=\"21\"/>\n" +
" <Item Code=\"CAM0022\" Name=\"摄像头22\" Status=\"1\" DecoderTag=\"H264\" Longitude=\"120.15\" Latitude=\"30.32\" SubNum=\"22\"/>\n" +
" <Item Code=\"CAM0023\" Name=\"摄像头23\" Status=\"1\" DecoderTag=\"H264\" Longitude=\"120.15\" Latitude=\"30.32\" SubNum=\"23\"/>\n" +
" <Item Code=\"CAM0024\" Name=\"摄像头24\" Status=\"1\" DecoderTag=\"H264\" Longitude=\"120.15\" Latitude=\"30.32\" SubNum=\"24\"/>\n" +
" <Item Code=\"CAM0025\" Name=\"摄像头25\" Status=\"1\" DecoderTag=\"H264\" Longitude=\"120.15\" Latitude=\"30.32\" SubNum=\"25\"/>\n" +
" <Item Code=\"CAM0026\" Name=\"摄像头26\" Status=\"1\" DecoderTag=\"H264\" Longitude=\"120.15\" Latitude=\"30.32\" SubNum=\"26\"/>\n" +
" <Item Code=\"CAM0027\" Name=\"摄像头27\" Status=\"1\" DecoderTag=\"H264\" Longitude=\"120.15\" Latitude=\"30.32\" SubNum=\"27\"/>\n" +
" <Item Code=\"CAM0028\" Name=\"摄像头28\" Status=\"1\" DecoderTag=\"H264\" Longitude=\"120.15\" Latitude=\"30.32\" SubNum=\"28\"/>\n" +
" <Item Code=\"CAM0029\" Name=\"摄像头29\" Status=\"1\" DecoderTag=\"H264\" Longitude=\"120.15\" Latitude=\"30.32\" SubNum=\"29\"/>\n" +
" <Item Code=\"CAM0030\" Name=\"摄像头30\" Status=\"1\" DecoderTag=\"H264\" Longitude=\"120.15\" Latitude=\"30.32\" SubNum=\"30\"/>\n" +
" <Item Code=\"CAM0031\" Name=\"摄像头31\" Status=\"1\" DecoderTag=\"H264\" Longitude=\"120.15\" Latitude=\"30.32\" SubNum=\"31\"/>\n" +
" <Item Code=\"CAM0032\" Name=\"摄像头32\" Status=\"1\" DecoderTag=\"H264\" Longitude=\"120.15\" Latitude=\"30.32\" SubNum=\"32\"/>\n" +
" <Item Code=\"CAM0033\" Name=\"摄像头33\" Status=\"1\" DecoderTag=\"H264\" Longitude=\"120.15\" Latitude=\"30.32\" SubNum=\"33\"/>\n" +
" <Item Code=\"CAM0034\" Name=\"摄像头34\" Status=\"1\" DecoderTag=\"H264\" Longitude=\"120.15\" Latitude=\"30.32\" SubNum=\"34\"/>\n" +
" <Item Code=\"CAM0035\" Name=\"摄像头35\" Status=\"1\" DecoderTag=\"H264\" Longitude=\"120.15\" Latitude=\"30.32\" SubNum=\"35\"/>\n" +
" <Item Code=\"CAM0036\" Name=\"摄像头36\" Status=\"1\" DecoderTag=\"H264\" Longitude=\"120.15\" Latitude=\"30.32\" SubNum=\"36\"/>\n" +
" <Item Code=\"CAM0037\" Name=\"摄像头37\" Status=\"1\" DecoderTag=\"H264\" Longitude=\"120.15\" Latitude=\"30.32\" SubNum=\"37\"/>\n" +
" <Item Code=\"CAM0038\" Name=\"摄像头38\" Status=\"1\" DecoderTag=\"H264\" Longitude=\"120.15\" Latitude=\"30.32\" SubNum=\"38\"/>\n" +
" <Item Code=\"CAM0039\" Name=\"摄像头39\" Status=\"1\" DecoderTag=\"H264\" Longitude=\"120.15\" Latitude=\"30.32\" SubNum=\"39\"/>\n" +
" <Item Code=\"CAM0040\" Name=\"摄像头40\" Status=\"1\" DecoderTag=\"H264\" Longitude=\"120.15\" Latitude=\"30.32\" SubNum=\"40\"/>\n" +
" <Item Code=\"CAM0041\" Name=\"摄像头41\" Status=\"1\" DecoderTag=\"H264\" Longitude=\"120.15\" Latitude=\"30.32\" SubNum=\"41\"/>\n" +
" <Item Code=\"CAM0042\" Name=\"摄像头42\" Status=\"1\" DecoderTag=\"H264\" Longitude=\"120.15\" Latitude=\"30.32\" SubNum=\"42\"/>\n" +
" <Item Code=\"CAM0043\" Name=\"摄像头43\" Status=\"1\" DecoderTag=\"H264\" Longitude=\"120.15\" Latitude=\"30.32\" SubNum=\"43\"/>\n" +
" <Item Code=\"CAM0044\" Name=\"摄像头44\" Status=\"1\" DecoderTag=\"H264\" Longitude=\"120.15\" Latitude=\"30.32\" SubNum=\"44\"/>\n" +
" <Item Code=\"CAM0045\" Name=\"摄像头45\" Status=\"1\" DecoderTag=\"H264\" Longitude=\"120.15\" Latitude=\"30.32\" SubNum=\"45\"/>\n" +
" <Item Code=\"CAM0046\" Name=\"摄像头46\" Status=\"1\" DecoderTag=\"H264\" Longitude=\"120.15\" Latitude=\"30.32\" SubNum=\"46\"/>\n" +
" <Item Code=\"CAM0047\" Name=\"摄像头47\" Status=\"1\" DecoderTag=\"H264\" Longitude=\"120.15\" Latitude=\"30.32\" SubNum=\"47\"/>\n" +
" <Item Code=\"CAM0048\" Name=\"摄像头48\" Status=\"1\" DecoderTag=\"H264\" Longitude=\"120.15\" Latitude=\"30.32\" SubNum=\"48\"/>\n" +
" <Item Code=\"CAM0049\" Name=\"摄像头49\" Status=\"1\" DecoderTag=\"H264\" Longitude=\"120.15\" Latitude=\"30.32\" SubNum=\"49\"/>\n" +
" <Item Code=\"CAM0050\" Name=\"摄像头50\" Status=\"1\" DecoderTag=\"H264\" Longitude=\"120.15\" Latitude=\"30.32\" SubNum=\"50\"/>\n" +
" </SubList>\n" +
"</SIP_XML>\n";
@PostConstruct
public void init() throws Exception {
sipFactory = SipFactory.getInstance();
sipFactory.setPathName("gov.nist");
headerFactory = sipFactory.createHeaderFactory();
addressFactory = sipFactory.createAddressFactory();
messageFactory = sipFactory.createMessageFactory();
Properties properties = new Properties();
properties.setProperty("javax.sip.STACK_NAME", "spring-boot-sip-stack");
properties.setProperty("gov.nist.javax.sip.IP_ADDRESS", localIp);
properties.setProperty("gov.nist.javax.sip.DEBUG_LOG", "sipdebug.txt");
properties.setProperty("gov.nist.javax.sip.SERVER_LOG", "sipserverlog.txt");
sipStack = sipFactory.createSipStack(properties);
ListeningPoint lp = sipStack.createListeningPoint(localIp, localPort, transport);
sipProvider = sipStack.createSipProvider(lp);
sipProvider.addSipListener(this);
sendRegister(null);
}
public void sendRegister(AuthorizationHeader authHeader) throws Exception {
SipURI requestUri = addressFactory.createSipURI(null, domain);
requestUri.setTransportParam(transport.toUpperCase());
requestUri.setPort(port);
Address fromAddress = addressFactory.createAddress(username, addressFactory.createSipURI(username, domain));
FromHeader fromHeader = headerFactory.createFromHeader(fromAddress, fromTag);
Address toAddress = addressFactory.createAddress(username, addressFactory.createSipURI(username, domain));
ToHeader toHeader = headerFactory.createToHeader(toAddress, null);
CallIdHeader callId = sipProvider.getNewCallId();
CSeqHeader cSeq = headerFactory.createCSeqHeader(1L, Request.REGISTER);
MaxForwardsHeader maxForwards = headerFactory.createMaxForwardsHeader(70);
// 创建带自定义deviceid参数的Contact URI
SipURI contactUri = addressFactory.createSipURI(username, localIp);
contactUri.setPort(localPort);
contactUri.setTransportParam(transport.toUpperCase());
contactUri.setParameter("deviceid", "123456");
Address contactAddress = addressFactory.createAddress(contactUri);
ContactHeader contactHeader = headerFactory.createContactHeader(contactAddress);
ExpiresHeader expiresHeader = headerFactory.createExpiresHeader(expires);
List<ViaHeader> viaHeaders = new ArrayList<>();
ViaHeader viaHeader = headerFactory.createViaHeader(localIp, localPort, transport.toUpperCase(), null);
viaHeaders.add(viaHeader);
Request request = messageFactory.createRequest(
requestUri,
Request.REGISTER,
callId,
cSeq,
fromHeader,
toHeader,
viaHeaders,
maxForwards
);
request.addHeader(contactHeader);
request.addHeader(expiresHeader);
// 添加自定义头携带设备编号
Header deviceIdHeader = headerFactory.createHeader("X-Device-ID", "123456"); // 设备编号
request.addHeader(deviceIdHeader);
if (authHeader != null) {
request.addHeader(authHeader);
}
lastRegisterTransaction = sipProvider.getNewClientTransaction(request);
lastRegisterTransaction.sendRequest();
}
public void sendXmlResource(String targetSipUri, String xml) throws Exception {
log.info("Sending XML resource to {}", targetSipUri);
SipURI fromUri = addressFactory.createSipURI(username, domain + ":" + port);
Address fromAddress = addressFactory.createAddress(fromUri);
FromHeader fromHeader = headerFactory.createFromHeader(fromAddress, fromTag);
javax.sip.address.URI toUri = addressFactory.createURI(targetSipUri);
Address toAddress = addressFactory.createAddress(toUri);
ToHeader toHeader = headerFactory.createToHeader(toAddress, null);
// 创建空的Via头列表
List<ViaHeader> viaHeaders = new ArrayList<>();
Request message = messageFactory.createRequest(
toUri,
Request.MESSAGE,
sipProvider.getNewCallId(),
headerFactory.createCSeqHeader(1L, Request.MESSAGE),
fromHeader,
toHeader,
viaHeaders,
headerFactory.createMaxForwardsHeader(70)
);
// 手动创建Via头并添加
ViaHeader viaHeader = headerFactory.createViaHeader(localIp, localPort, "UDP", null);
message.addHeader(viaHeader);
ContentTypeHeader ct = headerFactory.createContentTypeHeader("Application", "MANSCDP+xml");
message.setContent(xml, ct);
ClientTransaction tx = sipProvider.getNewClientTransaction(message);
tx.sendRequest();
}
@Override
public void processResponse(ResponseEvent responseEvent) {
Response response = responseEvent.getResponse();
int status = response.getStatusCode();
log.info("Received response: {}", status);
CSeqHeader cSeqHeader = (CSeqHeader) response.getHeader(CSeqHeader.NAME);
if (cSeqHeader == null) return;
String method = cSeqHeader.getMethod();
try {
if (Request.REGISTER.equalsIgnoreCase(method)) {
if (status == 401 || status == 407) {
log.info("Received 401/407, attempt auth...");
WWWAuthenticateHeader wwwAuth = (WWWAuthenticateHeader) response.getHeader(WWWAuthenticateHeader.NAME);
if (wwwAuth == null) {
log.error("No WWW-Authenticate header found, cannot authenticate");
return;
}
String realm = wwwAuth.getRealm();
String nonce = wwwAuth.getNonce();
String qop = wwwAuth.getQop();
String uri = "sip:" + domain + ":" + port;
String nc = "00000001";
String cNonce = generateCNonce();
String responseDigest = DigestUtil.computeResponse(
username,
password,
realm,
nonce,
"REGISTER",
uri,
nc,
cNonce,
qop
);
AuthorizationHeader authHeader = headerFactory.createAuthorizationHeader("Digest");
authHeader.setUsername(username);
authHeader.setRealm(realm);
authHeader.setNonce(nonce);
authHeader.setURI(addressFactory.createURI(uri));
authHeader.setResponse(responseDigest);
if (qop != null) {
authHeader.setQop(qop);
authHeader.setCNonce(cNonce);
authHeader.setNonceCount(Integer.parseInt(nc, 16));
}
sendRegister(authHeader);
} else if (status >= 200 && status < 300) {
log.info("REGISTER success: {}", status);
int delay = Math.max(5, expires - 60);
refreshTimer.schedule(new TimerTask() {
@Override
public void run() {
try {
sendRegister(null);
} catch (Exception e) {
log.error("Failed to refresh register", e);
}
}
}, delay * 1000L);
sendNotify(username, domain, port, testXml);
} else {
log.warn("REGISTER response: {}", status);
}
} else if (Request.MESSAGE.equalsIgnoreCase(method)) {
log.info("MESSAGE response: {}", status);
} else if (Request.NOTIFY.equalsIgnoreCase(method)) {
log.info("NOTIFY response: {}", status);
if (status == 401) {
WWWAuthenticateHeader wwwAuth = (WWWAuthenticateHeader) response.getHeader(WWWAuthenticateHeader.NAME);
if (wwwAuth != null) {
lastNonce = wwwAuth.getNonce();
lastRealm = wwwAuth.getRealm();
lastOpaque = wwwAuth.getOpaque();
lastQop = wwwAuth.getQop();
try {
resendNotifyWithAuth();
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
} catch (Exception ex) {
log.error("processResponse error", ex);
}
}
private void resendNotifyWithAuth() throws Exception {
String method = Request.NOTIFY;
String uriStr = lastDeviceCode + "@" + lastServerIp;
String nc = "00000001";
String cNonce = md5Hex(Long.toString(System.currentTimeMillis()));
String responseDigest = computeResponse(
username,
lastRealm,
password,
method,
"sip:" + uriStr,
lastNonce,
nc,
cNonce,
lastQop != null ? lastQop : "auth"
);
AuthorizationHeader authHeader = headerFactory.createAuthorizationHeader("Digest");
authHeader.setUsername(username);
authHeader.setRealm(lastRealm);
authHeader.setNonce(lastNonce);
authHeader.setURI(addressFactory.createURI("sip:" + uriStr));
authHeader.setResponse(responseDigest);
authHeader.setAlgorithm("MD5");
authHeader.setCNonce(cNonce);
if (lastOpaque != null) authHeader.setOpaque(lastOpaque);
authHeader.setQop(lastQop != null ? lastQop : "auth");
authHeader.setNonceCount(1);
Request requestWithAuth = createNotifyRequest(lastDeviceCode, lastServerIp, lastServerPort, lastXmlBody, authHeader);
ClientTransaction transaction = sipProvider.getNewClientTransaction(requestWithAuth);
transaction.sendRequest();
}
// 构造NOTIFY请求
private Request createNotifyRequest(String deviceCode, String serverIp, int serverPort, String xmlBody, AuthorizationHeader authHeader) throws Exception {
String notifierUser = "290010021201070000";
String localHost = this.localIp;
int localPort = this.localPort;
SipURI requestUri = addressFactory.createSipURI(deviceCode, serverIp);
requestUri.setTransportParam("tcp");
requestUri.setPort(serverPort);
Address fromAddress = addressFactory.createAddress("sip:" + username + "@" + localHost);
FromHeader fromHeader = headerFactory.createFromHeader(fromAddress, UUID.randomUUID().toString().substring(0, 8));
Address toAddress = addressFactory.createAddress("sip:" + deviceCode + "@" + serverIp);
ToHeader toHeader = headerFactory.createToHeader(toAddress, null);
CallIdHeader callId = sipProvider.getNewCallId();
CSeqHeader cSeq = headerFactory.createCSeqHeader(cSeqCounter.getAndIncrement(), Request.NOTIFY);
MaxForwardsHeader maxForwards = headerFactory.createMaxForwardsHeader(70);
List<ViaHeader> viaHeaders = new ArrayList<>();
ViaHeader viaHeader = headerFactory.createViaHeader(localHost, localPort, transport, null);
viaHeaders.add(viaHeader);
Address contactAddr = addressFactory.createAddress("sip:" + username + "@" + localHost + ":" + localPort);
ContactHeader contactHeader = headerFactory.createContactHeader(contactAddr);
EventHeader eventHeader = headerFactory.createEventHeader("Push_Resource");
SubscriptionStateHeader subscriptionStateHeader = headerFactory.createSubscriptionStateHeader(SubscriptionStateHeader.TERMINATED);
ContentTypeHeader contentTypeHeader = headerFactory.createContentTypeHeader("application", "xml");
Request request = messageFactory.createRequest(
requestUri,
Request.NOTIFY,
callId,
cSeq,
fromHeader,
toHeader,
viaHeaders,
maxForwards
);
request.addHeader(contactHeader);
request.addHeader(eventHeader);
request.addHeader(subscriptionStateHeader);
request.addHeader(contentTypeHeader);
request.setContent(xmlBody, contentTypeHeader);
if (authHeader != null) {
request.addHeader(authHeader);
}
return request;
}
private String generateCNonce() {
return Long.toHexString(System.currentTimeMillis() & 0xffffffffL);
}
@Override
public void processRequest(RequestEvent requestEvent) {
Request request = requestEvent.getRequest();
String method = request.getMethod();
log.info("Received SIP request: {}", method);
try {
if (Request.MESSAGE.equalsIgnoreCase(method)) {
log.info("Processing MESSAGE request from {}", ((FromHeader) request.getHeader(FromHeader.NAME)).getAddress());
ContentTypeHeader contentTypeHeader = (ContentTypeHeader) request.getHeader(ContentTypeHeader.NAME);
if (contentTypeHeader != null &&
"application".equalsIgnoreCase(contentTypeHeader.getContentType()) &&
"xml".equalsIgnoreCase(contentTypeHeader.getContentSubType())) {
String xml = new String(request.getRawContent(), StandardCharsets.UTF_8);
xml = xml.replace("”", "\"").replace("“", "\"");
log.info("MESSAGE body:\n{}", xml);
String eventType = SipXmlParser.peekEventType(xml);
log.info("RECOGNIZE EventType = {}", eventType);
Class<?> itemClass = SipEventRegistry.getItemClass(eventType);
if (itemClass == null) {
log.warn("UNREGISTERED EventType: {}", eventType);
return;
}
SipXmlEnvelope<?> envelope = SipXmlParser.parse(xml, itemClass);
//handleSipEvent(envelope);
}
} else {
Response notImpl = messageFactory.createResponse(Response.NOT_IMPLEMENTED, request);
requestEvent.getServerTransaction().sendResponse(notImpl);
log.warn("Method {} not implemented, replied 501", method);
}
} catch (Exception ex) {
log.error("Error processing request: {}", ex.getMessage(), ex);
try {
Response errorResponse = messageFactory.createResponse(Response.SERVER_INTERNAL_ERROR, request);
requestEvent.getServerTransaction().sendResponse(errorResponse);
} catch (Exception e) {
log.error("Failed to send error response", e);
}
}
}
@Override
public void processTimeout(TimeoutEvent timeoutEvent) {
log.warn("SIP timeout: {}", timeoutEvent);
}
@Override
public void processIOException(IOExceptionEvent exceptionEvent) {
log.error("SIP IO Exception: {}", exceptionEvent);
}
@Override
public void processTransactionTerminated(TransactionTerminatedEvent tte) {
}
@Override
public void processDialogTerminated(DialogTerminatedEvent dte) {
}
@PreDestroy
public void shutdown() {
log.info("Shutting down SIP stack...");
refreshTimer.cancel();
if (sipProvider != null) sipProvider.removeSipListener(this);
if (sipStack != null) sipStack.stop();
}
/**
* 发送 SIP NOTIFY 消息
*
* @param deviceCode 客户端编码也是 SIP 用户名
* @param serverIp 服务器 IP
* @param serverPort 服务器 平台端口
* @param xmlBody XML 消息体
* @throws Exception
*/
// 公开调用发送NOTIFY首次不带认证
public void sendNotify(String deviceCode, String serverIp, int serverPort, String xmlBody) throws Exception {
lastDeviceCode = deviceCode;
lastServerIp = serverIp;
lastServerPort = serverPort;
lastXmlBody = xmlBody;
Request request = createNotifyRequest(deviceCode, serverIp, serverPort, xmlBody, null);
ClientTransaction transaction = sipProvider.getNewClientTransaction(request);
transaction.sendRequest();
}
/**
* 分片 XML保证每块 <= maxBytes
*/
private List<String> splitXml(String xml, int maxBytes) throws UnsupportedEncodingException {
List<String> parts = new ArrayList<>();
byte[] data = xml.getBytes("UTF-8");
int offset = 0;
while (offset < data.length) {
int len = Math.min(maxBytes, data.length - offset);
parts.add(new String(data, offset, len, "UTF-8"));
offset += len;
}
return parts;
}
// 计算Digest响应值
private String computeResponse(String username, String realm, String password, String method,
String uri, String nonce, String nc, String cNonce, String qop) throws Exception {
MessageDigest md = MessageDigest.getInstance("MD5");
String ha1 = md5Hex(md, username + ":" + realm + ":" + password);
String ha2 = md5Hex(md, method + ":" + uri);
return md5Hex(md, ha1 + ":" + nonce + ":" + nc + ":" + cNonce + ":" + qop + ":" + ha2);
}
private String md5Hex(String data) throws Exception {
MessageDigest md = MessageDigest.getInstance("MD5");
return md5Hex(md, data);
}
private String md5Hex(MessageDigest md, String data) {
byte[] digest = md.digest(data.getBytes(StandardCharsets.UTF_8));
StringBuilder sb = new StringBuilder();
for (byte b : digest) {
sb.append(String.format("%02x", b & 0xff));
}
return sb.toString();
}
}

+ 229
- 0
src/main/java/com/inspect/tcpserver/sip/service/SipVideoClient.java View File

@ -0,0 +1,229 @@
package com.inspect.tcpserver.sip.service;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import javax.sip.*;
import javax.sip.address.Address;
import javax.sip.address.AddressFactory;
import javax.sip.address.SipURI;
import javax.sip.header.*;
import javax.sip.message.MessageFactory;
import javax.sip.message.Request;
import javax.sip.message.Response;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.List;
import java.util.Properties;
import java.util.Random;
@Slf4j
@Component
@ConditionalOnProperty(name = "sip.enable", havingValue = "true")
public class SipVideoClient implements SipListener {
private SipFactory sipFactory;
private SipStack sipStack;
private SipProvider sipProvider;
private HeaderFactory headerFactory;
private AddressFactory addressFactory;
private MessageFactory messageFactory;
@Value("${sip.local-ip}")
private String localIp; // 本地IP
@Value("${sip.local-port2:5062}")
private int localPort; // 本地监听端口
@Value("${sip.domain}")
private String remoteIp; // Asterisk IP
@Value("${sip.port}")
private int remotePort; // Asterisk端口
@Value("${sip.username}")
private String username;
@Value("${sip.domain}")
private String domain;
@Value("${sip.transport}")
private String transport;
@PostConstruct
public void init() throws Exception {
sipFactory = SipFactory.getInstance();
sipFactory.setPathName("gov.nist");
headerFactory = sipFactory.createHeaderFactory();
addressFactory = sipFactory.createAddressFactory();
messageFactory = sipFactory.createMessageFactory();
Properties properties = new Properties();
properties.setProperty("javax.sip.STACK_NAME", "sip-video-client");
properties.setProperty("gov.nist.javax.sip.IP_ADDRESS", localIp);
sipStack = sipFactory.createSipStack(properties);
ListeningPoint lp = sipStack.createListeningPoint(localIp, localPort, transport);
sipProvider = sipStack.createSipProvider(lp);
sipProvider.addSipListener(this);
}
public void sendInvite() throws Exception {
SipURI requestUri = addressFactory.createSipURI(null, domain);
requestUri.setTransportParam(transport);
requestUri.setPort(remotePort);
Address fromAddress = addressFactory.createAddress("sip:" + username + "@" + domain);
FromHeader fromHeader = headerFactory.createFromHeader(fromAddress, Integer.toHexString(new Random().nextInt()));
Address toAddress = addressFactory.createAddress("sip:" + username + "@" + domain);
ToHeader toHeader = headerFactory.createToHeader(toAddress, null);
CallIdHeader callIdHeader = sipProvider.getNewCallId();
CSeqHeader cSeqHeader = headerFactory.createCSeqHeader(1L, Request.INVITE);
MaxForwardsHeader maxForwards = headerFactory.createMaxForwardsHeader(70);
List<ViaHeader> viaHeaders = new ArrayList<>();
viaHeaders.add(headerFactory.createViaHeader(localIp, localPort, transport, null));
Request inviteRequest = messageFactory.createRequest(
requestUri,
Request.INVITE,
callIdHeader,
cSeqHeader,
fromHeader,
toHeader,
viaHeaders,
maxForwards);
// 添加Contact头
Address contactAddress = addressFactory.createAddress("sip:" + username + "@" + localIp + ":" + localPort);
ContactHeader contactHeader = headerFactory.createContactHeader(contactAddress);
inviteRequest.addHeader(contactHeader);
// 添加SDP Body 这描述视频流的RTP信息
String sdpData = buildSdpBody(localIp, 9000); // 本地RTP端口9000
ContentTypeHeader contentTypeHeader = headerFactory.createContentTypeHeader("application", "sdp");
inviteRequest.setContent(sdpData, contentTypeHeader);
ClientTransaction inviteTransaction = sipProvider.getNewClientTransaction(inviteRequest);
inviteTransaction.sendRequest();
}
private String buildSdpBody(String ip, int rtpPort) {
return "v=0\r\n" +
"o=- 0 0 IN IP4 " + ip + "\r\n" +
"s=Video Call\r\n" +
"c=IN IP4 " + ip + "\r\n" +
"t=0 0\r\n" +
"m=video " + rtpPort + " RTP/AVP 96\r\n" +
"a=rtpmap:96 H264/90000\r\n" +
"a=sendrecv\r\n";
}
@Override
public void processResponse(ResponseEvent responseEvent) {
Response response = responseEvent.getResponse();
CSeqHeader cSeq = (CSeqHeader) response.getHeader(CSeqHeader.NAME);
int statusCode = response.getStatusCode();
log.info("Received response: {} for: {}", statusCode, cSeq.getMethod());
if (statusCode == 200 && Request.INVITE.equals(cSeq.getMethod())) {
try {
ClientTransaction ct = responseEvent.getClientTransaction();
Dialog dialog = ct.getDialog();
Request ackRequest = dialog.createAck(cSeq.getSeqNumber());
dialog.sendAck(ackRequest);
log.info("Sent ACK, 开始推送视频流");
startVideoStreaming();
} catch (Exception e) {
e.printStackTrace();
}
}
}
@Override
public void processRequest(RequestEvent requestEvent) {
Request request = requestEvent.getRequest();
String method = request.getMethod();
log.info("Received request: {}", method);
if (Request.BYE.equals(method)) {
try {
ServerTransaction st = requestEvent.getServerTransaction();
if (st == null) {
st = sipProvider.getNewServerTransaction(request);
}
Response response = messageFactory.createResponse(200, request);
st.sendResponse(response);
log.info("Received BYE,通话结束,停止推流");
stopVideoStreaming();
} catch (Exception e) {
e.printStackTrace();
}
}
}
@Override
public void processTimeout(TimeoutEvent timeoutEvent) {}
@Override
public void processIOException(IOExceptionEvent exceptionEvent) {}
@Override
public void processTransactionTerminated(TransactionTerminatedEvent transactionTerminatedEvent) {}
@Override
public void processDialogTerminated(DialogTerminatedEvent dialogTerminatedEvent) {}
private Process ffmpegProcess;
private void startVideoStreaming() {
try {
String rtspUrl = "rtsp://localhost:8554/mystream";
ProcessBuilder pb = new ProcessBuilder(
"ffmpeg",
"-rtsp_transport", "tcp", // 如果rtsp拉流失败可试试tcp
"-i", rtspUrl,
"-vcodec", "copy",
"-an",
"-f", "rtp",
"rtp://" + remoteIp + ":9000"
);
pb.redirectErrorStream(true);
ffmpegProcess = pb.start();
// 启动一个线程打印FFMPEG日志
new Thread(() -> {
try (BufferedReader reader = new BufferedReader(new InputStreamReader(ffmpegProcess.getInputStream()))) {
String line;
while ((line = reader.readLine()) != null) {
log.info("[FFMPEG] {}", line);
}
} catch (IOException e) {
e.printStackTrace();
}
}).start();
log.info("视频推流开始");
} catch (Exception e) {
e.printStackTrace();
}
}
private void stopVideoStreaming() {
if (ffmpegProcess != null) {
ffmpegProcess.destroy();
System.out.println("视频推流停止");
}
}
}

+ 62
- 0
src/main/java/com/inspect/tcpserver/sip/utils/DigestUtil.java View File

@ -0,0 +1,62 @@
package com.inspect.tcpserver.sip.utils;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Formatter;
public class DigestUtil {
/**
* 计算Digest响应值
* @param username 用户名
* @param password 密码
* @param realm
* @param nonce 随机数
* @param method SIP请求方法如REGISTER
* @param uri 请求URI
* @param nc 请求计数可以传null或"00000001"初始值
* @param cnonce 客户端随机串可传null
* @param qop 质量保护参数可传null
* @return response摘要字符串
*/
public static String computeResponse(String username, String password, String realm, String nonce,
String method, String uri, String nc, String cnonce, String qop) {
try {
String ha1 = md5Hex(username + ":" + realm + ":" + password);
String ha2 = md5Hex(method + ":" + uri);
String response;
if (qop != null && (qop.equals("auth") || qop.equals("auth-int"))) {
response = md5Hex(ha1 + ":" + nonce + ":" + nc + ":" + cnonce + ":" + qop + ":" + ha2);
} else {
response = md5Hex(ha1 + ":" + nonce + ":" + ha2);
}
return response;
} catch (Exception e) {
throw new RuntimeException("Failed to compute digest response", e);
}
}
/**
* MD5加密返回16进制字符串
*/
private static String md5Hex(String data) throws NoSuchAlgorithmException {
MessageDigest md = MessageDigest.getInstance("MD5");
byte[] digest = md.digest(data.getBytes());
return byteArrayToHexString(digest);
}
/**
* 将字节数组转为16进制字符串
*/
private static String byteArrayToHexString(byte[] bytes) {
Formatter formatter = new Formatter();
for (byte b : bytes) {
formatter.format("%02x", b);
}
String res = formatter.toString();
formatter.close();
return res;
}
}

+ 20
- 0
src/main/java/com/inspect/tcpserver/sip/utils/SipXmlEnvelope.java View File

@ -0,0 +1,20 @@
package com.inspect.tcpserver.sip.utils;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper;
import lombok.Getter;
import lombok.Setter;
import java.util.List;
@Getter
@Setter
public class SipXmlEnvelope<T> {
@JacksonXmlProperty(isAttribute = true, localName = "EventType")
private String eventType;
@JacksonXmlElementWrapper(useWrapping = false)
@JacksonXmlProperty(localName = "Item")
private List<T> items;
}

+ 29
- 0
src/main/java/com/inspect/tcpserver/sip/utils/SipXmlParser.java View File

@ -0,0 +1,29 @@
package com.inspect.tcpserver.sip.utils;
import com.fasterxml.jackson.dataformat.xml.XmlMapper;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class SipXmlParser {
private static final XmlMapper XML_MAPPER = new XmlMapper();
public static String peekEventType(String xml) throws Exception {
EventTypeOnly temp = XML_MAPPER.readValue(xml, EventTypeOnly.class);
return temp.getEventType();
}
public static <T> SipXmlEnvelope<T> parse(String xml, Class<T> itemClass) throws Exception {
return XML_MAPPER.readValue(xml,
XML_MAPPER.getTypeFactory().constructParametricType(SipXmlEnvelope.class, itemClass));
}
public static class EventTypeOnly {
@com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty(isAttribute = true, localName = "EventType")
private String eventType;
public String getEventType() { return eventType; }
public void setEventType(String eventType) { this.eventType = eventType; }
}
}

+ 11
- 0
src/main/resources/application-dev.yml View File

@ -45,3 +45,14 @@ minio:
secret-key: Cgygs@2025
bucket-name: znxsxt-production-upload
sip:
enable: true
username: 290010021201070000
password: 123456
domain: 192.168.1.116 # 平台SIP服务IP或域名
port: 5060
transport: tcp # 也可以为udp
local-ip: 192.168.1.11 # 本服务的外网或可达IP
local-port: 5061
expires: 3600

+ 11
- 0
src/main/resources/application.yml View File

@ -46,4 +46,15 @@ minio:
secret-key: minioadmin
bucket-name: mybucket
sip:
enable: true
username: 290010021201070000
password: 123456
domain: 192.168.1.116 # 平台SIP服务IP或域名
port: 5060
transport: tcp # 也可以为udp
local-ip: 192.168.1.11 # 本服务的外网或可达IP
local-port: 5061
expires: 3600

Loading…
Cancel
Save