Sfoglia il codice sorgente

生成onvif摄像头SDK配置

james 14 ore fa
parent
commit
e6d6c0c9b0

+ 6 - 0
service-sas/service-sas-biz/pom.xml

@@ -86,6 +86,12 @@
             <artifactId>jna-platform</artifactId>
             <version>5.13.0</version>
         </dependency>
+        <dependency>
+            <groupId>com.alibaba.fastjson2</groupId>
+            <artifactId>fastjson2</artifactId>
+            <!-- 建议使用最新稳定版,可到Maven中央仓库确认最新版本号 -->
+            <version>2.0.54</version>
+        </dependency>
 
     </dependencies>
 

+ 0 - 0
service-sas/service-sas-biz/src/main/java/com/usky/sas/common/onvif


+ 148 - 0
service-sas/service-sas-biz/src/main/java/com/usky/sas/common/onvif/OnvifService.java

@@ -0,0 +1,148 @@
+package com.usky.sas.common.onvif;
+
+import com.baomidou.mybatisplus.core.conditions.Wrapper;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.usky.sas.common.exception.BusinessException;
+import com.usky.sas.common.util.GetIpUtils;
+import com.fjkj.agcp.entity.Device;
+import com.fjkj.agcp.entity.info.SearchDeviceInfo;
+import com.fjkj.agcp.mapper.DeviceMapper;
+import java.io.StringReader;
+import java.net.DatagramPacket;
+import java.net.Inet4Address;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.MulticastSocket;
+import java.net.NetworkInterface;
+import java.net.SocketTimeoutException;
+import java.net.URL;
+import java.net.URLDecoder;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.UUID;
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.stereotype.Service;
+import org.w3c.dom.Document;
+import org.w3c.dom.NodeList;
+import org.xml.sax.InputSource;
+
+@Service
+public class OnvifService {
+    private static final Logger log = LoggerFactory.getLogger(OnvifService.class);
+    private static final String DISCOVERY_MULTICAST_ADDRESS = "239.255.255.250";
+    private static final int DISCOVERY_PORT = 3702;
+
+    public OnvifService() {
+    }
+
+    private static String buildProbeMessage() {
+        return "<?xml version=\"1.0\" encoding=\"UTF-8\"?> \n<e:Envelope xmlns:e=\"http://www.w3.org/2003/05/soap-envelope\" \n xmlns:w=\"http://schemas.xmlsoap.org/ws/2004/08/addressing\" \n xmlns:d=\"http://schemas.xmlsoap.org/ws/2005/04/discovery\" \n xmlns:dn=\"http://www.onvif.org/ver10/network/wsdl\"> \n <e:Header> \n  <w:MessageID>uuid:" + UUID.randomUUID() + "</w:MessageID> \n  <w:To e:mustUnderstand=\"true\">urn:schemas-xmlsoap-org:ws:2005:04:discovery</w:To> \n  <w:Action a:mustUnderstand=\"true\">http://schemas.xmlsoap.org/ws/2005/04/discovery/Probe</w:Action> \n </e:Header> \n <e:Body> \n  <d:Probe> \n   <d:Types>dn:NetworkVideoTransmitter</d:Types> \n  </d:Probe> \n </e:Body> \n</e:Envelope>  ";
+    }
+
+    public static List<SearchDeviceInfo> searchDeviceByOnvif(Integer second, DeviceMapper deviceMapper) {
+        List<SearchDeviceInfo> deviceInfoList = new ArrayList();
+
+        try (MulticastSocket socket = new MulticastSocket(0)) {
+            InetAddress localAddr = InetAddress.getByName(GetIpUtils.getServerIP());
+            NetworkInterface ni = NetworkInterface.getByInetAddress(localAddr);
+            if (ni == null) {
+                throw new BusinessException("未找到与 IP " + GetIpUtils.getServerIP() + " 关联的网络接口", -1);
+            }
+
+            log.info(" 使用网卡: " + ni.getDisplayName() + " (" + ni.getName() + ")");
+            socket.setNetworkInterface(ni);
+            socket.setTimeToLive(4);
+            socket.setSoTimeout(second * 1000);
+            socket.joinGroup(new InetSocketAddress(InetAddress.getByName("239.255.255.250"), 3702), ni);
+            String probeMessage = buildProbeMessage();
+            DatagramPacket packet = new DatagramPacket(probeMessage.getBytes(StandardCharsets.UTF_8), probeMessage.getBytes(StandardCharsets.UTF_8).length, InetAddress.getByName("239.255.255.250"), 3702);
+            socket.send(packet);
+            log.info("已通过 {} 发送 Probe 到 {}:{}", new Object[]{GetIpUtils.getServerIP(), "239.255.255.250", 3702});
+            byte[] buffer = new byte[4096];
+            DatagramPacket response = new DatagramPacket(buffer, buffer.length);
+
+            for(long startTime = System.currentTimeMillis(); System.currentTimeMillis() - startTime < (long)(second * 1000); response = new DatagramPacket(buffer, buffer.length)) {
+                try {
+                    socket.receive(response);
+                    String responseData = new String(response.getData(), 0, response.getLength(), StandardCharsets.UTF_8);
+                    log.info("收到响应来自: " + response.getAddress().getHostAddress());
+                    SearchDeviceInfo searchDeviceInfo = parseProbeMatchResponse(responseData, deviceMapper);
+                    deviceInfoList.add(searchDeviceInfo);
+                } catch (SocketTimeoutException var24) {
+                    break;
+                }
+            }
+        } catch (Exception e) {
+            log.error("设备发现异常", e);
+        }
+
+        return deviceInfoList;
+    }
+
+    private static SearchDeviceInfo parseProbeMatchResponse(String responseData, DeviceMapper deviceMapper) {
+        try {
+            SearchDeviceInfo searchDeviceInfo = new SearchDeviceInfo();
+            DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
+            factory.setNamespaceAware(true);
+            DocumentBuilder builder = factory.newDocumentBuilder();
+            Document doc = builder.parse(new InputSource(new StringReader(responseData)));
+            NodeList xAddrsList = doc.getElementsByTagNameNS("http://schemas.xmlsoap.org/ws/2005/04/discovery", "XAddrs");
+            if (xAddrsList.getLength() > 0) {
+                String xAddrs = xAddrsList.item(0).getTextContent().trim();
+                log.info("原始 XAddrs: " + xAddrs);
+
+                for(String addr : xAddrs.split("\\s+")) {
+                    if (addr.startsWith("http://")) {
+                        try {
+                            URL url = new URL(addr);
+                            InetAddress ip = InetAddress.getByName(url.getHost());
+                            if (ip instanceof Inet4Address) {
+                                searchDeviceInfo.setIpAddr(url.getHost());
+                                searchDeviceInfo.setPort(80);
+                            }
+                        } catch (Exception e) {
+                            log.error("url解析失败:{}", e.getMessage());
+                        }
+                    }
+                }
+            }
+
+            NodeList scopesList = doc.getElementsByTagNameNS("http://schemas.xmlsoap.org/ws/2005/04/discovery", "Scopes");
+            if (scopesList.getLength() > 0) {
+                String scopesText = scopesList.item(0).getTextContent().trim();
+
+                for(String scope : scopesText.split("\\s+")) {
+                    String mac = extractScopeValue(scope, "onvif://www.onvif.org/MAC/");
+                    String hardware = extractScopeValue(scope, "onvif://www.onvif.org/hardware/");
+                    String name = extractScopeValue(scope, "onvif://www.onvif.org/name/");
+                    if (mac != null) {
+                        searchDeviceInfo.setMacAddr(mac);
+                    }
+
+                    if (hardware != null) {
+                        searchDeviceInfo.setNote(hardware);
+                    }
+
+                    if (name != null) {
+                        searchDeviceInfo.setDeviceId(URLDecoder.decode(name, "UTF-8"));
+                    }
+                }
+            }
+
+            Device device = (Device)deviceMapper.selectOne((Wrapper)(new LambdaQueryWrapper()).eq(Device::getMacAddr, searchDeviceInfo.getMacAddr()));
+            searchDeviceInfo.setIsAdded(device != null);
+            return searchDeviceInfo;
+        } catch (Exception e) {
+            log.error("解析响应失败: " + e.getMessage());
+            return null;
+        }
+    }
+
+    private static String extractScopeValue(String scope, String prefix) {
+        return scope.startsWith(prefix) ? scope.substring(prefix.length()).trim() : null;
+    }
+}

