-
需求
- 获取视频流并在内嵌video标签进行播放
- 操控监控进行转向、调焦操作
-
解决流程
- 官方SDK一个赛一个的不靠谱,尤其是mac开发无法加载dll文件,就算可以加载,服务器也加载不了,果断pass
- 从onvif入手,写一个全通用的微服务
- 不需要依赖,但需要如下几个包
org.oasis_open.docs
org.onvif.ver10
org.onvif.ver20
org.w3._2004_08.xop.include
org.w3._2005
org.xmlsoap.schemas.soap.envelope- 需要写一些实体类
ImagingDevice
,InitialDevice
,MediaDevice
,OnvifDevice
,PtzDevice
,Soap
,这里就只列一下主要的
import java.io.IOException; import java.net.ConnectException; import java.net.InetSocketAddress; import java.net.Socket; import java.net.SocketAddress; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.text.SimpleDateFormat; import java.util.*; import javax.xml.soap.SOAPException; import com.hjly.tour.common.core.exception.TourBizException; import lombok.Cleanup; import lombok.Data; import com.hjly.tour.edi.onvif.api.org.onvif.ver10.schema.Capabilities; /** * @author Mr. Cui */ @Data public class OnvifDevice { private final String HOST_IP; private String originalIp; private boolean isProxy; private final String username; private final String password; private String nonce; private String utcTime; private final String serverDeviceUri; private String serverPtzUri; private String serverMediaUri; private String serverImagingUri; private String serverEventsUri; private final SOAP soap; private final InitialDevices initialDevices; private final PtzDevices ptzDevices; private final MediaDevices mediaDevices; private final ImagingDevices imagingDevices; public OnvifDevice(String hostIp, String user, String password) throws ConnectException, SOAPException { this.HOST_IP = hostIp; if (!isOnline()) { throw new ConnectException("Host not available."); } this.serverDeviceUri = "http://" + HOST_IP + "/onvif/device_service"; this.username = user; this.password = password; this.soap = new SOAP(this); this.initialDevices = new InitialDevices(this); this.ptzDevices = new PtzDevices(this); this.mediaDevices = new MediaDevices(this); this.imagingDevices = new ImagingDevices(this); init(); } public OnvifDevice(String hostIp) throws ConnectException, SOAPException { this(hostIp, null, null); } private boolean isOnline() { String port = HOST_IP.contains(":") ? HOST_IP.substring(HOST_IP.indexOf(':') + 1) : "80"; String ip = HOST_IP.contains(":") ? HOST_IP.substring(0, HOST_IP.indexOf(':')) : HOST_IP; try { SocketAddress sockaddr = new InetSocketAddress(ip, new Integer(port)); @Cleanup Socket socket = new Socket(); socket.connect(sockaddr, 5000); } catch (NumberFormatException | IOException e) { return false; } return true; } protected void init() throws ConnectException, SOAPException { Capabilities capabilities = getInitialDevices().getCapabilities(); if (capabilities == null) { throw new TourBizException("Capabilities not reachable."); } String localDeviceUri = capabilities.getDevice().getXAddr(); if (localDeviceUri.startsWith("http://")) { originalIp = localDeviceUri.replace("http://", ""); originalIp = originalIp.substring(0, originalIp.indexOf('/')); } else { throw new TourBizException("Unknown/Not implemented local procotol!"); } if (!originalIp.equals(HOST_IP)) { isProxy = true; } if (capabilities.getMedia() != null && capabilities.getMedia().getXAddr() != null) { serverMediaUri = replaceLocalIpWithProxyIp(capabilities.getMedia().getXAddr()); } if (capabilities.getPTZ() != null && capabilities.getPTZ().getXAddr() != null) { serverPtzUri = replaceLocalIpWithProxyIp(capabilities.getPTZ().getXAddr()); } if (capabilities.getImaging() != null && capabilities.getImaging().getXAddr() != null) { serverImagingUri = replaceLocalIpWithProxyIp(capabilities.getImaging().getXAddr()); } if (capabilities.getMedia() != null && capabilities.getEvents().getXAddr() != null) { serverEventsUri = replaceLocalIpWithProxyIp(capabilities.getEvents().getXAddr()); } } public String replaceLocalIpWithProxyIp(String original) { if (original.startsWith("http:///")) { original.replace("http:///", "http://"+HOST_IP); } if (isProxy) { return original.replace(originalIp, HOST_IP); } return original; } public String getUsername() { return username; } public String getEncryptedPassword() { return encryptPassword(); } public String encryptPassword() { String nonce = getNonce(); String timestamp = getUTCTime(); String beforeEncryption = nonce + timestamp + password; byte[] encryptedRaw; try { encryptedRaw = sha1(beforeEncryption); } catch (NoSuchAlgorithmException e) { e.printStackTrace(); return null; } return Base64.getEncoder().encodeToString(encryptedRaw); } private static byte[] sha1(String s) throws NoSuchAlgorithmException { MessageDigest SHA1; SHA1 = MessageDigest.getInstance("SHA1"); SHA1.reset(); SHA1.update(s.getBytes()); return SHA1.digest(); } private String getNonce() { if (nonce == null) { createNonce(); } return nonce; } public String getEncryptedNonce() { if (nonce == null) { createNonce(); } return Base64.getEncoder().encodeToString(nonce.getBytes()); } public void createNonce() { Random generator = new Random(); nonce = "" + generator.nextInt(); } public String getUTCTime() { SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-d'T'HH:mm:ss'Z'"); sdf.setTimeZone(new SimpleTimeZone(SimpleTimeZone.UTC_TIME, "UTC")); Calendar cal = new GregorianCalendar(TimeZone.getTimeZone("UTC")); String utcTime = sdf.format(cal.getTime()); this.utcTime = utcTime; return utcTime; } public Date getDate() { return initialDevices.getDate(); } public String getName() { return initialDevices.getDeviceInformation().getModel(); } }
import java.net.ConnectException; import javax.xml.bind.JAXBContext; import javax.xml.bind.JAXBException; import javax.xml.bind.Marshaller; import javax.xml.bind.UnmarshalException; import javax.xml.bind.Unmarshaller; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; import javax.xml.soap.MessageFactory; import javax.xml.soap.SOAPConnection; import javax.xml.soap.SOAPConnectionFactory; import javax.xml.soap.SOAPConstants; 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 com.hjly.tour.common.core.exception.TourBizException; import lombok.Cleanup; import lombok.Data; import org.w3c.dom.Document; @Data public class SOAP { private OnvifDevice onvifDevice; public SOAP(OnvifDevice onvifDevice) { super(); this.onvifDevice = onvifDevice; } public Object createSOAPDeviceRequest(Object soapRequestElem, Object soapResponseElem) throws SOAPException, ConnectException { return createSOAPRequest(soapRequestElem, soapResponseElem, onvifDevice.getServerDeviceUri()); } public Object createSOAPPtzRequest(Object soapRequestElem, Object soapResponseElem) throws SOAPException, ConnectException { return createSOAPRequest(soapRequestElem, soapResponseElem, onvifDevice.getServerPtzUri()); } public Object createSOAPMediaRequest(Object soapRequestElem, Object soapResponseElem) throws SOAPException, ConnectException { return createSOAPRequest(soapRequestElem, soapResponseElem, onvifDevice.getServerMediaUri()); } public Object createSOAPImagingRequest(Object soapRequestElem, Object soapResponseElem) throws SOAPException, ConnectException { return createSOAPRequest(soapRequestElem, soapResponseElem, onvifDevice.getServerImagingUri()); } public Object createSOAPRequest(Object soapRequestElem, Object soapResponseElem, String soapUri) { try { SOAPConnectionFactory soapConnectionFactory = SOAPConnectionFactory.newInstance(); @Cleanup SOAPConnection soapConnection = soapConnectionFactory.createConnection(); SOAPMessage soapMessage = createSoapMessage(soapRequestElem); SOAPMessage soapResponse = soapConnection.call(soapMessage, soapUri); if (soapResponseElem == null) { throw new NullPointerException("Improper SOAP Response Element given (is null)."); } Unmarshaller unmarshaller = JAXBContext.newInstance(soapResponseElem.getClass()).createUnmarshaller(); try { try { soapResponseElem = unmarshaller.unmarshal(soapResponse.getSOAPBody().extractContentAsDocument()); } catch (SOAPException e) { soapResponseElem = unmarshaller.unmarshal(soapResponse.getSOAPBody().extractContentAsDocument()); } } catch (UnmarshalException e) { throw new TourBizException("Could not unmarshal, ended in SOAP fault."); } return soapResponseElem; } catch (SOAPException e) { throw new TourBizException("Unexpected response. Response should be from class " + soapResponseElem.getClass()); } catch (ParserConfigurationException | JAXBException e) { throw new TourBizException("Unhandled exception: " + e.getMessage()); } } protected SOAPMessage createSoapMessage(Object soapRequestElem) throws SOAPException, ParserConfigurationException, JAXBException { MessageFactory messageFactory = MessageFactory.newInstance(SOAPConstants.SOAP_1_2_PROTOCOL); SOAPMessage soapMessage = messageFactory.createMessage(); Document document = DocumentBuilderFactory.newInstance().newDocumentBuilder().newDocument(); Marshaller marshaller = JAXBContext.newInstance(soapRequestElem.getClass()).createMarshaller(); marshaller.marshal(soapRequestElem, document); soapMessage.getSOAPBody().addDocument(document); // if (needAuthentification) createSoapHeader(soapMessage); soapMessage.saveChanges(); return soapMessage; } protected void createSoapHeader(SOAPMessage soapMessage) throws SOAPException { onvifDevice.createNonce(); String encrypedPassword = onvifDevice.getEncryptedPassword(); if (encrypedPassword != null && onvifDevice.getUsername() != null) { SOAPPart sp = soapMessage.getSOAPPart(); SOAPEnvelope se = sp.getEnvelope(); SOAPHeader header = soapMessage.getSOAPHeader(); se.addNamespaceDeclaration("wsse", "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"); se.addNamespaceDeclaration("wsu", "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd"); SOAPElement securityElem = header.addChildElement("Security", "wsse"); // securityElem.setAttribute("SOAP-ENV:mustUnderstand", "1"); SOAPElement usernameTokenElem = securityElem.addChildElement("UsernameToken", "wsse"); SOAPElement usernameElem = usernameTokenElem.addChildElement("Username", "wsse"); usernameElem.setTextContent(onvifDevice.getUsername()); SOAPElement passwordElem = usernameTokenElem.addChildElement("Password", "wsse"); passwordElem.setAttribute("Type", "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordDigest"); passwordElem.setTextContent(encrypedPassword); SOAPElement nonceElem = usernameTokenElem.addChildElement("Nonce", "wsse"); nonceElem.setAttribute("EncodingType", "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-soap-message-security-1.0#Base64Binary"); nonceElem.setTextContent(onvifDevice.getEncryptedNonce()); SOAPElement createdElem = usernameTokenElem.addChildElement("Created", "wsu"); createdElem.setTextContent(onvifDevice.getUTCTime()); } } }
- 工具类
import com.hjly.tour.common.core.exception.TourBizException; import com.hjly.tour.edi.onvif.api.dto.GetVSDTO; import com.hjly.tour.edi.onvif.api.dto.TurnDTO; import com.hjly.tour.edi.onvif.api.entity.MediaDevices; import com.hjly.tour.edi.onvif.api.entity.OnvifDevice; import com.hjly.tour.edi.onvif.api.entity.PtzDevices; import com.hjly.tour.edi.onvif.api.org.onvif.ver10.schema.PTZNode; import com.hjly.tour.edi.onvif.api.org.onvif.ver10.schema.PTZVector; import javax.xml.soap.SOAPException; import java.net.ConnectException; import java.util.HashMap; import java.util.Map; /** * @author: Mr. Cui */ public class OnvifUtil { public static final Map<String,PTZNode> ptzNodeMap = new HashMap<>(); public static final Map<String,PtzDevices> ptzDevicesMap = new HashMap<>(); public static final Map<String,String> profileTokenMap = new HashMap<>(); public static final Map<String,OnvifDevice> deviceMap = new HashMap<>(); public static void connect(String ip, String user, String password) { try { if(deviceMap.get(ip) != null) return; final OnvifDevice onvifDevice = new OnvifDevice(ip, user, password); deviceMap.put(ip,onvifDevice); String profileToken = onvifDevice.getInitialDevices().getProfiles().get(0).getToken(); PtzDevices ptzDevices = new PtzDevices(onvifDevice); ptzNodeMap.put(ip, ptzDevices.getNode(profileToken)); ptzDevicesMap.put(ip , ptzDevices); profileTokenMap.put(ip , profileToken); } catch (ConnectException | SOAPException e) { e.printStackTrace(); } } public static boolean turn(TurnDTO turnDTO) { String ip = turnDTO.getTerminal(); connect(ip,turnDTO.getUsername(),turnDTO.getPassword()); PtzDevices ptzDevices = ptzDevicesMap.get(ip); PTZVector position = ptzDevices.getStatus(profileTokenMap.get(ip)).getPosition(); turnDTO.setX(turnDTO.getX()+position.getPanTilt().getX()); turnDTO.setY(turnDTO.getY()+position.getPanTilt().getY()); turnDTO.setZoom(turnDTO.getZoom()+position.getZoom().getX()); try { return deviceMap.get(ip).getPtzDevices() .absoluteMove(ptzNodeMap.get(ip), profileTokenMap.get(ip), turnDTO.getX(), turnDTO.getY(), turnDTO.getZoom()); } catch (SOAPException e) { e.printStackTrace(); return false; } } public static String getVS(GetVSDTO getVSDTO){ String ip = getVSDTO.getTerminal(); connect(ip,getVSDTO.getUsername(),getVSDTO.getPassword()); final MediaDevices mediaDevices = deviceMap.get(ip).getMediaDevices(); try { return mediaDevices.getHTTPStreamUri(profileTokenMap.get(ip)); } catch (ConnectException | SOAPException e) { e.printStackTrace(); throw new TourBizException("获取视频流失败"); } } public static String getSnapshot(GetVSDTO getVSDTO){ String ip = getVSDTO.getTerminal(); final MediaDevices mediaDevices = deviceMap.get(ip).getMediaDevices(); try { return mediaDevices.getSnapshotUri(profileTokenMap.get(ip)); } catch (ConnectException | SOAPException e) { e.printStackTrace(); throw new TourBizException("获取视频截图失败"); } } }
- 截止到现在,已经可以控制视频的转向、焦距,以及获取rtsp的视频流
-
追加解决问题
- 问题来源:前端说rtsp视频流无法在video标签进行播放,必须用插件,太麻烦,需要我解析成rtmp
- rtmp可以通过ffmpeg进行推流转换,需要nginx支持rtmp直播流,很遗憾,我弄了一下午也没弄出来,服务器是centos,无法下载相应的环境
当然如果有朋友在centos下的nginx配置了rtmp视频直播流,请告诉我,让我好好学习一下,万分感谢
- 转换成rtmp被pass后,考虑在前端获取视频流的时候,将视频分片截取放在服务器中,定时的清除缓存文件,在关闭的时候进行请求,结束视频的截取,删除对应的所有文件
- 先出ffmpeg命令,并在服务器上进行运行,可以成功,并且与前端沟通可以进行实时访问
ffmpeg -rtsp_transport tcp -i 'rtsp://admin:1234566@218.28.112.3:554/cam/realmonitor?channel=1&subtype=0&unicast=true&proto=Onvif' -fflags flush_packets -max_delay 1 -an -flags -global_header -hls_time 1 -hls_list_size 3 -hls_wrap 3 -vcodec copy -s 216x384 -b 1024k -y '/usr/local/nginx/document/my.m3u8'
- 命令成功接下来就是Java代码
import cn.hutool.core.thread.ThreadUtil; import cn.hutool.core.util.IdUtil; import cn.hutool.core.util.RuntimeUtil; import cn.hutool.core.util.StrUtil; import com.hjly.tour.edi.onvif.api.dto.GetVSDTO; import com.hjly.tour.edi.onvif.api.dto.TurnDTO; import com.hjly.tour.edi.onvif.api.util.OnvifUtil; import com.hjly.tour.edi.onvif.biz.service.IOnvifService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.scheduling.annotation.EnableScheduling; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Service; /** * @author: Mr. Cui **/ @Service @RequiredArgsConstructor @EnableScheduling @Slf4j public class OnvifServiceImpl implements IOnvifService { @Value("${document.dir}") private String documentDir; @Value("${document.url-prefix}") private String documentUrlPrefix; @Override public Boolean turn(TurnDTO turnDTO) { return OnvifUtil.turn(turnDTO); } @Scheduled(cron = "${task.clean-ts.cron}") public void cleanTs(){ log.info("定时删除.ts文件"); RuntimeUtil.execForStr("rm -rf " + documentDir+"*.ts"); } @Override public String getVS(GetVSDTO getVSDTO) { String rtsp = OnvifUtil.getVS(getVSDTO); rtsp = StrUtil.replace(rtsp, "rtsp://", "rtsp://" + getVSDTO.getUsername() + ":" + getVSDTO.getPassword() + "@"); log.info("rtsp {} ", rtsp); final String filePrefix = IdUtil.simpleUUID(); final String m3u8FileName = filePrefix + ".m3u8"; StringBuilder command = StrUtil.builder(); command.append("ffmpeg -rtsp_transport tcp -i '"); command.append(rtsp); command.append("' -fflags flush_packets -max_delay 1 -an -flags -global_header -hls_time 1 -hls_list_size 3 -hls_wrap 3 -vcodec copy -s 216x384 -b 1024k -y '"); command.append(documentDir).append(m3u8FileName); command.append("'"); ThreadUtil.execute(() -> { try { String[] cmd = new String[]{ "sh", "-c", command.toString()}; Process ffmpeg = Runtime.getRuntime().exec(cmd); int exitValue = ffmpeg.waitFor(); if (0 != exitValue) System.err.println("转换视频流失败"); } catch (Throwable e) { System.err.println("转换视频流失败"); } }); log.info("command {} ", command); return documentUrlPrefix + m3u8FileName; } public Boolean closeVS(String m3u8Url) { m3u8Url = m3u8Url.replace(documentUrlPrefix, ""); try { String[] cmd = new String[]{ "sh", "-c", "ps -ef | grep ffmpeg | grep " + m3u8Url + " | grep -v grep | awk '{print $2}' | xargs kill -9"}; Process killFfmpeg = Runtime.getRuntime().exec(cmd); int exitValue = killFfmpeg.waitFor(); if (0 != exitValue) log.info("杀死ffmpeg失败"); } catch (Throwable e) { log.info("杀死ffmpeg失败"); } RuntimeUtil.execForStr("rm -rf " + documentDir + m3u8Url); log.info("删除文件结束"); return Boolean.TRUE; } }
-
问题解决,对接结束
转载:https://blog.csdn.net/yang930207/article/details/114424424
查看评论