|
|
@@ -0,0 +1,679 @@
|
|
|
+package com.usky.cdi.service.sip.device;
|
|
|
+
|
|
|
+import com.usky.cdi.service.sip.config.SipClientProperties;
|
|
|
+import com.usky.cdi.service.sip.model.SdpMediaInfo;
|
|
|
+import com.usky.cdi.service.sip.push.RtspGb28181RtpSender;
|
|
|
+import com.usky.cdi.service.sip.sdp.SdpUtils;
|
|
|
+import com.usky.cdi.service.sip.util.NetworkUtils;
|
|
|
+import com.usky.cdi.service.sip.util.SipDigestAuth;
|
|
|
+import gov.nist.javax.sip.RequestEventExt;
|
|
|
+import gov.nist.javax.sip.SipStackImpl;
|
|
|
+import lombok.extern.slf4j.Slf4j;
|
|
|
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
|
|
+import org.springframework.boot.context.event.ApplicationReadyEvent;
|
|
|
+import org.springframework.context.event.EventListener;
|
|
|
+import org.springframework.stereotype.Service;
|
|
|
+import org.springframework.util.StringUtils;
|
|
|
+
|
|
|
+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.nio.charset.StandardCharsets;
|
|
|
+import java.util.ArrayList;
|
|
|
+import java.util.HashMap;
|
|
|
+import java.util.List;
|
|
|
+import java.util.Map;
|
|
|
+import java.util.UUID;
|
|
|
+import java.util.concurrent.*;
|
|
|
+import java.util.concurrent.atomic.AtomicBoolean;
|
|
|
+import java.util.concurrent.atomic.AtomicReference;
|
|
|
+
|
|
|
+/**
|
|
|
+ * 模拟海康摄像机 GB28181 接入:手动 REGISTER,平台点播 INVITE 后从 RTSP 推流。
|
|
|
+ */
|
|
|
+@Slf4j
|
|
|
+@Service
|
|
|
+@ConditionalOnProperty(prefix = "sip.client", name = "enabled", havingValue = "true")
|
|
|
+public class Gb28181DeviceSimulator implements SipListener {
|
|
|
+
|
|
|
+ private static final String GB28181_SSRC = "0100000001";
|
|
|
+
|
|
|
+ private final SipClientProperties properties;
|
|
|
+ private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(
|
|
|
+ r -> new Thread(r, "gb28181-device"));
|
|
|
+
|
|
|
+ private SipStack sipStack;
|
|
|
+ private SipProvider sipProvider;
|
|
|
+ private AddressFactory addressFactory;
|
|
|
+ private MessageFactory messageFactory;
|
|
|
+ private HeaderFactory headerFactory;
|
|
|
+
|
|
|
+ private String sipTransport;
|
|
|
+ private String localIp;
|
|
|
+ private int localSipPort;
|
|
|
+ private int cseq = 1;
|
|
|
+
|
|
|
+ private final AtomicBoolean registered = new AtomicBoolean(false);
|
|
|
+ private final AtomicBoolean registerAuthSent = new AtomicBoolean(false);
|
|
|
+ private final AtomicReference<WWWAuthenticateHeader> authChallenge = new AtomicReference<>();
|
|
|
+ private final AtomicReference<String> boundRtspUrl = new AtomicReference<>();
|
|
|
+ private final AtomicReference<PendingStream> pendingStream = new AtomicReference<>();
|
|
|
+ private volatile Future<?> streamingTask;
|
|
|
+ private volatile ScheduledFuture<?> reRegisterTask;
|
|
|
+ private volatile String activeDeviceId;
|
|
|
+
|
|
|
+ public Gb28181DeviceSimulator(SipClientProperties properties) {
|
|
|
+ this.properties = properties;
|
|
|
+ }
|
|
|
+
|
|
|
+ @EventListener(ApplicationReadyEvent.class)
|
|
|
+ public void onReady() {
|
|
|
+ if (properties.isAutoStart()) {
|
|
|
+ log.warn("sip.client.auto-start=true 已废弃,请使用 POST /api/sip/device/register?deviceId=...");
|
|
|
+ } else {
|
|
|
+ log.info("GB28181 设备模拟就绪,请手动调用 POST /api/sip/device/register?deviceId=... 注册");
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ @PreDestroy
|
|
|
+ public void destroy() {
|
|
|
+ cancelReRegister();
|
|
|
+ try {
|
|
|
+ unregisterInternal(true);
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.debug("销毁时注销失败: {}", e.getMessage());
|
|
|
+ }
|
|
|
+ scheduler.shutdownNow();
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 手动注册到 GB28181 平台。
|
|
|
+ *
|
|
|
+ * @param deviceId 20 位设备编码(动态传入)
|
|
|
+ */
|
|
|
+ public synchronized Map<String, Object> register(String deviceId) {
|
|
|
+ validateDeviceId(deviceId);
|
|
|
+ if (registered.get() && deviceId.equals(activeDeviceId)) {
|
|
|
+ log.info("设备已注册: deviceId={}", activeDeviceId);
|
|
|
+ return status();
|
|
|
+ }
|
|
|
+ if (sipStack != null) {
|
|
|
+ unregisterInternal(true);
|
|
|
+ }
|
|
|
+ try {
|
|
|
+ activeDeviceId = deviceId.trim();
|
|
|
+ initStack();
|
|
|
+ registerAuthSent.set(false);
|
|
|
+ authChallenge.set(null);
|
|
|
+ sendRegister(false, properties.getRegisterExpires());
|
|
|
+ scheduleReRegister();
|
|
|
+ log.info("GB28181 注册请求已发送: deviceId={}, platform={}@{}:{}, transport={}",
|
|
|
+ activeDeviceId, properties.getPlatformId(),
|
|
|
+ properties.getServerHost(), properties.getServerPort(), sipTransport);
|
|
|
+ } catch (Exception e) {
|
|
|
+ activeDeviceId = null;
|
|
|
+ shutdownStack();
|
|
|
+ sipStack = null;
|
|
|
+ sipProvider = null;
|
|
|
+ throw new IllegalStateException("GB28181 注册失败: " + e.getMessage(), e);
|
|
|
+ }
|
|
|
+ return status();
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 手动注销(REGISTER Expires=0)并释放 SIP 栈。 */
|
|
|
+ public synchronized Map<String, Object> unregister() {
|
|
|
+ unregisterInternal(true);
|
|
|
+ return status();
|
|
|
+ }
|
|
|
+
|
|
|
+ private void unregisterInternal(boolean sendExpiresZero) {
|
|
|
+ cancelReRegister();
|
|
|
+ if (streamingTask != null) {
|
|
|
+ streamingTask.cancel(true);
|
|
|
+ streamingTask = null;
|
|
|
+ }
|
|
|
+ if (sendExpiresZero && sipStack != null && StringUtils.hasText(activeDeviceId)) {
|
|
|
+ try {
|
|
|
+ registerAuthSent.set(false);
|
|
|
+ sendRegister(registered.get(), 0);
|
|
|
+ log.info("GB28181 注销请求已发送: deviceId={}", activeDeviceId);
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.warn("GB28181 注销请求发送失败: {}", e.getMessage());
|
|
|
+ }
|
|
|
+ }
|
|
|
+ registered.set(false);
|
|
|
+ activeDeviceId = null;
|
|
|
+ boundRtspUrl.set(null);
|
|
|
+ pendingStream.set(null);
|
|
|
+ shutdownStack();
|
|
|
+ sipStack = null;
|
|
|
+ sipProvider = null;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 绑定 RTSP 并等待平台点播(与海康摄像机:配置好平台后,平台发 INVITE 取流)。
|
|
|
+ */
|
|
|
+ public Gb28181StreamResult bindRtspAndWait(String rtspUrl, int inviteWaitSeconds, int streamDurationSeconds) {
|
|
|
+ if (!StringUtils.hasText(rtspUrl)) {
|
|
|
+ return fail("rtspUrl 不能为空");
|
|
|
+ }
|
|
|
+ if (!registered.get() || !StringUtils.hasText(activeDeviceId)) {
|
|
|
+ return fail("设备未注册,请先调用 POST /api/sip/device/register?deviceId=...");
|
|
|
+ }
|
|
|
+ if (sipStack == null) {
|
|
|
+ return fail("SIP 栈未就绪,请重新注册设备");
|
|
|
+ }
|
|
|
+
|
|
|
+ PendingStream session = new PendingStream(rtspUrl, streamDurationSeconds);
|
|
|
+ pendingStream.set(session);
|
|
|
+ boundRtspUrl.set(rtspUrl);
|
|
|
+
|
|
|
+ log.info("已绑定 RTSP,等待平台 INVITE: deviceId={}, wait={}s", activeDeviceId, inviteWaitSeconds);
|
|
|
+ log.info(">>> 请在 GB28181 平台对设备 {} 点击「实时预览」", activeDeviceId);
|
|
|
+
|
|
|
+ try {
|
|
|
+ if (!session.inviteReady.await(inviteWaitSeconds, TimeUnit.SECONDS)) {
|
|
|
+ pendingStream.compareAndSet(session, null);
|
|
|
+ return fail("等待平台 INVITE 超时,请在平台对设备 " + activeDeviceId + " 发起实时预览");
|
|
|
+ }
|
|
|
+ long streamWait = streamDurationSeconds + 15L;
|
|
|
+ if (!session.streamDone.await(streamWait, TimeUnit.SECONDS)) {
|
|
|
+ return fail("推流超时");
|
|
|
+ }
|
|
|
+ return session.result != null ? session.result : fail("推流未返回结果");
|
|
|
+ } catch (InterruptedException e) {
|
|
|
+ Thread.currentThread().interrupt();
|
|
|
+ return fail("等待被中断");
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ public Map<String, Object> status() {
|
|
|
+ Map<String, Object> map = new HashMap<>();
|
|
|
+ map.put("registered", registered.get());
|
|
|
+ map.put("deviceId", activeDeviceId);
|
|
|
+ map.put("platformId", properties.getPlatformId());
|
|
|
+ map.put("serverHost", properties.getServerHost());
|
|
|
+ map.put("serverPort", properties.getServerPort());
|
|
|
+ map.put("domain", properties.getDomain());
|
|
|
+ map.put("sipTransport", sipTransport);
|
|
|
+ map.put("localSipAddress", localIp != null ? localIp + ":" + localSipPort : null);
|
|
|
+ map.put("boundRtspUrl", boundRtspUrl.get());
|
|
|
+ map.put("streaming", streamingTask != null && !streamingTask.isDone());
|
|
|
+ return map;
|
|
|
+ }
|
|
|
+
|
|
|
+ private void initStack() throws Exception {
|
|
|
+ SipFactory sipFactory = SipFactory.getInstance();
|
|
|
+ sipFactory.setPathName("gov.nist");
|
|
|
+ java.util.Properties stackProps = new java.util.Properties();
|
|
|
+ stackProps.setProperty("javax.sip.STACK_NAME", "UskyGb28181Device");
|
|
|
+ stackProps.setProperty("gov.nist.javax.sip.TRACE_LEVEL", "0");
|
|
|
+
|
|
|
+ localIp = NetworkUtils.resolveLocalIp(null);
|
|
|
+ sipTransport = normalizeTransport(properties.getSipTransport());
|
|
|
+ stackProps.setProperty("javax.sip.OUTBOUND_PROXY",
|
|
|
+ properties.getServerHost() + ":" + properties.getServerPort() + "/" + sipTransport);
|
|
|
+
|
|
|
+ sipStack = sipFactory.createSipStack(stackProps);
|
|
|
+ addressFactory = sipFactory.createAddressFactory();
|
|
|
+ messageFactory = sipFactory.createMessageFactory();
|
|
|
+ headerFactory = sipFactory.createHeaderFactory();
|
|
|
+
|
|
|
+ localSipPort = findAvailablePort();
|
|
|
+ ListeningPoint lp = sipStack.createListeningPoint(localIp, localSipPort, sipTransport);
|
|
|
+ sipProvider = sipStack.createSipProvider(lp);
|
|
|
+ sipProvider.addSipListener(this);
|
|
|
+ }
|
|
|
+
|
|
|
+ private void scheduleReRegister() {
|
|
|
+ cancelReRegister();
|
|
|
+ long interval = Math.max(60, (long) (properties.getRegisterExpires() * 0.85));
|
|
|
+ reRegisterTask = scheduler.scheduleAtFixedRate(() -> {
|
|
|
+ if (sipStack != null && StringUtils.hasText(activeDeviceId)) {
|
|
|
+ try {
|
|
|
+ registerAuthSent.set(false);
|
|
|
+ sendRegister(registered.get(), properties.getRegisterExpires());
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.warn("GB28181 续注册失败: {}", e.getMessage());
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }, interval, interval, TimeUnit.SECONDS);
|
|
|
+ }
|
|
|
+
|
|
|
+ private void cancelReRegister() {
|
|
|
+ if (reRegisterTask != null) {
|
|
|
+ reRegisterTask.cancel(false);
|
|
|
+ reRegisterTask = null;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private void sendRegister(boolean withAuth, int expires) throws Exception {
|
|
|
+ ensureActiveDeviceId();
|
|
|
+ SipURI requestUri = createUri(activeDeviceId, properties.getDomain());
|
|
|
+ requestUri.setPort(properties.getServerPort());
|
|
|
+ requestUri.setTransportParam(sipTransport);
|
|
|
+
|
|
|
+ Request register = buildRequest(Request.REGISTER, requestUri, activeDeviceId, "reg-tag");
|
|
|
+ register.addHeader(headerFactory.createExpiresHeader(expires));
|
|
|
+
|
|
|
+ if (withAuth) {
|
|
|
+ AuthorizationHeader auth = buildAuthorization(Request.REGISTER, requestUri.toString());
|
|
|
+ if (auth != null) {
|
|
|
+ register.addHeader(auth);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ ClientTransaction ct = sipProvider.getNewClientTransaction(register);
|
|
|
+ ct.sendRequest();
|
|
|
+ log.debug("发送 REGISTER{} -> {}:{}", withAuth ? "(鉴权)" : "", properties.getServerHost(), properties.getServerPort());
|
|
|
+ }
|
|
|
+
|
|
|
+ private void handleIncomingInvite(RequestEvent event) throws Exception {
|
|
|
+ Request request = event.getRequest();
|
|
|
+ PendingStream session = pendingStream.get();
|
|
|
+ String rtspUrl = session != null ? session.rtspUrl : boundRtspUrl.get();
|
|
|
+ if (!StringUtils.hasText(rtspUrl)) {
|
|
|
+ sendResponse(event, Response.BUSY_HERE);
|
|
|
+ log.warn("收到平台 INVITE 但未绑定 RTSP,请先调用 POST /api/sip/push?rtspUrl=...");
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ String sdpBody = extractSdp(request);
|
|
|
+ if (!StringUtils.hasText(sdpBody)) {
|
|
|
+ sendResponse(event, Response.BAD_REQUEST);
|
|
|
+ failPending(fail("平台 INVITE 无 SDP"));
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ log.info("平台 INVITE SDP:\n{}", sdpBody);
|
|
|
+ SdpMediaInfo media = SdpUtils.parseVideoMedia(sdpBody);
|
|
|
+ if (media == null) {
|
|
|
+ sendResponse(event, Response.NOT_ACCEPTABLE);
|
|
|
+ log.warn("平台 INVITE SDP 解析失败,无法识别 video m= 行");
|
|
|
+ failPending(fail("平台 INVITE SDP 无效"));
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (session == null) {
|
|
|
+ session = new PendingStream(rtspUrl, properties.getDefaultDurationSeconds());
|
|
|
+ pendingStream.set(session);
|
|
|
+ }
|
|
|
+
|
|
|
+ int localRtpPort = findAvailablePort();
|
|
|
+ String remoteHost = resolveRemoteRtpHost(media, event);
|
|
|
+
|
|
|
+ String answerSdp = SdpUtils.buildGb28181AnswerSdp(
|
|
|
+ localIp, localRtpPort, media, activeDeviceId, GB28181_SSRC);
|
|
|
+
|
|
|
+ log.info("应答 SDP:\n{}", answerSdp);
|
|
|
+ Response ok = messageFactory.createResponse(Response.OK, request);
|
|
|
+ ToHeader toHeader = (ToHeader) ok.getHeader(ToHeader.NAME);
|
|
|
+ toHeader.setTag(generateTag());
|
|
|
+ ok.addHeader(createContactHeader());
|
|
|
+ Header subjectHeader = request.getHeader("Subject");
|
|
|
+ if (subjectHeader != null) {
|
|
|
+ ok.addHeader(subjectHeader);
|
|
|
+ }
|
|
|
+ ok.setContent(answerSdp, headerFactory.createContentTypeHeader("application", "sdp"));
|
|
|
+
|
|
|
+ ServerTransaction st = event.getServerTransaction();
|
|
|
+ if (st == null) {
|
|
|
+ st = sipProvider.getNewServerTransaction(request);
|
|
|
+ }
|
|
|
+ st.sendResponse(ok);
|
|
|
+
|
|
|
+ CallIdHeader callIdHeader = (CallIdHeader) request.getHeader(CallIdHeader.NAME);
|
|
|
+ session.callId = callIdHeader != null ? callIdHeader.getCallId() : null;
|
|
|
+ session.localRtpPort = localRtpPort;
|
|
|
+ session.remoteRtpHost = remoteHost;
|
|
|
+ session.remoteRtpPort = media.getPort();
|
|
|
+ if (!StringUtils.hasText(media.getSsrc())) {
|
|
|
+ media.setSsrc(GB28181_SSRC);
|
|
|
+ }
|
|
|
+ session.media = media;
|
|
|
+ session.rtspUrl = rtspUrl;
|
|
|
+
|
|
|
+ log.info("已应答平台 INVITE 200 OK: rtp={}:{}, protocol={}, codec={}, setup={}",
|
|
|
+ remoteHost, media.getPort(), media.getMediaProtocol(), media.getCodec(), media.getSetup());
|
|
|
+ }
|
|
|
+
|
|
|
+ private String extractSdp(Request request) {
|
|
|
+ byte[] raw = request.getRawContent();
|
|
|
+ if (raw != null && raw.length > 0) {
|
|
|
+ return new String(raw, StandardCharsets.UTF_8);
|
|
|
+ }
|
|
|
+ Object content = request.getContent();
|
|
|
+ if (content instanceof byte[]) {
|
|
|
+ return new String((byte[]) content, StandardCharsets.UTF_8);
|
|
|
+ }
|
|
|
+ return content != null ? content.toString() : null;
|
|
|
+ }
|
|
|
+
|
|
|
+ private String resolveRemoteRtpHost(SdpMediaInfo media, RequestEvent event) {
|
|
|
+ String host = media.getConnectionAddress();
|
|
|
+ if (host != null && !"0.0.0.0".equals(host)) {
|
|
|
+ return host;
|
|
|
+ }
|
|
|
+ if (event instanceof RequestEventExt) {
|
|
|
+ RequestEventExt ext = (RequestEventExt) event;
|
|
|
+ if (StringUtils.hasText(ext.getRemoteIpAddress())) {
|
|
|
+ return ext.getRemoteIpAddress();
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return properties.getServerHost();
|
|
|
+ }
|
|
|
+
|
|
|
+ private void handleIncomingAck(RequestEvent event) {
|
|
|
+ PendingStream session = pendingStream.get();
|
|
|
+ if (session == null || session.streamStarted) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ session.streamStarted = true;
|
|
|
+ session.inviteReady.countDown();
|
|
|
+ log.info("收到平台 ACK,开始 RTSP 推流: {}", maskUrl(session.rtspUrl));
|
|
|
+
|
|
|
+ streamingTask = scheduler.submit(() -> {
|
|
|
+ try {
|
|
|
+ long packets = RtspGb28181RtpSender.stream(
|
|
|
+ session.rtspUrl,
|
|
|
+ session.media,
|
|
|
+ session.remoteRtpHost,
|
|
|
+ session.remoteRtpPort,
|
|
|
+ session.localRtpPort,
|
|
|
+ session.durationSeconds,
|
|
|
+ properties.getRtspTransport());
|
|
|
+ session.result = Gb28181StreamResult.builder()
|
|
|
+ .success(packets > 0)
|
|
|
+ .rtspUrl(session.rtspUrl)
|
|
|
+ .callId(session.callId)
|
|
|
+ .localRtpPort(session.localRtpPort)
|
|
|
+ .remoteRtpHost(session.remoteRtpHost)
|
|
|
+ .remoteRtpPort(session.remoteRtpPort)
|
|
|
+ .packetsSent(packets)
|
|
|
+ .errors(packets > 0 ? new ArrayList<>() : singleError(
|
|
|
+ "未发出 RTP 包,请检查 RTSP 地址/网络或是否收到 IDR 关键帧"))
|
|
|
+ .build();
|
|
|
+ if (packets > 0) {
|
|
|
+ log.info("推流完成: packets={}", packets);
|
|
|
+ } else {
|
|
|
+ log.warn("推流结束但未发送 RTP 包");
|
|
|
+ }
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("推流失败: {}", e.getMessage(), e);
|
|
|
+ session.result = fail("推流失败: " + e.getMessage());
|
|
|
+ } finally {
|
|
|
+ session.streamDone.countDown();
|
|
|
+ pendingStream.compareAndSet(session, null);
|
|
|
+ }
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ private void handleIncomingBye(RequestEvent event) throws Exception {
|
|
|
+ log.info("收到平台 BYE");
|
|
|
+ if (streamingTask != null) {
|
|
|
+ streamingTask.cancel(true);
|
|
|
+ }
|
|
|
+ sendResponse(event, Response.OK);
|
|
|
+ PendingStream session = pendingStream.getAndSet(null);
|
|
|
+ if (session != null) {
|
|
|
+ session.inviteReady.countDown();
|
|
|
+ if (session.result == null) {
|
|
|
+ session.result = fail("平台 BYE 结束会话");
|
|
|
+ }
|
|
|
+ session.streamDone.countDown();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private Request buildRequest(String method, SipURI requestUri, String fromUser, String fromTag) throws Exception {
|
|
|
+ Address toAddress = addressFactory.createAddress(requestUri);
|
|
|
+ ToHeader toHeader = headerFactory.createToHeader(toAddress, null);
|
|
|
+
|
|
|
+ SipURI fromUri = createUri(fromUser, properties.getDomain());
|
|
|
+ fromUri.setPort(localSipPort);
|
|
|
+ fromUri.setTransportParam(sipTransport);
|
|
|
+ FromHeader fromHeader = headerFactory.createFromHeader(addressFactory.createAddress(fromUri), fromTag);
|
|
|
+
|
|
|
+ ContactHeader contactHeader = createContactHeader();
|
|
|
+
|
|
|
+ List<ViaHeader> viaHeaders = new ArrayList<>();
|
|
|
+ viaHeaders.add(headerFactory.createViaHeader(localIp, localSipPort, sipTransport, null));
|
|
|
+
|
|
|
+ CSeqHeader cSeqHeader = headerFactory.createCSeqHeader(cseq++, method);
|
|
|
+ Request request = messageFactory.createRequest(requestUri, method, sipProvider.getNewCallId(),
|
|
|
+ cSeqHeader, fromHeader, toHeader, viaHeaders, headerFactory.createMaxForwardsHeader(70));
|
|
|
+ request.addHeader(contactHeader);
|
|
|
+ return request;
|
|
|
+ }
|
|
|
+
|
|
|
+ private ContactHeader createContactHeader() throws Exception {
|
|
|
+ ensureActiveDeviceId();
|
|
|
+ SipURI contactUri = createUri(activeDeviceId, properties.getDomain());
|
|
|
+ contactUri.setHost(localIp);
|
|
|
+ contactUri.setPort(localSipPort);
|
|
|
+ contactUri.setTransportParam(sipTransport);
|
|
|
+ return headerFactory.createContactHeader(addressFactory.createAddress(contactUri));
|
|
|
+ }
|
|
|
+
|
|
|
+ private AuthorizationHeader buildAuthorization(String method, String uri) throws Exception {
|
|
|
+ WWWAuthenticateHeader challenge = authChallenge.get();
|
|
|
+ if (challenge == null) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ String cnonce = UUID.randomUUID().toString().replace("-", "").substring(0, 8);
|
|
|
+ String nc = "00000001";
|
|
|
+ String response = SipDigestAuth.computeResponse(
|
|
|
+ activeDeviceId, properties.getPassword(), challenge.getRealm(),
|
|
|
+ method, uri, challenge.getNonce(), nc, cnonce, challenge.getQop());
|
|
|
+
|
|
|
+ AuthorizationHeader auth = headerFactory.createAuthorizationHeader("Digest");
|
|
|
+ auth.setUsername(activeDeviceId);
|
|
|
+ auth.setRealm(challenge.getRealm());
|
|
|
+ auth.setNonce(challenge.getNonce());
|
|
|
+ auth.setURI(addressFactory.createURI(uri));
|
|
|
+ auth.setResponse(response);
|
|
|
+ auth.setAlgorithm("MD5");
|
|
|
+ if (challenge.getQop() != null && !challenge.getQop().isEmpty()) {
|
|
|
+ auth.setQop(challenge.getQop());
|
|
|
+ auth.setCNonce(cnonce);
|
|
|
+ auth.setNonceCount(Integer.parseInt(nc, 16));
|
|
|
+ }
|
|
|
+ return auth;
|
|
|
+ }
|
|
|
+
|
|
|
+ private void sendResponse(RequestEvent event, int code) throws Exception {
|
|
|
+ Response response = messageFactory.createResponse(code, event.getRequest());
|
|
|
+ if (code >= 200) {
|
|
|
+ ToHeader toHeader = (ToHeader) response.getHeader(ToHeader.NAME);
|
|
|
+ if (toHeader.getTag() == null) {
|
|
|
+ toHeader.setTag(generateTag());
|
|
|
+ }
|
|
|
+ }
|
|
|
+ ServerTransaction st = event.getServerTransaction();
|
|
|
+ if (st == null) {
|
|
|
+ st = sipProvider.getNewServerTransaction(event.getRequest());
|
|
|
+ }
|
|
|
+ st.sendResponse(response);
|
|
|
+ }
|
|
|
+
|
|
|
+ private SipURI createUri(String user, String domain) throws Exception {
|
|
|
+ return addressFactory.createSipURI(user, domain);
|
|
|
+ }
|
|
|
+
|
|
|
+ private void failPending(Gb28181StreamResult result) {
|
|
|
+ PendingStream session = pendingStream.getAndSet(null);
|
|
|
+ if (session != null) {
|
|
|
+ session.result = result;
|
|
|
+ session.inviteReady.countDown();
|
|
|
+ session.streamDone.countDown();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private static Gb28181StreamResult fail(String msg) {
|
|
|
+ List<String> errors = new ArrayList<>();
|
|
|
+ errors.add(msg);
|
|
|
+ return Gb28181StreamResult.builder().success(false).errors(errors).build();
|
|
|
+ }
|
|
|
+
|
|
|
+ private static List<String> singleError(String msg) {
|
|
|
+ List<String> errors = new ArrayList<>();
|
|
|
+ errors.add(msg);
|
|
|
+ return errors;
|
|
|
+ }
|
|
|
+
|
|
|
+ private static String maskUrl(String url) {
|
|
|
+ return url.replaceAll("://([^:]+):([^@]+)@", "://***:***@");
|
|
|
+ }
|
|
|
+
|
|
|
+ private static String normalizeTransport(String t) {
|
|
|
+ return t != null && t.equalsIgnoreCase("udp") ? "udp" : "tcp";
|
|
|
+ }
|
|
|
+
|
|
|
+ private static int findAvailablePort() throws Exception {
|
|
|
+ try (java.net.DatagramSocket socket = new java.net.DatagramSocket(0)) {
|
|
|
+ int port = socket.getLocalPort();
|
|
|
+ return port % 2 == 0 ? port : port + 1;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private static String generateTag() {
|
|
|
+ return Long.toHexString(System.nanoTime());
|
|
|
+ }
|
|
|
+
|
|
|
+ private void ensureActiveDeviceId() {
|
|
|
+ if (!StringUtils.hasText(activeDeviceId)) {
|
|
|
+ throw new IllegalStateException("设备 ID 未设置,请先注册");
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private static void validateDeviceId(String deviceId) {
|
|
|
+ if (!StringUtils.hasText(deviceId)) {
|
|
|
+ throw new IllegalArgumentException("deviceId 不能为空");
|
|
|
+ }
|
|
|
+ String id = deviceId.trim();
|
|
|
+ if (id.length() != 20 || !id.chars().allMatch(Character::isDigit)) {
|
|
|
+ throw new IllegalArgumentException("deviceId 必须为 20 位数字");
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private void shutdownStack() {
|
|
|
+ try {
|
|
|
+ if (sipProvider != null) {
|
|
|
+ sipProvider.removeSipListener(this);
|
|
|
+ }
|
|
|
+ } catch (Exception ignored) {
|
|
|
+ }
|
|
|
+ if (sipStack != null) {
|
|
|
+ try {
|
|
|
+ ((SipStackImpl) sipStack).stop();
|
|
|
+ } catch (Exception ignored) {
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public void processResponse(ResponseEvent event) {
|
|
|
+ ClientTransaction ct = event.getClientTransaction();
|
|
|
+ if (ct == null || !Request.REGISTER.equals(ct.getRequest().getMethod())) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ int status = event.getResponse().getStatusCode();
|
|
|
+ if (status == Response.UNAUTHORIZED || status == Response.PROXY_AUTHENTICATION_REQUIRED) {
|
|
|
+ WWWAuthenticateHeader challenge = (WWWAuthenticateHeader) event.getResponse().getHeader(WWWAuthenticateHeader.NAME);
|
|
|
+ if (challenge == null) {
|
|
|
+ challenge = (WWWAuthenticateHeader) event.getResponse().getHeader(ProxyAuthenticateHeader.NAME);
|
|
|
+ }
|
|
|
+ authChallenge.set(challenge);
|
|
|
+ if (!registerAuthSent.getAndSet(true)) {
|
|
|
+ try {
|
|
|
+ ExpiresHeader expires = (ExpiresHeader) ct.getRequest().getHeader(ExpiresHeader.NAME);
|
|
|
+ int expireSeconds = expires != null ? expires.getExpires() : properties.getRegisterExpires();
|
|
|
+ sendRegister(true, expireSeconds);
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("REGISTER 鉴权失败: {}", e.getMessage());
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } else if (status == Response.OK) {
|
|
|
+ ExpiresHeader expires = (ExpiresHeader) ct.getRequest().getHeader(ExpiresHeader.NAME);
|
|
|
+ if (expires != null && expires.getExpires() == 0) {
|
|
|
+ registered.set(false);
|
|
|
+ log.info("GB28181 注销成功: deviceId={}", activeDeviceId);
|
|
|
+ } else {
|
|
|
+ registered.set(true);
|
|
|
+ log.info("GB28181 注册成功: deviceId={}(平台应可见设备在线)", activeDeviceId);
|
|
|
+ }
|
|
|
+ } else if (status >= 300) {
|
|
|
+ registered.set(false);
|
|
|
+ log.warn("GB28181 注册失败: deviceId={}, status={}", activeDeviceId, status);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public void processRequest(RequestEvent event) {
|
|
|
+ String method = event.getRequest().getMethod();
|
|
|
+ log.info("收到 SIP 请求: {} from {}", method, formatRemote(event));
|
|
|
+ try {
|
|
|
+ if (Request.INVITE.equals(method)) {
|
|
|
+ handleIncomingInvite(event);
|
|
|
+ } else if (Request.ACK.equals(method)) {
|
|
|
+ handleIncomingAck(event);
|
|
|
+ } else if (Request.BYE.equals(method)) {
|
|
|
+ handleIncomingBye(event);
|
|
|
+ }
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("处理 {} 失败: {}", method, e.getMessage(), e);
|
|
|
+ failPending(fail("处理 " + method + " 失败: " + e.getMessage()));
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private String formatRemote(RequestEvent event) {
|
|
|
+ if (event instanceof RequestEventExt) {
|
|
|
+ RequestEventExt ext = (RequestEventExt) event;
|
|
|
+ return ext.getRemoteIpAddress() + ":" + ext.getRemotePort();
|
|
|
+ }
|
|
|
+ return properties.getServerHost();
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public void processTimeout(TimeoutEvent timeoutEvent) {
|
|
|
+ log.warn("SIP 超时");
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public void processIOException(IOExceptionEvent exceptionEvent) {
|
|
|
+ log.warn("SIP IO 异常: {}:{}", exceptionEvent.getHost(), exceptionEvent.getPort());
|
|
|
+ registered.set(false);
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public void processTransactionTerminated(TransactionTerminatedEvent event) {
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public void processDialogTerminated(DialogTerminatedEvent event) {
|
|
|
+ }
|
|
|
+
|
|
|
+ private static final class PendingStream {
|
|
|
+ private String rtspUrl;
|
|
|
+ private final int durationSeconds;
|
|
|
+ private final CountDownLatch inviteReady = new CountDownLatch(1);
|
|
|
+ private final CountDownLatch streamDone = new CountDownLatch(1);
|
|
|
+ private volatile boolean streamStarted;
|
|
|
+ private volatile Gb28181StreamResult result;
|
|
|
+ private String callId;
|
|
|
+ private int localRtpPort;
|
|
|
+ private String remoteRtpHost;
|
|
|
+ private int remoteRtpPort;
|
|
|
+ private SdpMediaInfo media;
|
|
|
+
|
|
|
+ private PendingStream(String rtspUrl, int durationSeconds) {
|
|
|
+ this.rtspUrl = rtspUrl;
|
|
|
+ this.durationSeconds = durationSeconds;
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|