+ 120 - 0
service-sas/service-sas-biz/src/main/java/com/usky/sas/common/onvif/RecordingInfo.java

@@ -0,0 +1,120 @@
+package com.usky.sas.common.onvif;
+
+public class RecordingInfo {
+    public String recordingToken;
+    public String sourceId;
+    public String cameraName;
+    public String cameraIp;
+
+    public RecordingInfo() {
+    }
+
+    public String getRecordingToken() {
+        return this.recordingToken;
+    }
+
+    public String getSourceId() {
+        return this.sourceId;
+    }
+
+    public String getCameraName() {
+        return this.cameraName;
+    }
+
+    public String getCameraIp() {
+        return this.cameraIp;
+    }
+
+    public void setRecordingToken(String recordingToken) {
+        this.recordingToken = recordingToken;
+    }
+
+    public void setSourceId(String sourceId) {
+        this.sourceId = sourceId;
+    }
+
+    public void setCameraName(String cameraName) {
+        this.cameraName = cameraName;
+    }
+
+    public void setCameraIp(String cameraIp) {
+        this.cameraIp = cameraIp;
+    }
+
+    public boolean equals(Object o) {
+        if (o == this) {
+            return true;
+        } else if (!(o instanceof RecordingInfo)) {
+            return false;
+        } else {
+            RecordingInfo other = (RecordingInfo)o;
+            if (!other.canEqual(this)) {
+                return false;
+            } else {
+                Object this$recordingToken = this.getRecordingToken();
+                Object other$recordingToken = other.getRecordingToken();
+                if (this$recordingToken == null) {
+                    if (other$recordingToken != null) {
+                        return false;
+                    }
+                } else if (!this$recordingToken.equals(other$recordingToken)) {
+                    return false;
+                }
+
+                Object this$sourceId = this.getSourceId();
+                Object other$sourceId = other.getSourceId();
+                if (this$sourceId == null) {
+                    if (other$sourceId != null) {
+                        return false;
+                    }
+                } else if (!this$sourceId.equals(other$sourceId)) {
+                    return false;
+                }
+
+                Object this$cameraName = this.getCameraName();
+                Object other$cameraName = other.getCameraName();
+                if (this$cameraName == null) {
+                    if (other$cameraName != null) {
+                        return false;
+                    }
+                } else if (!this$cameraName.equals(other$cameraName)) {
+                    return false;
+                }
+
+                Object this$cameraIp = this.getCameraIp();
+                Object other$cameraIp = other.getCameraIp();
+                if (this$cameraIp == null) {
+                    if (other$cameraIp != null) {
+                        return false;
+                    }
+                } else if (!this$cameraIp.equals(other$cameraIp)) {
+                    return false;
+                }
+
+                return true;
+            }
+        }
+    }
+
+    protected boolean canEqual(Object other) {
+        return other instanceof RecordingInfo;
+    }
+
+    public int hashCode() {
+        int PRIME = 59;
+        int result = 1;
+        Object $recordingToken = this.getRecordingToken();
+        result = result * 59 + ($recordingToken == null ? 43 : $recordingToken.hashCode());
+        Object $sourceId = this.getSourceId();
+        result = result * 59 + ($sourceId == null ? 43 : $sourceId.hashCode());
+        Object $cameraName = this.getCameraName();
+        result = result * 59 + ($cameraName == null ? 43 : $cameraName.hashCode());
+        Object $cameraIp = this.getCameraIp();
+        result = result * 59 + ($cameraIp == null ? 43 : $cameraIp.hashCode());
+        return result;
+    }
+
+    public String toString() {
+        return "RecordingInfo(recordingToken=" + this.getRecordingToken() + ", sourceId=" + this.getSourceId() + ", cameraName=" + this.getCameraName() + ", cameraIp=" + this.getCameraIp() + ")";
+    }
+}

