|
|
@@ -0,0 +1,332 @@
|
|
|
+package com.usky.cdi.service.sip.push;
|
|
|
+
|
|
|
+import com.usky.cdi.service.sip.model.SdpMediaInfo;
|
|
|
+import com.usky.cdi.service.sip.sdp.SdpUtils;
|
|
|
+import com.usky.cdi.service.sip.util.NetworkUtils;
|
|
|
+import gov.nist.javax.sip.SipStackImpl;
|
|
|
+import lombok.extern.slf4j.Slf4j;
|
|
|
+
|
|
|
+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.net.DatagramPacket;
|
|
|
+import java.net.DatagramSocket;
|
|
|
+import java.net.InetAddress;
|
|
|
+import java.nio.charset.StandardCharsets;
|
|
|
+import java.util.ArrayList;
|
|
|
+import java.util.List;
|
|
|
+import java.util.concurrent.CountDownLatch;
|
|
|
+import java.util.concurrent.TimeUnit;
|
|
|
+import java.util.concurrent.atomic.AtomicReference;
|
|
|
+
|
|
|
+/**
|
|
|
+ * SIP 客户端:向服务端发送 INVITE 并推送 H264 RTP 测试流
|
|
|
+ */
|
|
|
+@Slf4j
|
|
|
+public class SipVideoPusher implements SipListener {
|
|
|
+
|
|
|
+ private static final int PAYLOAD_TYPE = 96;
|
|
|
+ private static final long SSRC = 0x12345678L;
|
|
|
+
|
|
|
+ private final String serverHost;
|
|
|
+ private final int serverPort;
|
|
|
+ private final String deviceId;
|
|
|
+ private final String platformId;
|
|
|
+ private final int durationSeconds;
|
|
|
+ private final String forcedLocalIp;
|
|
|
+
|
|
|
+ private SipStack sipStack;
|
|
|
+ private SipProvider sipProvider;
|
|
|
+ private AddressFactory addressFactory;
|
|
|
+ private MessageFactory messageFactory;
|
|
|
+ private HeaderFactory headerFactory;
|
|
|
+
|
|
|
+ private final AtomicReference<String> remoteRtpHost = new AtomicReference<>();
|
|
|
+ private final AtomicReference<Integer> remoteRtpPort = new AtomicReference<>();
|
|
|
+ private final AtomicReference<Dialog> dialogRef = new AtomicReference<>();
|
|
|
+ private final AtomicReference<Long> inviteOkCseq = new AtomicReference<>();
|
|
|
+ private final CountDownLatch inviteLatch = new CountDownLatch(1);
|
|
|
+ private final List<String> errors = new ArrayList<>();
|
|
|
+
|
|
|
+ private int localRtpPort;
|
|
|
+ private String localIp;
|
|
|
+ private String callId;
|
|
|
+ private int cseq = 1;
|
|
|
+ private long packetsSent;
|
|
|
+
|
|
|
+ public SipVideoPusher(String serverHost, int serverPort, String deviceId,
|
|
|
+ String platformId, int durationSeconds) {
|
|
|
+ this(serverHost, serverPort, deviceId, platformId, durationSeconds, null);
|
|
|
+ }
|
|
|
+
|
|
|
+ public SipVideoPusher(String serverHost, int serverPort, String deviceId,
|
|
|
+ String platformId, int durationSeconds, String forcedLocalIp) {
|
|
|
+ this.serverHost = serverHost;
|
|
|
+ this.serverPort = serverPort;
|
|
|
+ this.deviceId = deviceId;
|
|
|
+ this.platformId = platformId;
|
|
|
+ this.durationSeconds = durationSeconds;
|
|
|
+ this.forcedLocalIp = forcedLocalIp;
|
|
|
+ }
|
|
|
+
|
|
|
+ public PushResult push() {
|
|
|
+ try {
|
|
|
+ initStack();
|
|
|
+ localIp = forcedLocalIp != null ? forcedLocalIp : NetworkUtils.resolveLocalIp(null);
|
|
|
+ localRtpPort = findAvailablePort();
|
|
|
+ sendInvite();
|
|
|
+ if (!inviteLatch.await(10, TimeUnit.SECONDS)) {
|
|
|
+ errors.add("等待 INVITE 响应超时");
|
|
|
+ return buildResult(false);
|
|
|
+ }
|
|
|
+ if (remoteRtpPort.get() == null) {
|
|
|
+ errors.add("未从 SDP 应答中解析到服务端 RTP 端口");
|
|
|
+ return buildResult(false);
|
|
|
+ }
|
|
|
+ sendAck();
|
|
|
+ packetsSent = sendRtpStream(durationSeconds);
|
|
|
+ sendBye();
|
|
|
+ return buildResult(true);
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("推流测试失败: {}", e.getMessage(), e);
|
|
|
+ errors.add(e.getMessage());
|
|
|
+ return buildResult(false);
|
|
|
+ } finally {
|
|
|
+ shutdown();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private void initStack() throws Exception {
|
|
|
+ SipFactory sipFactory = SipFactory.getInstance();
|
|
|
+ sipFactory.setPathName("gov.nist");
|
|
|
+ java.util.Properties props = new java.util.Properties();
|
|
|
+ props.setProperty("javax.sip.STACK_NAME", "UskySipVideoPusher");
|
|
|
+ props.setProperty("gov.nist.javax.sip.TRACE_LEVEL", "0");
|
|
|
+ sipStack = sipFactory.createSipStack(props);
|
|
|
+ addressFactory = sipFactory.createAddressFactory();
|
|
|
+ messageFactory = sipFactory.createMessageFactory();
|
|
|
+ headerFactory = sipFactory.createHeaderFactory();
|
|
|
+
|
|
|
+ localIp = forcedLocalIp != null ? forcedLocalIp : NetworkUtils.resolveLocalIp(null);
|
|
|
+ int sipLocalPort = findAvailablePort();
|
|
|
+ ListeningPoint lp = sipStack.createListeningPoint(localIp, sipLocalPort, "udp");
|
|
|
+ sipProvider = sipStack.createSipProvider(lp);
|
|
|
+ sipProvider.addSipListener(this);
|
|
|
+ }
|
|
|
+
|
|
|
+ private void sendInvite() throws Exception {
|
|
|
+ String sdp = SdpUtils.buildInviteSdp(localIp, localRtpPort, PAYLOAD_TYPE, deviceId);
|
|
|
+ SipURI requestUri = addressFactory.createSipURI(platformId, serverHost);
|
|
|
+ requestUri.setPort(serverPort);
|
|
|
+ Address toAddress = addressFactory.createAddress(requestUri);
|
|
|
+ ToHeader toHeader = headerFactory.createToHeader(toAddress, null);
|
|
|
+
|
|
|
+ SipURI fromUri = addressFactory.createSipURI(deviceId, localIp);
|
|
|
+ fromUri.setPort(sipProvider.getListeningPoint("udp").getPort());
|
|
|
+ Address fromAddress = addressFactory.createAddress(fromUri);
|
|
|
+ FromHeader fromHeader = headerFactory.createFromHeader(fromAddress, "push-tag");
|
|
|
+
|
|
|
+ SipURI contactUri = addressFactory.createSipURI(deviceId, localIp);
|
|
|
+ contactUri.setPort(sipProvider.getListeningPoint("udp").getPort());
|
|
|
+ Address contactAddress = addressFactory.createAddress(contactUri);
|
|
|
+ ContactHeader contactHeader = headerFactory.createContactHeader(contactAddress);
|
|
|
+
|
|
|
+ List<ViaHeader> viaHeaders = new ArrayList<>();
|
|
|
+ ViaHeader viaHeader = headerFactory.createViaHeader(localIp,
|
|
|
+ sipProvider.getListeningPoint("udp").getPort(), "udp", null);
|
|
|
+ viaHeaders.add(viaHeader);
|
|
|
+
|
|
|
+ CallIdHeader callIdHeader = sipProvider.getNewCallId();
|
|
|
+ callId = callIdHeader.getCallId();
|
|
|
+ CSeqHeader cSeqHeader = headerFactory.createCSeqHeader(cseq++, Request.INVITE);
|
|
|
+ MaxForwardsHeader maxForwards = headerFactory.createMaxForwardsHeader(70);
|
|
|
+ ContentTypeHeader contentType = headerFactory.createContentTypeHeader("application", "sdp");
|
|
|
+
|
|
|
+ Request invite = messageFactory.createRequest(requestUri, Request.INVITE, callIdHeader,
|
|
|
+ cSeqHeader, fromHeader, toHeader, viaHeaders, maxForwards);
|
|
|
+ invite.addHeader(contactHeader);
|
|
|
+ invite.setContent(sdp, contentType);
|
|
|
+
|
|
|
+ ClientTransaction ct = sipProvider.getNewClientTransaction(invite);
|
|
|
+ ct.sendRequest();
|
|
|
+ log.info("已发送 INVITE: {}:{} deviceId={} localRtpPort={}", serverHost, serverPort, deviceId, localRtpPort);
|
|
|
+ }
|
|
|
+
|
|
|
+ private void sendAck() throws Exception {
|
|
|
+ Dialog dialog = dialogRef.get();
|
|
|
+ if (dialog == null) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ long ackCseq = inviteOkCseq.get() != null ? inviteOkCseq.get() : 1L;
|
|
|
+ Request ack = dialog.createAck(ackCseq);
|
|
|
+ dialog.sendAck(ack);
|
|
|
+ log.info("已发送 ACK");
|
|
|
+ }
|
|
|
+
|
|
|
+ private void sendBye() throws Exception {
|
|
|
+ Dialog dialog = dialogRef.get();
|
|
|
+ if (dialog == null) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ Request bye = dialog.createRequest(Request.BYE);
|
|
|
+ CSeqHeader cSeqHeader = (CSeqHeader) bye.getHeader(CSeqHeader.NAME);
|
|
|
+ cSeqHeader.setSeqNumber(cseq++);
|
|
|
+ ClientTransaction ct = sipProvider.getNewClientTransaction(bye);
|
|
|
+ dialog.sendRequest(ct);
|
|
|
+ Thread.sleep(500);
|
|
|
+ log.info("已发送 BYE");
|
|
|
+ }
|
|
|
+
|
|
|
+ private long sendRtpStream(int seconds) throws Exception {
|
|
|
+ String host = remoteRtpHost.get();
|
|
|
+ int port = remoteRtpPort.get();
|
|
|
+ InetAddress address = InetAddress.getByName(host);
|
|
|
+ DatagramSocket socket = new DatagramSocket();
|
|
|
+ int seq = 0;
|
|
|
+ long timestamp = 0;
|
|
|
+ long count = 0;
|
|
|
+ long end = System.currentTimeMillis() + seconds * 1000L;
|
|
|
+ byte[] nalPayload = buildTestH264Nal();
|
|
|
+
|
|
|
+ while (System.currentTimeMillis() < end) {
|
|
|
+ byte[] rtp = buildRtpPacket(nalPayload, PAYLOAD_TYPE, seq++, timestamp, true);
|
|
|
+ DatagramPacket packet = new DatagramPacket(rtp, rtp.length, address, port);
|
|
|
+ socket.send(packet);
|
|
|
+ count++;
|
|
|
+ timestamp += 3000;
|
|
|
+ Thread.sleep(33);
|
|
|
+ }
|
|
|
+ socket.close();
|
|
|
+ log.info("RTP 推流完成: target={}:{}, packets={}", host, port, count);
|
|
|
+ return count;
|
|
|
+ }
|
|
|
+
|
|
|
+ private byte[] buildTestH264Nal() {
|
|
|
+ // 最小 SPS NAL (type 7)
|
|
|
+ return new byte[]{(byte) 0x67, (byte) 0x42, (byte) 0x00, (byte) 0x0A, (byte) 0xF8, (byte) 0x3C};
|
|
|
+ }
|
|
|
+
|
|
|
+ private byte[] buildRtpPacket(byte[] payload, int pt, int seq, long timestamp, boolean marker) {
|
|
|
+ byte[] packet = new byte[12 + payload.length];
|
|
|
+ packet[0] = (byte) 0x80;
|
|
|
+ packet[1] = (byte) ((marker ? 0x80 : 0) | (pt & 0x7F));
|
|
|
+ packet[2] = (byte) (seq >> 8);
|
|
|
+ packet[3] = (byte) (seq & 0xFF);
|
|
|
+ packet[4] = (byte) (timestamp >> 24);
|
|
|
+ packet[5] = (byte) (timestamp >> 16);
|
|
|
+ packet[6] = (byte) (timestamp >> 8);
|
|
|
+ packet[7] = (byte) (timestamp);
|
|
|
+ packet[8] = (byte) (SSRC >> 24);
|
|
|
+ packet[9] = (byte) (SSRC >> 16);
|
|
|
+ packet[10] = (byte) (SSRC >> 8);
|
|
|
+ packet[11] = (byte) (SSRC);
|
|
|
+ System.arraycopy(payload, 0, packet, 12, payload.length);
|
|
|
+ return packet;
|
|
|
+ }
|
|
|
+
|
|
|
+ private int findAvailablePort() throws Exception {
|
|
|
+ try (DatagramSocket socket = new DatagramSocket(0)) {
|
|
|
+ int port = socket.getLocalPort();
|
|
|
+ return port % 2 == 0 ? port : port + 1;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private void shutdown() {
|
|
|
+ if (sipStack != null) {
|
|
|
+ ((SipStackImpl) sipStack).stop();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private PushResult buildResult(boolean success) {
|
|
|
+ PushResult result = new PushResult();
|
|
|
+ result.setSuccess(success);
|
|
|
+ result.setCallId(callId);
|
|
|
+ result.setLocalRtpPort(localRtpPort);
|
|
|
+ result.setRemoteRtpHost(remoteRtpHost.get());
|
|
|
+ result.setRemoteRtpPort(remoteRtpPort.get());
|
|
|
+ result.setPacketsSent(packetsSent);
|
|
|
+ result.setErrors(errors);
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public void processResponse(ResponseEvent responseEvent) {
|
|
|
+ Response response = responseEvent.getResponse();
|
|
|
+ int status = response.getStatusCode();
|
|
|
+ log.info("收到 SIP 响应: {}", status);
|
|
|
+ if (status == Response.TRYING || status == Response.RINGING) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ if (status == Response.OK && responseEvent.getClientTransaction() != null
|
|
|
+ && Request.INVITE.equals(responseEvent.getClientTransaction().getRequest().getMethod())) {
|
|
|
+ try {
|
|
|
+ Dialog dialog = responseEvent.getDialog();
|
|
|
+ dialogRef.set(dialog);
|
|
|
+ CSeqHeader cSeqHeader = (CSeqHeader) response.getHeader(CSeqHeader.NAME);
|
|
|
+ if (cSeqHeader != null) {
|
|
|
+ inviteOkCseq.set(cSeqHeader.getSeqNumber());
|
|
|
+ }
|
|
|
+ byte[] raw = response.getRawContent();
|
|
|
+ if (raw != null) {
|
|
|
+ String sdp = new String(raw, StandardCharsets.UTF_8);
|
|
|
+ SdpMediaInfo media = SdpUtils.parseVideoMedia(sdp);
|
|
|
+ if (media != null) {
|
|
|
+ String host = media.getConnectionAddress();
|
|
|
+ if (host == null) {
|
|
|
+ host = serverHost;
|
|
|
+ }
|
|
|
+ remoteRtpHost.set(host);
|
|
|
+ remoteRtpPort.set(media.getPort());
|
|
|
+ log.info("解析 SDP 应答: rtp={}:{}", host, media.getPort());
|
|
|
+ }
|
|
|
+ }
|
|
|
+ inviteLatch.countDown();
|
|
|
+ } catch (Exception e) {
|
|
|
+ errors.add("处理 200 OK 失败: " + e.getMessage());
|
|
|
+ inviteLatch.countDown();
|
|
|
+ }
|
|
|
+ } else if (status >= 300) {
|
|
|
+ errors.add("INVITE 失败,状态码: " + status);
|
|
|
+ inviteLatch.countDown();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public void processRequest(RequestEvent requestEvent) {
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public void processTimeout(TimeoutEvent timeoutEvent) {
|
|
|
+ errors.add("SIP 超时");
|
|
|
+ inviteLatch.countDown();
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public void processIOException(IOExceptionEvent exceptionEvent) {
|
|
|
+ errors.add("SIP IO 异常");
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public void processTransactionTerminated(TransactionTerminatedEvent event) {
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public void processDialogTerminated(DialogTerminatedEvent event) {
|
|
|
+ }
|
|
|
+
|
|
|
+ @lombok.Data
|
|
|
+ public static class PushResult {
|
|
|
+ private boolean success;
|
|
|
+ private String callId;
|
|
|
+ private int localRtpPort;
|
|
|
+ private String remoteRtpHost;
|
|
|
+ private Integer remoteRtpPort;
|
|
|
+ private long packetsSent;
|
|
|
+ private List<String> errors = new ArrayList<>();
|
|
|
+ }
|
|
|
+}
|