与支持国标摄像头对接
- 前言:不想看教程?
- 1、准备阶段
- 1.1、我们会学到什么?
- 1.2、创建项目
- 1.3、pom中用到的依赖
- 1.4 打开摄像头的网址(了解配置方式)
- 2、代码编写
- 2.1、增加项目配置
- 2.2、在config目录下创建SipConfig
- 2.3、在service目录下创建SipService
- 2.4、在adapter目录下创建如下类
- 2.5、在listener准备如下类
- 2.7、准备基础的枚举类(enums目录下)
- 3、按照项目配置摄像头的SIP
- 4、启动项目
前言:不想看教程?
想直接拿源码?Qq:1101165230
1、准备阶段
电脑和idea这个肯定是要准备的,然后准备几台(或一台)支持国标对接的摄像头。要知道摄像头的访问地址账号和密码。
1.1、我们会学到什么?
在这个项目中我们会用到一些设计模式以及一些注解平时可能不太常用的注解。我们还额外的知道了与物联网相关的一些对接知识。(文章中可能更多的是代码上的,为了便于大家直接CV。对于理论知识的大家可以私我,我可以出一期视频讲解这个项目)
1.2、创建项目
我们在这里创建的是一个WebFlux的项目,便须后期处理设备的命令。
结构如下:(ps: 在idea的Terminal中输入tree或者tree -f,-f会包含目录下文件名)
<!--打印目录-->
tree
<!--打印目录带文件-->
tree -f
<!--将目录输出到文件并保存-->
tree /f >> D:/tree.txt
gb28181-sg
│
├─src
│ ├─main
│ │ ├─java
│ │ │ └─org
│ │ │ └─ougnuhs
│ │ │ └─gb
│ │ │ │ GbApplication.java
│ │ │ ├─adapter
│ │ │ ├─config
│ │ │ ├─controller
│ │ │ ├─enums
│ │ │ │ └─base
│ │ │ ├─listener
│ │ │ ├─service
│ │ │ └─utils
│ │ └─resources
│ └─test
│ └─java
└─target
1.3、pom中用到的依赖
&emsp(这里只放了dependencies)SIP依赖特别需要。
<dependencies><!-- SpringBoot 核心包 --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter</artifactId><exclusions><exclusion><artifactId>spring-boot-starter-logging</artifactId><groupId>org.springframework.boot</groupId></exclusion></exclusions></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-aop</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-webflux</artifactId></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><optional>true</optional></dependency><!--SIP--><!-- sip协议栈 --><dependency><groupId>javax.sip</groupId><artifactId>jain-sip-ri</artifactId><version>1.3.0-91</version></dependency><dependency><groupId>log4j</groupId><artifactId>log4j</artifactId><version>1.2.17</version></dependency><!-- 引入log4j2依赖 --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-log4j2</artifactId></dependency></dependencies>
1.4 打开摄像头的网址(了解配置方式)
这里大家先看一下,熟悉摄像头配置位置,不用填写。
我这里用的是海康摄像头,摄像头的网页配置地址就是摄像头的ip地址。我们可以看到在平台接入的地方需要填写SIP的一些信息,以及提供服务的ip。
这个是我之前配置过的,对于新摄像头可能没有,不过没关系,我们在项目中配置成什么这里就填写成什么即可。
2、代码编写
2.1、增加项目配置
在前面我们创建好了项目,并知道了摄像头SIP的配置位置。接着我们在application.yml文件中增加我们的sip配置。
application.yml
server:port: 8087sip:ip: 192.168.20.78port: 5060id: 34020000002000000001domain: 3402000000password: sg@123456
2.2、在config目录下创建SipConfig
需要在config目录下创建。
import lombok.Getter;
import lombok.Setter;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;/*** @author by Guoshun* @version 1.0.0* @description SipConfig* @date 2024/3/22 10:44*/
@Getter
@Setter
@Configuration
@ConfigurationProperties(prefix = SipConfig.SIP)
public class SipConfig {public static final String SIP = "sip";/*** 默认使用 0.0.0.0*/private String monitorIp = "0.0.0.0";private String ip;private Integer port;private String id;private String password;private String domain;}
2.3、在service目录下创建SipService
import gov.nist.javax.sip.SipProviderImpl;
import gov.nist.javax.sip.SipStackImpl;
import lombok.extern.slf4j.Slf4j;
import org.ougnuhs.gb.config.SipConfig;
import org.ougnuhs.gb.listener.MySipListener;
import org.ougnuhs.gb.utils.SIPDefaultProperties;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.DependsOn;import javax.sip.*;
import java.util.Properties;/*** @author by Guoshun* @version 1.0.0* @description SipService need SipConfig* @date 2024/3/22 11:02*/
@Slf4j
@Configuration
public class SipService {@Autowiredprivate SipConfig sipConfig;@Autowiredprivate MySipListener mySipListener;private SipFactory sipFactory;private SipStackImpl sipStack;@Bean("sipFactory")SipFactory createSipFactory(){sipFactory = SipFactory.getInstance();sipFactory.setPathName("gov.nist");return sipFactory;}@Bean("sipStack")@DependsOn("sipFactory")SipStackImpl createSipStackImpl() throws PeerUnavailableException {Properties sipProperties = SIPDefaultProperties.getSipProperties(sipConfig.getMonitorIp(), false);sipStack =(SipStackImpl) sipFactory.createSipStack(sipProperties);return sipStack;}/*** 监听TCP 对于产生的异常需要在这个地方处理* @return* @throws Exception*/@Bean("tcpSipProvider")@DependsOn("sipStack")public SipProviderImpl startTcpListener() throws Exception {// 创建SIP Provider并绑定到SIP StackListeningPoint lp = sipStack.createListeningPoint(sipConfig.getMonitorIp(), sipConfig.getPort(), "TCP");SipProviderImpl sipProvider = (SipProviderImpl)sipStack.createSipProvider(lp);// 注册SIP ServletsipProvider.addSipListener(mySipListener);log.info("TCP Start SUCCESS");return sipProvider;}/*** 监听TCP 对于产生的异常需要在这个地方处理* @return* @throws Exception*/@Bean("udpSipProvider")@DependsOn("sipStack")public SipProviderImpl startUdpListener() throws Exception {// 创建SIP Provider并绑定到SIP StackListeningPoint lp = sipStack.createListeningPoint(sipConfig.getMonitorIp(), sipConfig.getPort(), "UDP");SipProviderImpl sipProvider = (SipProviderImpl)sipStack.createSipProvider(lp);// 注册SIP ServletsipProvider.addSipListener(mySipListener);log.info("UDP Start SUCCESS");return sipProvider;}}
2.4、在adapter目录下创建如下类
import org.springframework.beans.factory.InitializingBean;import javax.sip.RequestEvent;/*** @author by Guoshun* @version 1.0.0* @description SIP事件处理接口* @date 2024/3/22 13:59*/
public interface ISIPEventHandler extends InitializingBean {/*** 事件处理方法* @param requestEvent*/void process(RequestEvent requestEvent);}
@Slf4j
public class SIPFactory {public static Map<String, ISIPEventHandler> requestHandlerMap = new ConcurrentHashMap<>();public static void register(String key, ISIPEventHandler handler){if(!StringUtils.hasText(key) || ObjectUtils.isEmpty(handler)){log.info("error: key or handler is null");return;}requestHandlerMap.put(key, handler);log.info("id:{}, handler:{}, register success", key, handler );}public static ISIPEventHandler getInvokeStrategy(String key) throws NoSuchMethodException {if(!requestHandlerMap.containsKey(key)){throw new NoSuchMethodException("未找到执行该方法的策略 key: " + key);}return requestHandlerMap.get(key);}
}
package org.ougnuhs.gb.adapter;import gov.nist.javax.sip.SipProviderImpl;
import gov.nist.javax.sip.SipStackImpl;
import gov.nist.javax.sip.message.SIPRequest;
import gov.nist.javax.sip.stack.SIPServerTransactionImpl;
import lombok.extern.slf4j.Slf4j;
import org.ougnuhs.gb.enums.TransportTypeEnum;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;import javax.sip.*;
import javax.sip.header.ViaHeader;
import javax.sip.message.MessageFactory;
import javax.sip.message.Request;/*** @author by Guoshun* @version 1.0.0* @description 模板方法* @date 2024/3/22 16:43*/
@Slf4j
public abstract class SIPRequestProcessorParent {@Autowired@Qualifier(value="tcpSipProvider")private SipProviderImpl tcpSipProvider;@Autowired@Qualifier(value="udpSipProvider")private SipProviderImpl udpSipProvider;/*** 根据 RequestEvent 获取 ServerTransaction* @param evt* @return*/public ServerTransaction getServerTransaction(RequestEvent evt) {Request request = evt.getRequest();SIPServerTransactionImpl serverTransaction = (SIPServerTransactionImpl)evt.getServerTransaction();// 判断TCP还是UDPboolean isTcp = transportTypeIsTcp(request);if (serverTransaction != null && serverTransaction.getOriginalRequest() == null) {serverTransaction.setOriginalRequest((SIPRequest) evt.getRequest());}if (serverTransaction == null) {try {if (isTcp) {SipStackImpl stack = (SipStackImpl)tcpSipProvider.getSipStack();serverTransaction = (SIPServerTransactionImpl) stack.findTransaction((SIPRequest)request, true);if (serverTransaction == null) {serverTransaction = (SIPServerTransactionImpl)tcpSipProvider.getNewServerTransaction(request);}} else {SipStackImpl stack = (SipStackImpl)udpSipProvider.getSipStack();serverTransaction = (SIPServerTransactionImpl) stack.findTransaction((SIPRequest)request, true);if (serverTransaction == null) {serverTransaction = (SIPServerTransactionImpl)udpSipProvider.getNewServerTransaction(request);}}} catch (TransactionAlreadyExistsException e) {log.error(e.getMessage());} catch (TransactionUnavailableException e) {log.error(e.getMessage());}}return serverTransaction;}/*** 判断通讯的方式是TCP吗* @param request* @return*/private boolean transportTypeIsTcp(Request request){ViaHeader reqViaHeader = (ViaHeader) request.getHeader(ViaHeader.NAME);String transport = reqViaHeader.getTransport();log.info("通讯方式是:{}", transport);return transport.equalsIgnoreCase(TransportTypeEnum.TCP.getValue());}//FIXME 在SipService中不是有一个这个对象吗?public MessageFactory getMessageFactory() {try {return SipFactory.getInstance().createMessageFactory();} catch (PeerUnavailableException e) {e.printStackTrace();}return null;}}
package org.ougnuhs.gb.adapter;import gov.nist.javax.sip.RequestEventExt;
import gov.nist.javax.sip.address.AddressImpl;
import gov.nist.javax.sip.address.SipUri;
import gov.nist.javax.sip.clientauthutils.DigestServerAuthenticationHelper;
import lombok.extern.slf4j.Slf4j;
import org.ougnuhs.gb.config.SipConfig;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.util.ObjectUtils;import javax.sip.InvalidArgumentException;
import javax.sip.RequestEvent;
import javax.sip.ServerTransaction;
import javax.sip.SipException;
import javax.sip.header.AuthorizationHeader;
import javax.sip.header.ExpiresHeader;
import javax.sip.header.FromHeader;
import javax.sip.header.ViaHeader;
import javax.sip.message.Request;
import javax.sip.message.Response;
import java.security.NoSuchAlgorithmException;
import java.text.ParseException;/*** @author by Guoshun* @version 1.0.0* @description 提供注册事件的处理器* TODO 返回Response; 以及错误信息返回需要统一,不能随便写返回的错误!* @date 2024/3/22 14:17*/
@Slf4j
@Component
public class SIPRegisterEventHandler extends SIPRequestProcessorParent implements ISIPEventHandler {@Autowiredprivate SipConfig sipConfig;private static final String KEY = "REGISTER";private Response response;@Overridepublic void afterPropertiesSet() throws Exception {SIPFactory.register(KEY, this);}@Overridepublic void process(RequestEvent requestEvent) {RequestEventExt requestEventExt = (RequestEventExt) requestEvent;String ipAddress = requestEventExt.getRemoteIpAddress() + ":" + requestEventExt.getRemotePort();log.info("ipAddress:{}", ipAddress);//取出request对象Request request = requestEventExt.getRequest();//FromHeaderfromHeader(request);//AuthorizationHeaderauthorizationHeader(request);//ExpiresHeaderexpiresHeader(request);//ViaHeaderviaHeader(request);//TODO 假设一切都ok。我们直接进行回复成功try {response = getMessageFactory().createResponse(Response.OK, request);sendResponse(requestEvent, response);}catch (ParseException parseException){parseException.printStackTrace();log.error("SIPRegisterEventHandler createResponse fail");}}/*** 注册参数-主要是为了拿到SIP用户名,这个用户不能重复,如果平台需要级联多个摄像头,该SIP必须唯一* deviceId 对应的就是SIP用户名* @param request*/private void fromHeader(Request request){FromHeader fromHeader =(FromHeader) request.getHeader(FromHeader.NAME);AddressImpl addressImpl = (AddressImpl) fromHeader.getAddress();SipUri sipUri = (SipUri)addressImpl.getURI();String deviceId = sipUri.getUser();log.info("fromHeader:{} \t addressImpl:{} \t sipUrl:{} \t deviceId:{}",fromHeader, addressImpl, sipUri, deviceId);}/*** 携带参数* @param request*/private void expiresHeader(Request request){ExpiresHeader expiresHeader =(ExpiresHeader) request.getHeader(ExpiresHeader.NAME);if(ObjectUtils.isEmpty(expiresHeader)){log.error("[注册请求] {}", Response.BAD_REQUEST);return;}if(expiresHeader.getExpires() == 0){log.error("[注册请求] 用户申请注销!");return;}else{log.info("[注册请求] 用户申请注册!");//TODO 放入数据库}log.info("expiresHeader:{}", expiresHeader);}/*** 口令密码等信息* @param request*/private void authorizationHeader(Request request) {AuthorizationHeader authorizationHeader =(AuthorizationHeader) request.getHeader(AuthorizationHeader.NAME);if (ObjectUtils.isEmpty(authorizationHeader) && ObjectUtils.isEmpty(sipConfig.getPassword())){log.error("[注册请求] 未携带授权");return;}try{boolean passwordCorrect = ObjectUtils.isEmpty(sipConfig.getPassword()) ||new DigestServerAuthenticationHelper().doAuthenticatePlainTextPassword(request, sipConfig.getPassword());if(!passwordCorrect){log.error("[注册请求] 密码/SIP服务器ID错误, 回复403");return;}}catch (NoSuchAlgorithmException e){log.error("[注册请求] 验证授权发生错误!");return;}log.info("authorizationHeader:{}", authorizationHeader.toString());}/*** 主要用来查看是UDP还是TCP传输* @param request*/private void viaHeader(Request request){ViaHeader viaHeader =(ViaHeader) request.getHeader(ViaHeader.NAME);String transport = viaHeader.getTransport();log.info("viaHeader:{} \t transport:{}", viaHeader, transport);}/*** 响应回复* @param requestEvent* @param response* @throws InvalidArgumentException* @throws SipException*/private void sendResponse(RequestEvent requestEvent, Response response){ServerTransaction serverTransaction = getServerTransaction(requestEvent);if (serverTransaction == null) {log.warn("[回复失败]:{}", response);return;}try {serverTransaction.sendResponse(response);log.info("SIPRegisterEventHandler sendResponse success");}catch (InvalidArgumentException | SipException exception){exception.printStackTrace();log.error("[回复发生异常]:{}", response);}finally {if (serverTransaction.getDialog() != null) {serverTransaction.getDialog().delete();}}}}
2.5、在listener准备如下类
import javax.sip.SipListener;/*** @author by Guoshun* @version 1.0.0* @description MySipService 继承 SipListener* @date 2024/3/22 11:14*/
public interface MySipListener extends SipListener {
}
import lombok.extern.slf4j.Slf4j;
import org.ougnuhs.gb.adapter.SIPFactory;
import org.springframework.stereotype.Service;import javax.sip.*;/*** @author by Guoshun* @version 1.0.0* @description IMySipService 实现 MySipService* @date 2024/3/22 11:16*/
@Slf4j
@Service
public class MySipListenerImpl implements MySipListener{/*** 摄像头上报事件* @param requestEvent*/@Overridepublic void processRequest(RequestEvent requestEvent) {String method = requestEvent.getRequest().getMethod();try {SIPFactory.getInvokeStrategy(method).process(requestEvent);}catch (Exception e){log.error("processRequest error", e);}}@Overridepublic void processResponse(ResponseEvent responseEvent) {log.info("processResponse");}@Overridepublic void processTimeout(TimeoutEvent timeoutEvent) {log.info("processTimeout");}@Overridepublic void processIOException(IOExceptionEvent exceptionEvent) {log.info("processIOException");}@Overridepublic void processTransactionTerminated(TransactionTerminatedEvent transactionTerminatedEvent) {log.info("processTransactionTerminated");}@Overridepublic void processDialogTerminated(DialogTerminatedEvent dialogTerminatedEvent) {log.info("processDialogTerminated");}
}
2.7、准备基础的枚举类(enums目录下)
/*** @author by Guoshun* @version 1.0.0* @description 传输方式类型* @date 2024/3/27 9:52*/
public enum TransportTypeEnum implements BaseEnum {TCP("TCP"),UDP("UDP");private String value;TransportTypeEnum(String value) {this.value = value;}public String getValue() {return value;}@Overridepublic String getName() {return null;}public void setValue(String value) {this.value = value;}
}
/*** @author by Guoshun* @version 1.0.0* @description 枚举基类 TODO 没有增加反序列化* @date 2024/3/27 9:54*/
public interface BaseEnum {Object getValue();default String getText() {return null;}String getName();/*** 根据枚举value生成枚举* @author liyuanxi* @date 2019/4/17 11:47*/static <E extends Enum<E> & BaseEnum> E valueOf(Object value, Class<E> clazz) {E em;E[] enums = clazz.getEnumConstants();String enumName = null;for (BaseEnum e : enums) {if (e.getValue().equals(value)) {enumName = e.getName();}}if (null != enumName) {em = Enum.valueOf(clazz, enumName);} else {throw new RuntimeException(value + "未匹配上对应的枚举");}return em;}/*** 根据枚举name生成枚举类型* @author liyuanxi* @date 2019/4/17 11:47*/static <E extends Enum<E> & BaseEnum> E nameOf(String name, Class<E> clazz) {return Enum.valueOf(clazz, name);}
}
以上全部cv走。
3、按照项目配置摄像头的SIP
需要说明的是多个摄像头的话,SIP用户名请保证唯一。
4、启动项目
启动项目后,设备会自动注册上来,我方会告知他注册成功。