+ 672 - 0
service-sas/service-sas-biz/src/main/java/com/usky/sas/common/onvif/StandardOnvifService.java

@@ -0,0 +1,672 @@
+package com.usky.sas.common.onvif;
+
+import com.alibaba.fastjson2.JSON;
+import com.usky.sas.common.exception.BusinessException;
+import java.io.BufferedReader;
+import java.io.BufferedWriter;
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.io.StringReader;
+import java.io.StringWriter;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.nio.charset.StandardCharsets;
+import java.security.MessageDigest;
+import java.security.SecureRandom;
+import java.time.ZonedDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.ArrayList;
+import java.util.Base64;
+import java.util.Iterator;
+import java.util.List;
+import javax.xml.namespace.NamespaceContext;
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.soap.MessageFactory;
+import javax.xml.soap.SOAPBody;
+import javax.xml.soap.SOAPElement;
+import javax.xml.soap.SOAPEnvelope;
+import javax.xml.soap.SOAPException;
+import javax.xml.soap.SOAPHeader;
+import javax.xml.soap.SOAPMessage;
+import javax.xml.soap.SOAPPart;
+import javax.xml.transform.Transformer;
+import javax.xml.transform.TransformerFactory;
+import javax.xml.transform.dom.DOMSource;
+import javax.xml.transform.stream.StreamResult;
+import javax.xml.xpath.XPath;
+import javax.xml.xpath.XPathConstants;
+import javax.xml.xpath.XPathFactory;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.stereotype.Service;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+import org.xml.sax.InputSource;
+
+@Service
+public class StandardOnvifService {
+    private static final Logger log = LoggerFactory.getLogger(StandardOnvifService.class);
+
+    public StandardOnvifService() {
+    }
+
+    public String getRealTimeStreamUrl(String cameraIp, String username, String password) {
+        try {
+            String mediaServiceUrl = this.getServiceEndpoint(cameraIp, "http://www.onvif.org/ver10/media/wsdl");
+            log.info("媒体服务端点: {}", mediaServiceUrl);
+            String profileToken = this.getProfileToken(mediaServiceUrl, username, password);
+            log.info("配置文件Token: {}", profileToken);
+            String streamUri = this.getStreamUri(mediaServiceUrl, profileToken, username, password);
+            log.info("获取到实时流地址: {}", streamUri);
+            return streamUri;
+        } catch (Exception e) {
+            log.error("获取实时流地址失败:{}", e.getMessage());
+            throw new BusinessException("播放失败", -1);
+        }
+    }
+
+    public String getServiceEndpoint(String deviceIp, String serviceNamespace) {
+        try {
+            String getServicesRequest = this.buildGetServicesRequest();
+            String response = this.sendSoapRequest("http://" + deviceIp + "/onvif/device_service", getServicesRequest);
+            Document doc = this.parseXml(response);
+            NodeList serviceNodes = doc.getElementsByTagNameNS("http://www.onvif.org/ver10/schema", "Media");
+            if (serviceNodes.getLength() == 0) {
+                log.error("未找到 tt:Media 节点");
+                throw new BusinessException("播放失败", -1);
+            } else {
+                Element mediaElement = (Element)serviceNodes.item(0);
+                NodeList xaddrNodes = mediaElement.getElementsByTagNameNS("http://www.onvif.org/ver10/schema", "XAddr");
+                if (xaddrNodes.getLength() > 0) {
+                    return xaddrNodes.item(0).getTextContent();
+                } else {
+                    log.error("未找到 Media 的 XAddr");
+                    throw new BusinessException("播放失败", -1);
+                }
+            }
+        } catch (Exception e) {
+            log.error("获取服务端点失败:{}", e.getMessage());
+            throw new BusinessException("播放失败", -1);
+        }
+    }
+
+    private String buildGetServicesRequest() throws SOAPException {
+        MessageFactory messageFactory = MessageFactory.newInstance();
+        SOAPMessage soapMessage = messageFactory.createMessage();
+        SOAPPart soapPart = soapMessage.getSOAPPart();
+        SOAPEnvelope envelope = soapPart.getEnvelope();
+        envelope.addNamespaceDeclaration("tds", "http://www.onvif.org/ver10/device/wsdl");
+        SOAPBody soapBody = envelope.getBody();
+        SOAPElement getServicesElement = soapBody.addChildElement("GetCapabilities", "tds");
+        SOAPElement includeCapabilityElement = getServicesElement.addChildElement("Category", "tds");
+        includeCapabilityElement.addTextNode("All");
+        return soapMessageToString(soapMessage);
+    }
+
+    public String getProfileToken(String mediaServiceUrl, String username, String password) {
+        try {
+            String getProfilesRequest = buildGetProfilesRequest(username, password);
+            String response = this.sendSoapRequest(mediaServiceUrl, getProfilesRequest);
+            Document doc = this.parseXml(response);
+            NodeList profileNodes = doc.getElementsByTagNameNS("http://www.onvif.org/ver10/media/wsdl", "Profiles");
+            if (profileNodes.getLength() > 0) {
+                Element profileElement = (Element)profileNodes.item(0);
+                return profileElement.getAttribute("token");
+            } else {
+                log.error("获取媒体配置文件失败");
+                throw new BusinessException("播放失败", -1);
+            }
+        } catch (Exception e) {
+            log.error("获取媒体配置文件失败:{}", e.getMessage());
+            throw new BusinessException("播放失败", -1);
+        }
+    }
+
+    private static String buildGetProfilesRequest(String username, String password) throws Exception {
+        MessageFactory messageFactory = MessageFactory.newInstance("SOAP 1.2 Protocol");
+        SOAPMessage soapMessage = messageFactory.createMessage();
+        SOAPPart soapPart = soapMessage.getSOAPPart();
+        SOAPEnvelope envelope = soapPart.getEnvelope();
+        envelope.setPrefix("soap");
+        SOAPHeader defaultHeader = envelope.getHeader();
+        if (defaultHeader != null) {
+            defaultHeader.detachNode();
+        }
+
+        SOAPHeader newHeader = envelope.addHeader();
+        SOAPElement securityElement = newHeader.addChildElement("Security", "wsse", "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd");
+        securityElement.setAttribute("mustUnderstand", "1");
+        SOAPElement usernameTokenElement = securityElement.addChildElement("UsernameToken", "wsse");
+        SOAPElement usernameElement = usernameTokenElement.addChildElement("Username", "wsse");
+        usernameElement.addTextNode(username);
+        SOAPElement nonceElement = usernameTokenElement.addChildElement("Nonce", "wsse");
+        String nonce = generateNonce();
+        nonceElement.setAttribute("EncodingType", "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-soap-message-security-1.0#Base64Binary");
+        nonceElement.addTextNode(Base64.getEncoder().encodeToString(nonce.getBytes()));
+        String created = ZonedDateTime.now().format(DateTimeFormatter.ISO_INSTANT);
+        SOAPElement createdElement = usernameTokenElement.addChildElement("Created", "wsu", "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd");
+        createdElement.addTextNode(created);
+        String passwordDigest = calculatePasswordDigest(nonce, created, password);
+        SOAPElement passwordElement = usernameTokenElement.addChildElement("Password", "wsse");
+        passwordElement.setAttribute("Type", "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordDigest");
+        passwordElement.addTextNode(passwordDigest);
+        envelope.addNamespaceDeclaration("trt", "http://www.onvif.org/ver10/media/wsdl");
+        SOAPBody soapBody = envelope.getBody();
+        soapBody.setPrefix("soap");
+        soapBody.addChildElement("GetProfiles", "trt");
+        return soapMessageToString(soapMessage);
+    }
+
+    public String getStreamUri(String mediaServiceUrl, String profileToken, String username, String password) {
+        try {
+            String getStreamUriRequest = buildGetStreamUriRequest(profileToken, username, password);
+            String response = this.sendSoapRequest(mediaServiceUrl, getStreamUriRequest);
+            Document doc = this.parseXml(response);
+            NodeList uriNodes = doc.getElementsByTagNameNS("*", "Uri");
+            if (uriNodes.getLength() > 0) {
+                return uriNodes.item(0).getTextContent();
+            } else {
+                log.error("获取rtsp流地址失败");
+                throw new BusinessException("播放失败", -1);
+            }
+        } catch (Exception e) {
+            log.error("获取流地址失败:{}", e.getMessage());
+            throw new BusinessException("播放失败", -1);
+        }
+    }
+
+    private static String buildGetStreamUriRequest(String profileToken, String username, String password) throws Exception {
+        MessageFactory messageFactory = MessageFactory.newInstance("SOAP 1.2 Protocol");
+        SOAPMessage soapMessage = messageFactory.createMessage();
+        SOAPPart soapPart = soapMessage.getSOAPPart();
+        SOAPEnvelope envelope = soapPart.getEnvelope();
+        SOAPHeader defaultHeader = envelope.getHeader();
+        if (defaultHeader != null) {
+            defaultHeader.detachNode();
+        }
+
+        SOAPHeader newHeader = envelope.addHeader();
+        SOAPElement securityElement = newHeader.addChildElement("Security", "wsse", "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd");
+        securityElement.setAttribute("mustUnderstand", "1");
+        SOAPElement usernameTokenElement = securityElement.addChildElement("UsernameToken", "wsse");
+        SOAPElement usernameElement = usernameTokenElement.addChildElement("Username", "wsse");
+        usernameElement.addTextNode(username);
+        SOAPElement nonceElement = usernameTokenElement.addChildElement("Nonce", "wsse");
+        String nonce = generateNonce();
+        nonceElement.setAttribute("EncodingType", "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-soap-message-security-1.0#Base64Binary");
+        nonceElement.addTextNode(Base64.getEncoder().encodeToString(nonce.getBytes()));
+        String created = ZonedDateTime.now().format(DateTimeFormatter.ISO_INSTANT);
+        SOAPElement createdElement = usernameTokenElement.addChildElement("Created", "wsu", "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd");
+        createdElement.addTextNode(created);
+        String passwordDigest = calculatePasswordDigest(nonce, created, password);
+        SOAPElement passwordElement = usernameTokenElement.addChildElement("Password", "wsse");
+        passwordElement.setAttribute("Type", "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordDigest");
+        passwordElement.addTextNode(passwordDigest);
+        envelope.addNamespaceDeclaration("trt", "http://www.onvif.org/ver10/media/wsdl");
+        envelope.addNamespaceDeclaration("tt", "http://www.onvif.org/ver10/schema");
+        SOAPBody soapBody = envelope.getBody();
+        SOAPElement getStreamUriElement = soapBody.addChildElement("GetStreamUri", "trt");
+        SOAPElement streamSetupElement = getStreamUriElement.addChildElement("StreamSetup", "trt");
+        SOAPElement streamElement = streamSetupElement.addChildElement("Stream", "tt");
+        streamElement.addTextNode("RTP-Unicast");
+        SOAPElement transportElement = streamSetupElement.addChildElement("Transport", "tt");
+        SOAPElement protocolElement = transportElement.addChildElement("Protocol", "tt");
+        protocolElement.addTextNode("RTSP");
+        SOAPElement profileTokenElement = getStreamUriElement.addChildElement("ProfileToken", "trt");
+        profileTokenElement.addTextNode(profileToken);
+        return soapMessageToString(soapMessage);
+    }
+
+    public String getReplayStreamUrl(String cameraIp, String username, String password) {
+        try {
+            String recordingServiceUrl = this.getServiceEndpoint(cameraIp, "http://www.onvif.org/ver10/recording/wsdl");
+            log.info("回放录制配置服务端点: {}", recordingServiceUrl);
+            List<RecordingInfo> recordingToken = this.getRecordingToken(recordingServiceUrl, username, password);
+            log.info("录制配置Token: {}", JSON.toJSONString(recordingToken));
+            String replayServiceUrl = this.getServiceEndpoint(cameraIp, "http://www.onvif.org/ver10/replay/wsdl");
+            log.info("回放流服务端点: {}", recordingServiceUrl);
+            String streamUri = this.getPlayStreamUri(replayServiceUrl, ((RecordingInfo)recordingToken.get(0)).getRecordingToken(), username, password);
+            log.info("获取到回放流地址: {}", streamUri);
+            return streamUri;
+        } catch (Exception e) {
+            log.error("获取回放流地址失败:{}", e.getMessage());
+            throw new BusinessException("播放失败", -1);
+        }
+    }
+
+    public List<RecordingInfo> getRecordingToken(String recordingServiceUrl, String username, String password) {
+        try {
+            String getRecordingRequest = buildGetRecordingRequest(username, password);
+            String response = this.sendSoapRequest(recordingServiceUrl, getRecordingRequest);
+            return this.parseRecordingInfos(response);
+        } catch (Exception e) {
+            log.error("获取媒体配置文件失败:{}", e.getMessage());
+            throw new BusinessException("播放失败", -1);
+        }
+    }
+
+    private static String buildGetRecordingRequest(String username, String password) throws Exception {
+        MessageFactory messageFactory = MessageFactory.newInstance("SOAP 1.2 Protocol");
+        SOAPMessage soapMessage = messageFactory.createMessage();
+        SOAPPart soapPart = soapMessage.getSOAPPart();
+        SOAPEnvelope envelope = soapPart.getEnvelope();
+        envelope.setPrefix("soap");
+        SOAPHeader defaultHeader = envelope.getHeader();
+        if (defaultHeader != null) {
+            defaultHeader.detachNode();
+        }
+
+        SOAPHeader newHeader = envelope.addHeader();
+        SOAPElement securityElement = newHeader.addChildElement("Security", "wsse", "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd");
+        securityElement.setAttribute("mustUnderstand", "1");
+        SOAPElement usernameTokenElement = securityElement.addChildElement("UsernameToken", "wsse");
+        SOAPElement usernameElement = usernameTokenElement.addChildElement("Username", "wsse");
+        usernameElement.addTextNode(username);
+        SOAPElement nonceElement = usernameTokenElement.addChildElement("Nonce", "wsse");
+        String nonce = generateNonce();
+        nonceElement.setAttribute("EncodingType", "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-soap-message-security-1.0#Base64Binary");
+        nonceElement.addTextNode(Base64.getEncoder().encodeToString(nonce.getBytes()));
+        String created = ZonedDateTime.now().format(DateTimeFormatter.ISO_INSTANT);
+        SOAPElement createdElement = usernameTokenElement.addChildElement("Created", "wsu", "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd");
+        createdElement.addTextNode(created);
+        String passwordDigest = calculatePasswordDigest(nonce, created, password);
+        SOAPElement passwordElement = usernameTokenElement.addChildElement("Password", "wsse");
+        passwordElement.setAttribute("Type", "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordDigest");
+        passwordElement.addTextNode(passwordDigest);
+        envelope.addNamespaceDeclaration("trc", "http://www.onvif.org/ver10/recording/wsdl");
+        SOAPBody soapBody = envelope.getBody();
+        soapBody.setPrefix("soap");
+        soapBody.addChildElement("GetRecordings", "trc");
+        return soapMessageToString(soapMessage);
+    }
+
+    private List<RecordingInfo> parseRecordingInfos(String xmlResponse) throws Exception {
+        DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
+        factory.setNamespaceAware(true);
+        DocumentBuilder builder = factory.newDocumentBuilder();
+        Document doc = builder.parse(new ByteArrayInputStream(xmlResponse.getBytes(StandardCharsets.UTF_8)));
+        XPathFactory xPathFactory = XPathFactory.newInstance();
+        XPath xpath = xPathFactory.newXPath();
+        xpath.setNamespaceContext(new NamespaceContext() {
+            public String getNamespaceURI(String prefix) {
+                switch (prefix) {
+                    case "soap":
+                        return "http://www.w3.org/2003/05/soap-envelope";
+                    case "trec":
+                        return "http://www.onvif.org/ver10/recording/wsdl";
+                    case "tt":
+                        return "http://www.onvif.org/ver10/schema";
+                    default:
+                        return null;
+                }
+            }
+
+            public String getPrefix(String namespaceURI) {
+                return null;
+            }
+
+            public Iterator<String> getPrefixes(String namespaceURI) {
+                return null;
+            }
+        });
+        NodeList items = (NodeList)xpath.evaluate("//trec:RecordingItem", doc, XPathConstants.NODESET);
+        List<RecordingInfo> list = new ArrayList();
+
+        for(int i = 0; i < items.getLength(); ++i) {
+            Node item = items.item(i);
+            RecordingInfo info = new RecordingInfo();
+            info.recordingToken = (String)xpath.evaluate("tt:RecordingToken", item, XPathConstants.STRING);
+            info.sourceId = (String)xpath.evaluate(".//tt:Source/tt:SourceId", item, XPathConstants.STRING);
+            info.cameraName = (String)xpath.evaluate(".//tt:Source/tt:Name", item, XPathConstants.STRING);
+            if (info.recordingToken != null) {
+                info.recordingToken = info.recordingToken.trim();
+            }
+
+            if (info.sourceId != null) {
+                info.sourceId = info.sourceId.trim();
+            }
+
+            if (info.cameraName != null) {
+                info.cameraName = info.cameraName.trim();
+            }
+
+            list.add(info);
+        }
+
+        return list;
+    }
+
+    public String getPlayStreamUri(String replayServiceUrl, String recordingToken, String username, String password) {
+        try {
+            String getStreamUriRequest = buildGetPlayStreamUriRequest(recordingToken, username, password);
+            String response = this.sendSoapRequest(replayServiceUrl, getStreamUriRequest);
+            Document doc = this.parseXml(response);
+            NodeList uriNodes = doc.getElementsByTagNameNS("*", "Uri");
+            if (uriNodes.getLength() > 0) {
+                return uriNodes.item(0).getTextContent();
+            } else {
+                log.error("获取回放流rtsp地址失败");
+                throw new BusinessException("播放失败", -1);
+            }
+        } catch (Exception e) {
+            log.error("获取回放流地址失败:{}", e.getMessage());
+            throw new BusinessException("播放失败", -1);
+        }
+    }
+
+    private static String buildGetPlayStreamUriRequest(String recordingToken, String username, String password) throws Exception {
+        MessageFactory messageFactory = MessageFactory.newInstance("SOAP 1.2 Protocol");
+        SOAPMessage soapMessage = messageFactory.createMessage();
+        SOAPPart soapPart = soapMessage.getSOAPPart();
+        SOAPEnvelope envelope = soapPart.getEnvelope();
+        SOAPHeader defaultHeader = envelope.getHeader();
+        if (defaultHeader != null) {
+            defaultHeader.detachNode();
+        }
+
+        SOAPHeader newHeader = envelope.addHeader();
+        SOAPElement securityElement = newHeader.addChildElement("Security", "wsse", "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd");
+        securityElement.setAttribute("mustUnderstand", "1");
+        SOAPElement usernameTokenElement = securityElement.addChildElement("UsernameToken", "wsse");
+        SOAPElement usernameElement = usernameTokenElement.addChildElement("Username", "wsse");
+        usernameElement.addTextNode(username);
+        SOAPElement nonceElement = usernameTokenElement.addChildElement("Nonce", "wsse");
+        String nonce = generateNonce();
+        nonceElement.setAttribute("EncodingType", "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-soap-message-security-1.0#Base64Binary");
+        nonceElement.addTextNode(Base64.getEncoder().encodeToString(nonce.getBytes()));
+        String created = ZonedDateTime.now().format(DateTimeFormatter.ISO_INSTANT);
+        SOAPElement createdElement = usernameTokenElement.addChildElement("Created", "wsu", "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd");
+        createdElement.addTextNode(created);
+        String passwordDigest = calculatePasswordDigest(nonce, created, password);
+        SOAPElement passwordElement = usernameTokenElement.addChildElement("Password", "wsse");
+        passwordElement.setAttribute("Type", "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordDigest");
+        passwordElement.addTextNode(passwordDigest);
+        envelope.addNamespaceDeclaration("trp", "http://www.onvif.org/ver10/replay/wsdl");
+        envelope.addNamespaceDeclaration("tt", "http://www.onvif.org/ver10/schema");
+        SOAPBody soapBody = envelope.getBody();
+        SOAPElement getStreamUriElement = soapBody.addChildElement("GetReplayUri", "trp");
+        SOAPElement streamSetupElement = getStreamUriElement.addChildElement("StreamSetup");
+        SOAPElement streamElement = streamSetupElement.addChildElement("Stream");
+        streamElement.addTextNode("RTP-Unicast");
+        SOAPElement transportElement = streamSetupElement.addChildElement("Transport");
+        SOAPElement protocolElement = transportElement.addChildElement("Protocol");
+        protocolElement.addTextNode("RTSP");
+        SOAPElement recordingTokenElement = getStreamUriElement.addChildElement("RecordingToken");
+        recordingTokenElement.addTextNode(recordingToken);
+        return soapMessageToString(soapMessage);
+    }
+
+    private String sendSoapRequest(String url, String soapRequest) {
+        try {
+            URL soapUrl = new URL(url);
+            HttpURLConnection connection = (HttpURLConnection)soapUrl.openConnection();
+            connection.setRequestMethod("POST");
+            connection.setDoOutput(true);
+            connection.setDoInput(true);
+            connection.setUseCaches(false);
+            connection.setRequestProperty("Content-Type", "text/xml; charset=utf-8");
+            connection.setRequestProperty("SOAPAction", "");
+
+            try (OutputStream os = connection.getOutputStream()) {
+                BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(os, StandardCharsets.UTF_8));
+                Throwable errorReader = null;
+
+                try {
+                    writer.write(soapRequest);
+                    writer.flush();
+                } catch (Throwable var160) {
+                    errorReader = var160;
+                    throw var160;
+                } finally {
+                    if (writer != null) {
+                        if (errorReader != null) {
+                            try {
+                                writer.close();
+                            } catch (Throwable var159) {
+                                errorReader.addSuppressed(var159);
+                            }
+                        } else {
+                            writer.close();
+                        }
+                    }
+
+                }
+            }
+
+            int responseCode = connection.getResponseCode();
+            if (responseCode == 200) {
+                String var12;
+                try (InputStream is = connection.getInputStream()) {
+                    BufferedReader reader = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8));
+                    Throwable var180 = null;
+
+                    try {
+                        StringBuilder responseBuilder = new StringBuilder();
+
+                        String line;
+                        while((line = reader.readLine()) != null) {
+                            responseBuilder.append(line).append("\n");
+                        }
+
+                        var12 = responseBuilder.toString();
+                    } catch (Throwable var164) {
+                        var180 = var164;
+                        throw var164;
+                    } finally {
+                        if (reader != null) {
+                            if (var180 != null) {
+                                try {
+                                    reader.close();
+                                } catch (Throwable var157) {
+                                    var180.addSuppressed(var157);
+                                }
+                            } else {
+                                reader.close();
+                            }
+                        }
+
+                    }
+                }
+
+                return var12;
+            } else {
+                try (InputStream errorStream = connection.getErrorStream()) {
+                    BufferedReader errorReader = new BufferedReader(new InputStreamReader(errorStream, StandardCharsets.UTF_8));
+                    Throwable var9 = null;
+
+                    try {
+                        StringBuilder errorBuilder = new StringBuilder();
+
+                        String line;
+                        while((line = errorReader.readLine()) != null) {
+                            errorBuilder.append(line).append("\n");
+                        }
+
+                        log.error("发送SOAP请求失败");
+                        throw new BusinessException("播放失败", -1);
+                    } catch (Throwable var168) {
+                        var9 = var168;
+                        throw var168;
+                    } finally {
+                        if (errorReader != null) {
+                            if (var9 != null) {
+                                try {
+                                    errorReader.close();
+                                } catch (Throwable var155) {
+                                    var9.addSuppressed(var155);
+                                }
+                            } else {
+                                errorReader.close();
+                            }
+                        }
+
+                    }
+                }
+            }
+        } catch (Exception e) {
+            log.error("发送 SOAP 请求失败: {}", e.getMessage());
+            throw new BusinessException("播放失败", -1);
+        }
+    }
+
+    private static String soapMessageToString(SOAPMessage message) throws SOAPException {
+        try {
+            TransformerFactory transformerFactory = TransformerFactory.newInstance();
+            Transformer transformer = transformerFactory.newTransformer();
+            transformer.setOutputProperty("indent", "yes");
+            transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "2");
+            StringWriter writer = new StringWriter();
+            transformer.transform(new DOMSource(message.getSOAPPart()), new StreamResult(writer));
+            return writer.toString();
+        } catch (Exception e) {
+            log.error("转换SOAP消息失败:{}", e.getMessage());
+            throw new BusinessException("播放失败", -1);
+        }
+    }
+
+    public List<String> getRealSources(String cameraIp, String username, String password) {
+        try {
+            String mediaServiceUrl = this.getServiceEndpoint(cameraIp, "http://www.onvif.org/ver10/media/wsdl");
+            log.info("媒体服务端点: {}", mediaServiceUrl);
+            List<String> sources = this.getSources(mediaServiceUrl, username, password);
+            return sources;
+        } catch (Exception e) {
+            log.error("获取NVR设备通道数失败:{}", e.getMessage());
+            throw new BusinessException("播放失败", -1);
+        }
+    }
+
+    public List<String> getSources(String mediaServiceUrl, String username, String password) {
+        try {
+            String getProfilesRequest = buildGetSourceRequest(username, password);
+            String response = this.sendSoapRequest(mediaServiceUrl, getProfilesRequest);
+            Document doc = this.parseXml(response);
+            NodeList sources = doc.getElementsByTagNameNS("http://www.onvif.org/ver10/media/wsdl", "VideoSources");
+            if (sources.getLength() <= 0) {
+                log.error("获取设备source失败");
+                throw new BusinessException("播放失败", -1);
+            } else {
+                List<String> videoSourceTokenList = new ArrayList();
+
+                for(int i = 0; i < sources.getLength(); ++i) {
+                    Element profileElement = (Element)sources.item(i);
+                    String videoSourceToken = profileElement.getAttribute("token");
+                    videoSourceTokenList.add(videoSourceToken);
+                }
+
+                return videoSourceTokenList;
+            }
+        } catch (Exception e) {
+            log.error("获取设备source失败:{}", e.getMessage());
+            throw new BusinessException("播放失败", -1);
+        }
+    }
+
+    public String getNvrRealTimeStreamUrl(String cameraIp, String username, String password, String videoSourceToken) {
+        try {
+            String mediaServiceUrl = this.getServiceEndpoint(cameraIp, "http://www.onvif.org/ver10/media/wsdl");
+            log.info("媒体服务端点: {}", mediaServiceUrl);
+            String profileToken = this.getNvrProfilesToken(mediaServiceUrl, username, password, videoSourceToken);
+            log.info("配置文件Token: {}", profileToken);
+            String streamUri = this.getStreamUri(mediaServiceUrl, profileToken, username, password);
+            log.info("获取到实时流地址: {}", streamUri);
+            return streamUri;
+        } catch (Exception e) {
+            log.error("获取实时流地址失败:{}", e.getMessage());
+            throw new BusinessException("播放失败", -1);
+        }
+    }
+
+    public String getNvrProfilesToken(String mediaServiceUrl, String username, String password, String videoSourceToken) {
+        try {
+            String getProfilesRequest = buildGetProfilesRequest(username, password);
+            String response = this.sendSoapRequest(mediaServiceUrl, getProfilesRequest);
+            Document doc = this.parseXml(response);
+            NodeList profileNodes = doc.getElementsByTagNameNS("http://www.onvif.org/ver10/media/wsdl", "Profiles");
+            if (profileNodes.getLength() > 0) {
+                for(int i = 0; i < profileNodes.getLength(); ++i) {
+                    Element profileElement = (Element)profileNodes.item(i);
+                    String profileToken = profileElement.getAttribute("token");
+                    NodeList videoSourceConfigNodes = profileElement.getElementsByTagNameNS("http://www.onvif.org/ver10/schema", "VideoSourceConfiguration");
+                    if (videoSourceConfigNodes.getLength() > 0) {
+                        Element videoSourceConfig = (Element)videoSourceConfigNodes.item(0);
+                        NodeList sourceTokenNodes = videoSourceConfig.getElementsByTagNameNS("http://www.onvif.org/ver10/schema", "SourceToken");
+                        if (sourceTokenNodes.getLength() > 0) {
+                            String sourceToken = sourceTokenNodes.item(0).getTextContent();
+                            if (videoSourceToken.equals(sourceToken)) {
+                                log.info("找到匹配的Profile: {} 对应 VideoSourceToken: {}", profileToken, videoSourceToken);
+                                return profileToken;
+                            }
+                        }
+                    }
+                }
+            }
+
+            log.error("获取媒体配置文件失败");
+            throw new BusinessException("播放失败", -1);
+        } catch (Exception e) {
+            log.error("获取媒体配置文件失败:{}", e.getMessage());
+            throw new BusinessException("播放失败", -1);
+        }
+    }
+
+    private static String buildGetSourceRequest(String username, String password) throws Exception {
+        MessageFactory messageFactory = MessageFactory.newInstance("SOAP 1.2 Protocol");
+        SOAPMessage soapMessage = messageFactory.createMessage();
+        SOAPPart soapPart = soapMessage.getSOAPPart();
+        SOAPEnvelope envelope = soapPart.getEnvelope();
+        SOAPHeader defaultHeader = envelope.getHeader();
+        if (defaultHeader != null) {
+            defaultHeader.detachNode();
+        }
+
+        SOAPHeader newHeader = envelope.addHeader();
+        SOAPElement securityElement = newHeader.addChildElement("Security", "wsse", "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd");
+        securityElement.setAttribute("mustUnderstand", "1");
+        SOAPElement usernameTokenElement = securityElement.addChildElement("UsernameToken", "wsse");
+        SOAPElement usernameElement = usernameTokenElement.addChildElement("Username", "wsse");
+        usernameElement.addTextNode(username);
+        SOAPElement nonceElement = usernameTokenElement.addChildElement("Nonce", "wsse");
+        String nonce = generateNonce();
+        nonceElement.setAttribute("EncodingType", "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-soap-message-security-1.0#Base64Binary");
+        nonceElement.addTextNode(Base64.getEncoder().encodeToString(nonce.getBytes()));
+        String created = ZonedDateTime.now().format(DateTimeFormatter.ISO_INSTANT);
+        SOAPElement createdElement = usernameTokenElement.addChildElement("Created", "wsu", "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd");
+        createdElement.addTextNode(created);
+        String passwordDigest = calculatePasswordDigest(nonce, created, password);
+        SOAPElement passwordElement = usernameTokenElement.addChildElement("Password", "wsse");
+        passwordElement.setAttribute("Type", "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordDigest");
+        passwordElement.addTextNode(passwordDigest);
+        envelope.addNamespaceDeclaration("trt", "http://www.onvif.org/ver10/media/wsdl");
+        SOAPBody soapBody = envelope.getBody();
+        SOAPElement getStreamUriElement = soapBody.addChildElement("GetVideoSources", "trt");
+        return soapMessageToString(soapMessage);
+    }
+
+    private Document parseXml(String xmlString) throws Exception {
+        DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
+        factory.setNamespaceAware(true);
+        DocumentBuilder builder = factory.newDocumentBuilder();
+        return builder.parse(new InputSource(new StringReader(xmlString)));
+    }
+
+    private static String generateNonce() {
+        SecureRandom random = new SecureRandom();
+        byte[] nonceBytes = new byte[16];
+        random.nextBytes(nonceBytes);
+        return new String(nonceBytes, StandardCharsets.UTF_8);
+    }
+
+    private static String calculatePasswordDigest(String nonce, String created, String password) throws Exception {
+        String combined = nonce + created + password;
+        MessageDigest md = MessageDigest.getInstance("SHA-1");
+        byte[] digest = md.digest(combined.getBytes(StandardCharsets.UTF_8));
+        return Base64.getEncoder().encodeToString(digest);
+    }
+}

