1. 需求

    1. 获取视频流并在内嵌video标签进行播放
    2. 操控监控进行转向、调焦操作
  2. 解决流程

    1. 官方SDK一个赛一个的不靠谱,尤其是mac开发无法加载dll文件,就算可以加载,服务器也加载不了,果断pass
    2. 从onvif入手,写一个全通用的微服务
    3. 不需要依赖,但需要如下几个包

    org.oasis_open.docs
    org.onvif.ver10
    org.onvif.ver20
    org.w3._2004_08.xop.include
    org.w3._2005
    org.xmlsoap.schemas.soap.envelope

    1. 需要写一些实体类ImagingDeviceInitialDeviceMediaDeviceOnvifDevicePtzDeviceSoap,这里就只列一下主要的
    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());
    		}
    	}
    }				
    
    1. 工具类
    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("获取视频截图失败");
            }
        }
    }
    
    1. 截止到现在,已经可以控制视频的转向、焦距,以及获取rtsp的视频流
  3. 追加解决问题

    1. 问题来源:前端说rtsp视频流无法在video标签进行播放,必须用插件,太麻烦,需要我解析成rtmp
    2. rtmp可以通过ffmpeg进行推流转换,需要nginx支持rtmp直播流,很遗憾,我弄了一下午也没弄出来,服务器是centos,无法下载相应的环境

    当然如果有朋友在centos下的nginx配置了rtmp视频直播流,请告诉我,让我好好学习一下,万分感谢

    1. 转换成rtmp被pass后,考虑在前端获取视频流的时候,将视频分片截取放在服务器中,定时的清除缓存文件,在关闭的时候进行请求,结束视频的截取,删除对应的所有文件
    2. 先出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'
    
    1. 命令成功接下来就是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;
        }
    }
    
  4. 问题解决,对接结束

Logo

权威|前沿|技术|干货|国内首个API全生命周期开发者社区

更多推荐