diff --git a/src/main/java/com/inspect/tcpserver/sip/items/DeviceInfoItem.java b/src/main/java/com/inspect/tcpserver/sip/items/DeviceInfoItem.java new file mode 100644 index 0000000..119741b --- /dev/null +++ b/src/main/java/com/inspect/tcpserver/sip/items/DeviceInfoItem.java @@ -0,0 +1,21 @@ +package com.inspect.tcpserver.sip.items; + +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty; +import lombok.Getter; +import lombok.Setter; + +/** + * 图片抓拍请求信令 + */ +@Getter +@Setter +public class DeviceInfoItem { + @JacksonXmlProperty(isAttribute = true, localName = "CmdType") + private String cmdType; + + @JacksonXmlProperty(isAttribute = true, localName = "sn") + private String sn; + + @JacksonXmlProperty(isAttribute = true, localName = "DeviceID") + private String deviceID; +} diff --git a/src/main/java/com/inspect/tcpserver/sip/registry/SipEventRegistry.java b/src/main/java/com/inspect/tcpserver/sip/registry/SipEventRegistry.java index 6516c2c..b3a0f82 100644 --- a/src/main/java/com/inspect/tcpserver/sip/registry/SipEventRegistry.java +++ b/src/main/java/com/inspect/tcpserver/sip/registry/SipEventRegistry.java @@ -9,6 +9,7 @@ public class SipEventRegistry { private static final Map> EVENT_MAP = new HashMap<>(); static { + EVENT_MAP.put("Device_Info", DeviceInfoItem.class); EVENT_MAP.put("Request_Resource", RequestResourceItem.class); EVENT_MAP.put("Request_History_Alarm", RequestHistoryAlarmItem.class); EVENT_MAP.put("Request_History_Video", RequestHistoryVideoItem.class); diff --git a/src/main/java/com/inspect/tcpserver/sip/service/SipClientService.java b/src/main/java/com/inspect/tcpserver/sip/service/SipClientService.java index e2472ec..d247102 100644 --- a/src/main/java/com/inspect/tcpserver/sip/service/SipClientService.java +++ b/src/main/java/com/inspect/tcpserver/sip/service/SipClientService.java @@ -20,6 +20,7 @@ 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.io.UnsupportedEncodingException; import java.net.*; @@ -211,6 +212,11 @@ public class SipClientService implements SipListener { Properties properties = new Properties(); properties.setProperty("javax.sip.STACK_NAME", "spring-boot-sip-stack"); properties.setProperty("gov.nist.javax.sip.IP_ADDRESS", localIp); + // 新增这些(JAIN-SIP RI 支持的 NAT 参数) + properties.setProperty("gov.nist.javax.sip.OUTBOUND_PROXY", domain + ":" + port + ";" + transport); // 可选:强制出站代理 + properties.setProperty("gov.nist.javax.sip.AUTOMATIC_NAT_SUPPORT", "true"); // 如果版本支持 + properties.setProperty("gov.nist.javax.sip.USE_RPORT_AS_OUTBOUND", "true"); // 使用 rport 作为出站 + properties.setProperty("gov.nist.javax.sip.FIX_CONTACT_HEADER", "true"); // 修复 Contact properties.setProperty("gov.nist.javax.sip.DEBUG_LOG", "sipdebug.txt"); properties.setProperty("gov.nist.javax.sip.SERVER_LOG", "sipserverlog.txt"); @@ -267,7 +273,7 @@ public class SipClientService implements SipListener { public void sendRegister(AuthorizationHeader authHeader) throws Exception { SipURI requestUri = addressFactory.createSipURI(null, domain); -// requestUri.setTransportParam(transport.toUpperCase()); + requestUri.setTransportParam(transport.toUpperCase()); requestUri.setPort(port); Address fromAddress = addressFactory.createAddress(username, addressFactory.createSipURI(username, domain)); @@ -279,11 +285,18 @@ public class SipClientService implements SipListener { CSeqHeader cSeq = headerFactory.createCSeqHeader(1L, Request.REGISTER); MaxForwardsHeader maxForwards = headerFactory.createMaxForwardsHeader(70); + //String natIp = "172.19.1.1"; // 避免硬编码:用环境变量注入 + //int natPort = localPort; // 通常保持本地端口,或映射后端口 + String natIp = System.getenv("SIP_NAT_IP") != null ? System.getenv("SIP_NAT_IP") : localIp; + int natPort = System.getenv("SIP_NAT_PORT") != null ? Integer.parseInt(System.getenv("SIP_NAT_PORT")) : localPort; + log.info("SEND_REGISTER NAT_IP: {}, NAT_PORT: {}", natIp, natPort); // 创建带自定义deviceid参数的Contact URI - SipURI contactUri = addressFactory.createSipURI(username, localIp); - contactUri.setPort(localPort); + //SipURI contactUri = addressFactory.createSipURI(username, localIp); + SipURI contactUri = addressFactory.createSipURI(username, natIp); + //contactUri.setPort(localPort); + contactUri.setPort(natPort); contactUri.setTransportParam(transport.toUpperCase()); -// contactUri.setParameter("deviceid", "123456"); + contactUri.setParameter("deviceid", "123456"); Address contactAddress = addressFactory.createAddress(contactUri); ContactHeader contactHeader = headerFactory.createContactHeader(contactAddress); @@ -291,6 +304,7 @@ public class SipClientService implements SipListener { List viaHeaders = new ArrayList<>(); ViaHeader viaHeader = headerFactory.createViaHeader(localIp, localPort, transport.toUpperCase(), null); + viaHeader.setRPort(); viaHeaders.add(viaHeader); Request request = messageFactory.createRequest( @@ -528,7 +542,8 @@ public class SipClientService implements SipListener { }, delay * 1000L); //sendNotify(username, domain, port, testXml); - sendSubscribe(username, domain, port, "Push_Resource"); + // 向SIP服务器订阅消息不符合SIP/GB28181标准 + //sendSubscribe(username, domain, port, "Push_Resource"); } else { log.warn("REGISTER response: {}", status); } @@ -741,9 +756,11 @@ public class SipClientService implements SipListener { 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); + log.info("Processing MESSAGE request from {}, contentType: {}, contentSubType: {}", + ((FromHeader) request.getHeader(FromHeader.NAME)).getAddress(), + contentTypeHeader != null ? contentTypeHeader.getContentType() : "null", + contentTypeHeader != null ? contentTypeHeader.getContentSubType() : "null"); if (contentTypeHeader != null && "application".equalsIgnoreCase(contentTypeHeader.getContentType()) && "xml".equalsIgnoreCase(contentTypeHeader.getContentSubType())) { @@ -761,6 +778,14 @@ public class SipClientService implements SipListener { return; } + // 200 OK -> SIP Server + ServerTransaction st = requestEvent.getServerTransaction(); + if (st == null) { + st = sipProvider.getNewServerTransaction(request); + } + Response ok = messageFactory.createResponse(Response.OK, request); + st.sendResponse(ok); + SipXmlEnvelope envelope = SipXmlParser.parse(xml, itemClass); //handleSipEvent(envelope); } @@ -788,19 +813,29 @@ public class SipClientService implements SipListener { ExpiresHeader expiresHeader = (ExpiresHeader) request.getHeader(ExpiresHeader.NAME); int expires = (expiresHeader != null) ? expiresHeader.getExpires() : 0; log.info("Received SUBSCRIBE expires: {}, XML:\n{}", expires, xml); + String eventType = SipXmlParser.peekEventType(xml); + log.info("SUBSCRIBE EventType: {}", eventType); ServerTransaction transaction = sipProvider.getNewServerTransaction(request); + // 对应协议B.9.1.2 F2 前端系统返回200 OK响应,指示已经接受订阅请求 Response response = messageFactory.createResponse(Response.OK, request); + response.setExpires(expiresHeader); transaction.sendResponse(response); if (expires > 0) { saveSubscription(request, expires); + // 对应协议B.9.1.2 F3 SIP客户端(前端系统)发送没有消息体的NOTIFY给平台,其中Subscription-State头部字段值为active, 指示订阅关系建立。 sendInitialNotify(request, transaction, "initial"); } else { removeSubscription(request); } + } else if (Request.INVITE.equals(request.getMethod())) { + handleInvite(requestEvent); + } else if(Request.ACK.equals(request.getMethod())) { + // RFC 3261: The ACK request does not generate a response. + // ACK 是“事务内请求”,不是普通 SIP 方法,不生成任何 Response,ACK 是“事务内请求”,不是普通 SIP 方法 + startRtspToRtp(); } else { - Response notImpl = messageFactory.createResponse(Response.NOT_IMPLEMENTED, request); requestEvent.getServerTransaction().sendResponse(notImpl); log.info("Method {} not implemented, replied 501", method); @@ -816,6 +851,100 @@ public class SipClientService implements SipListener { } } + private Process ffmpegProcess; + + private void startRtspToRtp() { + try { + String cmd = + "ffmpeg -rtsp_transport tcp " + + "-i \"rtsp://admin:wd19216811@192.168.1.244:554/h264/ch1/sub/av_stream\" " + + "-an -vcodec copy " + + "-f rtp -payload_type 96 " + + "\"rtp://192.168.1.116:50000?tcp\""; + + ProcessBuilder pb = new ProcessBuilder( + "bash", "-c", cmd + ); + pb.redirectErrorStream(true); + + ffmpegProcess = pb.start(); + + new Thread(() -> { + try (BufferedReader br = + new BufferedReader(new InputStreamReader( + ffmpegProcess.getInputStream()))) { + String line; + while ((line = br.readLine()) != null) { + System.out.println("[FFmpeg] " + line); + } + } catch (IOException ignored) {} + }).start(); + + } catch (Exception e) { + e.printStackTrace(); + } + } + + + private void handleInvite(RequestEvent requestEvent) throws Exception { + Request request = requestEvent.getRequest(); + log.info("Received INVITE:\n{}", request); + + SipProvider provider = (SipProvider) requestEvent.getSource(); + ServerTransaction st = requestEvent.getServerTransaction(); + if (st == null) { + st = provider.getNewServerTransaction(request); + } + + // ① 100 Trying(非常推荐) + Response trying = messageFactory.createResponse(Response.TRYING, request); + st.sendResponse(trying); + + // ② 解析对方 SDP(可先只打印) + byte[] raw = request.getRawContent(); + if (raw != null) { + String sdp = new String(raw, StandardCharsets.UTF_8); + log.info("INVITE SDP:\n{}", sdp); + } + + // ③ 构造 200 OK + SDP + Response ok = messageFactory.createResponse(Response.OK, request); + + String localSipId = "29001002120107000001"; + int localRtpPort = 554; + // Contact 必须 + Address contactAddress = addressFactory.createAddress( + "sip:" + localSipId + "@" + localIp + ":" + localPort + ); + ContactHeader contactHeader = headerFactory.createContactHeader(contactAddress); + ok.addHeader(contactHeader); + + // Content-Type + ContentTypeHeader contentType = + headerFactory.createContentTypeHeader("application", "sdp"); + + // 返回 SDP(示例) + String sdp = + "v=0\r\n" + + "o=" + localSipId + " 0 0 IN IP4 " + localIp + "\r\n" + + "s=Play\r\n" + + "c=IN IP4 " + localIp + "\r\n" + + "t=0 0\r\n" + + "m=video " + localRtpPort + " TCP/RTP/AVP 96\r\n" + + "a=sendonly\r\n" + + "a=setup:active\r\n" + + "a=connection:new\r\n" + + "a=rtpmap:96 PS/90000\r\n"; + + ok.setContent(sdp, contentType); + + // ④ 发送 200 OK + st.sendResponse(ok); + + log.info("INVITE handled: 200 OK sent"); + } + + @Override public void processTimeout(TimeoutEvent timeoutEvent) { log.warn("SIP timeout: {}", timeoutEvent); @@ -960,12 +1089,12 @@ public class SipClientService implements SipListener { notifyRequest.addHeader(ssHeader); // 消息体 - String xmlBody = "\n" + - "\n" + - " " + bodyText + "\n" + - ""; - ContentTypeHeader contentTypeHeader = headerFactory.createContentTypeHeader("application", "xml"); - notifyRequest.setContent(xmlBody, contentTypeHeader); +// String xmlBody = "\n" + +// "\n" + +// " " + bodyText + "\n" + +// ""; +// ContentTypeHeader contentTypeHeader = headerFactory.createContentTypeHeader("application", "xml"); +// notifyRequest.setContent(xmlBody, contentTypeHeader); // 发送 ClientTransaction ct = sipProvider.getNewClientTransaction(notifyRequest); @@ -978,6 +1107,54 @@ public class SipClientService implements SipListener { } } + private void sendSubscribeNotifyToSipServer(Request subscribeRequest) { + try { + // 获取必要头字段 + CallIdHeader callIdHeader = (CallIdHeader) subscribeRequest.getHeader(CallIdHeader.NAME); + CSeqHeader cseqHeader = (CSeqHeader) subscribeRequest.getHeader(CSeqHeader.NAME); + FromHeader fromHeader = (FromHeader) subscribeRequest.getHeader(FromHeader.NAME); + ToHeader toHeader = (ToHeader) subscribeRequest.getHeader(ToHeader.NAME); + EventHeader eventHeader = (EventHeader) subscribeRequest.getHeader(EventHeader.NAME); + ContactHeader contactHeader = (ContactHeader) subscribeRequest.getHeader(ContactHeader.NAME); + + List viaHeaders = new ArrayList<>(); + ViaHeader viaHeader = headerFactory.createViaHeader(localIp, localPort, transport.toUpperCase(), null); + viaHeaders.add(viaHeader); + // 创建 NOTIFY 请求 + Request notifyRequest = messageFactory.createRequest( + subscribeRequest.getRequestURI(), + Request.NOTIFY, + callIdHeader, + headerFactory.createCSeqHeader(cseqHeader.getSeqNumber() + 1, Request.NOTIFY), + fromHeader, + toHeader, + viaHeaders, + headerFactory.createMaxForwardsHeader(70) + ); + + // 加入 Contact + notifyRequest.addHeader(contactHeader); + + // 必须的事件头 + notifyRequest.addHeader(eventHeader); + + // Subscription-State 头 + SubscriptionStateHeader ssHeader = headerFactory.createSubscriptionStateHeader("active"); + ssHeader.setExpires(3600); + notifyRequest.addHeader(ssHeader); + + ContentTypeHeader contentTypeHeader = headerFactory.createContentTypeHeader("application", "xml"); + notifyRequest.setContent(null, contentTypeHeader); + + // 发送 + ClientTransaction ct = sipProvider.getNewClientTransaction(notifyRequest); + ct.sendRequest(); + log.info("sendSubscribeNotifyToSipServer: callId: {}", callIdHeader.getCallId()); + } catch (Exception e) { + e.printStackTrace(); + } + } + private void removeSubscription(Request request) { CallIdHeader callIdHeader = (CallIdHeader) request.getHeader(CallIdHeader.NAME); diff --git a/src/main/java/com/inspect/tcpserver/sip/service/SipVideoClient.java b/src/main/java/com/inspect/tcpserver/sip/service/SipVideoClient.java index 8b6c0e7..fddceed 100644 --- a/src/main/java/com/inspect/tcpserver/sip/service/SipVideoClient.java +++ b/src/main/java/com/inspect/tcpserver/sip/service/SipVideoClient.java @@ -66,6 +66,7 @@ public class SipVideoClient implements SipListener { sipStack = sipFactory.createSipStack(properties); ListeningPoint lp = sipStack.createListeningPoint(localIp, localPort, transport); + //ListeningPoint lp = sipStack.createListeningPoint("0.0.0.0", localPort, transport); sipProvider = sipStack.createSipProvider(lp); sipProvider.addSipListener(this); } diff --git a/src/main/java/com/inspect/tcpserver/sip/utils/SipXmlParser.java b/src/main/java/com/inspect/tcpserver/sip/utils/SipXmlParser.java index fd13301..d85d22c 100644 --- a/src/main/java/com/inspect/tcpserver/sip/utils/SipXmlParser.java +++ b/src/main/java/com/inspect/tcpserver/sip/utils/SipXmlParser.java @@ -1,6 +1,10 @@ package com.inspect.tcpserver.sip.utils; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.dataformat.xml.XmlMapper; +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement; +import lombok.Getter; +import lombok.Setter; import lombok.extern.slf4j.Slf4j; @Slf4j @@ -17,12 +21,30 @@ public class SipXmlParser { XML_MAPPER.getTypeFactory().constructParametricType(SipXmlEnvelope.class, itemClass)); } + @Setter + @Getter + @JacksonXmlRootElement(localName = "SIP_XML") + @JsonIgnoreProperties(ignoreUnknown = true) 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; } + } + + public static void main(String[] args) { + try { + String xml = "\n" + + "\n" + + " \n" + + ""; + + //String xml = "";//ok + //String xml = ""; + String eventType = SipXmlParser.peekEventType(xml); + System.out.println(eventType); + } catch (Exception e) { + e.printStackTrace(); + } } } diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml index ce12085..b8fa741 100644 --- a/src/main/resources/application-dev.yml +++ b/src/main/resources/application-dev.yml @@ -52,7 +52,7 @@ sip: domain: 192.168.1.116 # 平台SIP服务IP或域名 port: 5060 transport: tcp # 也可以为udp - local-ip: 192.168.1.11 # 本服务的外网或可达IP + local-ip: 192.168.1.8 # 本服务的外网或可达IP local-port: 5061 expires: 3600 diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index bb7ccee..5e8822d 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -47,13 +47,13 @@ minio: bucket-name: mybucket sip: - enable: false + 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-ip: 192.168.1.8 # 本服务的外网或可达IP local-port: 5061 expires: 3600