+ 126 - 0
service-sas/service-sas-biz/src/main/java/com/usky/sas/common/onvif/UnityVideoInfo.java

@@ -0,0 +1,126 @@
+package com.usky.sas.common.onvif;
+
+import io.swagger.annotations.ApiModelProperty;
+
+public class UnityVideoInfo {
+    @ApiModelProperty("设备mac地址")
+    private String macAddr;
+    @ApiModelProperty("ZLM主动拉流唯一标识")
+    private String key;
+    @ApiModelProperty("视频流唯一标识")
+    private String ssrc;
+    @ApiModelProperty("视频流播放地址")
+    private String url;
+
+    public UnityVideoInfo() {
+    }
+
+    public String getMacAddr() {
+        return this.macAddr;
+    }
+
+    public String getKey() {
+        return this.key;
+    }
+
+    public String getSsrc() {
+        return this.ssrc;
+    }
+
+    public String getUrl() {
+        return this.url;
+    }
+
+    public void setMacAddr(String macAddr) {
+        this.macAddr = macAddr;
+    }
+
+    public void setKey(String key) {
+        this.key = key;
+    }
+
+    public void setSsrc(String ssrc) {
+        this.ssrc = ssrc;
+    }
+
+    public void setUrl(String url) {
+        this.url = url;
+    }
+
+    public boolean equals(Object o) {
+        if (o == this) {
+            return true;
+        } else if (!(o instanceof UnityVideoInfo)) {
+            return false;
+        } else {
+            UnityVideoInfo other = (UnityVideoInfo)o;
+            if (!other.canEqual(this)) {
+                return false;
+            } else {
+                Object this$macAddr = this.getMacAddr();
+                Object other$macAddr = other.getMacAddr();
+                if (this$macAddr == null) {
+                    if (other$macAddr != null) {
+                        return false;
+                    }
+                } else if (!this$macAddr.equals(other$macAddr)) {
+                    return false;
+                }
+
+                Object this$key = this.getKey();
+                Object other$key = other.getKey();
+                if (this$key == null) {
+                    if (other$key != null) {
+                        return false;
+                    }
+                } else if (!this$key.equals(other$key)) {
+                    return false;
+                }
+
+                Object this$ssrc = this.getSsrc();
+                Object other$ssrc = other.getSsrc();
+                if (this$ssrc == null) {
+                    if (other$ssrc != null) {
+                        return false;
+                    }
+                } else if (!this$ssrc.equals(other$ssrc)) {
+                    return false;
+                }
+
+                Object this$url = this.getUrl();
+                Object other$url = other.getUrl();
+                if (this$url == null) {
+                    if (other$url != null) {
+                        return false;
+                    }
+                } else if (!this$url.equals(other$url)) {
+                    return false;
+                }
+
+                return true;
+            }
+        }
+    }
+
+    protected boolean canEqual(Object other) {
+        return other instanceof UnityVideoInfo;
+    }
+
+    public int hashCode() {
+        int PRIME = 59;
+        int result = 1;
+        Object $macAddr = this.getMacAddr();
+        result = result * 59 + ($macAddr == null ? 43 : $macAddr.hashCode());
+        Object $key = this.getKey();
+        result = result * 59 + ($key == null ? 43 : $key.hashCode());
+        Object $ssrc = this.getSsrc();
+        result = result * 59 + ($ssrc == null ? 43 : $ssrc.hashCode());
+        Object $url = this.getUrl();
+        result = result * 59 + ($url == null ? 43 : $url.hashCode());
+        return result;
+    }
+
+    public String toString() {
+        return "UnityVideoInfo(macAddr=" + this.getMacAddr() + ", key=" + this.getKey() + ", ssrc=" + this.getSsrc() + ", url=" + this.getUrl() + ")";
+    }
+}