commit 3fdfd28dcd21ec24fb59d0f11ce9f7ccf7db520e Author: 孙小云 Date: Tue Jan 13 18:34:03 2026 +0800 Initial commit: Add wvpjar source code diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9a74b11 --- /dev/null +++ b/.gitignore @@ -0,0 +1,81 @@ +# Maven 构建产物 +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +# 编译输出 +*.class +*.war +*.ear +*.zip +*.tar.gz +*.rar + +# Maven 构建的 jar 文件(但保留 libs 目录下的依赖 jar) +target/**/*.jar +!libs/**/*.jar + +# 日志文件 +*.log +logs/ + +# IDE 配置文件 +.idea/ +*.iml +*.ipr +*.iws +.vscode/ +.settings/ +.classpath +.project +.factorypath + +# Eclipse +.metadata +bin/ +tmp/ +*.tmp +*.bak +*.swp +*~.nib +local.properties + +# IntelliJ IDEA +out/ +.idea_modules/ + +# NetBeans +nbproject/private/ +build/ +nbbuild/ +dist/ +nbdist/ +.nb-gradle/ + +# macOS +.DS_Store +.AppleDouble +.LSOverride + +# Windows +Thumbs.db +ehthumbs.db +Desktop.ini + +# Linux +*~ + +# Node.js (web 前端) +web/node_modules/ +web/dist/ +web/.temp/ +web/.cache/ + +# Maven Wrapper +.mvn/timing.properties +.mvn/wrapper/maven-wrapper.jar + +# 临时文件 +*.tmp +*.temp diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..537b2cc --- /dev/null +++ b/pom.xml @@ -0,0 +1,512 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 3.4.4 + + + com.genersoft + wvp-pro + 2.7.4 + web video platform + 国标28181视频平台 + ${project.packaging} + + + + nexus-aliyun + Nexus aliyun + https://maven.aliyun.com/repository/public + default + + false + + + true + + + + ECC + https://maven.ecc.no/releases + + + + + + nexus-aliyun + Nexus aliyun + https://maven.aliyun.com/repository/public + + false + + + true + + + + + + UTF-8 + MMddHHmm + + + ${project.build.directory}/generated-snippets + ${project.basedir}/docs/asciidoc + ${project.build.directory}/asciidoc + ${project.build.directory}/asciidoc/html + ${project.build.directory}/asciidoc/pdf + + 21 + 21 + + + + + jar + + true + + + jar + + + + war + + war + + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-jetty + + + + + javax.servlet + javax.servlet-api + 3.1.0 + provided + + + + + + + + org.springframework.boot + spring-boot-starter-data-redis + + + org.springframework.boot + spring-boot-starter-cache + + + org.springframework.boot + spring-boot-starter-web + + + + org.springframework.boot + spring-boot-starter-websocket + + + + + org.springframework.boot + spring-boot-starter-aop + + + + org.springframework.session + spring-session-core + + + + org.springframework.boot + spring-boot-configuration-processor + true + + + org.mybatis.spring.boot + mybatis-spring-boot-starter + 3.0.4 + + + com.zaxxer + HikariCP + + + + + org.springframework.boot + spring-boot-starter-security + + + + org.springframework.boot + spring-boot-starter-jdbc + + + + + com.h2database + h2 + 2.3.232 + + + + + com.mysql + mysql-connector-j + 8.2.0 + + + + + org.postgresql + postgresql + 42.5.1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + com.github.pagehelper + pagehelper-spring-boot-starter + 2.1.1 + + + + + org.springdoc + springdoc-openapi-starter-webmvc-ui + 2.8.6 + + + org.springdoc + springdoc-openapi-starter-webmvc-api + 2.8.6 + + + org.springdoc + springdoc-openapi-security + 1.8.0 + + + + com.github.xiaoymin + knife4j-openapi3-jakarta-spring-boot-starter + 4.5.0 + + + + + + + + + + + + javax.sip + jain-sip-ri + 1.3.0-91 + + + + + org.slf4j + log4j-over-slf4j + 2.0.17 + + + + + org.dom4j + dom4j + 2.1.4 + + + + + com.alibaba.fastjson2 + fastjson2 + 2.0.57 + + + com.alibaba.fastjson2 + fastjson2-extension + 2.0.57 + + + com.alibaba.fastjson2 + fastjson2-extension-spring5 + 2.0.57 + + + + + com.squareup.okhttp3 + okhttp + 4.12.0 + + + + + com.squareup.okhttp3 + logging-interceptor + 4.12.0 + + + + + io.github.rburgst + okhttp-digest + 3.1.1 + + + + + + + + + + + + org.bitbucket.b_c + jose4j + 0.9.6 + + + + org.apache.httpcomponents + httpclient + 4.5.14 + + + + + com.alibaba + easyexcel + 4.0.3 + + + org.apache.commons + commons-compress + + + + + org.apache.commons + commons-compress + 1.27.1 + + + + + com.github.oshi + oshi-core + 6.6.5 + + + + + com.google.guava + guava + 33.4.8-jre + + + + + org.apache.ftpserver + ftpserver-core + 1.2.1 + + + + org.apache.ftpserver + ftplet-api + 1.2.1 + + + + + org.projectlombok + lombok + 1.18.38 + provided + + + + + + + + + + + io.github.sevdokimov.logviewer + log-viewer-spring-boot + 1.0.10 + + + + cn.hutool + hutool-all + 5.8.38 + + + + org.bouncycastle + bcpkix-jdk18on + 1.78.1 + + + + + no.ecc.vectortile + java-vector-tile + 1.4.1 + + + + + org.locationtech.jts + jts-core + 1.18.2 + + + + org.springframework.boot + spring-boot-starter-test + test + + + + + ${project.artifactId}-${project.version}-${maven.build.timestamp} + + + org.springframework.boot + spring-boot-maven-plugin + 3.4.10 + + true + true + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.14.0 + + 21 + 21 + + + org.projectlombok + lombok + 1.18.30 + + + + + + + pl.project13.maven + git-commit-id-plugin + 4.9.10 + + true + false + yyyyMMdd + + + + + org.apache.maven.plugins + maven-surefire-plugin + 3.2.5 + + true + + + + + org.apache.maven.plugins + maven-jar-plugin + 3.4.2 + + + maven-resources-plugin + + + copy-resources + package + + copy-resources + + + + + src/main/resources + + application.yml + application-*.yml + + + + ${project.build.directory} + + + + + + + + src/main/resources + + + src/main/java + + **/*.xml + + + + + diff --git a/src/main/java/com/genersoft/iot/vmp/VManageBootstrap.java b/src/main/java/com/genersoft/iot/vmp/VManageBootstrap.java new file mode 100644 index 0000000..cabb8b2 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/VManageBootstrap.java @@ -0,0 +1,68 @@ +package com.genersoft.iot.vmp; + +import com.genersoft.iot.vmp.jt1078.util.ClassUtil; +import com.genersoft.iot.vmp.utils.GitUtil; +import com.genersoft.iot.vmp.utils.SpringBeanFactory; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.builder.SpringApplicationBuilder; +import org.springframework.boot.web.servlet.ServletComponentScan; +import org.springframework.boot.web.servlet.support.SpringBootServletInitializer; +import org.springframework.cache.annotation.EnableCaching; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.scheduling.annotation.EnableScheduling; + +import jakarta.servlet.ServletContext; +import jakarta.servlet.ServletException; +import jakarta.servlet.SessionCookieConfig; +import jakarta.servlet.SessionTrackingMode; +import java.util.Collections; + +/** + * 启动类 + */ +@ServletComponentScan("com.genersoft.iot.vmp.conf") +@SpringBootApplication +@EnableScheduling +@EnableCaching +@Slf4j +public class VManageBootstrap extends SpringBootServletInitializer { + + private static String[] args; + private static ConfigurableApplicationContext context; + public static void main(String[] args) { + VManageBootstrap.args = args; + VManageBootstrap.context = SpringApplication.run(VManageBootstrap.class, args); + ClassUtil.context = VManageBootstrap.context; + GitUtil gitUtil = SpringBeanFactory.getBean("gitUtil"); + if (gitUtil == null) { + log.info("获取版本信息失败"); + }else { + log.info("构建版本: {}", gitUtil.getBuildVersion()); + log.info("构建时间: {}", gitUtil.getBuildDate()); + log.info("GIT信息: 分支: {}, ID: {}, 时间: {}", gitUtil.getBranch(), gitUtil.getCommitIdShort(), gitUtil.getCommitTime()); + } + } + // 项目重启 + public static void restart() { + context.close(); + VManageBootstrap.context = SpringApplication.run(VManageBootstrap.class, args); + } + + @Override + protected SpringApplicationBuilder configure(SpringApplicationBuilder application) { + return application.sources(VManageBootstrap.class); + } + + @Override + public void onStartup(ServletContext servletContext) throws ServletException { + super.onStartup(servletContext); + + servletContext.setSessionTrackingModes( + Collections.singleton(SessionTrackingMode.COOKIE) + ); + SessionCookieConfig sessionCookieConfig = servletContext.getSessionCookieConfig(); + sessionCookieConfig.setHttpOnly(true); + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/common/CivilCodePo.java b/src/main/java/com/genersoft/iot/vmp/common/CivilCodePo.java new file mode 100644 index 0000000..3885197 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/common/CivilCodePo.java @@ -0,0 +1,46 @@ +package com.genersoft.iot.vmp.common; + +import org.springframework.util.ObjectUtils; + +public class CivilCodePo { + + private String code; + + private String name; + + private String parentCode; + + public static CivilCodePo getInstance(String[] infoArray) { + CivilCodePo civilCodePo = new CivilCodePo(); + civilCodePo.setCode(infoArray[0]); + civilCodePo.setName(infoArray[1]); + if (!ObjectUtils.isEmpty(infoArray[2])) { + civilCodePo.setParentCode(infoArray[2]); + } + return civilCodePo; + } + + public String getCode() { + return code; + } + + public void setCode(String code) { + this.code = code; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getParentCode() { + return parentCode; + } + + public void setParentCode(String parentCode) { + this.parentCode = parentCode; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/common/CommonCallback.java b/src/main/java/com/genersoft/iot/vmp/common/CommonCallback.java new file mode 100644 index 0000000..819fe0d --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/common/CommonCallback.java @@ -0,0 +1,5 @@ +package com.genersoft.iot.vmp.common; + +public interface CommonCallback{ + public void run(T t); +} diff --git a/src/main/java/com/genersoft/iot/vmp/common/DeviceStatusCallback.java b/src/main/java/com/genersoft/iot/vmp/common/DeviceStatusCallback.java new file mode 100644 index 0000000..9ee9b9e --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/common/DeviceStatusCallback.java @@ -0,0 +1,7 @@ +package com.genersoft.iot.vmp.common; + +import com.genersoft.iot.vmp.gb28181.bean.SipTransactionInfo; + +public interface DeviceStatusCallback { + public void run(String deviceId, SipTransactionInfo transactionInfo); +} diff --git a/src/main/java/com/genersoft/iot/vmp/common/InviteInfo.java b/src/main/java/com/genersoft/iot/vmp/common/InviteInfo.java new file mode 100644 index 0000000..0c12796 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/common/InviteInfo.java @@ -0,0 +1,63 @@ +package com.genersoft.iot.vmp.common; + +import com.genersoft.iot.vmp.service.bean.SSRCInfo; +import lombok.Data; + +/** + * 记录每次发送invite消息的状态 + */ +@Data +public class InviteInfo { + + private String deviceId; + + private Integer channelId; + + private String stream; + + private SSRCInfo ssrcInfo; + + private String receiveIp; + + private Integer receivePort; + + private String streamMode; + + private InviteSessionType type; + + private InviteSessionStatus status; + + private StreamInfo streamInfo; + + private String mediaServerId; + + private Long expirationTime; + + private Long createTime; + + private Boolean record; + + private String startTime; + + private String endTime; + + + public static InviteInfo getInviteInfo(String deviceId, Integer channelId, String stream, SSRCInfo ssrcInfo, String mediaServerId, + String receiveIp, Integer receivePort, String streamMode, + InviteSessionType type, InviteSessionStatus status, Boolean record) { + InviteInfo inviteInfo = new InviteInfo(); + inviteInfo.setDeviceId(deviceId); + inviteInfo.setChannelId(channelId); + inviteInfo.setStream(stream); + inviteInfo.setSsrcInfo(ssrcInfo); + inviteInfo.setReceiveIp(receiveIp); + inviteInfo.setReceivePort(receivePort); + inviteInfo.setStreamMode(streamMode); + inviteInfo.setType(type); + inviteInfo.setStatus(status); + inviteInfo.setMediaServerId(mediaServerId); + inviteInfo.setRecord(record); + return inviteInfo; + } + +} diff --git a/src/main/java/com/genersoft/iot/vmp/common/InviteSessionStatus.java b/src/main/java/com/genersoft/iot/vmp/common/InviteSessionStatus.java new file mode 100644 index 0000000..04cc7c9 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/common/InviteSessionStatus.java @@ -0,0 +1,11 @@ +package com.genersoft.iot.vmp.common; + +/** + * 标识invite消息发出后的各个状态, + * 收到ok钱停止invite发送cancel, + * 收到200ok后发送BYE停止invite + */ +public enum InviteSessionStatus { + ready, + ok, +} diff --git a/src/main/java/com/genersoft/iot/vmp/common/InviteSessionType.java b/src/main/java/com/genersoft/iot/vmp/common/InviteSessionType.java new file mode 100644 index 0000000..9241305 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/common/InviteSessionType.java @@ -0,0 +1,9 @@ +package com.genersoft.iot.vmp.common; + +public enum InviteSessionType { + PLAY, + PLAYBACK, + DOWNLOAD, + BROADCAST, + TALK +} diff --git a/src/main/java/com/genersoft/iot/vmp/common/RemoteAddressInfo.java b/src/main/java/com/genersoft/iot/vmp/common/RemoteAddressInfo.java new file mode 100755 index 0000000..39478d8 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/common/RemoteAddressInfo.java @@ -0,0 +1,27 @@ +package com.genersoft.iot.vmp.common; + +public class RemoteAddressInfo { + private String ip; + private int port; + + public RemoteAddressInfo(String ip, int port) { + this.ip = ip; + this.port = port; + } + + public String getIp() { + return ip; + } + + public void setIp(String ip) { + this.ip = ip; + } + + public int getPort() { + return port; + } + + public void setPort(int port) { + this.port = port; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/common/ServerInfo.java b/src/main/java/com/genersoft/iot/vmp/common/ServerInfo.java new file mode 100644 index 0000000..fb1941a --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/common/ServerInfo.java @@ -0,0 +1,23 @@ +package com.genersoft.iot.vmp.common; + +import com.genersoft.iot.vmp.utils.DateUtil; +import lombok.Data; + +@Data +public class ServerInfo { + + private String ip; + private int port; + /** + * 现在使用的线程数 + */ + private String createTime; + + public static ServerInfo create(String ip, int port) { + ServerInfo serverInfo = new ServerInfo(); + serverInfo.setIp(ip); + serverInfo.setPort(port); + serverInfo.setCreateTime(DateUtil.getNow()); + return serverInfo; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/common/StatisticsInfo.java b/src/main/java/com/genersoft/iot/vmp/common/StatisticsInfo.java new file mode 100644 index 0000000..8d7ab28 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/common/StatisticsInfo.java @@ -0,0 +1,97 @@ +package com.genersoft.iot.vmp.common; + +import lombok.Data; + +/** + * 统计信息 + */ +@Data +public class StatisticsInfo { + + private long id; + + /** + * ID + */ + private String deviceId; + + /** + * 分支 + */ + private String branch; + + /** + * git提交版本ID + */ + private String gitCommitId; + + /** + * git地址 + */ + private String gitUrl; + + /** + * 构建版本 + */ + private String version; + + /** + * 操作系统名称 + */ + private String osName; + + /** + * 是否是docker环境 + */ + private Boolean docker; + + /** + * 架构 + */ + private String arch; + + /** + * jdk版本 + */ + private String jdkVersion; + + /** + * redis版本 + */ + private String redisVersion; + + /** + * sql数据库版本 + */ + private String sqlVersion; + + /** + * sql数据库类型, mysql/postgresql/金仓等 + */ + private String sqlType; + + /** + * 创建时间 + */ + private String time; + + @Override + public String toString() { + return "StatisticsInfo{" + + "id=" + id + + ", deviceId='" + deviceId + '\'' + + ", branch='" + branch + '\'' + + ", gitCommitId='" + gitCommitId + '\'' + + ", gitUrl='" + gitUrl + '\'' + + ", version='" + version + '\'' + + ", osName='" + osName + '\'' + + ", docker=" + docker + + ", arch='" + arch + '\'' + + ", jdkVersion='" + jdkVersion + '\'' + + ", redisVersion='" + redisVersion + '\'' + + ", sqlVersion='" + sqlVersion + '\'' + + ", sqlType='" + sqlType + '\'' + + ", time='" + time + '\'' + + '}'; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/common/StreamInfo.java b/src/main/java/com/genersoft/iot/vmp/common/StreamInfo.java new file mode 100644 index 0000000..78c93e5 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/common/StreamInfo.java @@ -0,0 +1,372 @@ +package com.genersoft.iot.vmp.common; + +import com.genersoft.iot.vmp.media.bean.MediaInfo; +import com.genersoft.iot.vmp.media.bean.MediaServer; +import com.genersoft.iot.vmp.service.bean.DownloadFileInfo; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.io.Serializable; +import java.util.Objects; + +@Data +@Schema(description = "流信息") +public class StreamInfo implements Serializable, Cloneable{ + + @Schema(description = "应用名") + private String app; + @Schema(description = "流ID") + private String stream; + @Schema(description = "设备编号") + private String deviceId; + @Schema(description = "通道ID") + private Integer channelId; + + @Schema(description = "IP") + private String ip; + + @Schema(description = "HTTP-FLV流地址") + private StreamURL flv; + + @Schema(description = "HTTPS-FLV流地址") + private StreamURL https_flv; + @Schema(description = "Websocket-FLV流地址") + private StreamURL ws_flv; + @Schema(description = "Websockets-FLV流地址") + private StreamURL wss_flv; + @Schema(description = "HTTP-FMP4流地址") + private StreamURL fmp4; + @Schema(description = "HTTPS-FMP4流地址") + private StreamURL https_fmp4; + @Schema(description = "Websocket-FMP4流地址") + private StreamURL ws_fmp4; + @Schema(description = "Websockets-FMP4流地址") + private StreamURL wss_fmp4; + @Schema(description = "HLS流地址") + private StreamURL hls; + @Schema(description = "HTTPS-HLS流地址") + private StreamURL https_hls; + @Schema(description = "Websocket-HLS流地址") + private StreamURL ws_hls; + @Schema(description = "Websockets-HLS流地址") + private StreamURL wss_hls; + @Schema(description = "HTTP-TS流地址") + private StreamURL ts; + @Schema(description = "HTTPS-TS流地址") + private StreamURL https_ts; + @Schema(description = "Websocket-TS流地址") + private StreamURL ws_ts; + @Schema(description = "Websockets-TS流地址") + private StreamURL wss_ts; + @Schema(description = "RTMP流地址") + private StreamURL rtmp; + @Schema(description = "RTMPS流地址") + private StreamURL rtmps; + @Schema(description = "RTSP流地址") + private StreamURL rtsp; + @Schema(description = "RTSPS流地址") + private StreamURL rtsps; + @Schema(description = "RTC流地址") + private StreamURL rtc; + + @Schema(description = "RTCS流地址") + private StreamURL rtcs; + @Schema(description = "流媒体节点") + private MediaServer mediaServer; + @Schema(description = "流编码信息") + private MediaInfo mediaInfo; + @Schema(description = "开始时间") + private String startTime; + @Schema(description = "结束时间") + private String endTime; + @Schema(description = "时长(回放时使用)") + private Double duration; + @Schema(description = "进度(录像下载使用)") + private double progress; + @Schema(description = "文件下载地址(录像下载使用)") + private DownloadFileInfo downLoadFilePath; + @Schema(description = "点播请求的callId") + private String callId; + + @Schema(description = "是否暂停(录像回放使用)") + private boolean pause; + + @Schema(description = "产生源类型,包括 unknown = 0,rtmp_push=1,rtsp_push=2,rtp_push=3,pull=4,ffmpeg_pull=5,mp4_vod=6,device_chn=7") + private int originType; + + @Schema(description = "originType的文本描述") + private String originTypeStr; + + @Schema(description = "转码后的视频流") + private StreamInfo transcodeStream; + + @Schema(description = "使用的WVP ID") + private String serverId; + + @Schema(description = "流绑定的流媒体操作key") + private String key; + + public void setRtmp(String host, Integer port, Integer sslPort, String app, String stream, String callIdParam) { + String file = String.format("%s/%s%s", app, stream, callIdParam); + if (port != null && port > 0) { + this.rtmp = new StreamURL("rtmp", host, port, file); + } + if (sslPort != null && sslPort > 0) { + this.rtmps = new StreamURL("rtmps", host, sslPort, file); + } + } + + public void setRtsp(String host, Integer port, Integer sslPort, String app, String stream, String callIdParam) { + String file = String.format("%s/%s%s", app, stream, callIdParam); + if (port != null && port > 0) { + this.rtsp = new StreamURL("rtsp", host, port, file); + } + if (sslPort != null && sslPort > 0) { + this.rtsps = new StreamURL("rtsps", host, sslPort, file); + } + } + + public void setFlv(String host, Integer port, Integer sslPort, String file) { + if (port != null && port > 0) { + this.flv = new StreamURL("http", host, port, file); + } + if (sslPort != null && sslPort > 0) { + this.https_flv = new StreamURL("https", host, sslPort, file); + } + } + + public void setWsFlv(String host, Integer port, Integer sslPort, String file) { + if (port != null && port > 0) { + this.ws_flv = new StreamURL("ws", host, port, file); + } + if (sslPort != null && sslPort > 0) { + this.wss_flv = new StreamURL("wss", host, sslPort, file); + } + } + + public void setFmp4(String host, Integer port, Integer sslPort, String file) { + if (port != null && port > 0) { + this.fmp4 = new StreamURL("http", host, port, file); + } + if (sslPort != null && sslPort > 0) { + this.https_fmp4 = new StreamURL("https", host, sslPort, file); + } + } + + public void setWsMp4(String host, Integer port, Integer sslPort, String file) { + if (port != null && port > 0) { + this.ws_fmp4 = new StreamURL("ws", host, port, file); + } + if (sslPort != null && sslPort > 0) { + this.wss_fmp4 = new StreamURL("wss", host, sslPort, file); + } + } + + public void setHls(String host, Integer port, Integer sslPort, String app, String stream, String callIdParam) { + String file = String.format("%s/%s/hls.m3u8%s", app, stream, callIdParam); + if (port != null && port > 0) { + this.hls = new StreamURL("http", host, port, file); + } + if (sslPort != null && sslPort > 0) { + this.https_hls = new StreamURL("https", host, sslPort, file); + } + } + + public void setWsHls(String host, Integer port, Integer sslPort, String app, String stream, String callIdParam) { + String file = String.format("%s/%s/hls.m3u8%s", app, stream, callIdParam); + if (port != null && port > 0) { + this.ws_hls = new StreamURL("ws", host, port, file); + } + if (sslPort != null && sslPort > 0) { + this.wss_hls = new StreamURL("wss", host, sslPort, file); + } + } + + public void setTs(String host, Integer port, Integer sslPort, String app, String stream, String callIdParam) { + String file = String.format("%s/%s.live.ts%s", app, stream, callIdParam); + + if (port != null && port > 0) { + this.ts = new StreamURL("http", host, port, file); + } + if (sslPort != null && sslPort > 0) { + this.https_ts = new StreamURL("https", host, sslPort, file); + } + } + + public void setWsTs(String host, Integer port, Integer sslPort, String app, String stream, String callIdParam) { + String file = String.format("%s/%s.live.ts%s", app, stream, callIdParam); + + if (port != null && port > 0) { + this.ws_ts = new StreamURL("ws", host, port, file); + } + if (sslPort != null && sslPort > 0) { + this.wss_ts = new StreamURL("wss", host, sslPort, file); + } + } + + public void setRtc(String host, Integer port, Integer sslPort, String app, String stream, String callIdParam, boolean isPlay) { + if (callIdParam != null) { + callIdParam = Objects.equals(callIdParam, "") ? callIdParam : callIdParam.replace("?", "&"); + } +// String file = String.format("%s/%s?type=%s%s", app, stream, isPlay?"play":"push", callIdParam); + String file = String.format("index/api/webrtc?app=%s&stream=%s&type=%s%s", app, stream, isPlay?"play":"push", callIdParam); + if (port > 0) { + this.rtc = new StreamURL("http", host, port, file); + } + if (sslPort > 0) { + this.rtcs = new StreamURL("https", host, sslPort, file); + } + } + + public void changeStreamIp(String localAddr) { + if (this.flv != null) { + this.flv.setHost(localAddr); + } + if (this.ws_flv != null ){ + this.ws_flv.setHost(localAddr); + } + if (this.hls != null ) { + this.hls.setHost(localAddr); + } + if (this.ws_hls != null ) { + this.ws_hls.setHost(localAddr); + } + if (this.ts != null ) { + this.ts.setHost(localAddr); + } + if (this.ws_ts != null ) { + this.ws_ts.setHost(localAddr); + } + if (this.fmp4 != null ) { + this.fmp4.setHost(localAddr); + } + if (this.ws_fmp4 != null ) { + this.ws_fmp4.setHost(localAddr); + } + if (this.rtc != null ) { + this.rtc.setHost(localAddr); + } + if (this.https_flv != null) { + this.https_flv.setHost(localAddr); + } + if (this.wss_flv != null) { + this.wss_flv.setHost(localAddr); + } + if (this.https_hls != null) { + this.https_hls.setHost(localAddr); + } + if (this.wss_hls != null) { + this.wss_hls.setHost(localAddr); + } + if (this.wss_ts != null) { + this.wss_ts.setHost(localAddr); + } + if (this.https_fmp4 != null) { + this.https_fmp4.setHost(localAddr); + } + if (this.wss_fmp4 != null) { + this.wss_fmp4.setHost(localAddr); + } + if (this.rtcs != null) { + this.rtcs.setHost(localAddr); + } + if (this.rtsp != null) { + this.rtsp.setHost(localAddr); + } + if (this.rtsps != null) { + this.rtsps.setHost(localAddr); + } + if (this.rtmp != null) { + this.rtmp.setHost(localAddr); + } + if (this.rtmps != null) { + this.rtmps.setHost(localAddr); + } + } + + + public static class TransactionInfo{ + public String callId; + public String localTag; + public String remoteTag; + public String branch; + } + + private TransactionInfo transactionInfo; + + + @Override + public StreamInfo clone() { + StreamInfo instance = null; + try{ + instance = (StreamInfo)super.clone(); + if (this.flv != null) { + instance.flv=this.flv.clone(); + } + if (this.ws_flv != null ){ + instance.ws_flv= this.ws_flv.clone(); + } + if (this.hls != null ) { + instance.hls= this.hls.clone(); + } + if (this.ws_hls != null ) { + instance.ws_hls= this.ws_hls.clone(); + } + if (this.ts != null ) { + instance.ts= this.ts.clone(); + } + if (this.ws_ts != null ) { + instance.ws_ts= this.ws_ts.clone(); + } + if (this.fmp4 != null ) { + instance.fmp4= this.fmp4.clone(); + } + if (this.ws_fmp4 != null ) { + instance.ws_fmp4= this.ws_fmp4.clone(); + } + if (this.rtc != null ) { + instance.rtc= this.rtc.clone(); + } + if (this.https_flv != null) { + instance.https_flv= this.https_flv.clone(); + } + if (this.wss_flv != null) { + instance.wss_flv= this.wss_flv.clone(); + } + if (this.https_hls != null) { + instance.https_hls= this.https_hls.clone(); + } + if (this.wss_hls != null) { + instance.wss_hls= this.wss_hls.clone(); + } + if (this.wss_ts != null) { + instance.wss_ts= this.wss_ts.clone(); + } + if (this.https_fmp4 != null) { + instance.https_fmp4= this.https_fmp4.clone(); + } + if (this.wss_fmp4 != null) { + instance.wss_fmp4= this.wss_fmp4.clone(); + } + if (this.rtcs != null) { + instance.rtcs= this.rtcs.clone(); + } + if (this.rtsp != null) { + instance.rtsp= this.rtsp.clone(); + } + if (this.rtsps != null) { + instance.rtsps= this.rtsps.clone(); + } + if (this.rtmp != null) { + instance.rtmp= this.rtmp.clone(); + } + if (this.rtmps != null) { + instance.rtmps= this.rtmps.clone(); + } + }catch(CloneNotSupportedException e) { + e.printStackTrace(); + } + return instance; + } + +} diff --git a/src/main/java/com/genersoft/iot/vmp/common/StreamURL.java b/src/main/java/com/genersoft/iot/vmp/common/StreamURL.java new file mode 100644 index 0000000..40bd7e2 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/common/StreamURL.java @@ -0,0 +1,84 @@ +package com.genersoft.iot.vmp.common; + +import io.swagger.v3.oas.annotations.media.Schema; + +import java.io.Serializable; + + +@Schema(description = "流地址信息") +public class StreamURL implements Serializable,Cloneable { + + @Schema(description = "协议") + private String protocol; + + @Schema(description = "主机地址") + private String host; + + @Schema(description = "端口") + private int port = -1; + + @Schema(description = "定位位置") + private String file; + + @Schema(description = "拼接后的地址") + private String url; + + public StreamURL() { + } + + public StreamURL(String protocol, String host, int port, String file) { + this.protocol = protocol; + this.host = host; + this.port = port; + this.file = file; + } + + public String getProtocol() { + return protocol; + } + + public void setProtocol(String protocol) { + this.protocol = protocol; + } + + public String getHost() { + return host; + } + + public void setHost(String host) { + this.host = host; + } + + public int getPort() { + return port; + } + + public void setPort(int port) { + this.port = port; + } + + public String getFile() { + return file; + } + + public void setFile(String file) { + this.file = file; + } + + public String getUrl() { + return this.toString(); + } + + @Override + public String toString() { + if (protocol != null && host != null && port != -1 ) { + return String.format("%s://%s:%s/%s", protocol, host, port, file); + }else { + return null; + } + } + @Override + public StreamURL clone() throws CloneNotSupportedException { + return (StreamURL) super.clone(); + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/common/SubscribeCallback.java b/src/main/java/com/genersoft/iot/vmp/common/SubscribeCallback.java new file mode 100644 index 0000000..5443b50 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/common/SubscribeCallback.java @@ -0,0 +1,7 @@ +package com.genersoft.iot.vmp.common; + +import com.genersoft.iot.vmp.gb28181.bean.SipTransactionInfo; + +public interface SubscribeCallback{ + public void run(String deviceId, SipTransactionInfo transactionInfo); +} diff --git a/src/main/java/com/genersoft/iot/vmp/common/SystemAllInfo.java b/src/main/java/com/genersoft/iot/vmp/common/SystemAllInfo.java new file mode 100644 index 0000000..48485da --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/common/SystemAllInfo.java @@ -0,0 +1,54 @@ +package com.genersoft.iot.vmp.common; + +import java.util.List; + +public class SystemAllInfo { + + private List cpu; + private List mem; + private List net; + + private long netTotal; + + private Object disk; + + public List getCpu() { + return cpu; + } + + public void setCpu(List cpu) { + this.cpu = cpu; + } + + public List getMem() { + return mem; + } + + public void setMem(List mem) { + this.mem = mem; + } + + public List getNet() { + return net; + } + + public void setNet(List net) { + this.net = net; + } + + public Object getDisk() { + return disk; + } + + public void setDisk(Object disk) { + this.disk = disk; + } + + public long getNetTotal() { + return netTotal; + } + + public void setNetTotal(long netTotal) { + this.netTotal = netTotal; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/common/VersionPo.java b/src/main/java/com/genersoft/iot/vmp/common/VersionPo.java new file mode 100644 index 0000000..f2c8454 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/common/VersionPo.java @@ -0,0 +1,149 @@ +package com.genersoft.iot.vmp.common; + +import com.alibaba.fastjson2.annotation.JSONField; + +public class VersionPo { + /** + * git的全版本号 + */ + @JSONField(name="GIT_Revision") + private String GIT_Revision; + /** + * maven版本 + */ + @JSONField(name = "Create_By") + private String Create_By; + /** + * git的分支 + */ + @JSONField(name = "GIT_BRANCH") + private String GIT_BRANCH; + /** + * git的url + */ + @JSONField(name = "GIT_URL") + private String GIT_URL; + /** + * 构建日期 + */ + @JSONField(name = "BUILD_DATE") + private String BUILD_DATE; + /** + * 构建日期 + */ + @JSONField(name = "GIT_DATE") + private String GIT_DATE; + /** + * 项目名称 配合pom使用 + */ + @JSONField(name = "artifactId") + private String artifactId; + /** + * git局部版本号 + */ + @JSONField(name = "GIT_Revision_SHORT") + private String GIT_Revision_SHORT; + /** + * 项目的版本如2.0.1.0 配合pom使用 + */ + @JSONField(name = "version") + private String version; + /** + * 子系统名称 + */ + @JSONField(name = "project") + private String project; + /** + * jdk版本 + */ + @JSONField(name="Build_Jdk") + private String Build_Jdk; + + public void setGIT_Revision(String GIT_Revision) { + this.GIT_Revision = GIT_Revision; + } + + public void setCreate_By(String create_By) { + Create_By = create_By; + } + + public void setGIT_BRANCH(String GIT_BRANCH) { + this.GIT_BRANCH = GIT_BRANCH; + } + + public void setGIT_URL(String GIT_URL) { + this.GIT_URL = GIT_URL; + } + + public void setBUILD_DATE(String BUILD_DATE) { + this.BUILD_DATE = BUILD_DATE; + } + + public void setArtifactId(String artifactId) { + this.artifactId = artifactId; + } + + public void setGIT_Revision_SHORT(String GIT_Revision_SHORT) { + this.GIT_Revision_SHORT = GIT_Revision_SHORT; + } + + public void setVersion(String version) { + this.version = version; + } + + public void setProject(String project) { + this.project = project; + } + + public void setBuild_Jdk(String build_Jdk) { + Build_Jdk = build_Jdk; + } + + public String getGIT_Revision() { + return GIT_Revision; + } + + public String getCreate_By() { + return Create_By; + } + + public String getGIT_BRANCH() { + return GIT_BRANCH; + } + + public String getGIT_URL() { + return GIT_URL; + } + + public String getBUILD_DATE() { + return BUILD_DATE; + } + + public String getArtifactId() { + return artifactId; + } + + public String getGIT_Revision_SHORT() { + return GIT_Revision_SHORT; + } + + public String getVersion() { + return version; + } + + public String getProject() { + return project; + } + + public String getBuild_Jdk() { + return Build_Jdk; + } + + public String getGIT_DATE() { + return GIT_DATE; + } + + public void setGIT_DATE(String GIT_DATE) { + this.GIT_DATE = GIT_DATE; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/common/VideoManagerConstants.java b/src/main/java/com/genersoft/iot/vmp/common/VideoManagerConstants.java new file mode 100644 index 0000000..6129cb2 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/common/VideoManagerConstants.java @@ -0,0 +1,180 @@ +package com.genersoft.iot.vmp.common; + +/** + * @description: 定义常量 + * @author: swwheihei + * @date: 2019年5月30日 下午3:04:04 + * + */ +public class VideoManagerConstants { + + public static final String WVP_SERVER_PREFIX = "VMP_SIGNALLING_SERVER_INFO_"; + + public static final String WVP_SERVER_LIST = "VMP_SERVER_LIST"; + + public static final String WVP_SERVER_STREAM_PREFIX = "VMP_SIGNALLING_STREAM_"; + + public static final String MEDIA_SERVER_PREFIX = "VMP_MEDIA_SERVER_INFO:"; + + public static final String ONLINE_MEDIA_SERVERS_PREFIX = "VMP_ONLINE_MEDIA_SERVERS:"; + + public static final String DEVICE_PREFIX = "VMP_DEVICE_INFO"; + + public static final String INVITE_PREFIX = "VMP_GB_INVITE_INFO"; + + public static final String SEND_RTP_PORT = "VM_SEND_RTP_PORT:"; + public static final String SEND_RTP_INFO_CALLID = "VMP_SEND_RTP_INFO:CALL_ID:"; + public static final String SEND_RTP_INFO_STREAM = "VMP_SEND_RTP_INFO:STREAM:"; + public static final String SEND_RTP_INFO_CHANNEL = "VMP_SEND_RTP_INFO:CHANNEL:"; + + public static final String SIP_INVITE_SESSION = "VMP_SIP_INVITE_SESSION_INFO:"; + public static final String SIP_INVITE_SESSION_CALL_ID = SIP_INVITE_SESSION + "CALL_ID:"; + public static final String SIP_INVITE_SESSION_STREAM = SIP_INVITE_SESSION + "STREAM:"; + + public static final String MEDIA_STREAM_AUTHORITY = "VMP_MEDIA_STREAM_AUTHORITY"; + + public static final String SIP_CSEQ_PREFIX = "VMP_SIP_CSEQ_"; + + public static final String SIP_SUBSCRIBE_PREFIX = "VMP_SIP_SUBSCRIBE_"; + + public static final String SYSTEM_INFO_CPU_PREFIX = "VMP_SYSTEM_INFO_CPU_"; + + public static final String SYSTEM_INFO_MEM_PREFIX = "VMP_SYSTEM_INFO_MEM_"; + + public static final String SYSTEM_INFO_NET_PREFIX = "VMP_SYSTEM_INFO_NET_"; + + public static final String SYSTEM_INFO_DISK_PREFIX = "VMP_SYSTEM_INFO_DISK_"; + public static final String BROADCAST_WAITE_INVITE = "task_broadcast_waite_invite_"; + + public static final String PUSH_STREAM_LIST = "VMP_PUSH_STREAM_LIST_"; + public static final String WAITE_SEND_PUSH_STREAM = "VMP_WAITE_SEND_PUSH_STREAM:"; + public static final String START_SEND_PUSH_STREAM = "VMP_START_SEND_PUSH_STREAM:"; + public static final String SSE_TASK_KEY = "SSE_TASK_"; + public static final String DRAW_THIN_PROCESS_PREFIX = "VMP_DRAW_THIN_PROCESS_"; + + + + + //************************** redis 消息********************************* + + /** + * 流变化的通知 + */ + public static final String WVP_MSG_STREAM_CHANGE_PREFIX = "WVP_MSG_STREAM_CHANGE_"; + + /** + * 接收推流设备的GPS变化通知 + */ + public static final String VM_MSG_GPS = "VM_MSG_GPS"; + + /** + * 接收推流设备的GPS变化通知 + */ + public static final String VM_MSG_PUSH_STREAM_STATUS_CHANGE = "VM_MSG_PUSH_STREAM_STATUS_CHANGE"; + /** + * 接收推流设备列表更新变化通知 + */ + public static final String VM_MSG_PUSH_STREAM_LIST_CHANGE = "VM_MSG_PUSH_STREAM_LIST_CHANGE"; + + /** + * 请求同步三方组织结构 + */ + public static final String VM_MSG_GROUP_LIST_REQUEST = "VM_MSG_GROUP_LIST_REQUEST"; + + /** + * 同步三方组织结构回复 + */ + public static final String VM_MSG_GROUP_LIST_RESPONSE = "VM_MSG_GROUP_LIST_RESPONSE"; + + /** + * 同步三方组织结构回复 + */ + public static final String VM_MSG_GROUP_LIST_CHANGE = "VM_MSG_GROUP_LIST_CHANGE"; + + /** + * redis 消息通知设备推流到平台 + */ + public static final String VM_MSG_STREAM_PUSH_REQUESTED = "VM_MSG_STREAM_PUSH_REQUESTED"; + + /** + * redis 消息通知上级平台开始观看流 + */ + public static final String VM_MSG_STREAM_START_PLAY_NOTIFY = "VM_MSG_STREAM_START_PLAY_NOTIFY"; + + /** + * redis 消息通知上级平台停止观看流 + */ + public static final String VM_MSG_STREAM_STOP_PLAY_NOTIFY = "VM_MSG_STREAM_STOP_PLAY_NOTIFY"; + + /** + * redis 消息接收关闭一个推流 + */ + public static final String VM_MSG_STREAM_PUSH_CLOSE_REQUESTED = "VM_MSG_STREAM_PUSH_CLOSE_REQUESTED"; + + + /** + * redis 消息通知平台通知设备推流结果 + */ + public static final String VM_MSG_STREAM_PUSH_RESPONSE = "VM_MSG_STREAM_PUSH_RESPONSE"; + + /** + * redis 通知平台关闭推流 + */ + public static final String VM_MSG_STREAM_PUSH_CLOSE = "VM_MSG_STREAM_PUSH_CLOSE"; + + /** + * redis 消息请求所有的在线通道 + */ + public static final String VM_MSG_GET_ALL_ONLINE_REQUESTED = "VM_MSG_GET_ALL_ONLINE_REQUESTED"; + + /** + * 报警订阅的通知(收到报警向redis发出通知) + */ + public static final String VM_MSG_SUBSCRIBE_ALARM = "alarm"; + + + /** + * 报警通知的发送 (收到redis发出的通知,转发给其他平台) + */ + public static final String VM_MSG_SUBSCRIBE_ALARM_RECEIVE= "alarm_receive"; + + /** + * 设备状态订阅的通知 + */ + public static final String VM_MSG_SUBSCRIBE_DEVICE_STATUS = "device"; + + + + + //************************** 第三方 **************************************** + + public static final String WVP_STREAM_GB_ID_PREFIX = "memberNo_"; + public static final String WVP_STREAM_GPS_MSG_PREFIX = "WVP_STREAM_GPS_MSG_"; + public static final String WVP_OTHER_SEND_RTP_INFO = "VMP_OTHER_SEND_RTP_INFO_"; + public static final String WVP_OTHER_SEND_PS_INFO = "VMP_OTHER_SEND_PS_INFO_"; + public static final String WVP_OTHER_RECEIVE_RTP_INFO = "VMP_OTHER_RECEIVE_RTP_INFO_"; + public static final String WVP_OTHER_RECEIVE_PS_INFO = "VMP_OTHER_RECEIVE_PS_INFO_"; + + /** + * Redis Const + * 设备录像信息结果前缀 + */ + public static final String REDIS_RECORD_INFO_RES_PRE = "GB_RECORD_INFO_RES_"; + /** + * Redis Const + * 设备录像信息结果前缀 + */ + public static final String REDIS_RECORD_INFO_RES_COUNT_PRE = "GB_RECORD_INFO_RES_COUNT:"; + + //************************** 1078 **************************************** + + + public static final String INVITE_INFO_1078_POSITION = "INVITE_INFO_1078_POSITION:"; + public static final String INVITE_INFO_1078_PLAY = "INVITE_INFO_1078_PLAY:"; + public static final String INVITE_INFO_1078_PLAYBACK = "INVITE_INFO_1078_PLAYBACK:"; + public static final String INVITE_INFO_1078_TALK = "INVITE_INFO_1078_TALK:"; + + + public static final String RECORD_LIST_1078 = "RECORD_LIST_1078:"; + +} diff --git a/src/main/java/com/genersoft/iot/vmp/common/enums/ChannelDataType.java b/src/main/java/com/genersoft/iot/vmp/common/enums/ChannelDataType.java new file mode 100644 index 0000000..f4b6b86 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/common/enums/ChannelDataType.java @@ -0,0 +1,34 @@ +package com.genersoft.iot.vmp.common.enums; + +/** + * 支持的通道数据类型 + */ + +public class ChannelDataType { + + public final static int GB28181 = 1; + public final static int STREAM_PUSH = 2; + public final static int STREAM_PROXY = 3; + public final static int JT_1078 = 200; + + public final static String PLAY_SERVICE = "sourceChannelPlayService"; + public final static String PLAYBACK_SERVICE = "sourceChannelPlaybackService"; + public final static String DOWNLOAD_SERVICE = "sourceChannelDownloadService"; + public final static String PTZ_SERVICE = "sourceChannelPTZService"; + + + public static String getDateTypeDesc(Integer dataType) { + if (dataType == null) { + return "未知"; + } + return switch (dataType) { + case ChannelDataType.GB28181 -> "国标28181"; + case ChannelDataType.STREAM_PUSH -> "推流设备"; + case ChannelDataType.STREAM_PROXY -> "拉流代理"; + case ChannelDataType.JT_1078 -> "部标设备"; + default -> "未知"; + }; + } + + +} diff --git a/src/main/java/com/genersoft/iot/vmp/common/enums/DeviceControlType.java b/src/main/java/com/genersoft/iot/vmp/common/enums/DeviceControlType.java new file mode 100644 index 0000000..02202d8 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/common/enums/DeviceControlType.java @@ -0,0 +1,77 @@ +package com.genersoft.iot.vmp.common.enums; + +import org.dom4j.Element; +import org.springframework.util.ObjectUtils; + + +/** + * @author gaofuwang + * @date 2023/01/18/ 10:09:00 + * @since 1.0 + */ +public enum DeviceControlType { + + /** + * 云台控制 + * 上下左右,预置位,扫描,辅助功能,巡航 + */ + PTZ("PTZCmd","云台控制"), + /** + * 远程启动 + */ + TELE_BOOT("TeleBoot","远程启动"), + /** + * 录像控制 + */ + RECORD("RecordCmd","录像控制"), + /** + * 布防撤防 + */ + GUARD("GuardCmd","布防撤防"), + /** + * 告警控制 + */ + ALARM("AlarmCmd","告警控制"), + /** + * 强制关键帧 + */ + I_FRAME("IFameCmd","强制关键帧"), + /** + * 拉框放大 + */ + DRAG_ZOOM_IN("DragZoomIn","拉框放大"), + /** + * 拉框缩小 + */ + DRAG_ZOOM_OUT("DragZoomOut","拉框缩小"), + /** + * 看守位 + */ + HOME_POSITION("HomePosition","看守位"); + + private final String val; + + private final String desc; + + DeviceControlType(String val, String desc) { + this.val = val; + this.desc = desc; + } + + public String getVal() { + return val; + } + + public String getDesc() { + return desc; + } + + public static DeviceControlType typeOf(Element rootElement) { + for (DeviceControlType item : DeviceControlType.values()) { + if (!ObjectUtils.isEmpty(rootElement.element(item.val)) || !ObjectUtils.isEmpty(rootElement.elements(item.val))) { + return item; + } + } + return null; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/conf/CivilCodeFileConf.java b/src/main/java/com/genersoft/iot/vmp/conf/CivilCodeFileConf.java new file mode 100644 index 0000000..15dd874 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/conf/CivilCodeFileConf.java @@ -0,0 +1,78 @@ +package com.genersoft.iot.vmp.conf; + +import com.genersoft.iot.vmp.common.CivilCodePo; +import com.genersoft.iot.vmp.utils.CivilCodeUtil; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.CommandLineRunner; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Lazy; +import org.springframework.core.annotation.Order; +import org.springframework.core.io.ClassPathResource; +import org.springframework.util.ObjectUtils; + +import java.io.BufferedReader; +import java.io.File; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; + +/** + * 启动时读取行政区划表 + */ +@Slf4j +@Configuration +@Order(value=15) +public class CivilCodeFileConf implements CommandLineRunner { + + @Autowired + @Lazy + private UserSetting userSetting; + + @Override + public void run(String... args) throws Exception { + if (ObjectUtils.isEmpty(userSetting.getCivilCodeFile())) { + log.warn("[行政区划] 文件未设置,可能造成目录刷新结果不完整"); + return; + } + InputStream inputStream; + if (userSetting.getCivilCodeFile().startsWith("classpath:")){ + String filePath = userSetting.getCivilCodeFile().substring("classpath:".length()); + ClassPathResource civilCodeFile = new ClassPathResource(filePath); + if (!civilCodeFile.exists()) { + log.warn("[行政区划] 文件<{}>不存在,可能造成目录刷新结果不完整", userSetting.getCivilCodeFile()); + return; + } + inputStream = civilCodeFile.getInputStream(); + + }else { + File civilCodeFile = new File(userSetting.getCivilCodeFile()); + if (!civilCodeFile.exists()) { + log.warn("[行政区划] 文件<{}>不存在,可能造成目录刷新结果不完整", userSetting.getCivilCodeFile()); + return; + } + inputStream = Files.newInputStream(civilCodeFile.toPath()); + } + + BufferedReader inputStreamReader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8)); + int index = -1; + String line; + while ((line = inputStreamReader.readLine()) != null) { + index ++; + if (index == 0) { + continue; + } + String[] infoArray = line.split(","); + CivilCodePo civilCodePo = CivilCodePo.getInstance(infoArray); + CivilCodeUtil.INSTANCE.add(civilCodePo); + } + inputStreamReader.close(); + inputStream.close(); + if (CivilCodeUtil.INSTANCE.isEmpty()) { + log.warn("[行政区划] 文件内容为空,可能造成目录刷新结果不完整"); + }else { + log.info("[行政区划] 加载成功,共加载数据{}条", CivilCodeUtil.INSTANCE.size()); + } + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/conf/CloudRecordTimer.java b/src/main/java/com/genersoft/iot/vmp/conf/CloudRecordTimer.java new file mode 100644 index 0000000..cf4fff3 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/conf/CloudRecordTimer.java @@ -0,0 +1,77 @@ +package com.genersoft.iot.vmp.conf; + + +import com.genersoft.iot.vmp.conf.exception.ControllerException; +import com.genersoft.iot.vmp.media.bean.MediaServer; +import com.genersoft.iot.vmp.media.service.IMediaServerService; +import com.genersoft.iot.vmp.service.bean.CloudRecordItem; +import com.genersoft.iot.vmp.storager.dao.CloudRecordServiceMapper; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +import java.io.File; +import java.util.Calendar; +import java.util.Date; +import java.util.List; + +/** + * 录像文件定时删除 + */ +@Slf4j +@Component +public class CloudRecordTimer { + + @Autowired + private IMediaServerService mediaServerService; + + @Autowired + private CloudRecordServiceMapper cloudRecordServiceMapper; + + /** + * 定时查询待删除的录像文件 + */ +// @Scheduled(fixedRate = 10000) //每五秒执行一次,方便测试 + @Scheduled(cron = "0 0 0 * * ?") //每天的0点执行 + public void execute(){ + log.info("[录像文件定时清理] 开始清理过期录像文件"); + // 获取配置了assist的流媒体节点 + List mediaServerItemList = mediaServerService.getAllOnline(); + if (mediaServerItemList.isEmpty()) { + return; + } + long result = 0; + for (MediaServer mediaServerItem : mediaServerItemList) { + + Calendar lastCalendar = Calendar.getInstance(); + if (mediaServerItem.getRecordDay() > 0) { + lastCalendar.setTime(new Date()); + // 获取保存的最后截至日[期,因为每个节点都有一个日期,也就是支持每个节点设置不同的保存日期, + lastCalendar.add(Calendar.DAY_OF_MONTH, -mediaServerItem.getRecordDay()); + Long lastDate = lastCalendar.getTimeInMillis(); + + // 获取到截至日期之前的录像文件列表,文件列表满足未被收藏和保持的。这两个字段目前共能一致, + // 为我自己业务系统相关的代码,大家使用的时候直接使用收藏(collect)这一个类型即可 + List cloudRecordItemList = cloudRecordServiceMapper.queryRecordListForDelete(lastDate, mediaServerItem.getId()); + if (cloudRecordItemList.isEmpty()) { + continue; + } + // TODO 后续可以删除空了的过期日期文件夹 + for (CloudRecordItem cloudRecordItem : cloudRecordItemList) { + String date = new File(cloudRecordItem.getFilePath()).getParentFile().getName(); + try { + boolean deleteResult = mediaServerService.deleteRecordDirectory(mediaServerItem, cloudRecordItem.getApp(), + cloudRecordItem.getStream(), date, cloudRecordItem.getFileName()); + if (deleteResult) { + log.warn("[录像文件定时清理] 删除磁盘文件成功: {}", cloudRecordItem.getFilePath()); + } + }catch (ControllerException ignored) {} + + } + result += cloudRecordServiceMapper.deleteList(cloudRecordItemList); + } + } + log.info("[录像文件定时清理] 共清理{}个过期录像文件", result); + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/conf/DynamicTask.java b/src/main/java/com/genersoft/iot/vmp/conf/DynamicTask.java new file mode 100644 index 0000000..3a1fb91 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/conf/DynamicTask.java @@ -0,0 +1,159 @@ +package com.genersoft.iot.vmp.conf; + +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.ObjectUtils; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler; +import org.springframework.stereotype.Component; + +import jakarta.annotation.PostConstruct; +import java.time.Instant; +import java.util.Date; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; + +/** + * 动态定时任务 + * @author lin + */ +@Slf4j +@Component +public class DynamicTask { + + private ThreadPoolTaskScheduler threadPoolTaskScheduler; + + private final Map> futureMap = new ConcurrentHashMap<>(); + private final Map runnableMap = new ConcurrentHashMap<>(); + + @PostConstruct + public void DynamicTask() { + threadPoolTaskScheduler = new ThreadPoolTaskScheduler(); + threadPoolTaskScheduler.setPoolSize(300); + threadPoolTaskScheduler.setWaitForTasksToCompleteOnShutdown(true); + threadPoolTaskScheduler.setAwaitTerminationSeconds(10); + threadPoolTaskScheduler.setThreadNamePrefix("dynamicTask-"); + threadPoolTaskScheduler.initialize(); + } + + /** + * 循环执行的任务 + * @param key 任务ID + * @param task 任务 + * @param cycleForCatalog 间隔 毫秒 + * @return + */ + public void startCron(String key, Runnable task, int cycleForCatalog) { + if(ObjectUtils.isEmpty(key)) { + return; + } + ScheduledFuture future = futureMap.get(key); + if (future != null) { + if (future.isCancelled()) { + log.debug("任务【{}】已存在但是关闭状态!!!", key); + } else { + log.debug("任务【{}】已存在且已启动!!!", key); + return; + } + } + // scheduleWithFixedDelay 必须等待上一个任务结束才开始计时period, cycleForCatalog表示执行的间隔 + + future = threadPoolTaskScheduler.scheduleAtFixedRate(task, new Date(System.currentTimeMillis() + cycleForCatalog), cycleForCatalog); + if (future != null){ + futureMap.put(key, future); + runnableMap.put(key, task); + log.debug("任务【{}】启动成功!!!", key); + }else { + log.debug("任务【{}】启动失败!!!", key); + } + } + + /** + * 延时任务 + * @param key 任务ID + * @param task 任务 + * @param delay 延时 /毫秒 + * @return + */ + public void startDelay(String key, Runnable task, int delay) { + if(ObjectUtils.isEmpty(key)) { + return; + } + stop(key); + + // 获取执行的时刻 + Instant startInstant = Instant.now().plusMillis(TimeUnit.MILLISECONDS.toMillis(delay)); + + ScheduledFuture future = futureMap.get(key); + if (future != null) { + if (future.isCancelled()) { + log.debug("任务【{}】已存在但是关闭状态!!!", key); + } else { + log.debug("任务【{}】已存在且已启动!!!", key); + return; + } + } + // scheduleWithFixedDelay 必须等待上一个任务结束才开始计时period, cycleForCatalog表示执行的间隔 + future = threadPoolTaskScheduler.schedule(task, startInstant); + if (future != null){ + futureMap.put(key, future); + runnableMap.put(key, task); + log.debug("任务【{}】启动成功!!!", key); + }else { + log.debug("任务【{}】启动失败!!!", key); + } + } + + public boolean stop(String key) { + if(ObjectUtils.isEmpty(key)) { + return false; + } + boolean result = false; + if (!ObjectUtils.isEmpty(futureMap.get(key)) && !futureMap.get(key).isCancelled() && !futureMap.get(key).isDone()) { + result = futureMap.get(key).cancel(false); + futureMap.remove(key); + runnableMap.remove(key); + } + return result; + } + + public boolean contains(String key) { + if(ObjectUtils.isEmpty(key)) { + return false; + } + return futureMap.get(key) != null; + } + + public Set getAllKeys() { + return futureMap.keySet(); + } + + public Runnable get(String key) { + if(ObjectUtils.isEmpty(key)) { + return null; + } + return runnableMap.get(key); + } + + /** + * 每五分钟检查失效的任务,并移除 + */ + @Scheduled(cron="0 0/5 * * * ?") + public void execute(){ + if (futureMap.size() > 0) { + for (String key : futureMap.keySet()) { + ScheduledFuture future = futureMap.get(key); + if (future.isDone() || future.isCancelled()) { + futureMap.remove(key); + runnableMap.remove(key); + } + } + } + } + + public boolean isAlive(String key) { + return futureMap.get(key) != null && !futureMap.get(key).isDone() && !futureMap.get(key).isCancelled(); + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/conf/GlobalExceptionHandler.java b/src/main/java/com/genersoft/iot/vmp/conf/GlobalExceptionHandler.java new file mode 100644 index 0000000..5e29ff4 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/conf/GlobalExceptionHandler.java @@ -0,0 +1,88 @@ +package com.genersoft.iot.vmp.conf; + +import com.genersoft.iot.vmp.conf.exception.ControllerException; +import com.genersoft.iot.vmp.vmanager.bean.ErrorCode; +import com.genersoft.iot.vmp.vmanager.bean.WVPResult; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.authentication.BadCredentialsException; +import org.springframework.web.HttpRequestMethodNotSupportedException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +/** + * 全局异常处理 + */ +@Slf4j +@RestControllerAdvice +public class GlobalExceptionHandler { + + /** + * 默认异常处理 + * @param e 异常 + * @return 统一返回结果 + */ + @ExceptionHandler(Exception.class) + @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) + public WVPResult exceptionHandler(Exception e) { + log.error("[全局异常]: ", e); + return WVPResult.fail(ErrorCode.ERROR500.getCode(), e.getMessage()); + } + + /** + * 默认异常处理 + * @param e 异常 + * @return 统一返回结果 + */ + @ExceptionHandler(IllegalStateException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public WVPResult exceptionHandler(IllegalStateException e) { + return WVPResult.fail(ErrorCode.ERROR400); + } + + /** + * 默认异常处理 + * @param e 异常 + * @return 统一返回结果 + */ + @ExceptionHandler(HttpRequestMethodNotSupportedException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public WVPResult exceptionHandler(HttpRequestMethodNotSupportedException e) { + return WVPResult.fail(ErrorCode.ERROR400); + } + /** + * 断言异常处理 + * @param e 异常 + * @return 统一返回结果 + */ + @ExceptionHandler(IllegalArgumentException.class) + @ResponseStatus(HttpStatus.OK) + public WVPResult exceptionHandler(IllegalArgumentException e) { + return WVPResult.fail(ErrorCode.ERROR100.getCode(), e.getMessage()); + } + + + /** + * 自定义异常处理, 处理controller中返回的错误 + * @param e 异常 + * @return 统一返回结果 + */ + @ExceptionHandler(ControllerException.class) + @ResponseStatus(HttpStatus.OK) + public ResponseEntity> exceptionHandler(ControllerException e) { + return new ResponseEntity<>(WVPResult.fail(e.getCode(), e.getMsg()), HttpStatus.OK); + } + + /** + * 登陆失败 + * @param e 异常 + * @return 统一返回结果 + */ + @ExceptionHandler(BadCredentialsException.class) + @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) + public ResponseEntity> exceptionHandler(BadCredentialsException e) { + return new ResponseEntity<>(WVPResult.fail(ErrorCode.ERROR100.getCode(), e.getMessage()), HttpStatus.OK); + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/conf/GlobalResponseAdvice.java b/src/main/java/com/genersoft/iot/vmp/conf/GlobalResponseAdvice.java new file mode 100644 index 0000000..2db3bb5 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/conf/GlobalResponseAdvice.java @@ -0,0 +1,76 @@ +package com.genersoft.iot.vmp.conf; + +import com.alibaba.fastjson2.JSON; +import com.genersoft.iot.vmp.vmanager.bean.ErrorCode; +import com.genersoft.iot.vmp.vmanager.bean.WVPResult; +import org.jetbrains.annotations.NotNull; +import org.springframework.core.MethodParameter; +import org.springframework.http.MediaType; +import org.springframework.http.converter.HttpMessageConverter; +import org.springframework.http.server.ServerHttpRequest; +import org.springframework.http.server.ServerHttpResponse; +import org.springframework.web.bind.annotation.RestControllerAdvice; +import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice; + +import java.util.LinkedHashMap; + +/** + * 全局统一返回结果 + * @author lin + */ +@RestControllerAdvice +public class GlobalResponseAdvice implements ResponseBodyAdvice { + + + @Override + public boolean supports(@NotNull MethodParameter returnType, @NotNull Class> converterType) { + return true; + } + + + @Override + public Object beforeBodyWrite(Object body, @NotNull MethodParameter returnType, @NotNull MediaType selectedContentType, @NotNull Class> selectedConverterType, @NotNull ServerHttpRequest request, @NotNull ServerHttpResponse response) { + // 排除api文档的接口,这个接口不需要统一 + String[] excludePath = {"/v3/api-docs","/api/v1","/index/hook","/api/video-"}; + for (String path : excludePath) { + if (request.getURI().getPath().startsWith(path)) { + return body; + } + } + + if (selectedContentType.equals(MediaType.parseMediaType("application/x-protobuf"))) { + return body; + } + + if (body instanceof WVPResult) { + return body; + } + + if (body instanceof ErrorCode) { + ErrorCode errorCode = (ErrorCode) body; + return new WVPResult<>(errorCode.getCode(), errorCode.getMsg(), null); + } + + if (body instanceof String) { + return JSON.toJSONString(WVPResult.success(body)); + } + + if (body instanceof LinkedHashMap) { + LinkedHashMap bodyMap = (LinkedHashMap) body; + if (bodyMap.get("status") != null && (Integer)bodyMap.get("status") != 200) { + return body; + } + } + + return WVPResult.success(body); + } + + /** + * 防止返回string时出错 + * @return + */ + /*@Bean + public HttpMessageConverters custHttpMessageConverter() { + return new HttpMessageConverters(new FastJsonHttpMessageConverter()); + }*/ +} diff --git a/src/main/java/com/genersoft/iot/vmp/conf/MediaConfig.java b/src/main/java/com/genersoft/iot/vmp/conf/MediaConfig.java new file mode 100644 index 0000000..51a586c --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/conf/MediaConfig.java @@ -0,0 +1,203 @@ +package com.genersoft.iot.vmp.conf; + +import com.genersoft.iot.vmp.media.bean.MediaServer; +import com.genersoft.iot.vmp.utils.DateUtil; +import lombok.Data; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.annotation.Order; +import org.springframework.util.ObjectUtils; + +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.util.regex.Pattern; + +@Slf4j +@Configuration("mediaConfig") +@Order(0) +@Data +public class MediaConfig{ + + // 修改必须配置,不再支持自动获取 + @Value("${media.id}") + private String id; + + @Value("${media.ip}") + private String ip; + + @Value("${media.wan_ip:}") + private String wanIp; + + @Value("${media.hook-ip:127.0.0.1}") + private String hookIp; + + @Value("${sip.domain}") + private String sipDomain; + + @Value("${media.sdp-ip:${media.wan_ip:}}") + private String sdpIp; + + @Value("${media.stream-ip:${media.wan_ip:}}") + private String streamIp; + + @Value("${media.http-port:0}") + private Integer httpPort; + + @Value("${media.flv-port:0}") + private Integer flvPort = 0; + + @Value("${media.mp4-port:0}") + private Integer mp4Port = 0; + + @Value("${media.ws-flv-port:0}") + private Integer wsFlvPort = 0; + + @Value("${media.http-ssl-port:0}") + private Integer httpSSlPort = 0; + + @Value("${media.flv-ssl-port:0}") + private Integer flvSSlPort = 0; + + @Value("${media.ws-flv-ssl-port:0}") + private Integer wsFlvSSlPort = 0; + + @Value("${media.rtmp-port:0}") + private Integer rtmpPort = 0; + + @Value("${media.rtmp-ssl-port:0}") + private Integer rtmpSSlPort = 0; + + @Value("${media.rtp-proxy-port:0}") + private Integer rtpProxyPort = 0; + + @Value("${media.jtt-proxy-port:0}") + private Integer jttProxyPort = 0; + + @Value("${media.rtsp-port:0}") + private Integer rtspPort = 0; + + @Value("${media.rtsp-ssl-port:0}") + private Integer rtspSSLPort = 0; + + @Value("${media.auto-config:true}") + private boolean autoConfig = true; + + @Value("${media.secret}") + private String secret; + + @Value("${media.rtp.enable}") + private boolean rtpEnable; + + @Value("${media.rtp.port-range}") + private String rtpPortRange; + + @Value("${media.rtp.send-port-range}") + private String rtpSendPortRange; + + @Value("${media.record-assist-port:0}") + private Integer recordAssistPort = 0; + + @Value("${media.record-day:7}") + private Integer recordDay; + + @Value("${media.record-path:}") + private String recordPath; + + @Value("${media.type:zlm}") + private String type; + + + + public int getRtpProxyPort() { + if (rtpProxyPort == null) { + return 0; + }else { + return rtpProxyPort; + } + + } + + public Integer getJttProxyPort() { + if (jttProxyPort == null) { + return 0; + }else { + return jttProxyPort; + } + } + + public String getSdpIp() { + if (ObjectUtils.isEmpty(sdpIp)){ + return ip; + }else { + if (isValidIPAddress(sdpIp)) { + return sdpIp; + }else { + // 按照域名解析 + String hostAddress = null; + try { + hostAddress = InetAddress.getByName(sdpIp).getHostAddress(); + } catch (UnknownHostException e) { + log.error("[获取SDP IP]: 域名解析失败"); + } + return hostAddress; + } + } + } + + public String getStreamIp() { + if (ObjectUtils.isEmpty(streamIp)){ + return ip; + }else { + return streamIp; + } + } + + public MediaServer getMediaSerItem(){ + MediaServer mediaServer = new MediaServer(); + mediaServer.setId(id); + mediaServer.setIp(ip); + mediaServer.setDefaultServer(true); + mediaServer.setHookIp(getHookIp()); + mediaServer.setSdpIp(getSdpIp()); + mediaServer.setStreamIp(getStreamIp()); + mediaServer.setHttpPort(httpPort); + mediaServer.setFlvPort(flvPort); + mediaServer.setMp4Port(mp4Port); + mediaServer.setWsFlvPort(wsFlvPort); + mediaServer.setFlvSSLPort(flvSSlPort); + mediaServer.setWsFlvSSLPort(wsFlvSSlPort); + + mediaServer.setHttpSSlPort(httpSSlPort); + mediaServer.setRtmpPort(rtmpPort); + mediaServer.setRtmpSSlPort(rtmpSSlPort); + mediaServer.setRtpProxyPort(getRtpProxyPort()); + mediaServer.setJttProxyPort(getJttProxyPort()); + mediaServer.setRtspPort(rtspPort); + mediaServer.setRtspSSLPort(rtspSSLPort); + mediaServer.setAutoConfig(autoConfig); + mediaServer.setSecret(secret); + mediaServer.setRtpEnable(rtpEnable); + mediaServer.setRtpPortRange(rtpPortRange); + mediaServer.setSendRtpPortRange(rtpSendPortRange); + mediaServer.setRecordAssistPort(recordAssistPort); + mediaServer.setHookAliveInterval(10f); + mediaServer.setRecordDay(recordDay); + mediaServer.setStatus(false); + mediaServer.setType(type); + if (recordPath != null) { + mediaServer.setRecordPath(recordPath); + } + mediaServer.setCreateTime(DateUtil.getNow()); + mediaServer.setUpdateTime(DateUtil.getNow()); + + return mediaServer; + } + + private boolean isValidIPAddress(String ipAddress) { + if ((ipAddress != null) && (!ipAddress.isEmpty())) { + return Pattern.matches("^([1-9]|[1-9]\\d|1\\d{2}|2[0-4]\\d|25[0-5])(\\.(\\d|[1-9]\\d|1\\d{2}|2[0-4]\\d|25[0-5])){3}$", ipAddress); + } + return false; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/conf/MediaStatusTimerTask.java b/src/main/java/com/genersoft/iot/vmp/conf/MediaStatusTimerTask.java new file mode 100644 index 0000000..761250e --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/conf/MediaStatusTimerTask.java @@ -0,0 +1,15 @@ +package com.genersoft.iot.vmp.conf; + +import org.springframework.scheduling.annotation.Scheduled; + +/** + * 定时向zlm同步媒体流状态 + */ +public class MediaStatusTimerTask { + + +// @Scheduled(fixedRate = 2 * 1000) //每3秒执行一次 + public void execute(){ + + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/conf/MybatisConfig.java b/src/main/java/com/genersoft/iot/vmp/conf/MybatisConfig.java new file mode 100644 index 0000000..7f25a36 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/conf/MybatisConfig.java @@ -0,0 +1,62 @@ +package com.genersoft.iot.vmp.conf; + +import org.apache.ibatis.logging.stdout.StdOutImpl; +import org.apache.ibatis.mapping.DatabaseIdProvider; +import org.apache.ibatis.mapping.VendorDatabaseIdProvider; +import org.apache.ibatis.session.SqlSessionFactory; +import org.mybatis.spring.SqlSessionFactoryBean; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.annotation.Order; + +import javax.sql.DataSource; +import java.util.Properties; + +/** + * 配置mybatis + */ +@Configuration +@Order(value=1) +public class MybatisConfig { + + @Autowired + private UserSetting userSetting; + + @Bean + public DatabaseIdProvider databaseIdProvider() { + VendorDatabaseIdProvider databaseIdProvider = new VendorDatabaseIdProvider(); + Properties properties = new Properties(); + properties.setProperty("Oracle", "oracle"); + properties.setProperty("MySQL", "mysql"); + properties.setProperty("DB2", "db2"); + properties.setProperty("Derby", "derby"); + properties.setProperty("H2", "h2"); + properties.setProperty("HSQL", "hsql"); + properties.setProperty("Informix", "informix"); + properties.setProperty("MS-SQL", "ms-sql"); + properties.setProperty("PostgreSQL", "postgresql"); + properties.setProperty("Sybase", "sybase"); + properties.setProperty("Hana", "hana"); + properties.setProperty("DM", "dm"); + properties.setProperty("KingbaseES", "kingbase"); + properties.setProperty("KingBase8", "kingbase"); + databaseIdProvider.setProperties(properties); + return databaseIdProvider; + } + + @Bean + public SqlSessionFactory sqlSessionFactory(DataSource dataSource, DatabaseIdProvider databaseIdProvider) throws Exception { + final SqlSessionFactoryBean sqlSessionFactory = new SqlSessionFactoryBean(); + sqlSessionFactory.setDataSource(dataSource); + org.apache.ibatis.session.Configuration config = new org.apache.ibatis.session.Configuration(); + if (userSetting.getSqlLog()){ + config.setLogImpl(StdOutImpl.class); + } + config.setMapUnderscoreToCamelCase(true); + sqlSessionFactory.setConfiguration(config); + sqlSessionFactory.setDatabaseIdProvider(databaseIdProvider); + return sqlSessionFactory.getObject(); + } + +} diff --git a/src/main/java/com/genersoft/iot/vmp/conf/ScheduleConfig.java b/src/main/java/com/genersoft/iot/vmp/conf/ScheduleConfig.java new file mode 100644 index 0000000..df88bcf --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/conf/ScheduleConfig.java @@ -0,0 +1,40 @@ +package com.genersoft.iot.vmp.conf; + +import org.apache.commons.lang3.concurrent.BasicThreadFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.context.annotation.Configuration; +import org.springframework.scheduling.annotation.SchedulingConfigurer; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; +import org.springframework.scheduling.config.ScheduledTaskRegistrar; + +import java.util.concurrent.ScheduledThreadPoolExecutor; +import java.util.concurrent.ThreadPoolExecutor; + +import static com.genersoft.iot.vmp.conf.ThreadPoolTaskConfig.cpuNum; + +/** + * "@Scheduled"是Spring框架提供的一种定时任务执行机制,默认情况下它是单线程的,在同时执行多个定时任务时可能会出现阻塞和性能问题。 + * 为了解决这种单线程瓶颈问题,可以将定时任务的执行机制改为支持多线程 + */ +@Configuration +public class ScheduleConfig implements SchedulingConfigurer { + + /** + * 核心线程数(默认线程数) + */ + private static final int corePoolSize = Math.max(cpuNum, 20); + + /** + * 线程池名前缀 + */ + private static final String threadNamePrefix = "schedule"; + + @Override + public void configureTasks(ScheduledTaskRegistrar taskRegistrar) { + ScheduledThreadPoolExecutor scheduledThreadPoolExecutor = new ScheduledThreadPoolExecutor(corePoolSize, + new BasicThreadFactory.Builder().namingPattern(threadNamePrefix).daemon(true).build(), + new ThreadPoolExecutor.CallerRunsPolicy()); + taskRegistrar.setScheduler(scheduledThreadPoolExecutor); + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/conf/ServiceInfo.java b/src/main/java/com/genersoft/iot/vmp/conf/ServiceInfo.java new file mode 100644 index 0000000..67a0e4f --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/conf/ServiceInfo.java @@ -0,0 +1,26 @@ +package com.genersoft.iot.vmp.conf; + +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.web.context.WebServerInitializedEvent; +import org.springframework.context.ApplicationListener; +import org.springframework.stereotype.Component; + +@Slf4j +@Component +public class ServiceInfo implements ApplicationListener { + + @Getter + private static int serverPort; + + @Override + public void onApplicationEvent(WebServerInitializedEvent event) { + // 项目启动获取启动的端口号 + ServiceInfo.serverPort = event.getWebServer().getPort(); + log.info("项目启动获取启动的端口号: {}", ServiceInfo.serverPort); + } + + public void setServerPort(int serverPort) { + ServiceInfo.serverPort = serverPort; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/conf/SipConfig.java b/src/main/java/com/genersoft/iot/vmp/conf/SipConfig.java new file mode 100644 index 0000000..5822d64 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/conf/SipConfig.java @@ -0,0 +1,38 @@ +package com.genersoft.iot.vmp.conf; + + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.core.annotation.Order; +import org.springframework.stereotype.Component; + +import java.util.List; + +@Component +@ConfigurationProperties(prefix = "sip", ignoreInvalidFields = true) +@Order(0) +@Data +public class SipConfig { + + private String ip; + + private String showIp; + + private List monitorIps; + + private Integer port; + + private String domain; + + private String id; + + private String password; + + Integer ptzSpeed = 50; + + Integer registerTimeInterval = 120; + + private boolean alarm = false; + + private long timeout = 1000; +} diff --git a/src/main/java/com/genersoft/iot/vmp/conf/SpringDocConfig.java b/src/main/java/com/genersoft/iot/vmp/conf/SpringDocConfig.java new file mode 100644 index 0000000..65ebc3f --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/conf/SpringDocConfig.java @@ -0,0 +1,117 @@ +package com.genersoft.iot.vmp.conf; + +import com.genersoft.iot.vmp.conf.security.JwtUtils; +import io.swagger.v3.oas.models.Components; +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.info.Contact; +import io.swagger.v3.oas.models.info.Info; +import io.swagger.v3.oas.models.info.License; +import io.swagger.v3.oas.models.security.SecurityScheme; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.core.annotation.Order; +import org.springdoc.core.GroupedOpenApi; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * @author lin + */ +@Configuration +@Order(1) +@ConditionalOnProperty(value = "user-settings.doc-enable", havingValue = "true", matchIfMissing = true) +public class SpringDocConfig { + + @Value("${doc.enabled: true}") + private boolean enable; + + @Bean + public OpenAPI springShopOpenApi() { + Contact contact = new Contact(); + contact.setName("pan"); + contact.setEmail("648540858@qq.com"); + + return new OpenAPI() + .components(new Components() + .addSecuritySchemes(JwtUtils.HEADER, new SecurityScheme() + .type(SecurityScheme.Type.HTTP) + .bearerFormat("JWT"))) + .info(new Info().title("WVP-PRO 接口文档") + .contact(contact) + .description("开箱即用的28181协议视频平台。
" + + "1. 打开登录接口" + + " 登录成功后返回AccessToken。
" + + "2. 填写到AccessToken到参数值 Token配置
" + + "后续接口就可以直接测试了") + .version("v3.1.0") + .license(new License().name("Apache 2.0").url("http://springdoc.org"))); + } + + /** + * 添加分组 + * @return + */ + @Bean + public GroupedOpenApi publicApi() { + return GroupedOpenApi.builder() + .group("1. 全部") + .packagesToScan("com.genersoft.iot.vmp") + .build(); + } + + @Bean + public GroupedOpenApi publicApi2() { + return GroupedOpenApi.builder() + .group("2. 国标28181") + .packagesToScan("com.genersoft.iot.vmp.gb28181") + .build(); + } + + @Bean + public GroupedOpenApi publicApi3() { + return GroupedOpenApi.builder() + .group("3. 拉流转发") + .packagesToScan("com.genersoft.iot.vmp.streamProxy") + .build(); + } + + @Bean + public GroupedOpenApi publicApi4() { + return GroupedOpenApi.builder() + .group("4. 推流管理") + .packagesToScan("com.genersoft.iot.vmp.streamPush") + .build(); + } + + @Bean + public GroupedOpenApi publicApi5() { + return GroupedOpenApi.builder() + .group("4. 服务管理") + .packagesToScan("com.genersoft.iot.vmp.server") + .build(); + } + + @Bean + public GroupedOpenApi publicApi6() { + return GroupedOpenApi.builder() + .group("5. 用户管理") + .packagesToScan("com.genersoft.iot.vmp.user") + .build(); + } + + @Bean + public GroupedOpenApi publicApi7() { + return GroupedOpenApi.builder() + .group("6. 部标设备") + .packagesToScan("com.genersoft.iot.vmp.jt1078.controller") + .build(); + } + + @Bean + public GroupedOpenApi publicApi99() { + return GroupedOpenApi.builder() + .group("99. 第三方接口") + .packagesToScan("com.genersoft.iot.vmp.web.custom") + .build(); + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/conf/StatisticsInfoTask.java b/src/main/java/com/genersoft/iot/vmp/conf/StatisticsInfoTask.java new file mode 100644 index 0000000..c558681 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/conf/StatisticsInfoTask.java @@ -0,0 +1,98 @@ +package com.genersoft.iot.vmp.conf; + +import com.alibaba.fastjson2.JSON; +import com.genersoft.iot.vmp.common.StatisticsInfo; +import com.genersoft.iot.vmp.utils.DateUtil; +import com.genersoft.iot.vmp.utils.GitUtil; +import com.genersoft.iot.vmp.utils.SystemInfoUtils; +import lombok.extern.slf4j.Slf4j; +import okhttp3.*; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.CommandLineRunner; +import org.springframework.core.annotation.Order; +import org.springframework.data.redis.connection.RedisConnection; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Component; + +import javax.sql.DataSource; +import java.io.File; +import java.sql.DatabaseMetaData; +import java.util.Objects; + +@Component +@Order(value=100) +@Slf4j +public class StatisticsInfoTask implements CommandLineRunner { + + @Autowired + private GitUtil gitUtil; + + @Autowired + private RedisTemplate redisTemplate; + + @Autowired + private DataSource dataSource; + + @Override + public void run(String... args) throws Exception { + try { + StatisticsInfo statisticsInfo = new StatisticsInfo(); + statisticsInfo.setDeviceId(SystemInfoUtils.getHardwareId()); + statisticsInfo.setBranch(gitUtil.getBranch()); + statisticsInfo.setGitCommitId(gitUtil.getGitCommitId()); + statisticsInfo.setGitUrl(gitUtil.getGitUrl()); + statisticsInfo.setVersion(gitUtil.getBuildVersion()); + + statisticsInfo.setOsName(System.getProperty("os.name")); + statisticsInfo.setArch(System.getProperty("os.arch")); + statisticsInfo.setJdkVersion(System.getProperty("java.version")); + + statisticsInfo.setDocker(new File("/.dockerenv").exists()); + try { + statisticsInfo.setRedisVersion(getRedisVersion()); + }catch (Exception ignored) {} + try { + DatabaseMetaData metaData = dataSource.getConnection().getMetaData(); + statisticsInfo.setSqlVersion(metaData.getDatabaseProductVersion()); + statisticsInfo.setSqlType(metaData.getDriverName()); + }catch (Exception ignored) {} + statisticsInfo.setTime(DateUtil.getNow()); + sendPost(statisticsInfo); + + + }catch (Exception e) { + log.error("[获取信息失败] ", e); + } + } + + public String getRedisVersion() { + if (redisTemplate.getConnectionFactory() == null) { + return null; + } + RedisConnection connection = redisTemplate.getConnectionFactory().getConnection(); + if (connection.info() == null) { + return null; + } + return connection.info().getProperty("redis_version"); + } + + public void sendPost(StatisticsInfo statisticsInfo) { + OkHttpClient.Builder httpClientBuilder = new OkHttpClient.Builder(); + OkHttpClient client = httpClientBuilder.build(); + + RequestBody requestBodyJson = RequestBody.create(MediaType.parse("application/json; charset=utf-8"), JSON.toJSONString(statisticsInfo)); + + Request request = new Request.Builder() + .post(requestBodyJson) + .url("http://api.wvp-pro.cn:136/api/statistics/ping") +// .url("http://127.0.0.1:11236/api/statistics/ping") + .addHeader("Content-Type", "application/json") + .build(); + try { + Response response = client.newCall(request).execute(); + response.close(); + Objects.requireNonNull(response.body()).close(); + + }catch (Exception ignored){} + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/conf/SystemInfoTimerTask.java b/src/main/java/com/genersoft/iot/vmp/conf/SystemInfoTimerTask.java new file mode 100644 index 0000000..0c6eb23 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/conf/SystemInfoTimerTask.java @@ -0,0 +1,41 @@ +package com.genersoft.iot.vmp.conf; + +import com.genersoft.iot.vmp.storager.IRedisCatchStorage; +import com.genersoft.iot.vmp.utils.SystemInfoUtils; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +import java.util.List; +import java.util.Map; + +/** + * 获取系统信息写入redis + */ +@Slf4j +@Component +public class SystemInfoTimerTask { + + @Autowired + private IRedisCatchStorage redisCatchStorage; + + @Scheduled(fixedRate = 2000) //每1秒执行一次 + public void execute(){ + try { + double cpuInfo = SystemInfoUtils.getCpuInfo(); + redisCatchStorage.addCpuInfo(cpuInfo); + double memInfo = SystemInfoUtils.getMemInfo(); + redisCatchStorage.addMemInfo(memInfo); + Map networkInterfaces = SystemInfoUtils.getNetworkInterfaces(); + redisCatchStorage.addNetInfo(networkInterfaces); + List> diskInfo =SystemInfoUtils.getDiskInfo(); + redisCatchStorage.addDiskInfo(diskInfo); + } catch (InterruptedException e) { + log.error("[获取系统信息失败] {}", e.getMessage()); + } + + } + + +} diff --git a/src/main/java/com/genersoft/iot/vmp/conf/ThreadPoolTaskConfig.java b/src/main/java/com/genersoft/iot/vmp/conf/ThreadPoolTaskConfig.java new file mode 100644 index 0000000..a549a05 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/conf/ThreadPoolTaskConfig.java @@ -0,0 +1,67 @@ +package com.genersoft.iot.vmp.conf; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.annotation.Order; +import org.springframework.scheduling.annotation.EnableAsync; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; + +import java.util.concurrent.ThreadPoolExecutor; + +/** + * ThreadPoolTask 配置类 + * @author lin + */ +@Configuration +@Order(1) +@EnableAsync(proxyTargetClass = true) +public class ThreadPoolTaskConfig { + + public static final int cpuNum = Runtime.getRuntime().availableProcessors(); + + /** + * 默认情况下,在创建了线程池后,线程池中的线程数为0,当有任务来之后,就会创建一个线程去执行任务, + * 当线程池中的线程数目达到corePoolSize后,就会把到达的任务放到缓存队列当中; + * 当队列满了,就继续创建线程,当线程数量大于等于maxPoolSize后,开始使用拒绝策略拒绝 + */ + + /** + * 核心线程数(默认线程数) + */ + private static final int corePoolSize = Math.max(cpuNum * 2, 16); + /** + * 最大线程数 + */ + private static final int maxPoolSize = corePoolSize * 10; + /** + * 允许线程空闲时间(单位:默认为秒) + */ + private static final int keepAliveTime = 30; + + /** + * 缓冲队列大小 + */ + private static final int queueCapacity = 10000; + /** + * 线程池名前缀 + */ + private static final String threadNamePrefix = "async-"; + + + @Bean("taskExecutor") // bean的名称,默认为首字母小写的方法名 + public ThreadPoolTaskExecutor taskExecutor() { + ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); + executor.setCorePoolSize(corePoolSize); + executor.setMaxPoolSize(maxPoolSize); + executor.setQueueCapacity(queueCapacity); + executor.setKeepAliveSeconds(keepAliveTime); + executor.setThreadNamePrefix(threadNamePrefix); + + // 线程池对拒绝任务的处理策略 + // CallerRunsPolicy:由调用线程(提交任务的线程)处理该任务 + executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); + // 初始化 + executor.initialize(); + return executor; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/conf/UserSetting.java b/src/main/java/com/genersoft/iot/vmp/conf/UserSetting.java new file mode 100644 index 0000000..77beba7 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/conf/UserSetting.java @@ -0,0 +1,222 @@ +package com.genersoft.iot.vmp.conf; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.core.annotation.Order; +import org.springframework.stereotype.Component; + +import java.util.ArrayList; +import java.util.List; + +/** + * 配置文件 user-settings 映射的配置信息 + */ +@Component +@ConfigurationProperties(prefix = "user-settings", ignoreInvalidFields = true) +@Order(0) +@Data +public class UserSetting { + + /** + * 是否保存位置的历史记录(轨迹) + */ + private Boolean savePositionHistory = Boolean.FALSE; + + /** + * 是否开始自动点播: 请求流为未拉起的流时,自动开启点播, 需要rtp.enable=true + */ + private Boolean autoApplyPlay = Boolean.FALSE; + + /** + * [可选] 部分设备需要扩展SDP,需要打开此设置,一般设备无需打开 + */ + private Boolean seniorSdp = Boolean.FALSE; + + /** + * 点播/录像回放 等待超时时间,单位:毫秒 + */ + private Integer playTimeout = 10000; + + /** + * 获取设备录像数据超时时间,单位:毫秒 + */ + private Integer recordInfoTimeout = 15000; + + /** + * 上级点播等待超时时间,单位:毫秒 + */ + private int platformPlayTimeout = 20000; + + /** + * 是否开启接口鉴权 + */ + private Boolean interfaceAuthentication = Boolean.TRUE; + + /** + * 接口鉴权例外的接口, 即不进行接口鉴权的接口,尽量详细书写,尽量不用/**,至少两级目录 + */ + private List interfaceAuthenticationExcludes = new ArrayList<>(); + + /** + * 推流直播是否录制 + */ + private Boolean recordPushLive = Boolean.TRUE; + + /** + * 国标是否录制 + */ + private Boolean recordSip = Boolean.TRUE; + + /** + * 使用推流状态作为推流通道状态 + */ + private Boolean usePushingAsStatus = Boolean.FALSE; + + /** + * 使用来源请求ip作为streamIp,当且仅当你只有zlm节点它与wvp在一起的情况下开启 + */ + private Boolean useSourceIpAsStreamIp = Boolean.FALSE; + + /** + * 是否使用设备来源Ip作为回复IP, 不设置则为 false + */ + private Boolean sipUseSourceIpAsRemoteAddress = Boolean.FALSE; + + /** + * 国标点播 按需拉流, true:有人观看拉流,无人观看释放, false:拉起后不自动释放 + */ + private Boolean streamOnDemand = Boolean.TRUE; + + /** + * 推流鉴权, 默认开启 + */ + private Boolean pushAuthority = Boolean.TRUE; + + /** + * 设备上线时是否自动同步通道 + */ + private Boolean syncChannelOnDeviceOnline = Boolean.FALSE; + + /** + * 是否开启sip日志 + */ + private Boolean sipLog = Boolean.FALSE; + + /** + * 是否开启mybatis-sql日志 + */ + private Boolean sqlLog = Boolean.FALSE; + + /** + * 消息通道功能-缺少国标ID是否给所有上级发送消息 + */ + private Boolean sendToPlatformsWhenIdLost = Boolean.FALSE; + + /** + * 保持通道状态,不接受notify通道状态变化, 兼容海康平台发送错误消息 + */ + private Boolean refuseChannelStatusChannelFormNotify = Boolean.FALSE; + + /** + * 设备/通道状态变化时发送消息 + */ + private Boolean deviceStatusNotify = Boolean.TRUE; + + /** + * 上级平台点播时不使用上级平台指定的ssrc,使用自定义的ssrc,参考国标文档-点播外域设备媒体流SSRC处理方式 + */ + private Boolean useCustomSsrcForParentInvite = Boolean.TRUE; + + /** + * 开启接口文档页面。 默认开启,生产环境建议关闭,遇到swagger相关的漏洞时也可以关闭 + */ + private Boolean docEnable = Boolean.TRUE; + + /** + * 服务ID,不写则为000000 + */ + private String serverId = "000000"; + + + /** + * 国标级联语音喊话发流模式 * UDP:udp传输 TCP-ACTIVE:tcp主动模式 TCP-PASSIVE:tcp被动模式 + */ + private String broadcastForPlatform = "UDP"; + + /** + * 行政区划信息文件,系统启动时会加载到系统里 + */ + private String civilCodeFile = "classpath:civilCode.csv"; + + /** + * 跨域配置,不配置此项则允许所有跨域请求,配置后则只允许配置的页面的地址请求, 可以配置多个 + */ + private List allowedOrigins = new ArrayList<>(); + + /** + * 设置notify缓存队列最大长度,超过此长度的数据将返回486 BUSY_HERE,消息丢弃, 默认100000 + */ + private int maxNotifyCountQueue = 100000; + + /** + * 国标级联离线后多久重试一次注册 + */ + private int registerAgainAfterTime = 60; + + /** + * 国标续订方式,true为续订,每次注册在同一个会话里,false为重新注册,每次使用新的会话 + */ + private boolean registerKeepIntDialog = false; + + /** + * # 国标设备离线后的上线策略, + * # 0: 国标标准实现,设备离线后不回复心跳,直到设备重新注册上线, + * # 1(默认): 对于离线设备,收到心跳就把设备设置为上线,并更新注册时间为上次这次心跳的时间。防止过期时间判断异常 + */ + private int gbDeviceOnline = 1; + + /** + * 登录超时时间(分钟), + */ + private long loginTimeout = 60; + + /** + * jwk文件路径,若不指定则使用resources目录下的jwk.json + */ + private String jwkFile = "classpath:jwk.json"; + + /** + * wvp集群模式下如果注册向上级的wvp奔溃,则自动选择一个其他wvp继续注册到上级 + */ + private boolean autoRegisterPlatform = false; + + /** + * 按需发送推流设备位置, 默认发送移动位置订阅时如果位置不变则不发送, 设置为false按照国标间隔持续发送 + */ + private boolean sendPositionOnDemand = true; + + /** + * 部分设备会在短时间内发送大量注册, 导致协议栈内存溢出, 开启此项可以防止这部分设备注册, 避免服务崩溃,但是会降低系统性能, 描述如下 + * 默认值为 true。 + * 将此设置为 false 会使 Stack 在 Server Transaction 进入 TERMINATED 状态后关闭服务器套接字。 + * 这允许服务器防止客户端发起的基于 TCP 的拒绝服务攻击(即发起数百个客户端事务)。 + * 如果为 true(默认作),则堆栈将保持套接字打开,以便以牺牲线程和内存资源为代价来最大化性能 - 使自身容易受到 DOS 攻击。 + */ + private boolean sipCacheServerConnections = true; + + /** + * 禁用date头,变相禁用了校时 + */ + private boolean disableDateHeader = false; + + /** + * 同步业务分组时自动生成分组国标编号的模板,不配置则默认参考当前的sip域信息生成 + */ + private String groupSyncDeviceTemplate; + + /** + * 与第三方进行分组同步时使用别名而不是分组ID, 如果没有设置此项为true,那么分组编号就是必须传递的。如果是设置为true则,自动为别名的分组生成新的编号 + */ + private boolean useAliasForGroupSync = false; + +} diff --git a/src/main/java/com/genersoft/iot/vmp/conf/VersionConfig.java b/src/main/java/com/genersoft/iot/vmp/conf/VersionConfig.java new file mode 100644 index 0000000..3d5bb5e --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/conf/VersionConfig.java @@ -0,0 +1,39 @@ +package com.genersoft.iot.vmp.conf; + +import org.springframework.core.annotation.Order; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +@Component +@ConfigurationProperties(prefix = "version") +@Order(0) +public class VersionConfig { + + private String version; + private String artifactId; + private String description; + + public void setVersion(String version) { + this.version = version; + } + + public void setArtifactId(String artifactId) { + this.artifactId = artifactId; + } + + public void setDescription(String description) { + this.description = description; + } + + public String getVersion() { + return version; + } + + public String getArtifactId() { + return artifactId; + } + + public String getDescription() { + return description; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/conf/VersionInfo.java b/src/main/java/com/genersoft/iot/vmp/conf/VersionInfo.java new file mode 100644 index 0000000..eb408ab --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/conf/VersionInfo.java @@ -0,0 +1,26 @@ +package com.genersoft.iot.vmp.conf; + +import com.genersoft.iot.vmp.common.VersionPo; +import com.genersoft.iot.vmp.utils.GitUtil; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +@Component +public class VersionInfo { + + @Autowired + GitUtil gitUtil; + + public VersionPo getVersion() { + VersionPo versionPo = new VersionPo(); + versionPo.setGIT_Revision(gitUtil.getGitCommitId()); + versionPo.setGIT_BRANCH(gitUtil.getBranch()); + versionPo.setGIT_URL(gitUtil.getGitUrl()); + versionPo.setBUILD_DATE(gitUtil.getBuildDate()); + versionPo.setGIT_Revision_SHORT(gitUtil.getCommitIdShort()); + versionPo.setVersion(gitUtil.getBuildVersion()); + versionPo.setGIT_DATE(gitUtil.getCommitTime()); + + return versionPo; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/conf/WVPTimerTask.java b/src/main/java/com/genersoft/iot/vmp/conf/WVPTimerTask.java new file mode 100644 index 0000000..4a2098a --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/conf/WVPTimerTask.java @@ -0,0 +1,28 @@ +package com.genersoft.iot.vmp.conf; + +import com.genersoft.iot.vmp.common.ServerInfo; +import com.genersoft.iot.vmp.storager.IRedisCatchStorage; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +import java.util.concurrent.TimeUnit; + +@Component +public class WVPTimerTask { + + @Autowired + private IRedisCatchStorage redisCatchStorage; + + @Value("${server.port}") + private int serverPort; + + @Autowired + private SipConfig sipConfig; + + @Scheduled(fixedDelay = 2, timeUnit = TimeUnit.SECONDS) //每3秒执行一次 + public void execute(){ + redisCatchStorage.updateWVPInfo(ServerInfo.create(sipConfig.getShowIp(), serverPort), 3); + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/conf/exception/ControllerException.java b/src/main/java/com/genersoft/iot/vmp/conf/exception/ControllerException.java new file mode 100644 index 0000000..d2e1206 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/conf/exception/ControllerException.java @@ -0,0 +1,37 @@ +package com.genersoft.iot.vmp.conf.exception; + +import com.genersoft.iot.vmp.vmanager.bean.ErrorCode; + +/** + * 自定义异常,controller出现错误时直接抛出异常由全局异常捕获并返回结果 + */ +public class ControllerException extends RuntimeException{ + + private int code; + private String msg; + + public ControllerException(int code, String msg) { + this.code = code; + this.msg = msg; + } + public ControllerException(ErrorCode errorCode) { + this.code = errorCode.getCode(); + this.msg = errorCode.getMsg(); + } + + public int getCode() { + return code; + } + + public void setCode(int code) { + this.code = code; + } + + public String getMsg() { + return msg; + } + + public void setMsg(String msg) { + this.msg = msg; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/conf/exception/ServiceException.java b/src/main/java/com/genersoft/iot/vmp/conf/exception/ServiceException.java new file mode 100644 index 0000000..5566d4b --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/conf/exception/ServiceException.java @@ -0,0 +1,27 @@ +package com.genersoft.iot.vmp.conf.exception; + +/** + * @author lin + */ +public class ServiceException extends Exception{ + private String msg; + + + + public ServiceException(String msg) { + this.msg = msg; + } + + public String getMsg() { + return msg; + } + + public void setMsg(String msg) { + this.msg = msg; + } + + @Override + public String getMessage() { + return msg; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/conf/exception/SsrcTransactionNotFoundException.java b/src/main/java/com/genersoft/iot/vmp/conf/exception/SsrcTransactionNotFoundException.java new file mode 100644 index 0000000..e1a738a --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/conf/exception/SsrcTransactionNotFoundException.java @@ -0,0 +1,49 @@ +package com.genersoft.iot.vmp.conf.exception; + +/** + * @author lin + */ +public class SsrcTransactionNotFoundException extends Exception{ + private String deviceId; + private String channelId; + private String callId; + private String stream; + + + + public SsrcTransactionNotFoundException(String deviceId, String channelId, String callId, String stream) { + this.deviceId = deviceId; + this.channelId = channelId; + this.callId = callId; + this.stream = stream; + } + + public String getDeviceId() { + return deviceId; + } + + public String getChannelId() { + return channelId; + } + + public String getCallId() { + return callId; + } + + public String getStream() { + return stream; + } + + @Override + public String getMessage() { + StringBuffer msg = new StringBuffer(); + msg.append(String.format("缓存事务信息未找到,device:%s channel: %s ", deviceId, channelId)); + if (callId != null) { + msg.append(",callId: " + callId); + } + if (stream != null) { + msg.append(",stream: " + stream); + } + return msg.toString(); + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/conf/ftpServer/FileCallback.java b/src/main/java/com/genersoft/iot/vmp/conf/ftpServer/FileCallback.java new file mode 100644 index 0000000..aa564fc --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/conf/ftpServer/FileCallback.java @@ -0,0 +1,8 @@ +package com.genersoft.iot.vmp.conf.ftpServer; + +import java.io.OutputStream; + +public interface FileCallback { + + OutputStream run(String path); +} diff --git a/src/main/java/com/genersoft/iot/vmp/conf/ftpServer/FtpAuthority.java b/src/main/java/com/genersoft/iot/vmp/conf/ftpServer/FtpAuthority.java new file mode 100644 index 0000000..0ccb144 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/conf/ftpServer/FtpAuthority.java @@ -0,0 +1,17 @@ +package com.genersoft.iot.vmp.conf.ftpServer; + +import org.apache.ftpserver.ftplet.Authority; +import org.apache.ftpserver.ftplet.AuthorizationRequest; + +public class FtpAuthority implements Authority { + + @Override + public boolean canAuthorize(AuthorizationRequest authorizationRequest) { + return true; + } + + @Override + public AuthorizationRequest authorize(AuthorizationRequest authorizationRequest) { + return authorizationRequest; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/conf/ftpServer/FtpFileSystemFactory.java b/src/main/java/com/genersoft/iot/vmp/conf/ftpServer/FtpFileSystemFactory.java new file mode 100644 index 0000000..5d771e9 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/conf/ftpServer/FtpFileSystemFactory.java @@ -0,0 +1,33 @@ +package com.genersoft.iot.vmp.conf.ftpServer; + +import org.apache.ftpserver.ftplet.FileSystemFactory; +import org.apache.ftpserver.ftplet.FileSystemView; +import org.apache.ftpserver.ftplet.FtpException; +import org.apache.ftpserver.ftplet.User; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.io.OutputStream; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +@Component +public class FtpFileSystemFactory implements FileSystemFactory { + + private final Map outputStreamMap = new ConcurrentHashMap<>(); + + @Override + public FileSystemView createFileSystemView(User user) throws FtpException { + return new FtpFileSystemView(user, path -> { + return outputStreamMap.get(path); + }); + } + + public void addOutputStream(String filePath, OutputStream outputStream) { + outputStreamMap.put(filePath, outputStream); + } + + public void removeOutputStream(String filePath) { + outputStreamMap.remove(filePath); + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/conf/ftpServer/FtpFileSystemView.java b/src/main/java/com/genersoft/iot/vmp/conf/ftpServer/FtpFileSystemView.java new file mode 100644 index 0000000..e7e8d3d --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/conf/ftpServer/FtpFileSystemView.java @@ -0,0 +1,63 @@ +package com.genersoft.iot.vmp.conf.ftpServer; + +import org.apache.ftpserver.ftplet.FileSystemView; +import org.apache.ftpserver.ftplet.FtpException; +import org.apache.ftpserver.ftplet.FtpFile; +import org.apache.ftpserver.ftplet.User; + +import java.io.OutputStream; + +public class FtpFileSystemView implements FileSystemView { + + private User user; + + private FileCallback fileCallback; + + public FtpFileSystemView(User user, FileCallback fileCallback) { + this.user = user; + this.fileCallback = fileCallback; + } + + public static String HOME_PATH = "root"; + + public FtpFile workDir = VirtualFtpFile.getDir(HOME_PATH); + + @Override + public FtpFile getHomeDirectory() throws FtpException { + return VirtualFtpFile.getDir(HOME_PATH); + } + + @Override + public FtpFile getWorkingDirectory() throws FtpException { + return workDir; + } + + @Override + public boolean changeWorkingDirectory(String dir) throws FtpException { + workDir = VirtualFtpFile.getDir(dir); + return true; + } + + @Override + public FtpFile getFile(String file) throws FtpException { + VirtualFtpFile ftpFile = VirtualFtpFile.getFile(file); + if (fileCallback != null) { + OutputStream outputStream = fileCallback.run(workDir.getName()); + if (outputStream != null) { + ftpFile.setOutputStream(outputStream); + } + } + return ftpFile; + } + + @Override + public boolean isRandomAccessible() throws FtpException { + return true; + } + + @Override + public void dispose() { + } + + +} diff --git a/src/main/java/com/genersoft/iot/vmp/conf/ftpServer/FtpServerConfig.java b/src/main/java/com/genersoft/iot/vmp/conf/ftpServer/FtpServerConfig.java new file mode 100644 index 0000000..d040163 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/conf/ftpServer/FtpServerConfig.java @@ -0,0 +1,69 @@ +package com.genersoft.iot.vmp.conf.ftpServer; + +import lombok.extern.slf4j.Slf4j; +import org.apache.ftpserver.*; +import org.apache.ftpserver.ftplet.FtpException; +import org.apache.ftpserver.listener.Listener; +import org.apache.ftpserver.listener.ListenerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.util.HashMap; +import java.util.Map; + +@Configuration +@ConditionalOnProperty(value = "ftp.enable", havingValue = "true") +@Slf4j +public class FtpServerConfig { + + @Autowired + private UserManager userManager; + + @Autowired + private FtpFileSystemFactory fileSystemFactory; + + @Autowired + private Ftplet ftplet; + + @Autowired + private FtpSetting ftpSetting; + + /** + * ftp server init + */ + @Bean + public FtpServer ftpServer() { + FtpServerFactory serverFactory = new FtpServerFactory(); + ListenerFactory listenerFactory = new ListenerFactory(); + // 1、设置服务端口 + listenerFactory.setPort(ftpSetting.getPort()); + // 2、设置被动模式数据上传的接口范围,云服务器需要开放对应区间的端口给客户端 + DataConnectionConfigurationFactory dataConnectionConfFactory = new DataConnectionConfigurationFactory(); + dataConnectionConfFactory.setPassivePorts(ftpSetting.getPassivePorts()); + listenerFactory.setDataConnectionConfiguration(dataConnectionConfFactory.createDataConnectionConfiguration()); + // 4、替换默认的监听器 + Listener listener = listenerFactory.createListener(); + serverFactory.addListener("default", listener); + // 5、配置自定义用户事件 + Map ftpLets = new HashMap<>(); + ftpLets.put("ftpService", ftplet); + serverFactory.setFtplets(ftpLets); + // 6、读取用户的配置信息 + // 6.2、设置用信息 + serverFactory.setUserManager(userManager); + serverFactory.setFileSystem(fileSystemFactory); + // 7、实例化FTP Server + FtpServer server = serverFactory.createServer(); + try { + server.start(); + if (!server.isStopped()) { + log.info("[FTP服务] 已启动, 端口: {}", ftpSetting.getPort()); + } + } catch (FtpException e) { + log.info("[FTP服务] 启动失败 ", e); + } + return server; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/conf/ftpServer/FtpSetting.java b/src/main/java/com/genersoft/iot/vmp/conf/ftpServer/FtpSetting.java new file mode 100644 index 0000000..91acc8b --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/conf/ftpServer/FtpSetting.java @@ -0,0 +1,21 @@ +package com.genersoft.iot.vmp.conf.ftpServer; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.core.annotation.Order; +import org.springframework.stereotype.Component; + +/** + * 配置文件 user-settings 映射的配置信息 + */ +@Component +@ConfigurationProperties(prefix = "ftp", ignoreInvalidFields = true) +@Order(0) +@Data +public class FtpSetting { + + private Boolean enable = Boolean.FALSE; + + private int port = 21; + private String passivePorts = "10000-10500"; +} diff --git a/src/main/java/com/genersoft/iot/vmp/conf/ftpServer/Ftplet.java b/src/main/java/com/genersoft/iot/vmp/conf/ftpServer/Ftplet.java new file mode 100644 index 0000000..b413a42 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/conf/ftpServer/Ftplet.java @@ -0,0 +1,59 @@ +package com.genersoft.iot.vmp.conf.ftpServer; + +import com.genersoft.iot.vmp.jt1078.event.FtpUploadEvent; +import org.apache.ftpserver.ftplet.*; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.stereotype.Component; + +import java.io.IOException; + + +@Component +public class Ftplet extends DefaultFtplet { + + private final Logger logger = LoggerFactory.getLogger(Ftplet.class); + + @Autowired + private ApplicationEventPublisher applicationEventPublisher; + + @Override + public FtpletResult onUploadEnd(FtpSession session, FtpRequest request) throws FtpException, IOException { + FtpFile file = session.getFileSystemView().getFile(request.getArgument()); + if (file == null) { + return super.onUploadEnd(session, request); + } + sendEvent(file.getAbsolutePath()); + return super.onUploadUniqueEnd(session, request); + } + + @Override + public FtpletResult onAppendEnd(FtpSession session, FtpRequest request) throws FtpException, IOException { + FtpFile file = session.getFileSystemView().getFile(request.getArgument()); + if (file == null) { + return super.onUploadEnd(session, request); + } + sendEvent(file.getAbsolutePath()); + return super.onUploadUniqueEnd(session, request); + } + + @Override + public FtpletResult onUploadUniqueEnd(FtpSession session, FtpRequest request) throws FtpException, IOException { + FtpFile file = session.getFileSystemView().getFile(request.getArgument()); + if (file == null) { + return super.onUploadEnd(session, request); + } + sendEvent(file.getAbsolutePath()); + return super.onUploadUniqueEnd(session, request); + } + + private void sendEvent(String filePath){ + FtpUploadEvent event = new FtpUploadEvent(this); + logger.info("[文件已上传]: {}", filePath); + event.setFileName(filePath); + applicationEventPublisher.publishEvent(event); + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/conf/ftpServer/UserManager.java b/src/main/java/com/genersoft/iot/vmp/conf/ftpServer/UserManager.java new file mode 100644 index 0000000..da5c5ed --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/conf/ftpServer/UserManager.java @@ -0,0 +1,86 @@ +package com.genersoft.iot.vmp.conf.ftpServer; + +import org.apache.commons.lang3.RandomStringUtils; +import org.apache.ftpserver.ftplet.*; +import org.apache.ftpserver.usermanager.UsernamePasswordAuthentication; +import org.apache.ftpserver.usermanager.impl.BaseUser; +import org.jetbrains.annotations.NotNull; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Component; + +import java.io.File; +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; + +@Component +public class UserManager implements org.apache.ftpserver.ftplet.UserManager { + + private static final String PREFIX = "VMP_FTP_USER_"; + + @Autowired + private RedisTemplate redisTemplate; + + + @Override + public User getUserByName(String username) throws FtpException { + return (BaseUser)redisTemplate.opsForValue().get(PREFIX + username); + } + + @Override + public String[] getAllUserNames() throws FtpException { + return new String[0]; + } + + @Override + public void delete(String username) throws FtpException { + + } + + @Override + public void save(User user) throws FtpException {} + + @Override + public boolean doesExist(String username) throws FtpException { + return redisTemplate.opsForValue().get(PREFIX + username) != null; + } + + @Override + public User authenticate(Authentication authentication) throws AuthenticationFailedException { + UsernamePasswordAuthentication usernamePasswordAuthentication = (UsernamePasswordAuthentication) authentication; + BaseUser user = (BaseUser)redisTemplate.opsForValue().get(PREFIX + usernamePasswordAuthentication.getUsername()); + if (user != null && usernamePasswordAuthentication.getPassword().equals(user.getPassword())) { + return user; + } + return null; + } + + @Override + public String getAdminName() throws FtpException { + return null; + } + + @Override + public boolean isAdmin(String username) throws FtpException { + return false; + } + + public BaseUser getRandomUser(){ + BaseUser use = new BaseUser(); + use.setName(RandomStringUtils.randomAlphabetic(6).toLowerCase()); + use.setPassword(RandomStringUtils.randomAlphabetic(6).toLowerCase()); + use.setEnabled(true); + use.setHomeDirectory("/"); + List authorities = new ArrayList<>(); + authorities.add(new FtpAuthority()); + use.setAuthorities(authorities); + String key = PREFIX + use.getName(); + + // 随机用户信息十分钟自动失效 + Duration duration = Duration.ofMinutes(10); + redisTemplate.opsForValue().set(key, use, duration); + return use; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/conf/ftpServer/VirtualFtpFile.java b/src/main/java/com/genersoft/iot/vmp/conf/ftpServer/VirtualFtpFile.java new file mode 100644 index 0000000..5ee48ab --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/conf/ftpServer/VirtualFtpFile.java @@ -0,0 +1,166 @@ +package com.genersoft.iot.vmp.conf.ftpServer; + +import lombok.Setter; +import org.apache.ftpserver.ftplet.FtpFile; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.Collections; +import java.util.List; + +public class VirtualFtpFile implements FtpFile { + + @Setter + private String name; + + @Setter + private boolean hidden = false; + + @Setter + private boolean directory = false; + + @Setter + private String ownerName; + + private Long lastModified = null; + + @Setter + private long size = 0; + + @Setter + private OutputStream outputStream; + + public static VirtualFtpFile getFile(String name) { + VirtualFtpFile virtualFtpFile = new VirtualFtpFile(); + virtualFtpFile.setName(name); + return virtualFtpFile; + } + + public static VirtualFtpFile getDir(String name) { + if (name.endsWith("/")) { + name = name.replaceAll("/", ""); + } + VirtualFtpFile virtualFtpFile = new VirtualFtpFile(); + virtualFtpFile.setName(name); + virtualFtpFile.setDirectory(true); + return virtualFtpFile; + } + + @Override + public String getAbsolutePath() { + return FtpFileSystemView.HOME_PATH + "/" + name; + } + + @Override + public String getName() { + return name; + } + + @Override + public boolean isHidden() { + return hidden; + } + + @Override + public boolean isDirectory() { + return directory; + } + + @Override + public boolean isFile() { + return !directory; + } + + @Override + public boolean doesExist() { + return false; + } + + @Override + public boolean isReadable() { + return true; + } + + @Override + public boolean isWritable() { + return true; + } + + @Override + public boolean isRemovable() { + return true; + } + + @Override + public String getOwnerName() { + return ownerName; + } + + @Override + public String getGroupName() { + return "root"; + } + + @Override + public int getLinkCount() { + return 0; + } + + @Override + public long getLastModified() { + if (lastModified == null) { + lastModified = System.currentTimeMillis(); + } + return lastModified; + } + + @Override + public boolean setLastModified(long time) { + lastModified = time; + return true; + } + + @Override + public long getSize() { + return size; + } + + @Override + public Object getPhysicalFile() { + System.err.println("getPhysicalFile"); + return null; + } + + @Override + public boolean mkdir() { + return true; + } + + @Override + public boolean delete() { + return true; + } + + @Override + public boolean move(FtpFile destination) { + this.name = destination.getName(); + return true; + } + + @Override + public List listFiles() { + return Collections.emptyList(); + } + + @Override + public OutputStream createOutputStream(long offset) throws IOException { + return outputStream; + } + + @Override + public InputStream createInputStream(long offset) throws IOException { + System.out.println("createInputStream----"); + return null; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/conf/redis/RedisMsgListenConfig.java b/src/main/java/com/genersoft/iot/vmp/conf/redis/RedisMsgListenConfig.java new file mode 100644 index 0000000..bbb94b3 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/conf/redis/RedisMsgListenConfig.java @@ -0,0 +1,76 @@ +package com.genersoft.iot.vmp.conf.redis; + + +import com.genersoft.iot.vmp.common.VideoManagerConstants; +import com.genersoft.iot.vmp.service.redisMsg.*; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.annotation.Order; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.listener.PatternTopic; +import org.springframework.data.redis.listener.RedisMessageListenerContainer; + + +/** + * @description:Redis中间件配置类,使用spring-data-redis集成,自动从application.yml中加载redis配置 + * @author: swwheihei + * @date: 2019年5月30日 上午10:58:25 + * + */ +@Configuration +@Order(value=1) +public class RedisMsgListenConfig { + + @Autowired + private RedisGpsMsgListener redisGPSMsgListener; + + @Autowired + private RedisAlarmMsgListener redisAlarmMsgListener; + + @Autowired + private RedisPushStreamStatusMsgListener redisPushStreamStatusMsgListener; + + @Autowired + private RedisPushStreamListMsgListener pushStreamListMsgListener; + + @Autowired + private RedisGroupMsgListener groupMsgListener; + + @Autowired + private RedisGroupChangeListener groupChangeListener; + + @Autowired + private RedisCloseStreamMsgListener redisCloseStreamMsgListener; + + @Autowired + private RedisRpcConfig redisRpcConfig; + + @Autowired + private RedisPushStreamResponseListener redisPushStreamCloseResponseListener; + + + /** + * redis消息监听器容器 可以添加多个监听不同话题的redis监听器,只需要把消息监听器和相应的消息订阅处理器绑定,该消息监听器 + * 通过反射技术调用消息订阅处理器的相关方法进行一些业务处理 + * + * @param connectionFactory + * @return + */ + @Bean + RedisMessageListenerContainer container(RedisConnectionFactory connectionFactory) { + + RedisMessageListenerContainer container = new RedisMessageListenerContainer(); + container.setConnectionFactory(connectionFactory); + container.addMessageListener(redisGPSMsgListener, new PatternTopic(VideoManagerConstants.VM_MSG_GPS)); + container.addMessageListener(redisAlarmMsgListener, new PatternTopic(VideoManagerConstants.VM_MSG_SUBSCRIBE_ALARM_RECEIVE)); + container.addMessageListener(redisPushStreamStatusMsgListener, new PatternTopic(VideoManagerConstants.VM_MSG_PUSH_STREAM_STATUS_CHANGE)); + container.addMessageListener(pushStreamListMsgListener, new PatternTopic(VideoManagerConstants.VM_MSG_PUSH_STREAM_LIST_CHANGE)); + container.addMessageListener(redisCloseStreamMsgListener, new PatternTopic(VideoManagerConstants.VM_MSG_STREAM_PUSH_CLOSE)); + container.addMessageListener(redisRpcConfig, new PatternTopic(RedisRpcConfig.REDIS_REQUEST_CHANNEL_KEY)); + container.addMessageListener(redisPushStreamCloseResponseListener, new PatternTopic(VideoManagerConstants.VM_MSG_STREAM_PUSH_RESPONSE)); + container.addMessageListener(groupMsgListener, new PatternTopic(VideoManagerConstants.VM_MSG_GROUP_LIST_RESPONSE)); + container.addMessageListener(groupChangeListener, new PatternTopic(VideoManagerConstants.VM_MSG_GROUP_LIST_CHANGE)); + return container; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/conf/redis/RedisRpcConfig.java b/src/main/java/com/genersoft/iot/vmp/conf/redis/RedisRpcConfig.java new file mode 100644 index 0000000..e413d72 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/conf/redis/RedisRpcConfig.java @@ -0,0 +1,261 @@ +package com.genersoft.iot.vmp.conf.redis; + +import com.alibaba.fastjson2.JSON; +import com.genersoft.iot.vmp.common.CommonCallback; +import com.genersoft.iot.vmp.conf.UserSetting; +import com.genersoft.iot.vmp.conf.redis.bean.RedisRpcClassHandler; +import com.genersoft.iot.vmp.conf.redis.bean.RedisRpcMessage; +import com.genersoft.iot.vmp.conf.redis.bean.RedisRpcRequest; +import com.genersoft.iot.vmp.conf.redis.bean.RedisRpcResponse; +import com.genersoft.iot.vmp.service.redisMsg.dto.RpcController; +import com.genersoft.iot.vmp.vmanager.bean.ErrorCode; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.data.redis.connection.Message; +import org.springframework.data.redis.connection.MessageListener; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; +import org.springframework.stereotype.Component; + +import java.lang.reflect.Method; +import java.util.HashMap; +import java.util.Map; +import java.util.Random; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.SynchronousQueue; +import java.util.concurrent.TimeUnit; + +@Slf4j +@Component +public class RedisRpcConfig implements MessageListener { + + public final static String REDIS_REQUEST_CHANNEL_KEY = "WVP_REDIS_REQUEST_CHANNEL_KEY"; + + private final Random random = new Random(); + + @Autowired + private UserSetting userSetting; + + @Autowired + private RedisTemplate redisTemplate; + + private ConcurrentLinkedQueue taskQueue = new ConcurrentLinkedQueue<>(); + + @Qualifier("taskExecutor") + @Autowired + private ThreadPoolTaskExecutor taskExecutor; + + private final static Map protocolHash = new HashMap<>(); + + public void addHandler(String path, RedisRpcClassHandler handler) { + protocolHash.put(path, handler); + } + +// @Override +// public void run(String... args) throws Exception { +// List> classList = ClassUtil.getClassList("com.genersoft.iot.vmp.service.redisMsg.control", RedisRpcController.class); +// for (Class handlerClass : classList) { +// String controllerPath = handlerClass.getAnnotation(RedisRpcController.class).value(); +// Object bean = ClassUtil.getBean(controllerPath, handlerClass); +// // 扫描其下的方法 +// Method[] methods = handlerClass.getDeclaredMethods(); +// for (Method method : methods) { +// RedisRpcMapping annotation = method.getAnnotation(RedisRpcMapping.class); +// if (annotation != null) { +// String methodPath = annotation.value(); +// if (methodPath != null) { +// protocolHash.put(controllerPath + "/" + methodPath, new RedisRpcClassHandler(bean, method)); +// } +// } +// +// } +// +// } +// for (String s : protocolHash.keySet()) { +// System.out.println(s); +// } +// if (log.isDebugEnabled()) { +// log.debug("消息ID缓存表 protocolHash:{}", protocolHash); +// } +// } + + @Override + public void onMessage(Message message, byte[] pattern) { + boolean isEmpty = taskQueue.isEmpty(); + taskQueue.offer(message); + if (isEmpty) { + taskExecutor.execute(() -> { + while (!taskQueue.isEmpty()) { + Message msg = taskQueue.poll(); + try { + RedisRpcMessage redisRpcMessage = JSON.parseObject(new String(msg.getBody()), RedisRpcMessage.class); + if (redisRpcMessage.getRequest() != null) { + handlerRequest(redisRpcMessage.getRequest()); + } else if (redisRpcMessage.getResponse() != null){ + handlerResponse(redisRpcMessage.getResponse()); + } else { + log.error("[redis-rpc]解析失败 {}", JSON.toJSONString(redisRpcMessage)); + } + } catch (Exception e) { + log.error("[redis-rpc]解析异常 {}",new String(msg.getBody()), e); + } + } + }); + } + } + + private void handlerResponse(RedisRpcResponse response) { + if (userSetting.getServerId().equals(response.getToId())) { + return; + } + log.info("[redis-rpc] << {}", response); + response(response); + } + + private void handlerRequest(RedisRpcRequest request) { + try { + if (userSetting.getServerId().equals(request.getFromId())) { + return; + } + log.info("[redis-rpc] << {}", request); + RedisRpcClassHandler redisRpcClassHandler = protocolHash.get(request.getUri()); + if (redisRpcClassHandler == null) { + log.error("[redis-rpc] 路径: {}不存在", request.getUri()); + return; + } + RpcController controller = redisRpcClassHandler.getController(); + Method method = redisRpcClassHandler.getMethod(); + // 没有携带目标ID的可以理解为哪个wvp有结果就哪个回复,携带目标ID,但是如果是不存在的uri则直接回复404 + if (userSetting.getServerId().equals(request.getToId())) { + if (method == null) { + // 回复404结果 + RedisRpcResponse response = request.getResponse(); + response.setStatusCode(ErrorCode.ERROR404.getCode()); + sendResponse(response); + return; + } + RedisRpcResponse response = (RedisRpcResponse)method.invoke(controller, request); + if(response != null) { + sendResponse(response); + } + }else { + if (method == null) { + // 回复404结果 + RedisRpcResponse response = request.getResponse(); + response.setStatusCode(ErrorCode.ERROR404.getCode()); + sendResponse(response); + return; + } + RedisRpcResponse response = (RedisRpcResponse)method.invoke(controller, request); + if (response != null) { + sendResponse(response); + } + } + }catch (Exception e) { + log.error("[redis-rpc ] 处理请求失败 ", e); + RedisRpcResponse response = request.getResponse(); + response.setStatusCode(ErrorCode.ERROR100.getCode()); + sendResponse(response); + } + } + + private void sendResponse(RedisRpcResponse response){ + log.info("[redis-rpc] >> {}", response); + response.setToId(userSetting.getServerId()); + RedisRpcMessage message = new RedisRpcMessage(); + message.setResponse(response); + redisTemplate.convertAndSend(REDIS_REQUEST_CHANNEL_KEY, message); + } + + private void sendRequest(RedisRpcRequest request){ + log.info("[redis-rpc] >> {}", request); + RedisRpcMessage message = new RedisRpcMessage(); + message.setRequest(request); + redisTemplate.convertAndSend(REDIS_REQUEST_CHANNEL_KEY, message); + } + + private final Map> topicSubscribers = new ConcurrentHashMap<>(); + private final Map> callbacks = new ConcurrentHashMap<>(); + + public RedisRpcResponse request(RedisRpcRequest request, long timeOut) { + return request(request, timeOut, TimeUnit.SECONDS); + } + + public RedisRpcResponse request(RedisRpcRequest request, long timeOut, TimeUnit timeUnit) { + request.setSn((long) random.nextInt(1000) + 1); + SynchronousQueue subscribe = subscribe(request.getSn()); + + try { + sendRequest(request); + return subscribe.poll(timeOut, timeUnit); + } catch (InterruptedException e) { + log.warn("[redis rpc timeout] uri: {}, sn: {}", request.getUri(), request.getSn(), e); + RedisRpcResponse redisRpcResponse = new RedisRpcResponse(); + redisRpcResponse.setStatusCode(ErrorCode.ERROR486.getCode()); + return redisRpcResponse; + } finally { + this.unsubscribe(request.getSn()); + } + } + + public void request(RedisRpcRequest request, CommonCallback callback) { + request.setSn((long) random.nextInt(1000) + 1); + setCallback(request.getSn(), callback); + sendRequest(request); + } + + public Boolean response(RedisRpcResponse response) { + SynchronousQueue queue = topicSubscribers.get(response.getSn()); + CommonCallback callback = callbacks.get(response.getSn()); + if (queue != null) { + try { + return queue.offer(response, 2, TimeUnit.SECONDS); + } catch (InterruptedException e) { + log.error("{}", e.getMessage(), e); + } + }else if (callback != null) { + callback.run(response); + callbacks.remove(response.getSn()); + } + return false; + } + + private void unsubscribe(long key) { + topicSubscribers.remove(key); + } + + + private SynchronousQueue subscribe(long key) { + SynchronousQueue queue = null; + if (!topicSubscribers.containsKey(key)) + topicSubscribers.put(key, queue = new SynchronousQueue<>()); + return queue; + } + + private void setCallback(long key, CommonCallback callback) { + // TODO 如果多个上级点播同一个通道会有问题 + callbacks.put(key, callback); + } + + public void removeCallback(long key) { + callbacks.remove(key); + } + + + public int getCallbackCount(){ + return callbacks.size(); + } + + + + +// @Scheduled(fixedRate = 1000) //每1秒执行一次 +// public void execute(){ +// logger.info("callbacks的长度: " + callbacks.size()); +// logger.info("队列的长度: " + topicSubscribers.size()); +// logger.info("HOOK监听的长度: " + hookSubscribe.size()); +// logger.info(""); +// } +} diff --git a/src/main/java/com/genersoft/iot/vmp/conf/redis/RedisTemplateConfig.java b/src/main/java/com/genersoft/iot/vmp/conf/redis/RedisTemplateConfig.java new file mode 100644 index 0000000..f36a4d5 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/conf/redis/RedisTemplateConfig.java @@ -0,0 +1,46 @@ +package com.genersoft.iot.vmp.conf.redis; + +import com.alibaba.fastjson2.support.spring.data.redis.GenericFastJsonRedisSerializer; +import com.genersoft.iot.vmp.gb28181.bean.MobilePosition; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.serializer.StringRedisSerializer; + +@Configuration +public class RedisTemplateConfig { + + @Bean + public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory) { + RedisTemplate redisTemplate = new RedisTemplate<>(); + // 使用fastJson序列化 + GenericFastJsonRedisSerializer fastJsonRedisSerializer = new GenericFastJsonRedisSerializer(); + // value值的序列化采用fastJsonRedisSerializer + redisTemplate.setValueSerializer(fastJsonRedisSerializer); + redisTemplate.setHashValueSerializer(fastJsonRedisSerializer); + + // key的序列化采用StringRedisSerializer + redisTemplate.setKeySerializer(new StringRedisSerializer()); + redisTemplate.setHashKeySerializer(new StringRedisSerializer()); + redisTemplate.setConnectionFactory(redisConnectionFactory); + return redisTemplate; + } + + @Bean + public RedisTemplate getRedisTemplateForMobilePosition(RedisConnectionFactory redisConnectionFactory) { + RedisTemplate redisTemplate = new RedisTemplate<>(); + // 使用fastJson序列化 + GenericFastJsonRedisSerializer fastJsonRedisSerializer = new GenericFastJsonRedisSerializer(); + // value值的序列化采用fastJsonRedisSerializer + redisTemplate.setValueSerializer(fastJsonRedisSerializer); + redisTemplate.setHashValueSerializer(fastJsonRedisSerializer); + + // key的序列化采用StringRedisSerializer + redisTemplate.setKeySerializer(new StringRedisSerializer()); + redisTemplate.setHashKeySerializer(new StringRedisSerializer()); + redisTemplate.setConnectionFactory(redisConnectionFactory); + return redisTemplate; + } + +} diff --git a/src/main/java/com/genersoft/iot/vmp/conf/redis/bean/RedisRpcClassHandler.java b/src/main/java/com/genersoft/iot/vmp/conf/redis/bean/RedisRpcClassHandler.java new file mode 100644 index 0000000..1fab24b --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/conf/redis/bean/RedisRpcClassHandler.java @@ -0,0 +1,18 @@ +package com.genersoft.iot.vmp.conf.redis.bean; + +import com.genersoft.iot.vmp.service.redisMsg.dto.RpcController; +import lombok.Data; + +import java.lang.reflect.Method; + +@Data +public class RedisRpcClassHandler { + + private RpcController controller; + private Method method; + + public RedisRpcClassHandler(RpcController controller, Method method) { + this.controller = controller; + this.method = method; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/conf/redis/bean/RedisRpcMessage.java b/src/main/java/com/genersoft/iot/vmp/conf/redis/bean/RedisRpcMessage.java new file mode 100644 index 0000000..061df6f --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/conf/redis/bean/RedisRpcMessage.java @@ -0,0 +1,24 @@ +package com.genersoft.iot.vmp.conf.redis.bean; + +public class RedisRpcMessage { + + private RedisRpcRequest request; + + private RedisRpcResponse response; + + public RedisRpcRequest getRequest() { + return request; + } + + public void setRequest(RedisRpcRequest request) { + this.request = request; + } + + public RedisRpcResponse getResponse() { + return response; + } + + public void setResponse(RedisRpcResponse response) { + this.response = response; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/conf/redis/bean/RedisRpcRequest.java b/src/main/java/com/genersoft/iot/vmp/conf/redis/bean/RedisRpcRequest.java new file mode 100644 index 0000000..a02db67 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/conf/redis/bean/RedisRpcRequest.java @@ -0,0 +1,93 @@ +package com.genersoft.iot.vmp.conf.redis.bean; + +/** + * 通过redis发送请求 + */ +public class RedisRpcRequest { + + /** + * 来自的WVP ID + */ + private String fromId; + + + /** + * 目标的WVP ID + */ + private String toId; + + /** + * 序列号 + */ + private long sn; + + /** + * 访问的路径 + */ + private String uri; + + /** + * 参数 + */ + private Object param; + + public String getFromId() { + return fromId; + } + + public void setFromId(String fromId) { + this.fromId = fromId; + } + + public String getToId() { + return toId; + } + + public void setToId(String toId) { + this.toId = toId; + } + + public String getUri() { + return uri; + } + + public void setUri(String uri) { + this.uri = uri; + } + + public Object getParam() { + return param; + } + + public void setParam(Object param) { + this.param = param; + } + + public long getSn() { + return sn; + } + + public void setSn(long sn) { + this.sn = sn; + } + + @Override + public String toString() { + return "RedisRpcRequest{" + + "uri='" + uri + '\'' + + ", fromId='" + fromId + '\'' + + ", toId='" + toId + '\'' + + ", sn=" + sn + + ", param=" + param + + '}'; + } + + public RedisRpcResponse getResponse() { + RedisRpcResponse response = new RedisRpcResponse(); + response.setFromId(fromId); + response.setToId(toId); + response.setSn(sn); + response.setUri(uri); + return response; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/conf/redis/bean/RedisRpcResponse.java b/src/main/java/com/genersoft/iot/vmp/conf/redis/bean/RedisRpcResponse.java new file mode 100644 index 0000000..21f9e7e --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/conf/redis/bean/RedisRpcResponse.java @@ -0,0 +1,99 @@ +package com.genersoft.iot.vmp.conf.redis.bean; + +/** + * 通过redis发送回复 + */ +public class RedisRpcResponse { + + /** + * 来自的WVP ID + */ + private String fromId; + + + /** + * 目标的WVP ID + */ + private String toId; + + + /** + * 序列号 + */ + private long sn; + + /** + * 状态码 + */ + private int statusCode; + + /** + * 访问的路径 + */ + private String uri; + + /** + * 参数 + */ + private Object body; + + public String getFromId() { + return fromId; + } + + public void setFromId(String fromId) { + this.fromId = fromId; + } + + public String getToId() { + return toId; + } + + public void setToId(String toId) { + this.toId = toId; + } + + public long getSn() { + return sn; + } + + public void setSn(long sn) { + this.sn = sn; + } + + public int getStatusCode() { + return statusCode; + } + + public void setStatusCode(int statusCode) { + this.statusCode = statusCode; + } + + public String getUri() { + return uri; + } + + public void setUri(String uri) { + this.uri = uri; + } + + public Object getBody() { + return body; + } + + public void setBody(Object body) { + this.body = body; + } + + @Override + public String toString() { + return "RedisRpcResponse{" + + "uri='" + uri + '\'' + + ", fromId='" + fromId + '\'' + + ", toId='" + toId + '\'' + + ", sn=" + sn + + ", statusCode=" + statusCode + + ", body=" + body + + '}'; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/conf/security/AnonymousAuthenticationEntryPoint.java b/src/main/java/com/genersoft/iot/vmp/conf/security/AnonymousAuthenticationEntryPoint.java new file mode 100644 index 0000000..08827c5 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/conf/security/AnonymousAuthenticationEntryPoint.java @@ -0,0 +1,48 @@ +package com.genersoft.iot.vmp.conf.security; + +import com.alibaba.fastjson2.JSONObject; +import com.genersoft.iot.vmp.conf.security.dto.JwtUser; +import com.genersoft.iot.vmp.vmanager.bean.ErrorCode; +import org.springframework.http.MediaType; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.web.AuthenticationEntryPoint; +import org.springframework.stereotype.Component; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.nio.charset.StandardCharsets; + +/** + * 处理匿名用户访问逻辑 + * @author lin + */ +@Component +public class AnonymousAuthenticationEntryPoint implements AuthenticationEntryPoint { + + @Override + public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) { + String jwt = request.getHeader(JwtUtils.getHeader()); + JwtUser jwtUser = JwtUtils.verifyToken(jwt); + String username = jwtUser.getUserName(); + UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(username, jwtUser.getPassword() ); + SecurityContextHolder.getContext().setAuthentication(token); + JSONObject jsonObject = new JSONObject(); + jsonObject.put("code", ErrorCode.ERROR401.getCode()); + jsonObject.put("msg", ErrorCode.ERROR401.getMsg()); + String logUri = "api/user/login"; + if (request.getRequestURI().contains(logUri)){ + jsonObject.put("msg", e.getMessage()); + } + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + response.setContentType(MediaType.APPLICATION_JSON_VALUE); + response.setCharacterEncoding(StandardCharsets.UTF_8.name()); + try { + response.getWriter().print(jsonObject.toJSONString()); + } catch (IOException ioException) { + ioException.printStackTrace(); + } + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/conf/security/DefaultUserDetailsServiceImpl.java b/src/main/java/com/genersoft/iot/vmp/conf/security/DefaultUserDetailsServiceImpl.java new file mode 100644 index 0000000..d21fdaa --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/conf/security/DefaultUserDetailsServiceImpl.java @@ -0,0 +1,49 @@ +package com.genersoft.iot.vmp.conf.security; + +import com.alibaba.excel.util.StringUtils; +import com.genersoft.iot.vmp.conf.security.dto.LoginUser; +import com.genersoft.iot.vmp.service.IUserService; +import com.genersoft.iot.vmp.storager.dao.dto.User; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.stereotype.Component; + +import java.time.LocalDateTime; + +/** + * 用户登录认证逻辑 + */ +@Slf4j +@Component +public class DefaultUserDetailsServiceImpl implements UserDetailsService { + + @Autowired + private IUserService userService; + + @Override + public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { + if (StringUtils.isBlank(username)) { + log.info("登录用户:{} 不存在", username); + throw new UsernameNotFoundException("登录用户:" + username + " 不存在"); + } + + // 查出密码 + User user = userService.getUserByUsername(username); + if (user == null) { + log.info("登录用户:{} 不存在", username); + throw new UsernameNotFoundException("登录用户:" + username + " 不存在"); + } + String password = SecurityUtils.encryptPassword(user.getPassword()); + user.setPassword(password); + return new LoginUser(user, LocalDateTime.now()); + } + + + + + + +} diff --git a/src/main/java/com/genersoft/iot/vmp/conf/security/JwtAuthenticationFilter.java b/src/main/java/com/genersoft/iot/vmp/conf/security/JwtAuthenticationFilter.java new file mode 100644 index 0000000..19fd4f3 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/conf/security/JwtAuthenticationFilter.java @@ -0,0 +1,114 @@ +package com.genersoft.iot.vmp.conf.security; + +import com.genersoft.iot.vmp.conf.UserSetting; +import com.genersoft.iot.vmp.conf.security.dto.JwtUser; +import com.genersoft.iot.vmp.storager.dao.dto.Role; +import com.genersoft.iot.vmp.storager.dao.dto.User; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; +import org.springframework.web.util.ContentCachingRequestWrapper; + +import java.io.IOException; +import java.util.ArrayList; + +/** + * jwt token 过滤器 + */ + +@Slf4j +@Component +public class JwtAuthenticationFilter extends OncePerRequestFilter { + + private final static String WSHeader = "sec-websocket-protocol"; + + + @Autowired + private UserSetting userSetting; + + + @Override + protected void doFilterInternal(HttpServletRequest servletRequest, HttpServletResponse response, FilterChain chain) throws IOException, ServletException { + ContentCachingRequestWrapper request = new ContentCachingRequestWrapper(servletRequest); + // 忽略登录请求的token验证 + String requestURI = request.getRequestURI(); + if ((requestURI.startsWith("/doc.html") || requestURI.startsWith("/swagger-ui") ) && !userSetting.getDocEnable()) { + response.setStatus(HttpServletResponse.SC_NOT_FOUND); + return; + } + + if (requestURI.equalsIgnoreCase("/api/user/login")) { + chain.doFilter(request, response); + return; + } + + if (!userSetting.getInterfaceAuthentication()) { + UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(null, null, new ArrayList<>() ); + SecurityContextHolder.getContext().setAuthentication(token); + chain.doFilter(request, response); + return; + } + + String jwt = request.getHeader(JwtUtils.getHeader()); + // 这里如果没有jwt,继续往后走,因为后面还有鉴权管理器等去判断是否拥有身份凭证,所以是可以放行的 + // 没有jwt相当于匿名访问,若有一些接口是需要权限的,则不能访问这些接口 + + // websocket 鉴权信息默认存储在这里 + String secWebsocketProtocolHeader = request.getHeader(WSHeader); + if (StringUtils.isBlank(jwt)) { + + if (secWebsocketProtocolHeader != null) { + jwt = secWebsocketProtocolHeader; + response.setHeader(WSHeader, secWebsocketProtocolHeader); + }else { + jwt = request.getParameter(JwtUtils.getHeader()); + } + if (StringUtils.isBlank(jwt)) { + jwt = request.getHeader(JwtUtils.getApiKeyHeader()); + if (StringUtils.isBlank(jwt)) { + chain.doFilter(request, response); + return; + } + } + } + + JwtUser jwtUser = JwtUtils.verifyToken(jwt); + String username = jwtUser.getUserName(); + // TODO 处理各个状态 + switch (jwtUser.getStatus()){ + case EXPIRED: + response.setStatus(401); + chain.doFilter(request, response); + // 异常 + return; + case EXCEPTION: + // 过期 + response.setStatus(400); + chain.doFilter(request, response); + return; + case EXPIRING_SOON: + // 即将过期 +// return; + default: + } + // 构建UsernamePasswordAuthenticationToken,这里密码为null,是因为提供了正确的JWT,实现自动登录 + User user = new User(); + user.setId(jwtUser.getUserId()); + user.setUsername(jwtUser.getUserName()); + user.setPassword(jwtUser.getPassword()); + Role role = new Role(); + role.setId(jwtUser.getRoleId()); + user.setRole(role); + UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(user, jwtUser.getPassword(), new ArrayList<>() ); + SecurityContextHolder.getContext().setAuthentication(token); + chain.doFilter(request, response); + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/conf/security/JwtUtils.java b/src/main/java/com/genersoft/iot/vmp/conf/security/JwtUtils.java new file mode 100644 index 0000000..f6528a0 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/conf/security/JwtUtils.java @@ -0,0 +1,274 @@ +package com.genersoft.iot.vmp.conf.security; + +import com.genersoft.iot.vmp.conf.UserSetting; +import com.genersoft.iot.vmp.conf.security.dto.JwtUser; +import com.genersoft.iot.vmp.service.IUserApiKeyService; +import com.genersoft.iot.vmp.service.IUserService; +import com.genersoft.iot.vmp.storager.dao.dto.User; +import com.genersoft.iot.vmp.storager.dao.dto.UserApiKey; +import lombok.extern.slf4j.Slf4j; +import org.jose4j.jwk.JsonWebKey; +import org.jose4j.jwk.JsonWebKeySet; +import org.jose4j.jwk.RsaJsonWebKey; +import org.jose4j.jwk.RsaJwkGenerator; +import org.jose4j.jws.AlgorithmIdentifiers; +import org.jose4j.jws.JsonWebSignature; +import org.jose4j.jwt.JwtClaims; +import org.jose4j.jwt.NumericDate; +import org.jose4j.jwt.consumer.ErrorCodes; +import org.jose4j.jwt.consumer.InvalidJwtException; +import org.jose4j.jwt.consumer.JwtConsumer; +import org.jose4j.jwt.consumer.JwtConsumerBuilder; +import org.jose4j.lang.JoseException; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.core.io.ClassPathResource; +import org.springframework.stereotype.Component; + +import jakarta.annotation.Resource; +import java.io.BufferedReader; +import java.io.File; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.time.LocalDateTime; +import java.time.ZoneOffset; +import java.util.List; +import java.util.Map; + +@Slf4j +@Component +public class JwtUtils implements InitializingBean { + + public static final String HEADER = "access-token"; + + public static final String API_KEY_HEADER = "api-key"; + + private static final String AUDIENCE = "Audience"; + + private static final String keyId = "3e79646c4dbc408383a9eed09f2b85ae"; + + /** + * token过期时间(分钟) + */ + public static final long EXPIRATION_TIME = 30; + + private static RsaJsonWebKey rsaJsonWebKey; + + private static IUserService userService; + + private static IUserApiKeyService userApiKeyService; + + private static UserSetting userSetting; + + public static String getApiKeyHeader() { + return API_KEY_HEADER; + } + + @Resource + public void setUserService(IUserService userService) { + JwtUtils.userService = userService; + } + + @Resource + public void setUserApiKeyService(IUserApiKeyService userApiKeyService) { + JwtUtils.userApiKeyService = userApiKeyService; + } + + @Resource + public void setUserSetting(UserSetting userSetting) { + JwtUtils.userSetting = userSetting; + } + + @Override + public void afterPropertiesSet() { + try { + rsaJsonWebKey = generateRsaJsonWebKey(); + } catch (JoseException e) { + log.error("生成RsaJsonWebKey报错。", e); + } + } + + /** + * 创建密钥对 + * + * @throws JoseException JoseException + */ + private RsaJsonWebKey generateRsaJsonWebKey() throws JoseException { + RsaJsonWebKey rsaJsonWebKey = null; + try { + String jwkFile = userSetting.getJwkFile(); + InputStream inputStream = null; + if (jwkFile.startsWith("classpath:")){ + String filePath = jwkFile.substring("classpath:".length()); + ClassPathResource civilCodeFile = new ClassPathResource(filePath); + if (civilCodeFile.exists()) { + inputStream = civilCodeFile.getInputStream(); + } + }else { + File civilCodeFile = new File(userSetting.getCivilCodeFile()); + if (civilCodeFile.exists()) { + inputStream = Files.newInputStream(civilCodeFile.toPath()); + } + + } + if (inputStream == null ) { + log.warn("[API AUTH] 读取jwk.json失败,文件不存在,将使用新生成的随机RSA密钥对"); + // 生成一个RSA密钥对,该密钥对将用于JWT的签名和验证,包装在JWK中 + rsaJsonWebKey = RsaJwkGenerator.generateJwk(2048); + // 给JWK一个密钥ID + rsaJsonWebKey.setKeyId(keyId); + return rsaJsonWebKey; + } + BufferedReader inputStreamReader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8)); + int index = -1; + String line; + StringBuilder content = new StringBuilder(); + while ((line = inputStreamReader.readLine()) != null) { + content.append(line); + index ++; + if (index == 0) { + continue; + } + } + inputStreamReader.close(); + inputStream.close(); + + + String jwkJson = content.toString(); + JsonWebKeySet jsonWebKeySet = new JsonWebKeySet(jwkJson); + List jsonWebKeys = jsonWebKeySet.getJsonWebKeys(); + if (!jsonWebKeys.isEmpty()) { + JsonWebKey jsonWebKey = jsonWebKeys.get(0); + if (jsonWebKey instanceof RsaJsonWebKey) { + rsaJsonWebKey = (RsaJsonWebKey) jsonWebKey; + } + } + } catch (Exception ignore) {} + if (rsaJsonWebKey == null) { + log.warn("[API AUTH] 读取jwk.json失败,获取内容失败,将使用新生成的随机RSA密钥对"); + // 生成一个RSA密钥对,该密钥对将用于JWT的签名和验证,包装在JWK中 + rsaJsonWebKey = RsaJwkGenerator.generateJwk(2048); + // 给JWK一个密钥ID + rsaJsonWebKey.setKeyId(keyId); + }else { + log.info("[API AUTH] 读取jwk.json成功"); + } + return rsaJsonWebKey; + } + + public static String createToken(String username, Long expirationTime, Map extra) { + try { + /* + * “iss” (issuer) 发行人 + * “sub” (subject) 主题 + * “aud” (audience) 接收方 用户 + * “exp” (expiration time) 到期时间 + * “nbf” (not before) 在此之前不可用 + * “iat” (issued at) jwt的签发时间 + */ + JwtClaims claims = new JwtClaims(); + claims.setGeneratedJwtId(); + claims.setIssuedAtToNow(); + // 令牌将过期的时间 分钟 + if (expirationTime != null) { + claims.setExpirationTimeMinutesInTheFuture(expirationTime); + } + claims.setNotBeforeMinutesInThePast(0); + claims.setSubject("login"); + claims.setAudience(AUDIENCE); + //添加自定义参数,必须是字符串类型 + claims.setClaim("userName", username); + if (extra != null) { + extra.forEach(claims::setClaim); + } + //jws + JsonWebSignature jws = new JsonWebSignature(); + //签名算法RS256 + jws.setAlgorithmHeaderValue(AlgorithmIdentifiers.RSA_USING_SHA256); + jws.setKeyIdHeaderValue(keyId); + jws.setPayload(claims.toJson()); + + jws.setKey(rsaJsonWebKey.getPrivateKey()); + + //get token + return jws.getCompactSerialization(); + } catch (JoseException e) { + log.error("[Token生成失败]: {}", e.getMessage()); + } + return null; + } + + public static String createToken(String username, Long expirationTime) { + return createToken(username, expirationTime, null); + } + + public static String createToken(String username) { + return createToken(username, userSetting.getLoginTimeout()); + } + + public static String getHeader() { + return HEADER; + } + + public static JwtUser verifyToken(String token) { + + JwtUser jwtUser = new JwtUser(); + + try { + JwtConsumer consumer = new JwtConsumerBuilder() + //.setRequireExpirationTime() + //.setMaxFutureValidityInMinutes(5256000) + .setAllowedClockSkewInSeconds(30) + .setRequireSubject() + //.setExpectedIssuer("") + .setExpectedAudience(AUDIENCE) + .setVerificationKey(rsaJsonWebKey.getPublicKey()) + .build(); + + JwtClaims claims = consumer.processToClaims(token); + NumericDate expirationTime = claims.getExpirationTime(); + if (expirationTime != null) { + // 判断是否即将过期, 默认剩余时间小于5分钟未即将过期 + // 剩余时间 (秒) + long timeRemaining = expirationTime.getValue() - LocalDateTime.now().toEpochSecond(ZoneOffset.ofHours(8)); + if (timeRemaining < 5 * 60) { + jwtUser.setStatus(JwtUser.TokenStatus.EXPIRING_SOON); + } else { + jwtUser.setStatus(JwtUser.TokenStatus.NORMAL); + } + } else { + jwtUser.setStatus(JwtUser.TokenStatus.NORMAL); + } + + Long apiKeyId = claims.getClaimValue("apiKeyId", Long.class); + if (apiKeyId != null) { + UserApiKey userApiKey = userApiKeyService.getUserApiKeyById(apiKeyId.intValue()); + if (userApiKey == null || !userApiKey.isEnable()) { + jwtUser.setStatus(JwtUser.TokenStatus.EXPIRED); + } + } + + String username = (String) claims.getClaimValue("userName"); + User user = userService.getUserByUsername(username); + + jwtUser.setUserName(username); + jwtUser.setPassword(user.getPassword()); + jwtUser.setRoleId(user.getRole().getId()); + jwtUser.setUserId(user.getId()); + + return jwtUser; + } catch (InvalidJwtException e) { + if (e.hasErrorCode(ErrorCodes.EXPIRED)) { + jwtUser.setStatus(JwtUser.TokenStatus.EXPIRED); + } else { + jwtUser.setStatus(JwtUser.TokenStatus.EXCEPTION); + } + return jwtUser; + } catch (Exception e) { + log.error("[Token解析失败]: {}", e.getMessage()); + jwtUser.setStatus(JwtUser.TokenStatus.EXPIRED); + return jwtUser; + } + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/conf/security/LogoutHandler.java b/src/main/java/com/genersoft/iot/vmp/conf/security/LogoutHandler.java new file mode 100644 index 0000000..4809e32 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/conf/security/LogoutHandler.java @@ -0,0 +1,25 @@ +package com.genersoft.iot.vmp.conf.security; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.core.Authentication; +import org.springframework.security.web.authentication.logout.LogoutSuccessHandler; +import org.springframework.stereotype.Component; + +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; + +/** + * 退出登录成功 + */ +@Slf4j +@Component +public class LogoutHandler implements LogoutSuccessHandler { + + @Override + public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException { + String username = request.getParameter("username"); + log.info("[退出登录成功] - [{}]", username); + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/conf/security/SecurityUtils.java b/src/main/java/com/genersoft/iot/vmp/conf/security/SecurityUtils.java new file mode 100644 index 0000000..f012f7e --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/conf/security/SecurityUtils.java @@ -0,0 +1,84 @@ +package com.genersoft.iot.vmp.conf.security; + +import com.genersoft.iot.vmp.conf.security.dto.LoginUser; +import com.genersoft.iot.vmp.storager.dao.dto.User; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; + +import javax.security.sasl.AuthenticationException; +import java.time.LocalDateTime; + +public class SecurityUtils { + + /** + * 描述根据账号密码进行调用security进行认证授权 主动调 + * 用AuthenticationManager的authenticate方法实现 + * 授权成功后将用户信息存入SecurityContext当中 + * @param username 用户名 + * @param password 密码 + * @param authenticationManager 认证授权管理器, + * @see AuthenticationManager + * @return UserInfo 用户信息 + */ + public static LoginUser login(String username, String password, AuthenticationManager authenticationManager) throws AuthenticationException { + //使用security框架自带的验证token生成器 也可以自定义。 + UsernamePasswordAuthenticationToken token =new UsernamePasswordAuthenticationToken(username,password); + //认证 如果失败,这里会自动异常后返回,所以这里不需要判断返回值是否为空,确定是否登录成功 + Authentication authenticate = authenticationManager.authenticate(token); + LoginUser user = (LoginUser) authenticate.getPrincipal(); + + SecurityContextHolder.getContext().setAuthentication(token); + + return user; + } + + /** + * 获取当前登录的所有认证信息 + * @return + */ + public static Authentication getAuthentication(){ + SecurityContext context = SecurityContextHolder.getContext(); + return context.getAuthentication(); + } + + /** + * 获取当前登录用户信息 + * @return + */ + public static LoginUser getUserInfo(){ + Authentication authentication = getAuthentication(); + if(authentication!=null){ + Object principal = authentication.getPrincipal(); + if(principal!=null && !"anonymousUser".equals(principal.toString())){ + + User user = (User) principal; + return new LoginUser(user, LocalDateTime.now()); + } + } + return null; + } + + /** + * 获取当前登录用户ID + * @return + */ + public static int getUserId(){ + LoginUser user = getUserInfo(); + return user.getId(); + } + + /** + * 生成BCryptPasswordEncoder密码 + * + * @param password 密码 + * @return 加密字符串 + */ + public static String encryptPassword(String password) { + BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder(); + return passwordEncoder.encode(password); + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/conf/security/WebSecurityConfig.java b/src/main/java/com/genersoft/iot/vmp/conf/security/WebSecurityConfig.java new file mode 100644 index 0000000..7099337 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/conf/security/WebSecurityConfig.java @@ -0,0 +1,159 @@ +package com.genersoft.iot.vmp.conf.security; + +import com.genersoft.iot.vmp.conf.UserSetting; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.annotation.Order; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.AuthenticationProvider; +import org.springframework.security.authentication.dao.DaoAuthenticationProvider; +import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; +import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.CorsConfigurationSource; +import org.springframework.web.cors.CorsUtils; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * 配置Spring Security + * + * @author lin + */ +@Configuration +@EnableWebSecurity +@EnableGlobalMethodSecurity(prePostEnabled = true) +@Order(1) +@Slf4j +public class WebSecurityConfig { + + @Autowired + private UserSetting userSetting; + + @Autowired + private DefaultUserDetailsServiceImpl userDetailsService; + /** + * 登出成功的处理 + */ + @Autowired + private LogoutHandler logoutHandler; + /** + * 未登录的处理 + */ + @Autowired + private AnonymousAuthenticationEntryPoint anonymousAuthenticationEntryPoint; + @Autowired + private JwtAuthenticationFilter jwtAuthenticationFilter; + + @Bean + public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception { + return config.getAuthenticationManager(); + } + + @Bean + public AuthenticationProvider authProvider() { + DaoAuthenticationProvider provider = new DaoAuthenticationProvider(); + // 设置不隐藏 未找到用户异常 + provider.setHideUserNotFoundExceptions(true); + // 用户认证service - 查询数据库的逻辑 + provider.setUserDetailsService(userDetailsService); + // 设置密码加密算法 + provider.setPasswordEncoder(passwordEncoder()); + return provider; + } + + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + List defaultExcludes = new ArrayList<>(); + defaultExcludes.add("/"); + defaultExcludes.add("/#/**"); + defaultExcludes.add("/static/**"); + + defaultExcludes.add("/swagger-ui.html"); + defaultExcludes.add("/swagger-ui/**"); + defaultExcludes.add("/swagger-resources/**"); + defaultExcludes.add("/doc.html"); + defaultExcludes.add("/doc.html#/**"); + defaultExcludes.add("/v3/api-docs/**"); + + defaultExcludes.add("/index.html"); + defaultExcludes.add("/webjars/**"); + + defaultExcludes.add("/js/**"); + defaultExcludes.add("/api/device/query/snap/**"); + defaultExcludes.add("/record_proxy/*/**"); + defaultExcludes.add("/api/emit"); + defaultExcludes.add("/favicon.ico"); + defaultExcludes.add("/api/user/login"); + defaultExcludes.add("/index/hook/**"); + defaultExcludes.add("/api/device/query/snap/**"); + defaultExcludes.add("/index/hook/abl/**"); + defaultExcludes.add("/api/jt1078/playback/download"); + defaultExcludes.add("/api/jt1078/snap"); + + if (userSetting.getInterfaceAuthentication() && !userSetting.getInterfaceAuthenticationExcludes().isEmpty()) { + defaultExcludes.addAll(userSetting.getInterfaceAuthenticationExcludes()); + } + + http + .headers(headers -> headers.contentTypeOptions(contentType -> contentType.disable())) + .cors(cors -> cors.configurationSource(configurationSource())) + .csrf(csrf -> csrf.disable()) + .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.ALWAYS)) + .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class) + // 配置拦截规则 + .authorizeHttpRequests(auth -> auth + .requestMatchers(CorsUtils::isPreFlightRequest).permitAll() + .requestMatchers(defaultExcludes.toArray(new String[0])).permitAll() + .anyRequest().authenticated() + ) + // 异常处理器 + .exceptionHandling(exception -> exception.authenticationEntryPoint(anonymousAuthenticationEntryPoint)) + .logout(logout -> logout.logoutUrl("/api/user/logout") + .permitAll() + .logoutSuccessHandler(logoutHandler)); + + return http.build(); + } + + CorsConfigurationSource configurationSource() { + // 配置跨域 + CorsConfiguration corsConfiguration = new CorsConfiguration(); + corsConfiguration.setAllowedHeaders(Arrays.asList("*")); + corsConfiguration.setAllowedMethods(Arrays.asList("*")); + corsConfiguration.setMaxAge(3600L); + if (userSetting.getAllowedOrigins() != null && !userSetting.getAllowedOrigins().isEmpty()) { + corsConfiguration.setAllowCredentials(true); + corsConfiguration.setAllowedOrigins(userSetting.getAllowedOrigins()); + } else { + // 在SpringBoot 2.4及以上版本处理跨域时,遇到错误提示:当allowCredentials为true时,allowedOrigins不能包含特殊值"*"。 + // 解决方法是明确指定allowedOrigins或使用allowedOriginPatterns。 + corsConfiguration.setAllowCredentials(true); + corsConfiguration.addAllowedOriginPattern(CorsConfiguration.ALL); // 默认全部允许所有跨域 + } + + corsConfiguration.setExposedHeaders(Arrays.asList(JwtUtils.getHeader())); + + UrlBasedCorsConfigurationSource url = new UrlBasedCorsConfigurationSource(); + url.registerCorsConfiguration("/**", corsConfiguration); + return url; + } + + /** + * 描述: 密码加密算法 BCrypt 推荐使用 + **/ + public BCryptPasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } +} \ No newline at end of file diff --git a/src/main/java/com/genersoft/iot/vmp/conf/security/dto/JwtUser.java b/src/main/java/com/genersoft/iot/vmp/conf/security/dto/JwtUser.java new file mode 100644 index 0000000..df29c33 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/conf/security/dto/JwtUser.java @@ -0,0 +1,72 @@ +package com.genersoft.iot.vmp.conf.security.dto; + +public class JwtUser { + + public enum TokenStatus{ + /** + * 正常的使用状态 + */ + NORMAL, + /** + * 过期而失效 + */ + EXPIRED, + /** + * 即将过期 + */ + EXPIRING_SOON, + /** + * 异常 + */ + EXCEPTION + } + + private int userId; + private String userName; + + private String password; + + private int roleId; + + private TokenStatus status; + + public int getUserId() { + return userId; + } + + public void setUserId(int userId) { + this.userId = userId; + } + + public String getUserName() { + return userName; + } + + public void setUserName(String userName) { + this.userName = userName; + } + + public TokenStatus getStatus() { + return status; + } + + public void setStatus(TokenStatus status) { + this.status = status; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } + + public int getRoleId() { + return roleId; + } + + public void setRoleId(int roleId) { + this.roleId = roleId; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/conf/security/dto/LoginUser.java b/src/main/java/com/genersoft/iot/vmp/conf/security/dto/LoginUser.java new file mode 100644 index 0000000..65dc2b7 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/conf/security/dto/LoginUser.java @@ -0,0 +1,114 @@ +package com.genersoft.iot.vmp.conf.security.dto; + +import com.genersoft.iot.vmp.storager.dao.dto.Role; +import com.genersoft.iot.vmp.storager.dao.dto.User; +import lombok.Getter; +import lombok.Setter; +import org.springframework.security.core.CredentialsContainer; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.SpringSecurityCoreVersion; +import org.springframework.security.core.userdetails.UserDetails; + +import java.time.LocalDateTime; +import java.util.Collection; + +public class LoginUser implements UserDetails, CredentialsContainer { + + private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID; + + /** + * 用户 + */ + private User user; + + @Getter + @Setter + private String accessToken; + + @Setter + @Getter + private String serverId; + + /** + * 登录时间 + */ + private LocalDateTime loginTime; + + public LoginUser(User user, LocalDateTime loginTime) { + this.user = user; + this.loginTime = loginTime; + } + + + @Override + public Collection getAuthorities() { + return null; + } + + @Override + public String getPassword() { + return user.getPassword(); + } + + @Override + public String getUsername() { + return user.getUsername(); + } + + /** + * 账户是否未过期,过期无法验证 + */ + @Override + public boolean isAccountNonExpired() { + return true; + } + + /** + * 指定用户是否解锁,锁定的用户无法进行身份验证 + *

+ * 密码锁定 + *

+ */ + @Override + public boolean isAccountNonLocked() { + return true; + } + + /** + * 指示是否已过期的用户的凭据(密码),过期的凭据防止认证 + */ + @Override + public boolean isCredentialsNonExpired() { + return true; + } + + /** + * 用户是否被启用或禁用。禁用的用户无法进行身份验证。 + */ + @Override + public boolean isEnabled() { + return true; + } + + /** + * 认证完成后,擦除密码 + */ + @Override + public void eraseCredentials() { + user.setPassword(null); + } + + + public int getId() { + return user.getId(); + } + + public Role getRole() { + return user.getRole(); + } + + public String getPushKey() { + return user.getPushKey(); + } + +} diff --git a/src/main/java/com/genersoft/iot/vmp/conf/webLog/LogChannel.java b/src/main/java/com/genersoft/iot/vmp/conf/webLog/LogChannel.java new file mode 100644 index 0000000..bd94fbc --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/conf/webLog/LogChannel.java @@ -0,0 +1,65 @@ +package com.genersoft.iot.vmp.conf.webLog; + +import lombok.extern.slf4j.Slf4j; + +import jakarta.websocket.*; +import jakarta.websocket.server.ServerEndpoint; +import java.io.IOException; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +@ServerEndpoint(value = "/channel/log") +@Slf4j +public class LogChannel { + + public static final ConcurrentMap CHANNELS = new ConcurrentHashMap<>(); + + private Session session; + + @OnMessage(maxMessageSize = 1) // MaxMessage 1 byte + public void onMessage(String message) { + + try { + this.session.close(new CloseReason(CloseReason.CloseCodes.TOO_BIG, "此节点不接收任何客户端信息")); + } catch (IOException e) { + log.error("[Web-Log] 连接关闭失败: id={}, err={}", this.session.getId(), e.getMessage()); + } + } + + @OnOpen + public void onOpen(Session session, EndpointConfig endpointConfig) { + this.session = session; + this.session.setMaxIdleTimeout(0); + CHANNELS.put(this.session.getId(), this); + + log.info("[Web-Log] 连接已建立: id={}", this.session.getId()); + } + + @OnClose + public void onClose(CloseReason closeReason) { + + log.info("[Web-Log] 连接已断开: id={}, err={}", this.session.getId(), closeReason); + CHANNELS.remove(this.session.getId()); + } + + @OnError + public void onError(Throwable throwable) throws IOException { + log.info("[Web-Log] 连接错误: id={}, err= {}", this.session.getId(), throwable.getMessage()); + if (this.session.isOpen()) { + this.session.close(new CloseReason(CloseReason.CloseCodes.UNEXPECTED_CONDITION, throwable.getMessage())); + } + } + + /** + * Push messages to all clients + * + * @param message + */ + public static void push(String message) { + CHANNELS.values().stream().forEach(endpoint -> { + if (endpoint.session.isOpen()) { + endpoint.session.getAsyncRemote().sendText(message); + } + }); + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/conf/webLog/WebSocketAppender.java b/src/main/java/com/genersoft/iot/vmp/conf/webLog/WebSocketAppender.java new file mode 100644 index 0000000..000ab86 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/conf/webLog/WebSocketAppender.java @@ -0,0 +1,24 @@ +package com.genersoft.iot.vmp.conf.webLog; + +import ch.qos.logback.classic.encoder.PatternLayoutEncoder; +import ch.qos.logback.classic.spi.ILoggingEvent; +import ch.qos.logback.core.AppenderBase; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.nio.charset.StandardCharsets; + +@Data +@EqualsAndHashCode(callSuper = true) +public class WebSocketAppender extends AppenderBase { + + private PatternLayoutEncoder encoder; + + @Override + protected void append(ILoggingEvent loggingEvent) { + byte[] data = this.encoder.encode(loggingEvent); + // Push to client. +// LogChannel.push(DateUtil.timestampMsTo_yyyy_MM_dd_HH_mm_ss(loggingEvent.getTimeStamp()) + " " + loggingEvent.getFormattedMessage()); + LogChannel.push(new String(data, StandardCharsets.UTF_8)); + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/conf/websocket/WebSocketConfig.java b/src/main/java/com/genersoft/iot/vmp/conf/websocket/WebSocketConfig.java new file mode 100644 index 0000000..0ef5267 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/conf/websocket/WebSocketConfig.java @@ -0,0 +1,19 @@ +package com.genersoft.iot.vmp.conf.websocket; + +import com.genersoft.iot.vmp.conf.webLog.LogChannel; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.socket.server.standard.ServerEndpointExporter; + +@Configuration +public class WebSocketConfig { + + @Bean + public ServerEndpointExporter serverEndpointExporter(){ + ServerEndpointExporter endpointExporter = new ServerEndpointExporter(); + + endpointExporter.setAnnotatedEndpointClasses(LogChannel.class); + + return endpointExporter; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/SipLayer.java b/src/main/java/com/genersoft/iot/vmp/gb28181/SipLayer.java new file mode 100644 index 0000000..f633c2b --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/SipLayer.java @@ -0,0 +1,185 @@ +package com.genersoft.iot.vmp.gb28181; + +import com.genersoft.iot.vmp.conf.SipConfig; +import com.genersoft.iot.vmp.conf.UserSetting; +import com.genersoft.iot.vmp.gb28181.bean.GbStringMsgParserFactory; +import com.genersoft.iot.vmp.gb28181.conf.DefaultProperties; +import com.genersoft.iot.vmp.gb28181.transmit.ISIPProcessorObserver; +import gov.nist.javax.sip.SipProviderImpl; +import gov.nist.javax.sip.SipStackImpl; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.CommandLineRunner; +import org.springframework.core.annotation.Order; +import org.springframework.stereotype.Component; +import org.springframework.util.ObjectUtils; + +import javax.sip.*; +import java.net.Inet4Address; +import java.net.InetAddress; +import java.net.NetworkInterface; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; + +@Slf4j +@Component +@Order(value=10) +public class SipLayer implements CommandLineRunner { + + @Autowired + private SipConfig sipConfig; + + @Autowired + private ISIPProcessorObserver sipProcessorObserver; + + @Autowired + private UserSetting userSetting; + + private final Map tcpSipProviderMap = new ConcurrentHashMap<>(); + private final Map udpSipProviderMap = new ConcurrentHashMap<>(); + private final List monitorIps = new ArrayList<>(); + + @Override + public void run(String... args) { + if (ObjectUtils.isEmpty(sipConfig.getIp())) { + try { + // 获得本机的所有网络接口 + Enumeration nifs = NetworkInterface.getNetworkInterfaces(); + while (nifs.hasMoreElements()) { + NetworkInterface nif = nifs.nextElement(); + // 获得与该网络接口绑定的 IP 地址,一般只有一个 + Enumeration addresses = nif.getInetAddresses(); + while (addresses.hasMoreElements()) { + InetAddress addr = addresses.nextElement(); + if (addr instanceof Inet4Address) { + if (addr.getHostAddress().equals("127.0.0.1")){ + continue; + } + if (nif.getName().startsWith("docker")) { + continue; + } + log.info("[自动配置SIP监听网卡] 网卡接口地址: {}", addr.getHostAddress());// 只关心 IPv4 地址 + monitorIps.add(addr.getHostAddress()); + } + } + } + }catch (Exception e) { + log.error("[读取网卡信息失败]", e); + } + if (monitorIps.isEmpty()) { + log.error("[自动配置SIP监听网卡信息失败], 请手动配置SIP.IP后重新启动"); + System.exit(1); + } + }else { + // 使用逗号分割多个ip + String separator = ","; + if (sipConfig.getIp().indexOf(separator) > 0) { + String[] split = sipConfig.getIp().split(separator); + monitorIps.addAll(Arrays.asList(split)); + }else { + monitorIps.add(sipConfig.getIp()); + } + } + sipConfig.setMonitorIps(monitorIps); + if (ObjectUtils.isEmpty(sipConfig.getShowIp())){ + sipConfig.setShowIp(String.join(",", monitorIps)); + } + SipFactory.getInstance().setPathName("gov.nist"); + if (!monitorIps.isEmpty()) { + for (String monitorIp : monitorIps) { + addListeningPoint(monitorIp, sipConfig.getPort()); + } + if (udpSipProviderMap.size() + tcpSipProviderMap.size() == 0) { + System.exit(1); + } + } + } + + private void addListeningPoint(String monitorIp, int port){ + SipStackImpl sipStack; + try { + sipStack = (SipStackImpl)SipFactory.getInstance().createSipStack(DefaultProperties.getProperties("GB28181_SIP", userSetting.getSipLog(), userSetting.isSipCacheServerConnections())); + sipStack.setMessageParserFactory(new GbStringMsgParserFactory()); + } catch (PeerUnavailableException e) { + log.error("[SIP SERVER] SIP服务启动失败, 监听地址{}失败,请检查ip是否正确", monitorIp); + return; + } + + try { + ListeningPoint tcpListeningPoint = sipStack.createListeningPoint(monitorIp, port, "TCP"); + SipProviderImpl tcpSipProvider = (SipProviderImpl)sipStack.createSipProvider(tcpListeningPoint); + + tcpSipProvider.setDialogErrorsAutomaticallyHandled(); + tcpSipProvider.addSipListener(sipProcessorObserver); + tcpSipProviderMap.put(monitorIp, tcpSipProvider); + log.info("[SIP SERVER] tcp://{}:{} 启动成功", monitorIp, port); + } catch (TransportNotSupportedException + | TooManyListenersException + | ObjectInUseException + | InvalidArgumentException e) { + log.error("[SIP SERVER] tcp://{}:{} SIP服务启动失败,请检查端口是否被占用或者ip是否正确" + , monitorIp, port); + } + + try { + ListeningPoint udpListeningPoint = sipStack.createListeningPoint(monitorIp, port, "UDP"); + + SipProviderImpl udpSipProvider = (SipProviderImpl)sipStack.createSipProvider(udpListeningPoint); + udpSipProvider.addSipListener(sipProcessorObserver); + udpSipProvider.setDialogErrorsAutomaticallyHandled(); + udpSipProviderMap.put(monitorIp, udpSipProvider); + + log.info("[SIP SERVER] udp://{}:{} 启动成功", monitorIp, port); + } catch (TransportNotSupportedException + | TooManyListenersException + | ObjectInUseException + | InvalidArgumentException e) { + log.error("[SIP SERVER] udp://{}:{} SIP服务启动失败,请检查端口是否被占用或者ip是否正确" + , monitorIp, port); + } + } + + public SipProviderImpl getUdpSipProvider(String ip) { + if (udpSipProviderMap.size() == 1) { + return udpSipProviderMap.values().stream().findFirst().get(); + } + if (ObjectUtils.isEmpty(ip)) { + return null; + } + return udpSipProviderMap.get(ip); + } + + public SipProviderImpl getUdpSipProvider() { + if (udpSipProviderMap.size() != 1) { + return null; + } + return udpSipProviderMap.values().stream().findFirst().get(); + } + + public SipProviderImpl getTcpSipProvider() { + if (tcpSipProviderMap.size() != 1) { + return null; + } + return tcpSipProviderMap.values().stream().findFirst().get(); + } + + public SipProviderImpl getTcpSipProvider(String ip) { + if (tcpSipProviderMap.size() == 1) { + return tcpSipProviderMap.values().stream().findFirst().get(); + } + if (ObjectUtils.isEmpty(ip)) { + return null; + } + return tcpSipProviderMap.get(ip); + } + + public String getLocalIp(String deviceLocalIp) { + if (monitorIps.size() == 1) { + return monitorIps.get(0); + } + if (!ObjectUtils.isEmpty(deviceLocalIp)) { + return deviceLocalIp; + } + return getUdpSipProvider().getListeningPoint().getIPAddress(); + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/auth/DigestServerAuthenticationHelper.java b/src/main/java/com/genersoft/iot/vmp/gb28181/auth/DigestServerAuthenticationHelper.java new file mode 100644 index 0000000..27fb7b8 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/auth/DigestServerAuthenticationHelper.java @@ -0,0 +1,238 @@ +/* + * Conditions Of Use + * + * This software was developed by employees of the National Institute of + * Standards and Technology (NIST), an agency of the Federal Government. + * Pursuant to title 15 Untied States Code Section 105, works of NIST + * employees are not subject to copyright protection in the United States + * and are considered to be in the public domain. As a result, a formal + * license is not needed to use the software. + * + * This software is provided by NIST as a service and is expressly + * provided "AS IS." NIST MAKES NO WARRANTY OF ANY KIND, EXPRESS, IMPLIED + * OR STATUTORY, INCLUDING, WITHOUT LIMITATION, THE IMPLIED WARRANTY OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, NON-INFRINGEMENT + * AND DATA ACCURACY. NIST does not warrant or make any representations + * regarding the use of the software or the results thereof, including but + * not limited to the correctness, accuracy, reliability or usefulness of + * the software. + * + * Permission to use this software is contingent upon your acceptance + * of the terms of this agreement + * + * . + * + */ +package com.genersoft.iot.vmp.gb28181.auth; + +import gov.nist.core.InternalErrorHandler; +import lombok.extern.slf4j.Slf4j; + +import javax.sip.address.URI; +import javax.sip.header.AuthorizationHeader; +import javax.sip.header.HeaderFactory; +import javax.sip.header.WWWAuthenticateHeader; +import javax.sip.message.Request; +import javax.sip.message.Response; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.time.Instant; +import java.util.Random; + +/** + * Implements the HTTP digest authentication method server side functionality. + * + * @author M. Ranganathan + * @author Marc Bednarek + */ +@Slf4j +public class DigestServerAuthenticationHelper { + + private final MessageDigest messageDigest; + + public static final String DEFAULT_ALGORITHM = "MD5"; + public static final String DEFAULT_SCHEME = "Digest"; + + /** to hex converter */ + private static final char[] toHex = { '0', '1', '2', '3', '4', '5', '6', + '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' }; + + /** + * Default constructor. + */ + public DigestServerAuthenticationHelper() + throws NoSuchAlgorithmException { + messageDigest = MessageDigest.getInstance(DEFAULT_ALGORITHM); + } + + public static String toHexString(byte[] b) { + int pos = 0; + char[] c = new char[b.length * 2]; + for (byte value : b) { + c[pos++] = toHex[(value >> 4) & 0x0F]; + c[pos++] = toHex[value & 0x0f]; + } + return new String(c); + } + + /** + * Generate the challenge string. + * + * @return a generated nonce. + */ + private String generateNonce() { + long time = Instant.now().toEpochMilli(); + Random rand = new Random(); + long pad = rand.nextLong(); + String nonceString = Long.valueOf(time).toString() + + Long.valueOf(pad).toString(); + byte[] mdBytes = messageDigest.digest(nonceString.getBytes()); + return toHexString(mdBytes); + } + + public Response generateChallenge(HeaderFactory headerFactory, Response response, String realm) { + try { + WWWAuthenticateHeader proxyAuthenticate = headerFactory + .createWWWAuthenticateHeader(DEFAULT_SCHEME); + proxyAuthenticate.setParameter("realm", realm); + proxyAuthenticate.setParameter("qop", "auth"); + proxyAuthenticate.setParameter("nonce", generateNonce()); + proxyAuthenticate.setParameter("algorithm", DEFAULT_ALGORITHM); + + response.setHeader(proxyAuthenticate); + } catch (Exception ex) { + InternalErrorHandler.handleException(ex); + } + return response; + } + /** + * Authenticate the inbound request. + * + * @param request - the request to authenticate. + * @param hashedPassword -- the MD5 hashed string of username:realm:plaintext password. + * + * @return true if authentication succeded and false otherwise. + */ + public boolean doAuthenticateHashedPassword(Request request, String hashedPassword) { + AuthorizationHeader authHeader = (AuthorizationHeader) request.getHeader(AuthorizationHeader.NAME); + if ( authHeader == null ) { + return false; + } + String realm = authHeader.getRealm(); + String username = authHeader.getUsername(); + + if ( username == null || realm == null ) { + return false; + } + + String nonce = authHeader.getNonce(); + URI uri = authHeader.getURI(); + if (uri == null) { + return false; + } + + String A2 = request.getMethod().toUpperCase() + ":" + uri.toString(); + String HA1 = hashedPassword; + + + byte[] mdbytes = messageDigest.digest(A2.getBytes()); + String HA2 = toHexString(mdbytes); + + String cnonce = authHeader.getCNonce(); + String KD = HA1 + ":" + nonce; + if (cnonce != null) { + KD += ":" + cnonce; + } + KD += ":" + HA2; + mdbytes = messageDigest.digest(KD.getBytes()); + String mdString = toHexString(mdbytes); + String response = authHeader.getResponse(); + + + return mdString.equals(response); + } + + /** + * 鉴权 + * + * A1 = username + ":" + realm + ":" + password + * A2 = REGISTER:URI + * + * HA1 = md5(A1) + * HA2 = md5(A2) + * + * KD = HA2:HA2:qop + * + * response = md5(KD) + * @param request - the request to authenticate. + * @param pass -- the plain text password. + * + * @return true if authentication succeded and false otherwise. + */ + public boolean doAuthenticatePlainTextPassword(Request request, String pass) { + AuthorizationHeader authHeader = (AuthorizationHeader) request.getHeader(AuthorizationHeader.NAME); + if ( authHeader == null || authHeader.getRealm() == null) { + return false; + } + if ( authHeader.getUsername() == null || authHeader.getRealm() == null ) { + return false; + } + String realm = authHeader.getRealm().trim(); + String username = authHeader.getUsername().trim(); + + String nonce = authHeader.getNonce(); + URI uri = authHeader.getURI(); + if (uri == null) { + return false; + } + // qop 保护质量 包含auth(默认的)和auth-int(增加了报文完整性检测)两种策略 + String qop = authHeader.getQop(); + + // 客户端随机数,这是一个不透明的字符串值,由客户端提供,并且客户端和服务器都会使用,以避免用明文文本。 + // 这使得双方都可以查验对方的身份,并对消息的完整性提供一些保护 + String cnonce = authHeader.getCNonce(); + + // nonce计数器,是一个16进制的数值,表示同一nonce下客户端发送出请求的数量 + int nc = authHeader.getNonceCount(); + String ncStr = String.format("%08x", nc).toUpperCase(); + + String A1 = username + ":" + realm + ":" + pass; + + String A2 = request.getMethod().toUpperCase() + ":" + uri.toString(); + + byte[] mdbytes = messageDigest.digest(A1.getBytes()); + String HA1 = toHexString(mdbytes); + log.debug("A1: {}", A1); + log.debug("A2: {}", A2); + mdbytes = messageDigest.digest(A2.getBytes()); + String HA2 = toHexString(mdbytes); + log.debug("HA1: {}", HA1); + log.debug("HA2: {}", HA2); + // String cnonce = authHeader.getCNonce(); + log.debug("nonce: {}", nonce); + log.debug("nc: {}", ncStr); + log.debug("cnonce: {}", cnonce); + log.debug("qop: {}", qop); + String KD = HA1 + ":" + nonce; + + if (qop != null && qop.equalsIgnoreCase("auth") ) { + if (nc != -1) { + KD += ":" + ncStr; + } + if (cnonce != null) { + KD += ":" + cnonce; + } + KD += ":" + qop; + } + KD += ":" + HA2; + log.debug("KD: {}", KD); + mdbytes = messageDigest.digest(KD.getBytes()); + String mdString = toHexString(mdbytes); + log.debug("mdString: {}", mdString); + String response = authHeader.getResponse(); + log.debug("response: {}", response); + return mdString.equals(response); + + } + +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/bean/AlarmChannelMessage.java b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/AlarmChannelMessage.java new file mode 100644 index 0000000..4b3bb13 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/AlarmChannelMessage.java @@ -0,0 +1,30 @@ +package com.genersoft.iot.vmp.gb28181.bean; + + +import lombok.Data; + +/** + * 通过redis分发报警消息 + */ +@Data +public class AlarmChannelMessage { + /** + * 通道国标编号 + */ + private String gbId; + + /** + * 报警编号 + */ + private int alarmSn; + + /** + * 告警类型 + */ + private int alarmType; + + /** + * 报警描述 + */ + private String alarmDescription; +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/bean/AudioBroadcastCatch.java b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/AudioBroadcastCatch.java new file mode 100644 index 0000000..e1c9c9c --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/AudioBroadcastCatch.java @@ -0,0 +1,89 @@ +package com.genersoft.iot.vmp.gb28181.bean; + + +import com.genersoft.iot.vmp.gb28181.controller.bean.AudioBroadcastEvent; +import com.genersoft.iot.vmp.media.bean.MediaServer; +import gov.nist.javax.sip.message.SIPResponse; +import lombok.Data; + +/** + * 缓存语音广播的状态 + * @author lin + */ +@Data +public class AudioBroadcastCatch { + + + public AudioBroadcastCatch( + String deviceId, + Integer channelId, + MediaServer mediaServerItem, + String app, + String stream, + AudioBroadcastEvent event, + AudioBroadcastCatchStatus status, + boolean isFromPlatform + ) { + this.deviceId = deviceId; + this.channelId = channelId; + this.status = status; + this.event = event; + this.isFromPlatform = isFromPlatform; + this.app = app; + this.stream = stream; + this.mediaServerItem = mediaServerItem; + } + + public AudioBroadcastCatch() { + } + + /** + * 设备编号 + */ + private String deviceId; + + /** + * 通道编号 + */ + private Integer channelId; + + /** + * 流媒体信息 + */ + private MediaServer mediaServerItem; + + /** + * 关联的流APP + */ + private String app; + + /** + * 关联的流STREAM + */ + private String stream; + + /** + * 是否是级联语音喊话 + */ + private boolean isFromPlatform; + + /** + * 语音广播状态 + */ + private AudioBroadcastCatchStatus status; + + /** + * 请求信息 + */ + private SipTransactionInfo sipTransactionInfo; + + /** + * 请求结果回调 + */ + private AudioBroadcastEvent event; + + + public void setSipTransactionInfoByRequest(SIPResponse sipResponse) { + this.sipTransactionInfo = new SipTransactionInfo(sipResponse); + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/bean/AudioBroadcastCatchStatus.java b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/AudioBroadcastCatchStatus.java new file mode 100644 index 0000000..7d4f7c8 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/AudioBroadcastCatchStatus.java @@ -0,0 +1,15 @@ +package com.genersoft.iot.vmp.gb28181.bean; + +/** + * 语音广播状态 + * @author lin + */ +public enum AudioBroadcastCatchStatus { + + // 发送语音广播消息等待对方回复语音广播 + Ready, + // 收到回复等待invite消息 + WaiteInvite, + // 收到invite消息 + Ok, +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/bean/BaiduPoint.java b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/BaiduPoint.java new file mode 100644 index 0000000..8f33f30 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/BaiduPoint.java @@ -0,0 +1,24 @@ +package com.genersoft.iot.vmp.gb28181.bean; + +public class BaiduPoint { + + String bdLng; + + String bdLat; + + public String getBdLng() { + return bdLng; + } + + public void setBdLng(String bdLng) { + this.bdLng = bdLng; + } + + public String getBdLat() { + return bdLat; + } + + public void setBdLat(String bdLat) { + this.bdLat = bdLat; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/bean/BasicParam.java b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/BasicParam.java new file mode 100644 index 0000000..1ec7a72 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/BasicParam.java @@ -0,0 +1,49 @@ +package com.genersoft.iot.vmp.gb28181.bean; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +/** + * 基础配置 + */ +@Data +@Schema(description = "基础配置") +public class BasicParam { + + @Schema(description = "设备ID") + private String deviceId; + + @Schema(description = "通道ID,如果时对设备配置直接设置同设备ID一样即可") + private String channelId; + + @Schema(description = "名称") + private String name; + + @Schema(description = "注册过期时间") + private String expiration; + + @Schema(description = "心跳间隔时间") + private Integer heartBeatInterval; + + @Schema(description = "心跳超时次数") + private Integer heartBeatCount; + + @Schema(description = "定位功能支持情况。取值:0-不支持;1-支持 GPS定位;2-支持北斗定位(可选,默认取值为0)," + + "用于接受配置查询结果, 基础配置时无效") + private Integer positionCapability; + + @Schema(description = "经度(可选),用于接受配置查询结果, 基础配置时无效") + private Double longitude; + + @Schema(description = "纬度(可选),用于接受配置查询结果, 基础配置时无效") + private Double latitude; + + public static BasicParam getInstance(String name, String expiration, Integer heartBeatInterval, Integer heartBeatCount) { + BasicParam basicParam = new BasicParam(); + basicParam.setName(name); + basicParam.setExpiration(expiration); + basicParam.setHeartBeatInterval(heartBeatInterval); + basicParam.setHeartBeatCount(heartBeatCount); + return basicParam; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/bean/CatalogChannelEvent.java b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/CatalogChannelEvent.java new file mode 100644 index 0000000..cf7c9cb --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/CatalogChannelEvent.java @@ -0,0 +1,38 @@ +package com.genersoft.iot.vmp.gb28181.bean; + +import com.genersoft.iot.vmp.gb28181.event.subscribe.catalog.CatalogEvent; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.extern.slf4j.Slf4j; +import org.dom4j.Element; + +import java.lang.reflect.InvocationTargetException; + +@Data +@Slf4j +@EqualsAndHashCode(callSuper = true) +public class CatalogChannelEvent extends DeviceChannel{ + + private String event; + + private DeviceChannel channel; + + public static CatalogChannelEvent decode(Element element) throws InvocationTargetException, NoSuchMethodException, InstantiationException, IllegalAccessException { + Element eventElement = element.element("Event"); + CatalogChannelEvent catalogChannelEvent = new CatalogChannelEvent(); + if (eventElement != null) { + catalogChannelEvent.setEvent(eventElement.getText()); + }else { + catalogChannelEvent.setEvent(CatalogEvent.ADD); + } + DeviceChannel deviceChannel; + if (CatalogEvent.ADD.equalsIgnoreCase(catalogChannelEvent.getEvent()) || + CatalogEvent.UPDATE.equalsIgnoreCase(catalogChannelEvent.getEvent()) ){ + deviceChannel = DeviceChannel.decode(element); + }else { + deviceChannel = DeviceChannel.decodeWithOnlyDeviceId(element); + } + catalogChannelEvent.setChannel(deviceChannel); + return catalogChannelEvent; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/bean/CatalogData.java b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/CatalogData.java new file mode 100644 index 0000000..fb687b3 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/CatalogData.java @@ -0,0 +1,32 @@ +package com.genersoft.iot.vmp.gb28181.bean; + +import lombok.Data; + +import java.time.Instant; +import java.util.HashSet; +import java.util.Set; + +/** + * @author lin + */ +@Data +public class CatalogData { + /** + * 命令序列号 + */ + private int sn; + private Integer total; + private Instant time; + private Device device; + private String errorMsg; + private Set redisKeysForChannel = new HashSet<>(); + private Set errorChannel = new HashSet<>(); + private Set redisKeysForRegion = new HashSet<>(); + private Set redisKeysForGroup = new HashSet<>(); + + public enum CatalogDataStatus{ + ready, runIng, end + } + private CatalogDataStatus status; + +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/bean/CmdType.java b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/CmdType.java new file mode 100644 index 0000000..ed5cad6 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/CmdType.java @@ -0,0 +1,8 @@ +package com.genersoft.iot.vmp.gb28181.bean; + +public class CmdType { + + public static final String CATALOG = "Catalog"; + public static final String ALARM = "Alarm"; + public static final String MOBILE_POSITION = "MobilePosition"; +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/bean/CommonGBChannel.java b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/CommonGBChannel.java new file mode 100644 index 0000000..a9d1936 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/CommonGBChannel.java @@ -0,0 +1,399 @@ +package com.genersoft.iot.vmp.gb28181.bean; + +import com.genersoft.iot.vmp.gb28181.event.subscribe.catalog.CatalogEvent; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Data +@Schema(description = "国标通道") +public class CommonGBChannel { + + @Schema(description = "国标-数据库自增ID") + private int gbId; + + @Schema(description = "国标-编码") + private String gbDeviceId; + + @Schema(description = "国标-名称") + private String gbName; + + @Schema(description = "国标-设备厂商") + private String gbManufacturer; + + @Schema(description = "国标-设备型号") + private String gbModel; + + // 2016 + @Schema(description = "国标-设备归属") + private String gbOwner; + + @Schema(description = "国标-行政区域") + private String gbCivilCode; + + @Schema(description = "国标-警区") + private String gbBlock; + + @Schema(description = "国标-安装地址") + private String gbAddress; + + @Schema(description = "国标-是否有子设备") + private Integer gbParental; + + @Schema(description = "国标-父节点ID") + private String gbParentId; + + // 2016 + @Schema(description = "国标-信令安全模式") + private Integer gbSafetyWay; + + @Schema(description = "国标-注册方式") + private Integer gbRegisterWay; + + // 2016 + @Schema(description = "国标-证书序列号") + private String gbCertNum; + + // 2016 + @Schema(description = "国标-证书有效标识") + private Integer gbCertifiable; + + // 2016 + @Schema(description = "国标-无效原因码(有证书且证书无效的设备必选)") + private Integer gbErrCode; + + // 2016 + @Schema(description = "国标-证书终止有效期(有证书且证书无效的设备必选)") + private String gbEndTime; + + @Schema(description = "国标-保密属性(必选)缺省为0;0-不涉密,1-涉密") + private Integer gbSecrecy; + + @Schema(description = "国标-设备/系统IPv4/IPv6地址") + private String gbIpAddress; + + @Schema(description = "国标-设备/系统端口") + private Integer gbPort; + + @Schema(description = "国标-设备口令") + private String gbPassword; + + @Schema(description = "国标-设备状态") + private String gbStatus; + + @Schema(description = "国标-经度 WGS-84坐标系") + private Double gbLongitude; + + @Schema(description = "国标-纬度 WGS-84坐标系") + private Double gbLatitude; + + private Double gpsAltitude; + + private Double gpsSpeed; + + private Double gpsDirection; + + private String gpsTime; + + @Schema(description = "国标-虚拟组织所属的业务分组ID") + private String gbBusinessGroupId; + + @Schema(description = "国标-摄像机结构类型,标识摄像机类型: 1-球机; 2-半球; 3-固定枪机; 4-遥控枪机;5-遥控半球;6-多目设备的全景/拼接通道;" + + "7-多目设备的分割通道; 99-移动设备(非标)98-会议设备(非标)") + private Integer gbPtzType; + + // 2016 + @Schema(description = "-摄像机位置类型扩展。1-省际检查站、2-党政机关、3-车站码头、4-中心广场、5-体育场馆、6-商业中心、7-宗教场所、" + + "8-校园周边、9-治安复杂区域、10-交通干线。当目录项为摄像机时可选。") + private Integer gbPositionType; + + @Schema(description = "国标-摄像机安装位置室外、室内属性。1-室外、2-室内。") + private Integer gbRoomType; + + // 2016 + @Schema(description = "国标-用途属性") + private Integer gbUseType; + + @Schema(description = "国标-摄像机补光属性。1-无补光;2-红外补光;3-白光补光;4-激光补光;9-其他") + private Integer gbSupplyLightType; + + @Schema(description = "国标-摄像机监视方位(光轴方向)属性。1-东(西向东)、2-西(东向西)、3-南(北向南)、4-北(南向北)、" + + "5-东南(西北到东南)、6-东北(西南到东北)、7-西南(东北到西南)、8-西北(东南到西北)") + private Integer gbDirectionType; + + @Schema(description = "国标-摄像机支持的分辨率,可多值") + private String gbResolution; + + @Schema(description = "国标-下载倍速(可选),可多值") + private String gbDownloadSpeed; + + @Schema(description = "国标-空域编码能力,取值0-不支持;1-1级增强(1个增强层);2-2级增强(2个增强层);3-3级增强(3个增强层)") + private Integer gbSvcSpaceSupportMod; + + @Schema(description = "国标-时域编码能力,取值0-不支持;1-1级增强;2-2级增强;3-3级增强(可选)") + private Integer gbSvcTimeSupportMode; + + @Schema(description = "二进制保存的录制计划, 每一位表示每个小时的前半个小时") + private Long recordPLan; + + @Schema(description = "关联的数据类型") + private Integer dataType; + + @Schema(description = "关联的设备ID") + private Integer dataDeviceId; + + @Schema(description = "创建时间") + private String createTime; + + @Schema(description = "更新时间") + private String updateTime; + + @Schema(description = "流唯一编号,存在表示正在直播") + private String streamId; + + @Schema(description = "是否支持对讲 1支持,0不支持") + private Integer enableBroadcast; + + @Schema(description = "抽稀后的图层层级") + private Integer mapLevel; + + public String encode(String serverDeviceId) { + return encode(null, serverDeviceId); + } + + public String encode(String event,String serverDeviceId) { + String content; + if (event == null) { + return getFullContent(null, serverDeviceId); + } + switch (event) { + case CatalogEvent.DEL: + case CatalogEvent.DEFECT: + case CatalogEvent.VLOST: + content = "\n" + + "" + this.getGbDeviceId() + "\n" + + "" + event + "\n" + + "\n"; + break; + case CatalogEvent.ON: + case CatalogEvent.OFF: + content = "\n" + + "" + this.getGbDeviceId() + "\n" + + "" + event + "\r\n" + + "\n"; + break; + case CatalogEvent.ADD: + case CatalogEvent.UPDATE: + content = getFullContent(event, serverDeviceId); + break; + default: + content = null; + break; + } + return content; + } + + private String getFullContent(String event, String serverDeviceId) { + StringBuilder content = new StringBuilder(); + // 行政区划目录项 + content.append("\n") + .append("" + this.getGbDeviceId() + "\n") + .append("" + this.getGbName() + "\n"); + + + if (this.getGbDeviceId().length() > 8) { + + String type = this.getGbDeviceId().substring(10, 13); + if (type.equals("200")) { + // 业务分组目录项 + if (this.getGbManufacturer() != null) { + content.append("" + this.getGbManufacturer() + "\n"); + } + if (this.getGbModel() != null) { + content.append("" + this.getGbModel() + "\n"); + } + if (this.getGbOwner() != null) { + content.append("" + this.getGbOwner() + "\n"); + } + if (this.getGbCivilCode() != null) { + content.append("" + this.getGbCivilCode() + "\n"); + } + if (this.getGbAddress() != null) { + content.append("
" + this.getGbAddress() + "
\n"); + } + if (this.getGbRegisterWay() != null) { + content.append("" + this.getGbRegisterWay() + "\n"); + } + if (this.getGbSecrecy() != null) { + content.append("" + this.getGbSecrecy() + "\n"); + } + } else if (type.equals("215")) { + // 业务分组 + if (this.getGbCivilCode() != null) { + content.append("" + this.getGbCivilCode() + "\n"); + } + content.append("" + serverDeviceId + "\n"); + } else if (type.equals("216")) { + // 虚拟组织目录项 + if (this.getGbCivilCode() != null) { + content.append("" + this.getGbCivilCode() + "\n"); + } + if (this.getGbParentId() != null) { + content.append("" + this.getGbParentId() + "\n"); + } + content.append("" + this.getGbBusinessGroupId() + "\n"); + } else { + if (this.getGbManufacturer() != null) { + content.append("" + this.getGbManufacturer() + "\n"); + } + if (this.getGbModel() != null) { + content.append("" + this.getGbModel() + "\n"); + } + if (this.getGbOwner() != null) { + content.append("" + this.getGbOwner() + "\n"); + } + if (this.getGbCivilCode() != null) { + content.append("" + this.getGbCivilCode() + "\n"); + } + if (this.getGbAddress() != null) { + content.append("
" + this.getGbAddress() + "
\n"); + } + if (this.getGbRegisterWay() != null) { + content.append("" + this.getGbRegisterWay() + "\n"); + } + if (this.getGbSecrecy() != null) { + content.append("" + this.getGbSecrecy() + "\n"); + } + if (this.getGbParentId() != null) { + content.append("" + this.getGbParentId() + "\n"); + } + if (this.getGbParental() != null) { + content.append("" + this.getGbParental() + "\n"); + } + if (this.getGbSafetyWay() != null) { + content.append("" + this.getGbSafetyWay() + "\n"); + } + if (this.getGbRegisterWay() != null) { + content.append("" + this.getGbRegisterWay() + "\n"); + } + if (this.getGbCertNum() != null) { + content.append("" + this.getGbCertNum() + "\n"); + } + if (this.getGbCertifiable() != null) { + content.append("" + this.getGbCertifiable() + "\n"); + } + if (this.getGbErrCode() != null) { + content.append("" + this.getGbErrCode() + "\n"); + } + if (this.getGbEndTime() != null) { + content.append("" + this.getGbEndTime() + "\n"); + } + if (this.getGbSecrecy() != null) { + content.append("" + this.getGbSecrecy() + "\n"); + } + if (this.getGbIpAddress() != null) { + content.append("" + this.getGbIpAddress() + "\n"); + } + if (this.getGbPort() != null) { + content.append("" + this.getGbPort() + "\n"); + } + if (this.getGbPassword() != null) { + content.append("" + this.getGbPassword() + "\n"); + } + if (this.getGbStatus() != null) { + content.append("" + this.getGbStatus() + "\n"); + } + if (this.getGbLongitude() != null) { + content.append("" + this.getGbLongitude() + "\n"); + } + if (this.getGbLatitude() != null) { + content.append("" + this.getGbLatitude() + "\n"); + } + content.append("\n"); + + if (this.getGbPtzType() != null) { + content.append(" " + this.getGbPtzType() + "\n"); + } + if (this.getGbPositionType() != null) { + content.append(" " + this.getGbPositionType() + "\n"); + } + if (this.getGbRoomType() != null) { + content.append(" " + this.getGbRoomType() + "\n"); + } + if (this.getGbUseType() != null) { + content.append(" " + this.getGbUseType() + "\n"); + } + if (this.getGbSupplyLightType() != null) { + content.append(" " + this.getGbSupplyLightType() + "\n"); + } + if (this.getGbDirectionType() != null) { + content.append(" " + this.getGbDirectionType() + "\n"); + } + if (this.getGbResolution() != null) { + content.append(" " + this.getGbResolution() + "\n"); + } + if (this.getGbBusinessGroupId() != null) { + content.append(" " + this.getGbBusinessGroupId() + "\n"); + } + if (this.getGbDownloadSpeed() != null) { + content.append(" " + this.getGbDownloadSpeed() + "\n"); + } + if (this.getGbSvcSpaceSupportMod() != null) { + content.append(" " + this.getGbSvcSpaceSupportMod() + "\n"); + } + if (this.getGbSvcTimeSupportMode() != null) { + content.append(" " + this.getGbSvcTimeSupportMode() + "\n"); + } + if (this.getEnableBroadcast() != null) { + content.append(" " + this.getEnableBroadcast() + "\n"); + } + content.append("\n"); + } + } + if (event != null) { + content.append("" + event + "\n"); + } + content.append("
\n"); + return content.toString(); + } + + public static CommonGBChannel build(Group group) { + GbCode gbCode = GbCode.decode(group.getDeviceId()); + CommonGBChannel channel = new CommonGBChannel(); + if (gbCode.getTypeCode().equals("215")) { + // 业务分组 + channel.setGbName(group.getName()); + channel.setGbDeviceId(group.getDeviceId()); + channel.setGbCivilCode(group.getCivilCode()); + } else { + // 虚拟组织 + channel.setGbName(group.getName()); + channel.setGbDeviceId(group.getDeviceId()); + channel.setGbParentId(group.getParentDeviceId()); + channel.setGbBusinessGroupId(group.getBusinessGroup()); + channel.setGbCivilCode(group.getCivilCode()); + } + return channel; + } + + public static CommonGBChannel build(Platform platform) { + CommonGBChannel commonGBChannel = new CommonGBChannel(); + commonGBChannel.setGbDeviceId(platform.getDeviceGBId()); + commonGBChannel.setGbName(platform.getName()); + commonGBChannel.setGbManufacturer(platform.getManufacturer()); + commonGBChannel.setGbModel(platform.getModel()); + commonGBChannel.setGbCivilCode(platform.getCivilCode()); + commonGBChannel.setGbAddress(platform.getAddress()); + commonGBChannel.setGbRegisterWay(platform.getRegisterWay()); + commonGBChannel.setGbSecrecy(platform.getSecrecy()); + commonGBChannel.setGbStatus(platform.isStatus() ? "ON" : "OFF"); + return commonGBChannel; + } + + public static CommonGBChannel build(Region region) { + CommonGBChannel commonGBChannel = new CommonGBChannel(); + commonGBChannel.setGbDeviceId(region.getDeviceId()); + commonGBChannel.setGbName(region.getName()); + return commonGBChannel; + } + +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/bean/CommonRecordInfo.java b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/CommonRecordInfo.java new file mode 100644 index 0000000..90d8306 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/CommonRecordInfo.java @@ -0,0 +1,17 @@ +package com.genersoft.iot.vmp.gb28181.bean; + +import lombok.Data; + +@Data +public class CommonRecordInfo { + + // 开始时间 + private String startTime; + + // 结束时间 + private String endTime; + + // 文件大小 单位byte + private String fileSize; + +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/bean/Device.java b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/Device.java new file mode 100644 index 0000000..83a302e --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/Device.java @@ -0,0 +1,219 @@ +package com.genersoft.iot.vmp.gb28181.bean; + + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +/** + * 国标设备/平台 + * @author lin + */ +@Data +@Schema(description = "国标设备/平台") +public class Device { + + @Schema(description = "数据库自增ID") + private int id; + + /** + * 设备国标编号 + */ + @Schema(description = "设备国标编号") + private String deviceId; + + /** + * 设备名 + */ + @Schema(description = "名称") + private String name; + + /** + * 生产厂商 + */ + @Schema(description = "生产厂商") + private String manufacturer; + + /** + * 型号 + */ + @Schema(description = "型号") + private String model; + + /** + * 固件版本 + */ + @Schema(description = "固件版本") + private String firmware; + + /** + * 传输协议 + * UDP/TCP + */ + @Schema(description = "传输协议(UDP/TCP)") + private String transport; + + /** + * 数据流传输模式 + * UDP:udp传输 + * TCP-ACTIVE:tcp主动模式 + * TCP-PASSIVE:tcp被动模式 + */ + @Schema(description = "数据流传输模式") + private String streamMode; + + /** + * wan地址_ip + */ + @Schema(description = "IP") + private String ip; + + /** + * wan地址_port + */ + @Schema(description = "端口") + private int port; + + /** + * wan地址 + */ + @Schema(description = "wan地址") + private String hostAddress; + + /** + * 在线 + */ + @Schema(description = "是否在线,true为在线,false为离线") + private boolean onLine; + + + /** + * 注册时间 + */ + @Schema(description = "注册时间") + private String registerTime; + + + /** + * 心跳时间 + */ + @Schema(description = "心跳时间") + private String keepaliveTime; + + + /** + * 心跳间隔 + */ + @Schema(description = "心跳间隔") + private Integer heartBeatInterval; + + + /** + * 心跳超时次数 + */ + @Schema(description = "心跳超时次数") + private Integer heartBeatCount; + + + /** + * 定位功能支持情况 + */ + @Schema(description = "定位功能支持情况。取值:0-不支持;1-支持 GPS定位;2-支持北斗定位(可选,默认取值为0") + private Integer positionCapability; + + /** + * 通道个数 + */ + @Schema(description = "通道个数") + private int channelCount; + + /** + * 注册有效期 + */ + @Schema(description = "注册有效期") + private int expires; + + /** + * 创建时间 + */ + @Schema(description = "创建时间") + private String createTime; + + /** + * 更新时间 + */ + @Schema(description = "更新时间") + private String updateTime; + + /** + * 设备使用的媒体id, 默认为null + */ + @Schema(description = "设备使用的媒体id, 默认为null") + private String mediaServerId; + + /** + * 字符集, 支持 UTF-8 与 GB2312 + */ + @Schema(description = "符集, 支持 UTF-8 与 GB2312") + private String charset ; + + /** + * 目录订阅周期,0为不订阅 + */ + @Schema(description = "目录订阅周期,o为不订阅") + private int subscribeCycleForCatalog; + + /** + * 移动设备位置订阅周期,0为不订阅 + */ + @Schema(description = "移动设备位置订阅周期,0为不订阅") + private int subscribeCycleForMobilePosition; + + /** + * 移动设备位置信息上报时间间隔,单位:秒,默认值5 + */ + @Schema(description = "移动设备位置信息上报时间间隔,单位:秒,默认值5") + private int mobilePositionSubmissionInterval = 5; + + /** + * 报警订阅周期,0为不订阅 + */ + @Schema(description = "报警心跳时间订阅周期,0为不订阅") + private int subscribeCycleForAlarm; + + /** + * 是否开启ssrc校验,默认关闭,开启可以防止串流 + */ + @Schema(description = "是否开启ssrc校验,默认关闭,开启可以防止串流") + private boolean ssrcCheck = false; + + /** + * 地理坐标系, 目前支持 WGS84,GCJ02, 此字段保留,暂无用 + */ + @Schema(description = "地理坐标系, 目前支持 WGS84,GCJ02") + private String geoCoordSys; + + @Schema(description = "密码") + private String password; + + @Schema(description = "收流IP") + private String sdpIp; + + @Schema(description = "SIP交互IP(设备访问平台的IP)") + private String localIp; + + @Schema(description = "是否作为消息通道") + private boolean asMessageChannel; + + @Schema(description = "设备注册的事务信息") + private SipTransactionInfo sipTransactionInfo; + + @Schema(description = "控制语音对讲流程,释放收到ACK后发流") + private boolean broadcastPushAfterAck; + + @Schema(description = "所属服务Id") + private String serverId; + + public boolean checkWgs84() { + return geoCoordSys.equalsIgnoreCase("WGS84"); + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/bean/DeviceAlarm.java b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/DeviceAlarm.java new file mode 100644 index 0000000..22d3dd2 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/DeviceAlarm.java @@ -0,0 +1,269 @@ +package com.genersoft.iot.vmp.gb28181.bean; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.util.HashSet; +import java.util.Set; + +/** + * @author lin + */ +@Schema(description = "报警信息") +@Data +public class DeviceAlarm { + + @Schema(description = "数据库id") + private String id; + + @Schema(description = "设备的国标编号") + private String deviceId; + + @Schema(description = "设备名称") + private String deviceName; + + /** + * 通道Id + */ + @Schema(description = "通道的国标编号") + private String channelId; + + /** + * 报警级别, 1为一级警情, 2为二级警情, 3为三级警情, 4为四级警情 + */ + @Schema(description = "报警级别, 1为一级警情, 2为二级警情, 3为三级警情, 4为四级警情") + private String alarmPriority; + + @Schema(description = "报警级别, 1为一级警情, 2为二级警情, 3为三级警情, 4为四级警情") + private String alarmPriorityDescription; + + public String getAlarmPriorityDescription() { + switch (alarmPriority) { + case "1": + return "一级警情"; + case "2": + return "二级警情"; + case "3": + return "三级警情"; + case "4": + return "四级警情"; + default: + return alarmPriority; + } + } + + /** + * 报警方式 , 1为电话报警, 2为设备报警, 3为短信报警, 4为 GPS报警, 5为视频报警, 6为设备故障报警, + * 7其他报警;可以为直接组合如12为电话报警或 设备报警- + */ + @Schema(description = "报警方式 , 1为电话报警, 2为设备报警, 3为短信报警, 4为 GPS报警, 5为视频报警, 6为设备故障报警,\n" + + "\t * 7其他报警;可以为直接组合如12为电话报警或设备报警") + private String alarmMethod; + + + private String alarmMethodDescription; + + public String getAlarmMethodDescription() { + StringBuilder stringBuilder = new StringBuilder(); + char[] charArray = alarmMethod.toCharArray(); + for (char c : charArray) { + switch (c) { + case '1': + stringBuilder.append("-电话报警"); + break; + case '2': + stringBuilder.append("-设备报警"); + break; + case '3': + stringBuilder.append("-短信报警"); + break; + case '4': + stringBuilder.append("-GPS报警"); + break; + case '5': + stringBuilder.append("-视频报警"); + break; + case '6': + stringBuilder.append("-设备故障报警"); + break; + case '7': + stringBuilder.append("-其他报警"); + break; + } + } + stringBuilder.delete(0, 1); + return stringBuilder.toString(); + } + + /** + * 报警时间 + */ + @Schema(description = "报警时间") + private String alarmTime; + + /** + * 报警内容描述 + */ + @Schema(description = "报警内容描述") + private String alarmDescription; + + /** + * 经度 + */ + @Schema(description = "经度") + private double longitude; + + /** + * 纬度 + */ + @Schema(description = "纬度") + private double latitude; + + /** + * 报警类型, + * 报警方式为2时,不携带 AlarmType为默认的报警设备报警, + * 携带 AlarmType取值及对应报警类型如下: + * 1-视频丢失报警; + * 2-设备防拆报警; + * 3-存储设备磁盘满报警; + * 4-设备高温报警; + * 5-设备低温报警。 + * 报警方式为5时,取值如下: + * 1-人工视频报警; + * 2-运动目标检测报警; + * 3-遗留物检测报警; + * 4-物体移除检测报警; + * 5-绊线检测报警; + * 6-入侵检测报警; + * 7-逆行检测报警; + * 8-徘徊检测报警; + * 9-流量统计报警; + * 10-密度检测报警; + * 11-视频异常检测报警; + * 12-快速移动报警。 + * 报警方式为6时,取值下: + * 1-存储设备磁盘故障报警; + * 2-存储设备风扇故障报警。 + */ + @Schema(description = "报警类型") + private String alarmType; + + public String getAlarmTypeDescription() { + if (alarmType == null) { + return ""; + } + char[] charArray = alarmMethod.toCharArray(); + Set alarmMethodSet = new HashSet<>(); + for (char c : charArray) { + alarmMethodSet.add(Character.toString(c)); + } + String result = alarmType; + if (alarmMethodSet.contains("2")) { + switch (alarmType) { + case "1": + result = "视频丢失报警"; + break; + case "2": + result = "设备防拆报警"; + break; + case "3": + result = "存储设备磁盘满报警"; + break; + case "4": + result = "设备高温报警"; + break; + case "5": + result = "设备低温报警"; + break; + } + } + if (alarmMethodSet.contains("5")) { + switch (alarmType) { + case "1": + result = "人工视频报警"; + break; + case "2": + result = "运动目标检测报警"; + break; + case "3": + result = "遗留物检测报警"; + break; + case "4": + result = "物体移除检测报警"; + break; + case "5": + result = "绊线检测报警"; + break; + case "6": + result = "入侵检测报警"; + break; + case "7": + result = "逆行检测报警"; + break; + case "8": + result = "徘徊检测报警"; + break; + case "9": + result = "流量统计报警"; + break; + case "10": + result = "密度检测报警"; + break; + case "11": + result = "视频异常检测报警"; + break; + case "12": + result = "快速移动报警"; + break; + } + } + if (alarmMethodSet.contains("6")) { + switch (alarmType) { + case "1": + result = "人工视频报警"; + break; + case "2": + result = "运动目标检测报警"; + break; + case "3": + result = "遗留物检测报警"; + break; + case "4": + result = "物体移除检测报警"; + break; + case "5": + result = "绊线检测报警"; + break; + case "6": + result = "入侵检测报警"; + break; + case "7": + result = "逆行检测报警"; + break; + case "8": + result = "徘徊检测报警"; + break; + case "9": + result = "流量统计报警"; + break; + case "10": + result = "密度检测报警"; + break; + case "11": + result = "视频异常检测报警"; + break; + case "12": + result = "快速移动报警"; + break; + } + } + return result; + } + + @Schema(description = "报警类型描述") + private String alarmTypeDescription; + + @Schema(description = "创建时间") + private String createTime; + +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/bean/DeviceAlarmMethod.java b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/DeviceAlarmMethod.java new file mode 100644 index 0000000..d1fb6db --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/DeviceAlarmMethod.java @@ -0,0 +1,54 @@ +package com.genersoft.iot.vmp.gb28181.bean; + +/** + * 报警方式 + * @author lin + * 1为电话报警, 2为设备报警, 3为短信报警, 4为 GPS报警, 5为视频报警, 6为设备故障报警, + * 7其他报警;可以为直接组合如12为电话报警或 设备报警- + */ +public enum DeviceAlarmMethod { + // 1为电话报警 + Telephone(1), + + // 2为设备报警 + Device(2), + + // 3为短信报警 + SMS(3), + + // 4为 GPS报警 + GPS(4), + + // 5为视频报警 + Video(5), + + // 6为设备故障报警 + DeviceFailure(6), + + // 7其他报警 + Other(7); + + private final int val; + + DeviceAlarmMethod(int val) { + this.val=val; + } + + public int getVal() { + return val; + } + + /** + * 查询是否匹配类型 + * @param code + * @return + */ + public static DeviceAlarmMethod typeOf(int code) { + for (DeviceAlarmMethod item : DeviceAlarmMethod.values()) { + if (code==item.getVal()) { + return item; + } + } + return null; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/bean/DeviceChannel.java b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/DeviceChannel.java new file mode 100644 index 0000000..1ba09b5 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/DeviceChannel.java @@ -0,0 +1,265 @@ +package com.genersoft.iot.vmp.gb28181.bean; + +import com.genersoft.iot.vmp.common.enums.ChannelDataType; +import com.genersoft.iot.vmp.gb28181.utils.MessageElementForCatalog; +import com.genersoft.iot.vmp.gb28181.utils.XmlUtil; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.extern.slf4j.Slf4j; +import org.dom4j.Element; +import org.springframework.util.ObjectUtils; + +import java.lang.reflect.InvocationTargetException; + +@Data +@Slf4j +@Schema(description = "通道信息") +@EqualsAndHashCode(callSuper = true) +public class DeviceChannel extends CommonGBChannel { + + @Schema(description = "数据库自增ID") + private int id; + + @Schema(description = "父设备编码") + private String parentDeviceId; + + @Schema(description = "父设备名称") + private String parentName; + + @MessageElementForCatalog("DeviceID") + @Schema(description = "编码") + private String deviceId; + + @MessageElementForCatalog("Name") + @Schema(description = "名称") + private String name; + + @MessageElementForCatalog("Manufacturer") + @Schema(description = "设备厂商") + private String manufacturer; + + @MessageElementForCatalog("Model") + @Schema(description = "设备型号") + private String model; + + // 2016 + @MessageElementForCatalog("Owner") + @Schema(description = "设备归属") + private String owner; + + @MessageElementForCatalog("CivilCode") + @Schema(description = "行政区域") + private String civilCode; + + @MessageElementForCatalog("Block") + @Schema(description = "警区") + private String block; + + @MessageElementForCatalog("Address") + @Schema(description = "安装地址") + private String address; + + @MessageElementForCatalog("Parental") + @Schema(description = "是否有子设备(必选)1有,0没有") + private Integer parental; + + + @MessageElementForCatalog("ParentID") + @Schema(description = "父节点ID") + private String parentId; + + // 2016 + @MessageElementForCatalog("SafetyWay") + @Schema(description = "信令安全模式") + private Integer safetyWay; + + @MessageElementForCatalog("RegisterWay") + @Schema(description = "注册方式") + private Integer registerWay; + + // 2016 + @MessageElementForCatalog("CertNum") + @Schema(description = "证书序列号") + private String certNum; + + // 2016 + @MessageElementForCatalog("Certifiable") + @Schema(description = "证书有效标识, 缺省为0;证书有效标识:0:无效 1:有效") + private Integer certifiable; + + // 2016 + @MessageElementForCatalog("ErrCode") + @Schema(description = "无效原因码(有证书且证书无效的设备必选)") + private Integer errCode; + + // 2016 + @MessageElementForCatalog("EndTime") + @Schema(description = "证书终止有效期(有证书且证书无效的设备必选)") + private String endTime; + + @MessageElementForCatalog("Secrecy") + @Schema(description = "保密属性(必选)缺省为0;0-不涉密,1-涉密") + private Integer secrecy; + + @MessageElementForCatalog("IPAddress") + @Schema(description = "设备/系统IPv4/IPv6地址") + private String ipAddress; + + @MessageElementForCatalog("Port") + @Schema(description = "设备/系统端口") + private Integer port; + + @MessageElementForCatalog("Password") + @Schema(description = "设备口令") + private String password; + + @MessageElementForCatalog("Status") + @Schema(description = "设备状态") + private String status; + + @MessageElementForCatalog("Longitude") + @Schema(description = "经度 WGS-84坐标系") + private Double longitude; + + @MessageElementForCatalog("Latitude") + @Schema(description = ",纬度 WGS-84坐标系") + private Double latitude; + + @MessageElementForCatalog("Info.PTZType") + @Schema(description = "摄像机结构类型,标识摄像机类型: 1-球机; 2-半球; 3-固定枪机; 4-遥控枪机;5-遥控半球;6-多目设备的全景/拼接通道;7-多目设备的分割通道") + private Integer ptzType; + + @MessageElementForCatalog("Info.PositionType") + @Schema(description = "摄像机位置类型扩展。1-省际检查站、2-党政机关、3-车站码头、4-中心广场、5-体育场馆、" + + "6-商业中心、7-宗教场所、8-校园周边、9-治安复杂区域、10-交通干线") + private Integer positionType; + + @MessageElementForCatalog("Info.RoomType") + @Schema(description = "摄像机安装位置室外、室内属性。1-室外、2-室内。") + private Integer roomType; + + @MessageElementForCatalog("Info.UseType") + @Schema(description = "用途属性, 1-治安、2-交通、3-重点。") + private Integer useType; + + @MessageElementForCatalog("Info.SupplyLightType") + @Schema(description = "摄像机补光属性。1-无补光;2-红外补光;3-白光补光;4-激光补光;9-其他") + private Integer supplyLightType; + + @MessageElementForCatalog("Info.DirectionType") + @Schema(description = "摄像机监视方位(光轴方向)属性。1-东(西向东)、2-西(东向西)、3-南(北向南)、4-北(南向北)、" + + "5-东南(西北到东南)、6-东北(西南到东北)、7-西南(东北到西南)、8-西北(东南到西北)") + private Integer directionType; + + @MessageElementForCatalog("Info.Resolution") + @Schema(description = "摄像机支持的分辨率,可多值") + private String resolution; + + @MessageElementForCatalog({"BusinessGroupID","Info.BusinessGroupID"}) + @Schema(description = "虚拟组织所属的业务分组ID") + private String businessGroupId; + + @MessageElementForCatalog("Info.DownloadSpeed") + @Schema(description = "下载倍速(可选),可多值") + private String downloadSpeed; + + @MessageElementForCatalog("Info.SVCSpaceSupportMode") + @Schema(description = "空域编码能力,取值0-不支持;1-1级增强(1个增强层);2-2级增强(2个增强层);3-3级增强(3个增强层)") + private Integer svcSpaceSupportMod; + + @MessageElementForCatalog("Info.SVCTimeSupportMode") + @Schema(description = "时域编码能力,取值0-不支持;1-1级增强;2-2级增强;3-3级增强(可选)") + private Integer svcTimeSupportMode; + + @Schema(description = "云台类型描述字符串") + private String ptzTypeText; + + @Schema(description = "子设备数") + private int subCount; + + @Schema(description = "是否含有音频") + private boolean hasAudio; + + @Schema(description = "GPS的更新时间") + private String gpsTime; + + @Schema(description = "码流标识,优先级高于设备中码流标识," + + "用于选择码流时组成码流标识。默认为null,不设置。可选值: stream/streamnumber/streamprofile/streamMode") + private String streamIdentification; + + @Schema(description = "通道类型, 默认0, 0: 普通通道,1 行政区划 2 业务分组/虚拟组织") + private int channelType; + + private Integer dataType = ChannelDataType.GB28181; + + public void setPtzType(int ptzType) { + this.ptzType = ptzType; + switch (ptzType) { + case 0: + this.ptzTypeText = "未知"; + break; + case 1: + this.ptzTypeText = "球机"; + break; + case 2: + this.ptzTypeText = "半球"; + break; + case 3: + this.ptzTypeText = "固定枪机"; + break; + case 4: + this.ptzTypeText = "遥控枪机"; + break; + case 5: + this.ptzTypeText = "遥控半球"; + break; + case 6: + this.ptzTypeText = "多目设备的全景/拼接通道"; + break; + case 7: + this.ptzTypeText = "多目设备的分割通道"; + break; + } + } + + public static DeviceChannel decode(Element element) throws InvocationTargetException, NoSuchMethodException, InstantiationException, IllegalAccessException { + DeviceChannel deviceChannel = XmlUtil.elementDecode(element, DeviceChannel.class); + if(deviceChannel.getCivilCode() != null ) { + if (ObjectUtils.isEmpty(deviceChannel.getCivilCode()) + || deviceChannel.getCivilCode().length() > 8 ){ + deviceChannel.setCivilCode(null); + } + // 此处对于不在wvp缓存中的行政区划,默认直接存储.保证即使出现wvp的行政区划缓存过老,也可以通过用户自主创建的方式正常使用系统 + } + GbCode gbCode = GbCode.decode(deviceChannel.getDeviceId()); + if (gbCode != null && "138".equals(gbCode.getTypeCode())) { + deviceChannel.setHasAudio(true); + if (deviceChannel.getEnableBroadcast() == null && "138".equals(gbCode.getTypeCode())) { + deviceChannel.setEnableBroadcast(1); + } + } + + return deviceChannel; + } + + public static DeviceChannel decodeWithOnlyDeviceId(Element element) { + Element deviceElement = element.element("DeviceID"); + DeviceChannel deviceChannel = new DeviceChannel(); + deviceChannel.setDeviceId(deviceElement.getText()); + deviceChannel.setDataType(ChannelDataType.GB28181); + return deviceChannel; + } + + public CommonGBChannel buildCommonGBChannelForStatus() { + CommonGBChannel commonGBChannel = new CommonGBChannel(); + commonGBChannel.setGbId(id); + commonGBChannel.setGbDeviceId(deviceId); + commonGBChannel.setGbName(name); + commonGBChannel.setDataType(ChannelDataType.GB28181); + commonGBChannel.setDataDeviceId(getDataDeviceId()); + return commonGBChannel; + } + + +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/bean/DeviceChannelInPlatform.java b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/DeviceChannelInPlatform.java new file mode 100644 index 0000000..c61bb08 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/DeviceChannelInPlatform.java @@ -0,0 +1,23 @@ +package com.genersoft.iot.vmp.gb28181.bean; + +public class DeviceChannelInPlatform extends DeviceChannel{ + + private String platFormId; + private String catalogId; + + public String getPlatFormId() { + return platFormId; + } + + public void setPlatFormId(String platFormId) { + this.platFormId = platFormId; + } + + public String getCatalogId() { + return catalogId; + } + + public void setCatalogId(String catalogId) { + this.catalogId = catalogId; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/bean/DeviceNotFoundEvent.java b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/DeviceNotFoundEvent.java new file mode 100644 index 0000000..bb65dbf --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/DeviceNotFoundEvent.java @@ -0,0 +1,16 @@ +package com.genersoft.iot.vmp.gb28181.bean; + +import lombok.Data; + +import javax.sip.Dialog; +import java.util.EventObject; + +@Data +public class DeviceNotFoundEvent { + + private String callId; + + public DeviceNotFoundEvent(String callId) { + this.callId = callId; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/bean/DeviceType.java b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/DeviceType.java new file mode 100644 index 0000000..5a82526 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/DeviceType.java @@ -0,0 +1,58 @@ +package com.genersoft.iot.vmp.gb28181.bean; + +import org.jetbrains.annotations.NotNull; + +public class DeviceType implements Comparable{ + + /** + * 编号 + */ + private String name; + + /** + * 名称 + */ + private String code; + + /** + * 归属名称 + */ + private String ownerName; + public static DeviceType getInstance(DeviceTypeEnum typeEnum) { + DeviceType deviceType = new DeviceType(); + deviceType.setName(typeEnum.getName()); + deviceType.setCode(typeEnum.getCode()); + deviceType.setOwnerName(typeEnum.getOwnerName()); + return deviceType; + } + + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getCode() { + return code; + } + + public void setCode(String code) { + this.code = code; + } + + public String getOwnerName() { + return ownerName; + } + + public void setOwnerName(String ownerName) { + this.ownerName = ownerName; + } + + @Override + public int compareTo(@NotNull DeviceType deviceType) { + return Integer.compare(Integer.parseInt(this.code), Integer.parseInt(deviceType.getCode())); + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/bean/DeviceTypeEnum.java b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/DeviceTypeEnum.java new file mode 100644 index 0000000..8a7c07a --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/DeviceTypeEnum.java @@ -0,0 +1,98 @@ +package com.genersoft.iot.vmp.gb28181.bean; + +/** + * 收录行业编码 + */ +public enum DeviceTypeEnum { + DVR("111", "DVR编码", "前端主设备"), + VIDEO_SERVER("112", "视频服务器编码", "前端主设备"), + ENCODER("113", "编码器编码", "前端主设备"), + DECODER("114", "解码器编码", "前端主设备"), + VIDEO_SWITCHING_MATRIX("115", "视频切换矩阵编码", "前端主设备"), + AUDIO_SWITCHING_MATRIX("116", "音频切换矩阵编码", "前端主设备"), + ALARM_CONTROLLER("117", "报警控制器编码", "前端主设备"), + NVR("118", "网络视频录像机(NVR)编码", "前端主设备"), + RESERVE("119", "预留", "前端主设备"), + ONLINE_VIDEO_IMAGE_INFORMATION_ACQUISITION_SYSTEM("120", "在线视频图像信息采集系统编码", "前端主设备"), + VIDEO_CHECKPOINT("121", "视频卡口编码", "前端主设备"), + MULTI_CAMERA_DEVICE("122", "多目设备编码", "前端主设备"), + PARKING_LOT_ENTRANCE_AND_EXIT_CONTROL_EQUIPMENT("123", "停车场出入口控制设备编码", "前端主设备"), + PERSONNEL_ACCESS_CONTROL_EQUIPMENT("124", "人员出入口控制设备编码", "前端主设备"), + SECURITY_INSPECTION_EQUIPMENT("125", "安检设备编码", "前端主设备"), + HVR("130", "混合硬盘录像机(HVR)编码", "前端主设备"), + CAMERA("131", "摄像机编码", "前端外围设备"), + IPC("132", "网络摄像机(IPC)/在线视频图像信息采集设备编码", "前端外围设备"), + MONITOR("133", "显示器编码", "前端外围设备"), + ALARM_INPUT_DEVICE("134", "报警输入设备编码(如红外、烟感、门禁等报警设备)", "前端外围设备"), + ALARM_OUTPUT_DEVICE("135", "报警输出设备编码(如警灯、警铃等设备)", "前端外围设备"), + VOICE_INPUT_DEVICE("136", "语音输入设备编码", "前端外围设备"), + VOICE_OUTPUT_DEVICE("137", "语音输出设备", "前端外围设备"), + MOBILE_TRANSMISSION_EQUIPMENT("138", "移动传输设备编码", "前端外围设备"), + OTHER_PERIPHERAL_DEVICES("139", "其他外围设备编码", "前端外围设备"), + ALARM_OUTPUT_DEVICE2("140", "报警输出设备编码(如继电器或触发器控制的设备)", "前端外围设备"), + BARRIER_GATE("141", "道闸(控制车辆通行)", "前端外围设备"), + SMART_DOOR("142", "智能门(控制人员通行)", "前端外围设备"), + VOUCHER_RECOGNITION_UNIT("143", "凭证识别单元", "前端外围设备"), + CENTRAL_SIGNALING_CONTROL_SERVER("200", "中心信令控制服务器编码", "平台设备"), + WEB_APPLICATION_SERVER("201", "Web应用服务器编码", "平台设备"), + PROXY_SERVER("203", "代理服务器编码", "平台设备"), + SECURITY_SERVER("204", "安全服务器编码", "平台设备"), + ALARM_SERVER("205", "报警服务器编码", "平台设备"), + DATABASE_SERVER("206", "数据库服务器编码", "平台设备"), + GIS_SERVER("207", "GIS服务器编码", "平台设备"), + MANAGER_SERVER("208", "管理服务器编码", "平台设备"), + ACCESS_GATEWAY("209", "接入网关编码", "平台设备"), + MEDIA_STORAGE_SERVER("210", "媒体存储服务器编码", "平台设备"), + SIGNALING_SECURITY_ROUTING_GATEWAY("211", "信令安全路由网关编码", "平台设备"), + BUSINESS_GROUP("215", "业务分组编码", "平台设备"), + VIRTUAL_ORGANIZATION("216", "虚拟组织编码", "平台设备"), + CENTRAL_USER("300", "中心用户", "中心用户"), + END_USER("400", "终端用户", "终端用户"), + VIDEO_IMAGE_INFORMATION_SYNTHESIS("500", "视频图像信息综合应用平台", "平台外接服务器"), + VIDEO_IMAGE_INFORMATION_OPERATION_AND_MAINTENANCE_MANAGEMENT("501", "视频图像信息运维管理平台", "平台外接服务器"), + VIDEO_IMAGE_ANALYSIS("502", "视频图像分析系统", "平台外接服务器"), + VIDEO_IMAGE_INFORMATION_DATABASE("503", "视频图像信息数据库", "平台外接服务器"), + VIDEO_IMAGE_ANALYSIS_EQUIPMENT("505", "视频图像分析设备", "平台外接服务器"), + ; + + /** + * 编号 + */ + private final String name; + + /** + * 名称 + */ + private String code; + + /** + * 归属名称 + */ + private String ownerName; + + DeviceTypeEnum(String code, String name, String ownerName) { + this.name = name; + this.code = code; + this.ownerName = ownerName; + } + + public String getName() { + return name; + } + + public String getCode() { + return code; + } + + public void setCode(String code) { + this.code = code; + } + + public String getOwnerName() { + return ownerName; + } + + public void setOwnerName(String ownerName) { + this.ownerName = ownerName; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/bean/DragZoomParam.java b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/DragZoomParam.java new file mode 100644 index 0000000..d9b3a81 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/DragZoomParam.java @@ -0,0 +1,34 @@ +package com.genersoft.iot.vmp.gb28181.bean; + +import com.genersoft.iot.vmp.gb28181.utils.MessageElement; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Data +@Schema(description = "拉框放大/缩小控制参数") +public class DragZoomParam { + + @MessageElement("Length") + @Schema(description = "播放窗口长度像素值(必选)") + protected Integer length; + + @MessageElement("Width") + @Schema(description = "播放窗口宽度像素值(必选)") + protected Integer width; + + @MessageElement("MidPointX") + @Schema(description = "拉框中心的横轴坐标像素值(必选)") + protected Integer midPointX; + + @MessageElement("MidPointY") + @Schema(description = "拉框中心的纵轴坐标像素值(必选)") + protected Integer midPointY; + + @MessageElement("LengthX") + @Schema(description = "拉框长度像素值(必选)") + protected Integer lengthX; + + @MessageElement("LengthY") + @Schema(description = "拉框宽度像素值(必选)") + protected Integer lengthY; +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/bean/DragZoomRequest.java b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/DragZoomRequest.java new file mode 100644 index 0000000..1a58c10 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/DragZoomRequest.java @@ -0,0 +1,30 @@ +package com.genersoft.iot.vmp.gb28181.bean; + +import com.genersoft.iot.vmp.gb28181.utils.MessageElement; +import lombok.Data; + +/** + * 设备信息查询响应 + * + * @author Y.G + * @version 1.0 + * @date 2022/6/28 14:55 + */ +@Data +public class DragZoomRequest { + /** + * 序列号 + */ + @MessageElement("SN") + private String sn; + + @MessageElement("DeviceID") + private String deviceId; + + @MessageElement(value = "DragZoomIn") + private DragZoomParam dragZoomIn; + + @MessageElement(value = "DragZoomOut") + private DragZoomParam dragZoomOut; + +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/bean/DrawThinProcess.java b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/DrawThinProcess.java new file mode 100644 index 0000000..9438798 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/DrawThinProcess.java @@ -0,0 +1,17 @@ +package com.genersoft.iot.vmp.gb28181.bean; + +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class DrawThinProcess { + + private double process; + private String msg; + + public DrawThinProcess(double process, String msg) { + this.process = process; + this.msg = msg; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/bean/FrontEndCode.java b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/FrontEndCode.java new file mode 100644 index 0000000..c155b53 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/FrontEndCode.java @@ -0,0 +1,184 @@ +package com.genersoft.iot.vmp.gb28181.bean; + +import jakarta.validation.constraints.NotNull; +import lombok.Data; + + +/** + * 解析收到的前端控制指令 + */ +@Data +public class FrontEndCode { + + + public static String encode(IFrontEndControlCode frontEndControlCode){ + return frontEndControlCode.encode(); + } + + public static IFrontEndControlCode decode(@NotNull String cmdStr) { + if (cmdStr.length() != 16) { + return null; + } + String cmdCodeStr = cmdStr.substring(6, 8); + int cmdCode = Integer.parseInt(cmdCodeStr, 16); + if (cmdCode < 39) { + // PTZ指令 + FrontEndControlCodeForPTZ codeForPTZ = new FrontEndControlCodeForPTZ(); + int zoomOut = cmdCode >> 5 & 1; + if (zoomOut == 1) { + codeForPTZ.setZoom(0); + } + int zoomIn = cmdCode >> 4 & 1; + if (zoomIn == 1) { + codeForPTZ.setZoom(1); + } + int tiltUp = cmdCode >> 3 & 1; + if (tiltUp == 1) { + codeForPTZ.setTilt(0); + } + int tiltDown = cmdCode >> 2 & 1; + if (tiltDown == 1) { + codeForPTZ.setTilt(1); + } + int panLeft = cmdCode >> 1 & 1; + if (panLeft == 1) { + codeForPTZ.setPan(0); + } + int panRight = cmdCode & 1; + if (panRight == 1) { + codeForPTZ.setPan(1); + } + String param1Str = cmdStr.substring(8, 10); + codeForPTZ.setPanSpeed(Integer.parseInt(param1Str, 16)); + String param2Str = cmdStr.substring(10, 12); + codeForPTZ.setTiltSpeed(Integer.parseInt(param2Str, 16)); + String param3Str = cmdStr.substring(12, 13); + codeForPTZ.setZoomSpeed(Integer.parseInt(param3Str, 16)); + return codeForPTZ; + }else if (cmdCode < 74) { + // FI指令 + FrontEndControlCodeForFI codeForFI = new FrontEndControlCodeForFI(); + int irisOut = cmdCode >> 3 & 1; + if (irisOut == 1) { + codeForFI.setIris(0); + } + int irisIn = cmdCode >> 2 & 1; + if (irisIn == 1) { + codeForFI.setIris(1); + } + int focusNear = cmdCode >> 1 & 1; + if (focusNear == 1) { + codeForFI.setFocus(0); + } + int focusFar = cmdCode & 1; + if (focusFar == 1) { + codeForFI.setFocus(1); + } + + String param1Str = cmdStr.substring(8, 10); + codeForFI.setFocusSpeed(Integer.parseInt(param1Str, 16)); + String param2Str = cmdStr.substring(10, 12); + codeForFI.setIrisSpeed(Integer.parseInt(param2Str, 16)); + return codeForFI; + }else if (cmdCode < 131) { + // 预置位指令 + FrontEndControlCodeForPreset codeForPreset = new FrontEndControlCodeForPreset(); + switch (cmdCode) { + case 0x81: // 设置预置位 + codeForPreset.setCode(1); + break; + case 0x82: // 调用预置位 + codeForPreset.setCode(2); + break; + case 0x83: // 删除预置位 + codeForPreset.setCode(3); + break; + default: + return null; + } + // 预置位编号 + String param2Str = cmdStr.substring(10, 12); + codeForPreset.setPresetId(Integer.parseInt(param2Str, 16)); + return codeForPreset; + }else if (cmdCode < 136) { + // 巡航指令 + FrontEndControlCodeForTour codeForTour = new FrontEndControlCodeForTour(); + String param3Str = cmdStr.substring(12, 13); + switch (cmdCode) { + case 0x84: // 加入巡航点 + codeForTour.setCode(1); + break; + case 0x85: // 删除一个巡航点 + codeForTour.setCode(2); + break; + case 0x86: // 设置巡航速度 + codeForTour.setCode(3); + codeForTour.setTourSpeed(Integer.parseInt(param3Str, 16)); + break; + case 0x87: // 设置巡航停留时间 + codeForTour.setCode(4); + codeForTour.setTourTime(Integer.parseInt(param3Str, 16)); + break; + case 0x88: // 开始巡航 + codeForTour.setCode(5); + break; + default: + return null; + } + String param1Str = cmdStr.substring(8, 10); + codeForTour.setTourId(Integer.parseInt(param1Str, 16)); + String param2Str = cmdStr.substring(10, 12); + codeForTour.setPresetId(Integer.parseInt(param2Str, 16)); + return codeForTour; + }else if (cmdCode < 138) { + // 扫描指令 + FrontEndControlCodeForScan controlCodeForScan = new FrontEndControlCodeForScan(); + String param2Str = cmdStr.substring(10, 11); + int param2Code = Integer.parseInt(param2Str, 16); + switch (cmdCode) { + case 0x89: + switch (param2Code) { + case 0x00: // 开始自动扫描 + controlCodeForScan.setCode(1); + break; + case 0x01: // 设置自动扫描左边界 + controlCodeForScan.setCode(2); + break; + case 0x02: // 设置自动扫描右边界 + controlCodeForScan.setCode(3); + break; + } + break; + case 0x8A: // 删除一个巡航点 + controlCodeForScan.setCode(4); + String param3Str = cmdStr.substring(12, 13); + controlCodeForScan.setScanSpeed(Integer.parseInt(param3Str, 16)); + break; + default: + return null; + } + String param1Str = cmdStr.substring(8, 10); + controlCodeForScan.setScanId(Integer.parseInt(param1Str, 16)); + return controlCodeForScan; + }else if (cmdCode < 141) { + // 辅助开关 + FrontEndControlCodeForAuxiliary codeForAuxiliary = new FrontEndControlCodeForAuxiliary(); + switch (cmdCode) { + case 0x8C: // 开 + codeForAuxiliary.setCode(1); + break; + case 0x8D: // 关 + codeForAuxiliary.setCode(2); + break; + default: + return null; + } + // 预置位编号 + String param2Str = cmdStr.substring(10, 12); + codeForAuxiliary.setAuxiliaryId(Integer.parseInt(param2Str, 16)); + return codeForAuxiliary; + }else { + return null; + } + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/bean/FrontEndControlCodeForAuxiliary.java b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/FrontEndControlCodeForAuxiliary.java new file mode 100644 index 0000000..4dc994a --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/FrontEndControlCodeForAuxiliary.java @@ -0,0 +1,34 @@ +package com.genersoft.iot.vmp.gb28181.bean; + + +import lombok.Getter; +import lombok.Setter; + +public class FrontEndControlCodeForAuxiliary implements IFrontEndControlCode { + + private final FrontEndControlType type = FrontEndControlType.AUXILIARY; + + @Override + public FrontEndControlType getType() { + return type; + } + + /** + * 辅助开关控制指令: 1为开, 2为关 + */ + @Getter + @Setter + private Integer code; + + /** + * 辅助开关编号 + */ + @Getter + @Setter + private Integer auxiliaryId; + + @Override + public String encode() { + return ""; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/bean/FrontEndControlCodeForFI.java b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/FrontEndControlCodeForFI.java new file mode 100644 index 0000000..89c9f8d --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/FrontEndControlCodeForFI.java @@ -0,0 +1,48 @@ +package com.genersoft.iot.vmp.gb28181.bean; + + +import lombok.Getter; +import lombok.Setter; + +public class FrontEndControlCodeForFI implements IFrontEndControlCode { + + private final FrontEndControlType type = FrontEndControlType.FI; + + @Override + public FrontEndControlType getType() { + return type; + } + + /** + * 光圈,0为缩小 1为放大 + */ + @Getter + @Setter + private Integer iris; + + /** + * 聚焦 0 近, 1远 + */ + @Getter + @Setter + private Integer focus; + + /** + * 聚焦速度 + */ + @Getter + @Setter + private Integer focusSpeed; + + /** + * 光圈速度 + */ + @Getter + @Setter + private Integer irisSpeed; + + @Override + public String encode() { + return ""; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/bean/FrontEndControlCodeForPTZ.java b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/FrontEndControlCodeForPTZ.java new file mode 100644 index 0000000..3759860 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/FrontEndControlCodeForPTZ.java @@ -0,0 +1,62 @@ +package com.genersoft.iot.vmp.gb28181.bean; + + +import lombok.Getter; +import lombok.Setter; + +public class FrontEndControlCodeForPTZ implements IFrontEndControlCode { + + private final FrontEndControlType type = FrontEndControlType.PTZ; + + @Override + public FrontEndControlType getType() { + return type; + } + + /** + * 镜头变倍,0为缩小 1为放大 + */ + @Getter + @Setter + private Integer zoom; + + /** + * 云台垂直方向控制 0 为上, 1为下 + */ + @Getter + @Setter + private Integer tilt; + + /** + * 云台水平方向控制 0 为左, 1为右 + */ + @Getter + @Setter + private Integer pan; + + /** + * 水平控制速度相对值 + */ + @Getter + @Setter + private Integer panSpeed; + + /** + * 垂直控制速度相对值 + */ + @Getter + @Setter + private Integer tiltSpeed; + + /** + * 变倍控制速度相对值 + */ + @Getter + @Setter + private Integer zoomSpeed; + + @Override + public String encode() { + return ""; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/bean/FrontEndControlCodeForPreset.java b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/FrontEndControlCodeForPreset.java new file mode 100644 index 0000000..8212d6a --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/FrontEndControlCodeForPreset.java @@ -0,0 +1,42 @@ +package com.genersoft.iot.vmp.gb28181.bean; + + +import lombok.Getter; +import lombok.Setter; + +public class FrontEndControlCodeForPreset implements IFrontEndControlCode { + + private final FrontEndControlType type = FrontEndControlType.PRESET; + + @Override + public FrontEndControlType getType() { + return type; + } + + /** + * 预置位指令: 1为设置预置位, 2为调用预置位, 3为删除预置位 + */ + @Getter + @Setter + private Integer code; + + /** + * 预置位编号 + */ + @Getter + @Setter + private Integer presetId; + + /** + * 预置位名称 + */ + @Getter + @Setter + private String presetName; + + + @Override + public String encode() { + return ""; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/bean/FrontEndControlCodeForScan.java b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/FrontEndControlCodeForScan.java new file mode 100644 index 0000000..ce16537 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/FrontEndControlCodeForScan.java @@ -0,0 +1,41 @@ +package com.genersoft.iot.vmp.gb28181.bean; + + +import lombok.Getter; +import lombok.Setter; + +public class FrontEndControlCodeForScan implements IFrontEndControlCode { + + private final FrontEndControlType type = FrontEndControlType.SCAN; + + @Override + public FrontEndControlType getType() { + return type; + } + + /** + * 预置位指令: 1为开始自动扫描, 2为设置自动扫描左边界, 3为设置自动扫描右边界, 4为设置自动扫描速度, 5为停止自动扫描 + */ + @Getter + @Setter + private Integer code; + + /** + * 自动扫描速度 + */ + @Getter + @Setter + private Integer scanSpeed; + + /** + * 扫描组号 + */ + @Getter + @Setter + private Integer scanId; + + @Override + public String encode() { + return ""; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/bean/FrontEndControlCodeForTour.java b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/FrontEndControlCodeForTour.java new file mode 100644 index 0000000..91ecb25 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/FrontEndControlCodeForTour.java @@ -0,0 +1,55 @@ +package com.genersoft.iot.vmp.gb28181.bean; + + +import lombok.Getter; +import lombok.Setter; + +public class FrontEndControlCodeForTour implements IFrontEndControlCode { + + private final FrontEndControlType type = FrontEndControlType.TOUR; + + @Override + public FrontEndControlType getType() { + return type; + } + + /** + * 巡航指令: 1为加入巡航点, 2为删除一个巡航点, 3为设置巡航速度, 4为设置巡航停留时间, 5为开始巡航, 6为停止巡航 + */ + @Getter + @Setter + private Integer code; + + /** + * 巡航点 + */ + @Getter + @Setter + private Integer tourId; + + /** + * 巡航停留时间 + */ + @Getter + @Setter + private Integer tourTime; + + /** + * 巡航速度 + */ + @Getter + @Setter + private Integer tourSpeed; + + /** + * 预置位编号 + */ + @Getter + @Setter + private Integer presetId; + + @Override + public String encode() { + return ""; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/bean/FrontEndControlCodeForWiper.java b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/FrontEndControlCodeForWiper.java new file mode 100644 index 0000000..9e1af0e --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/FrontEndControlCodeForWiper.java @@ -0,0 +1,27 @@ +package com.genersoft.iot.vmp.gb28181.bean; + + +import lombok.Getter; +import lombok.Setter; + +public class FrontEndControlCodeForWiper implements IFrontEndControlCode { + + private final FrontEndControlType type = FrontEndControlType.AUXILIARY; + + @Override + public FrontEndControlType getType() { + return type; + } + + /** + * 辅助开关控制指令: 1为开, 2为关 + */ + @Getter + @Setter + private Integer code; + + @Override + public String encode() { + return ""; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/bean/FrontEndControlType.java b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/FrontEndControlType.java new file mode 100644 index 0000000..b79fbed --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/FrontEndControlType.java @@ -0,0 +1,6 @@ +package com.genersoft.iot.vmp.gb28181.bean; + +public enum FrontEndControlType { + + PTZ,FI,PRESET,TOUR,SCAN,AUXILIARY +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/bean/GBStringMsgParser.java b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/GBStringMsgParser.java new file mode 100755 index 0000000..d4dab07 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/GBStringMsgParser.java @@ -0,0 +1,455 @@ +package com.genersoft.iot.vmp.gb28181.bean; + +import gov.nist.core.Host; +import gov.nist.core.HostNameParser; +import gov.nist.javax.sip.SIPConstants; +import gov.nist.javax.sip.address.AddressImpl; +import gov.nist.javax.sip.address.GenericURI; +import gov.nist.javax.sip.address.SipUri; +import gov.nist.javax.sip.address.TelephoneNumber; +import gov.nist.javax.sip.header.*; +import gov.nist.javax.sip.message.SIPMessage; +import gov.nist.javax.sip.message.SIPRequest; +import gov.nist.javax.sip.message.SIPResponse; +import gov.nist.javax.sip.parser.*; +import lombok.extern.slf4j.Slf4j; + +import java.io.UnsupportedEncodingException; +import java.text.ParseException; + +@Slf4j +public class GBStringMsgParser implements MessageParser { + + protected static boolean computeContentLengthFromMessage = false; + + /** + * @since v0.9 + */ + public GBStringMsgParser() { + super(); + } + + /** + * Parse a buffer containing a single SIP Message where the body is an array + * of un-interpreted bytes. This is intended for parsing the message from a + * memory buffer when the buffer. Incorporates a bug fix for a bug that was + * noted by Will Sullin of Callcast + * + * @param msgBuffer + * a byte buffer containing the messages to be parsed. This can + * consist of multiple SIP Messages concatenated together. + * @return a SIPMessage[] structure (request or response) containing the + * parsed SIP message. + * @exception ParseException + * is thrown when an illegal message has been encountered + * (and the rest of the buffer is discarded). + * @see ParseExceptionListener + */ + public SIPMessage parseSIPMessage(byte[] msgBuffer, boolean readBody, boolean strict, ParseExceptionListener parseExceptionListener) throws ParseException { + if (msgBuffer == null || msgBuffer.length == 0) + return null; + + int i = 0; + + // Squeeze out any leading control character. + try { + while (msgBuffer[i] < 0x20) + i++; + } + catch (ArrayIndexOutOfBoundsException e) { + // Array contains only control char, return null. + if (log.isDebugEnabled()) { + log.debug("handled only control char so returning null"); + } + return null; + } + + // Iterate thru the request/status line and headers. + String currentLine = null; + String currentHeader = null; + boolean isFirstLine = true; + SIPMessage message = null; + do + { + int lineStart = i; + + // Find the length of the line. + try { + while (msgBuffer[i] != '\r' && msgBuffer[i] != '\n') + i++; + } + catch (ArrayIndexOutOfBoundsException e) { + // End of the message. + break; + } + int lineLength = i - lineStart; + + // Make it a String. + try { + currentLine = new String(msgBuffer, lineStart, lineLength, "UTF-8"); + } catch (UnsupportedEncodingException e) { + throw new ParseException("Bad message encoding!", 0); + } + + currentLine = trimEndOfLine(currentLine); + + if (currentLine.length() == 0) { + // Last header line, process the previous buffered header. + if (currentHeader != null && message != null) { + processHeader(currentHeader, message, parseExceptionListener, msgBuffer); + } + + } + else { + if (isFirstLine) { + message = processFirstLine(currentLine, parseExceptionListener, msgBuffer); + } else { + char firstChar = currentLine.charAt(0); + if (firstChar == '\t' || firstChar == ' ') { + if (currentHeader == null) + throw new ParseException("Bad header continuation.", 0); + + // This is a continuation, append it to the previous line. + currentHeader += currentLine.substring(1); + } + else { + if (currentHeader != null && message != null) { + processHeader(currentHeader, message, parseExceptionListener, msgBuffer); + } + currentHeader = currentLine; + } + } + } + + if (msgBuffer[i] == '\r' && msgBuffer.length > i+1 && msgBuffer[i+1] == '\n') + i++; + + i++; + + isFirstLine = false; + } while (currentLine.length() > 0); // End do - while + + if (message == null) throw new ParseException("Bad message", 0); + message.setSize(i); + + // Check for content legth header + if (readBody && message.getContentLength() != null ) { + if ( message.getContentLength().getContentLength() != 0) { + int bodyLength = msgBuffer.length - i; + + byte[] body = new byte[bodyLength]; + System.arraycopy(msgBuffer, i, body, 0, bodyLength); + message.setMessageContent(body,!strict,computeContentLengthFromMessage,message.getContentLength().getContentLength()); + } else if (message.getCSeqHeader().getMethod().equalsIgnoreCase("MESSAGE")) { + int bodyLength = msgBuffer.length - i; + + byte[] body = new byte[bodyLength]; + System.arraycopy(msgBuffer, i, body, 0, bodyLength); + message.setMessageContent(body,!strict,computeContentLengthFromMessage,bodyLength); + }else if (!computeContentLengthFromMessage && strict) { + String last4Chars = new String(msgBuffer, msgBuffer.length - 4, 4); + if(!"\r\n\r\n".equals(last4Chars)) { + throw new ParseException("Extraneous characters at the end of the message ",i); + } + } + } + + return message; + } + + protected static String trimEndOfLine(String line) { + if (line == null) + return line; + + int i = line.length() - 1; + while (i >= 0 && line.charAt(i) <= 0x20) + i--; + + if (i == line.length() - 1) + return line; + + if (i == -1) + return ""; + + return line.substring(0, i+1); + } + + protected SIPMessage processFirstLine(String firstLine, ParseExceptionListener parseExceptionListener, byte[] msgBuffer) throws ParseException { + SIPMessage message; + if (!firstLine.startsWith(SIPConstants.SIP_VERSION_STRING)) { + message = new SIPRequest(); + try { + RequestLine requestLine = new RequestLineParser(firstLine + "\n") + .parse(); + ((SIPRequest) message).setRequestLine(requestLine); + } catch (ParseException ex) { + if (parseExceptionListener != null) + try { + parseExceptionListener.handleException(ex, message, + RequestLine.class, firstLine, new String(msgBuffer, "UTF-8")); + } catch (UnsupportedEncodingException e) { + e.printStackTrace(); + } + else + throw ex; + + } + } else { + message = new SIPResponse(); + try { + StatusLine sl = new StatusLineParser(firstLine + "\n").parse(); + ((SIPResponse) message).setStatusLine(sl); + } catch (ParseException ex) { + if (parseExceptionListener != null) { + try { + parseExceptionListener.handleException(ex, message, + StatusLine.class, firstLine, new String(msgBuffer, "UTF-8")); + } catch (UnsupportedEncodingException e) { + e.printStackTrace(); + } + } else + throw ex; + + } + } + return message; + } + + protected void processHeader(String header, SIPMessage message, ParseExceptionListener parseExceptionListener, byte[] rawMessage) throws ParseException { + if (header == null || header.length() == 0) + return; + + HeaderParser headerParser = null; + try { + headerParser = ParserFactory.createParser(header + "\n"); + } catch (ParseException ex) { + // https://java.net/jira/browse/JSIP-456 + if (parseExceptionListener != null) { + parseExceptionListener.handleException(ex, message, null, + header, null); + return; + } else { + throw ex; + } + } + + try { + SIPHeader sipHeader = headerParser.parse(); + message.attachHeader(sipHeader, false); + } catch (ParseException ex) { + if (parseExceptionListener != null) { + String headerName = Lexer.getHeaderName(header); + Class headerClass = NameMap.getClassFromName(headerName); + if (headerClass == null) { + headerClass = ExtensionHeaderImpl.class; + + } + try { + parseExceptionListener.handleException(ex, message, + headerClass, header, new String(rawMessage, "UTF-8")); + } catch (UnsupportedEncodingException e) { + e.printStackTrace(); + } + + } + } + } + + /** + * Parse an address (nameaddr or address spec) and return and address + * structure. + * + * @param address + * is a String containing the address to be parsed. + * @return a parsed address structure. + * @since v1.0 + * @exception ParseException + * when the address is badly formatted. + */ + public AddressImpl parseAddress(String address) throws ParseException { + AddressParser addressParser = new AddressParser(address); + return addressParser.address(true); + } + + /** + * Parse a host:port and return a parsed structure. + * + * @param hostport + * is a String containing the host:port to be parsed + * @return a parsed address structure. + * @since v1.0 + * @exception throws + * a ParseException when the address is badly formatted. + * + public HostPort parseHostPort(String hostport) throws ParseException { + Lexer lexer = new Lexer("charLexer", hostport); + return new HostNameParser(lexer).hostPort(); + + } + */ + + /** + * Parse a host name and return a parsed structure. + * + * @param host + * is a String containing the host name to be parsed + * @return a parsed address structure. + * @since v1.0 + * @exception ParseException + * a ParseException when the hostname is badly formatted. + */ + public Host parseHost(String host) throws ParseException { + Lexer lexer = new Lexer("charLexer", host); + return new HostNameParser(lexer).host(); + + } + + /** + * Parse a telephone number return a parsed structure. + * + * @param telephone_number + * is a String containing the telephone # to be parsed + * @return a parsed address structure. + * @since v1.0 + * @exception ParseException + * a ParseException when the address is badly formatted. + */ + public TelephoneNumber parseTelephoneNumber(String telephone_number) + throws ParseException { + // Bug fix contributed by Will Scullin + return new URLParser(telephone_number).parseTelephoneNumber(true); + + } + + /** + * Parse a SIP url from a string and return a URI structure for it. + * + * @param url + * a String containing the URI structure to be parsed. + * @return A parsed URI structure + * @exception ParseException + * if there was an error parsing the message. + */ + + public SipUri parseSIPUrl(String url) throws ParseException { + try { + return new URLParser(url).sipURL(true); + } catch (ClassCastException ex) { + throw new ParseException(url + " Not a SIP URL ", 0); + } + } + + /** + * Parse a uri from a string and return a URI structure for it. + * + * @param url + * a String containing the URI structure to be parsed. + * @return A parsed URI structure + * @exception ParseException + * if there was an error parsing the message. + */ + + public GenericURI parseUrl(String url) throws ParseException { + return new URLParser(url).parse(); + } + + /** + * Parse an individual SIP message header from a string. + * + * @param header + * String containing the SIP header. + * @return a SIPHeader structure. + * @exception ParseException + * if there was an error parsing the message. + */ + public static SIPHeader parseSIPHeader(String header) throws ParseException { + int start = 0; + int end = header.length() - 1; + try { + // Squeeze out any leading control character. + while (header.charAt(start) <= 0x20) + start++; + + // Squeeze out any trailing control character. + while (header.charAt(end) <= 0x20) + end--; + } + catch (ArrayIndexOutOfBoundsException e) { + // Array contains only control char. + throw new ParseException("Empty header.", 0); + } + + StringBuilder buffer = new StringBuilder(end + 1); + int i = start; + int lineStart = start; + boolean endOfLine = false; + while (i <= end) { + char c = header.charAt(i); + if (c == '\r' || c == '\n') { + if (!endOfLine) { + buffer.append(header.substring(lineStart, i)); + endOfLine = true; + } + } + else { + if (endOfLine) { + endOfLine = false; + if (c == ' ' || c == '\t') { + buffer.append(' '); + lineStart = i + 1; + } + else { + lineStart = i; + } + } + } + + i++; + } + buffer.append(header.substring(lineStart, i)); + buffer.append('\n'); + + HeaderParser hp = ParserFactory.createParser(buffer.toString()); + if (hp == null) + throw new ParseException("could not create parser", 0); + return hp.parse(); + } + + /** + * Parse the SIP Request Line + * + * @param requestLine + * a String containing the request line to be parsed. + * @return a RequestLine structure that has the parsed RequestLine + * @exception ParseException + * if there was an error parsing the requestLine. + */ + + public RequestLine parseSIPRequestLine(String requestLine) + throws ParseException { + requestLine += "\n"; + return new RequestLineParser(requestLine).parse(); + } + + /** + * Parse the SIP Response message status line + * + * @param statusLine + * a String containing the Status line to be parsed. + * @return StatusLine class corresponding to message + * @exception ParseException + * if there was an error parsing + * @see StatusLine + */ + + public StatusLine parseSIPStatusLine(String statusLine) + throws ParseException { + statusLine += "\n"; + return new StatusLineParser(statusLine).parse(); + } + + public static void setComputeContentLengthFromMessage( + boolean computeContentLengthFromMessage) { + GBStringMsgParser.computeContentLengthFromMessage = computeContentLengthFromMessage; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/bean/Gb28181Sdp.java b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/Gb28181Sdp.java new file mode 100644 index 0000000..4b9e26a --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/Gb28181Sdp.java @@ -0,0 +1,46 @@ +package com.genersoft.iot.vmp.gb28181.bean; + +import javax.sdp.SessionDescription; + +/** + * 28181 的SDP解析器 + */ +public class Gb28181Sdp { + private SessionDescription baseSdb; + private String ssrc; + + private String mediaDescription; + + public static Gb28181Sdp getInstance(SessionDescription baseSdb, String ssrc, String mediaDescription) { + Gb28181Sdp gb28181Sdp = new Gb28181Sdp(); + gb28181Sdp.setBaseSdb(baseSdb); + gb28181Sdp.setSsrc(ssrc); + gb28181Sdp.setMediaDescription(mediaDescription); + return gb28181Sdp; + } + + + public SessionDescription getBaseSdb() { + return baseSdb; + } + + public void setBaseSdb(SessionDescription baseSdb) { + this.baseSdb = baseSdb; + } + + public String getSsrc() { + return ssrc; + } + + public void setSsrc(String ssrc) { + this.ssrc = ssrc; + } + + public String getMediaDescription() { + return mediaDescription; + } + + public void setMediaDescription(String mediaDescription) { + this.mediaDescription = mediaDescription; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/bean/GbCode.java b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/GbCode.java new file mode 100644 index 0000000..bc5e508 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/GbCode.java @@ -0,0 +1,48 @@ +package com.genersoft.iot.vmp.gb28181.bean; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +/** + * 国标编码对象 + */ +@Data +@Schema(description = "国标编码对象") +public class GbCode { + + @Schema(description = "中心编码,由监控中心所在地的行政区划代码确定,符合GB/T2260—2007的要求") + private String centerCode; + + @Schema(description = "行业编码") + private String industryCode; + + @Schema(description = "类型编码") + private String typeCode; + + @Schema(description = "网络标识") + private String netCode; + + @Schema(description = "序号") + private String sn; + + /** + * 解析国标编号 + */ + public static GbCode decode(String code){ + if (code == null || code.trim().length() != 20) { + return null; + } + code = code.trim(); + GbCode gbCode = new GbCode(); + gbCode.setCenterCode(code.substring(0, 8)); + gbCode.setIndustryCode(code.substring(8, 10)); + gbCode.setTypeCode(code.substring(10, 13)); + gbCode.setNetCode(code.substring(13, 14)); + gbCode.setSn(code.substring(14)); + return gbCode; + } + + public String ecode(){ + return centerCode + industryCode + typeCode + netCode + sn; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/bean/GbSipDate.java b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/GbSipDate.java new file mode 100644 index 0000000..e02954f --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/GbSipDate.java @@ -0,0 +1,148 @@ +package com.genersoft.iot.vmp.gb28181.bean; + +import gov.nist.core.InternalErrorHandler; +import gov.nist.javax.sip.header.SIPDate; + +import java.io.Serial; +import java.util.*; + +/** + * 重写jain sip的SIPDate解决与国标时间格式不一致的问题 + */ +public class GbSipDate extends SIPDate { + + @Serial + private static final long serialVersionUID = 1L; + + private Calendar javaCal; + + public GbSipDate(long timeMillis) { + this.javaCal = new GregorianCalendar(TimeZone.getDefault(), Locale.getDefault()); + Date date = new Date(timeMillis); + this.javaCal.setTime(date); + this.wkday = this.javaCal.get(7); + switch(this.wkday) { + case 1: + this.sipWkDay = "Sun"; + break; + case 2: + this.sipWkDay = "Mon"; + break; + case 3: + this.sipWkDay = "Tue"; + break; + case 4: + this.sipWkDay = "Wed"; + break; + case 5: + this.sipWkDay = "Thu"; + break; + case 6: + this.sipWkDay = "Fri"; + break; + case 7: + this.sipWkDay = "Sat"; + break; + default: + InternalErrorHandler.handleException("No date map for wkday " + this.wkday); + } + + this.day = this.javaCal.get(5); + this.month = this.javaCal.get(2); + switch(this.month) { + case 0: + this.sipMonth = "Jan"; + break; + case 1: + this.sipMonth = "Feb"; + break; + case 2: + this.sipMonth = "Mar"; + break; + case 3: + this.sipMonth = "Apr"; + break; + case 4: + this.sipMonth = "May"; + break; + case 5: + this.sipMonth = "Jun"; + break; + case 6: + this.sipMonth = "Jul"; + break; + case 7: + this.sipMonth = "Aug"; + break; + case 8: + this.sipMonth = "Sep"; + break; + case 9: + this.sipMonth = "Oct"; + break; + case 10: + this.sipMonth = "Nov"; + break; + case 11: + this.sipMonth = "Dec"; + break; + default: + InternalErrorHandler.handleException("No date map for month " + this.month); + } + + this.year = this.javaCal.get(1); + this.hour = this.javaCal.get(11); + this.minute = this.javaCal.get(12); + this.second = this.javaCal.get(13); + } + + @Override + public StringBuilder encode(StringBuilder var1) { + String var2; + if (this.month < 9) { + var2 = "0" + (this.month + 1); + } else { + var2 = "" + (this.month + 1); + } + + String var3; + if (this.day < 10) { + var3 = "0" + this.day; + } else { + var3 = "" + this.day; + } + + String var4; + if (this.hour < 10) { + var4 = "0" + this.hour; + } else { + var4 = "" + this.hour; + } + + String var5; + if (this.minute < 10) { + var5 = "0" + this.minute; + } else { + var5 = "" + this.minute; + } + + String var6; + if (this.second < 10) { + var6 = "0" + this.second; + } else { + var6 = "" + this.second; + } + + int var8 = this.javaCal.get(14); + String var7; + if (var8 < 10) { + var7 = "00" + var8; + } else if (var8 < 100) { + var7 = "0" + var8; + } else { + var7 = "" + var8; + } + + return var1.append(this.year).append("-").append(var2).append("-").append(var3).append("T").append(var4).append(":").append(var5).append(":").append(var6).append(".").append(var7); + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/bean/GbSteamIdentification.java b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/GbSteamIdentification.java new file mode 100644 index 0000000..63c17a8 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/GbSteamIdentification.java @@ -0,0 +1,44 @@ +package com.genersoft.iot.vmp.gb28181.bean; + +/** + * 码流索引标识 + */ +public enum GbSteamIdentification { + /** + * 主码流 stream:0 + * 子码流 stream:1s + */ + streamMain("stream", new String[]{"0","1"}), + /** + * 国标28181-2022定义的方式 + * 主码流 streamnumber:0 + * 子码流 streamnumber:1 + */ + streamnumber("streamnumber", new String[]{"0","1"}), + /** + * 主码流 streamprofile:0 + * 子码流 streamprofile:1 + */ + streamprofile("streamprofile", new String[]{"0","1"}), + /** + * 适用的品牌: TP-LINK + */ + streamMode("streamMode", new String[]{"main","sub"}), + ; + + GbSteamIdentification(String value, String[] indexArray) { + this.value = value; + this.indexArray = indexArray; + } + + private String value; + private String[] indexArray; + + public String getValue() { + return value; + } + + public String[] getIndexArray() { + return indexArray; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/bean/GbStream.java b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/GbStream.java new file mode 100755 index 0000000..67058f3 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/GbStream.java @@ -0,0 +1,125 @@ +package com.genersoft.iot.vmp.gb28181.bean; + +import io.swagger.v3.oas.annotations.media.Schema; + +/** + * 直播流关联国标上级平台 + * @author lin + */ +@Schema(description = "直播流关联国标上级平台") +public class GbStream extends PlatformGbStream{ + + @Schema(description = "ID") + private int gbStreamId; + @Schema(description = "应用名") + private String app; + @Schema(description = "流ID") + private String stream; + @Schema(description = "国标ID") + private String gbId; + @Schema(description = "名称") + private String name; + @Schema(description = "流媒体ID") + private String mediaServerId; + @Schema(description = "经度") + private double longitude; + @Schema(description = "纬度") + private double latitude; + @Schema(description = "流类型(拉流/推流)") + private String streamType; + @Schema(description = "状态") + private boolean status; + + @Schema(description = "创建时间") + public String createTime; + + @Override + public Integer getGbStreamId() { + return gbStreamId; + } + + @Override + public void setGbStreamId(Integer gbStreamId) { + this.gbStreamId = gbStreamId; + } + + public String getApp() { + return app; + } + + public void setApp(String app) { + this.app = app; + } + + public String getStream() { + return stream; + } + + public void setStream(String stream) { + this.stream = stream; + } + + public String getGbId() { + return gbId; + } + + public void setGbId(String gbId) { + this.gbId = gbId; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public double getLongitude() { + return longitude; + } + + public void setLongitude(double longitude) { + this.longitude = longitude; + } + + public double getLatitude() { + return latitude; + } + + public void setLatitude(double latitude) { + this.latitude = latitude; + } + + public String getStreamType() { + return streamType; + } + + public void setStreamType(String streamType) { + this.streamType = streamType; + } + + public boolean isStatus() { + return status; + } + + public void setStatus(boolean status) { + this.status = status; + } + + public String getMediaServerId() { + return mediaServerId; + } + + public void setMediaServerId(String mediaServerId) { + this.mediaServerId = mediaServerId; + } + + public String getCreateTime() { + return createTime; + } + + public void setCreateTime(String createTime) { + this.createTime = createTime; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/bean/GbStringMsgParserFactory.java b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/GbStringMsgParserFactory.java new file mode 100755 index 0000000..3a9a1d1 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/GbStringMsgParserFactory.java @@ -0,0 +1,21 @@ +package com.genersoft.iot.vmp.gb28181.bean; + +import gov.nist.javax.sip.parser.MessageParser; +import gov.nist.javax.sip.parser.MessageParserFactory; +import gov.nist.javax.sip.stack.SIPTransactionStack; + +public class GbStringMsgParserFactory implements MessageParserFactory { + + /** + * msg parser is completely stateless, reuse isntance for the whole stack + * fixes https://github.com/RestComm/jain-sip/issues/92 + */ + private static GBStringMsgParser msgParser = new GBStringMsgParser(); + /* + * (non-Javadoc) + * @see gov.nist.javax.sip.parser.MessageParserFactory#createMessageParser(gov.nist.javax.sip.stack.SIPTransactionStack) + */ + public MessageParser createMessageParser(SIPTransactionStack stack) { + return msgParser; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/bean/Group.java b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/Group.java new file mode 100644 index 0000000..0d6729d --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/Group.java @@ -0,0 +1,100 @@ +package com.genersoft.iot.vmp.gb28181.bean; + +import com.genersoft.iot.vmp.utils.DateUtil; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import org.jetbrains.annotations.NotNull; + +/** + * 业务分组 + */ +@Data +@Schema(description = "业务分组") +public class Group implements Comparable{ + /** + * 数据库自增ID + */ + @Schema(description = "数据库自增ID") + private int id; + + /** + * 区域国标编号 + */ + @Schema(description = "区域国标编号") + private String deviceId; + + /** + * 区域名称 + */ + @Schema(description = "区域名称") + private String name; + + /** + * 父分组ID + */ + @Schema(description = "父分组ID") + private Integer parentId; + + /** + * 父区域国标ID + */ + @Schema(description = "父区域国标ID") + private String parentDeviceId; + + /** + * 所属的业务分组国标编号 + */ + @Schema(description = "所属的业务分组国标编号") + private String businessGroup; + + /** + * 创建时间 + */ + @Schema(description = "创建时间") + private String createTime; + + /** + * 更新时间 + */ + @Schema(description = "更新时间") + private String updateTime; + + /** + * 行政区划 + */ + @Schema(description = "行政区划") + private String civilCode; + + /** + * 别名 + */ + @Schema(description = "别名, 此别名为唯一值,可以对接第三方是存储对方的ID") + private String alias; + + public static Group getInstance(DeviceChannel channel) { + GbCode gbCode = GbCode.decode(channel.getDeviceId()); + if (gbCode == null || (!gbCode.getTypeCode().equals("215") && !gbCode.getTypeCode().equals("216"))) { + return null; + } + Group group = new Group(); + group.setName(channel.getName()); + group.setDeviceId(channel.getDeviceId()); + group.setCreateTime(DateUtil.getNow()); + group.setUpdateTime(DateUtil.getNow()); + if (gbCode.getTypeCode().equals("215")) { + group.setBusinessGroup(channel.getDeviceId()); + }else if (gbCode.getTypeCode().equals("216")) { + group.setBusinessGroup(channel.getBusinessGroupId()); + group.setParentDeviceId(channel.getParentId()); + } + if (group.getBusinessGroup() == null) { + return null; + } + return group; + } + + @Override + public int compareTo(@NotNull Group region) { + return Integer.compare(Integer.parseInt(this.deviceId), Integer.parseInt(region.getDeviceId())); + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/bean/GroupTree.java b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/GroupTree.java new file mode 100644 index 0000000..44789f0 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/GroupTree.java @@ -0,0 +1,27 @@ +package com.genersoft.iot.vmp.gb28181.bean; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 业务分组 + */ +@EqualsAndHashCode(callSuper = true) +@Data +@Schema(description = "业务分组树") +public class GroupTree extends Group{ + + @Schema(description = "树节点ID") + private String treeId; + + @Schema(description = "是否有子节点") + private boolean isLeaf; + + @Schema(description = "类型, 行政区划:0 摄像头: 1") + private int type; + + @Schema(description = "在线状态") + private String status; + +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/bean/HandlerCatchData.java b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/HandlerCatchData.java new file mode 100755 index 0000000..97da863 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/HandlerCatchData.java @@ -0,0 +1,44 @@ +package com.genersoft.iot.vmp.gb28181.bean; + +import org.dom4j.Element; + +import javax.sip.RequestEvent; + +/** + * @author lin + */ +public class HandlerCatchData { + private RequestEvent evt; + private Device device; + private Element rootElement; + + public HandlerCatchData(RequestEvent evt, Device device, Element rootElement) { + this.evt = evt; + this.device = device; + this.rootElement = rootElement; + } + + public RequestEvent getEvt() { + return evt; + } + + public void setEvt(RequestEvent evt) { + this.evt = evt; + } + + public Device getDevice() { + return device; + } + + public void setDevice(Device device) { + this.device = device; + } + + public Element getRootElement() { + return rootElement; + } + + public void setRootElement(Element rootElement) { + this.rootElement = rootElement; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/bean/HomePositionRequest.java b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/HomePositionRequest.java new file mode 100755 index 0000000..2c20713 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/HomePositionRequest.java @@ -0,0 +1,94 @@ +package com.genersoft.iot.vmp.gb28181.bean; + +import com.genersoft.iot.vmp.gb28181.utils.MessageElement; + +/** + * 设备信息查询响应 + * + * @author Y.G + * @version 1.0 + * @date 2022/6/28 14:55 + */ +public class HomePositionRequest { + /** + * 序列号 + */ + @MessageElement("SN") + private String sn; + + @MessageElement("DeviceID") + private String deviceId; + + @MessageElement(value = "HomePosition") + private HomePosition homePosition; + + + /** + * 基本参数 + */ + public static class HomePosition { + /** + * 播放窗口长度像素值 + */ + @MessageElement("Enabled") + protected String enabled; + /** + * 播放窗口宽度像素值 + */ + @MessageElement("ResetTime") + protected String resetTime; + /** + * 拉框中心的横轴坐标像素值 + */ + @MessageElement("PresetIndex") + protected String presetIndex; + + public String getEnabled() { + return enabled; + } + + public void setEnabled(String enabled) { + this.enabled = enabled; + } + + public String getResetTime() { + return resetTime; + } + + public void setResetTime(String resetTime) { + this.resetTime = resetTime; + } + + public String getPresetIndex() { + return presetIndex; + } + + public void setPresetIndex(String presetIndex) { + this.presetIndex = presetIndex; + } + } + + public String getSn() { + return sn; + } + + public void setSn(String sn) { + this.sn = sn; + } + + public String getDeviceId() { + return deviceId; + } + + public void setDeviceId(String deviceId) { + this.deviceId = deviceId; + } + + public HomePosition getHomePosition() { + return homePosition; + } + + public void setHomePosition(HomePosition homePosition) { + this.homePosition = homePosition; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/bean/Host.java b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/Host.java new file mode 100755 index 0000000..1b14560 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/Host.java @@ -0,0 +1,35 @@ +package com.genersoft.iot.vmp.gb28181.bean; + + + +public class Host { + + private String ip; + private int port; + private String address; + + + public String getIp() { + return ip; + } + + public void setIp(String ip) { + this.ip = ip; + } + + public int getPort() { + return port; + } + + public void setPort(int port) { + this.port = port; + } + + public String getAddress() { + return address; + } + + public void setAddress(String address) { + this.address = address; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/bean/IFrontEndControlCode.java b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/IFrontEndControlCode.java new file mode 100644 index 0000000..8264e53 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/IFrontEndControlCode.java @@ -0,0 +1,7 @@ +package com.genersoft.iot.vmp.gb28181.bean; + +public interface IFrontEndControlCode { + + FrontEndControlType getType(); + String encode(); +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/bean/IndustryCodeType.java b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/IndustryCodeType.java new file mode 100644 index 0000000..d3414a2 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/IndustryCodeType.java @@ -0,0 +1,59 @@ +package com.genersoft.iot.vmp.gb28181.bean; + +import org.jetbrains.annotations.NotNull; + +public class IndustryCodeType implements Comparable{ + + /** + * 接入类型码 + */ + private String name; + + /** + * 名称 + */ + private String code; + + /** + * 备注 + */ + private String notes; + + public static IndustryCodeType getInstance(IndustryCodeTypeEnum typeEnum) { + IndustryCodeType industryCodeType = new IndustryCodeType(); + industryCodeType.setName(typeEnum.getName()); + industryCodeType.setCode(typeEnum.getCode()); + industryCodeType.setNotes(typeEnum.getNotes()); + return industryCodeType; + } + + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getCode() { + return code; + } + + public void setCode(String code) { + this.code = code; + } + + public String getNotes() { + return notes; + } + + public void setNotes(String notes) { + this.notes = notes; + } + + @Override + public int compareTo(@NotNull IndustryCodeType industryCodeType) { + return Integer.compare(Integer.parseInt(this.code), Integer.parseInt(industryCodeType.getCode())); + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/bean/IndustryCodeTypeEnum.java b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/IndustryCodeTypeEnum.java new file mode 100644 index 0000000..8d25fe9 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/IndustryCodeTypeEnum.java @@ -0,0 +1,55 @@ +package com.genersoft.iot.vmp.gb28181.bean; + +import lombok.Getter; + +/** + * 收录行业编码 + */ +public enum IndustryCodeTypeEnum { + SOCIAL_SECURITY_ROAD("00", "社会治安路面接入", "包括城市路面、商业街、公共区域、重点区域"), + SOCIAL_SECURITY_COMMUNITY("01", "社会治安社区接入", "包括社区、楼宇、网吧等"), + SOCIAL_SECURITY__INTERNAL("02", "社会治安内部接入 ", "包括公安办公楼、留置室等"), + SOCIAL_SECURITY_OTHER("03", "社会治安其他接入", ""), + TRAFFIC_ROAD("04", "交通路面接入 ", "包括城市主要干道、国道、高速交通状况监视"), + TRAFFIC_BAYONET("05", "交通卡口接入", "包括交叉路口、“电子警察”、关口、收费站等"), + TRAFFIC_INTERNAL("06", "交通内部接入", "包括交管办公楼等"), + TRAFFIC_OTHER("07", "交通其他接入", ""), + CITY_MANAGEMENT("08", "城市管理接入", ""), + HEALTH_ENVIRONMENTAL_PROTECTION("09", "卫生环保接入", ""), + COMMODITY_INSPECTION_CUSTOMHOUSE("10", "商检海关接入", ""), + EDUCATION_SECTOR("11", "教育部门接入", ""), + CIVIL_AVIATION("12", "民航接入", ""), + RAILWAY("13", "铁路接入", ""), + SHIPPING("14", "航运接入", ""), + AGRICULTURE_FORESTRY_ANIMAL_HUSBANDRY_FISHING("40", "农、林、牧、渔业接入", ""), + MINING("41", "采矿业接入", ""), + MANUFACTURING_INDUSTRY("42", "制造业接入", ""), + ELECTRICITY_HEAT_GAS_AND_WATER_PRODUCTION_AND_SUPPLY("43", "电力、热力、燃气及水生产和供应业接入", ""), + CONSTRUCTION("44", "建筑业接入", ""), + WHOLESALE_AND_RETAIL("45", "批发和零售业接入", ""), + ; + + /** + * 接入类型码 + */ + @Getter + private String name; + + /** + * 名称 + */ + @Getter + private String code; + + /** + * 备注 + */ + @Getter + private String notes; + + IndustryCodeTypeEnum(String code, String name, String notes) { + this.name = name; + this.code = code; + this.notes = notes; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/bean/InviteDecodeException.java b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/InviteDecodeException.java new file mode 100644 index 0000000..e9943f1 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/InviteDecodeException.java @@ -0,0 +1,14 @@ +package com.genersoft.iot.vmp.gb28181.bean; + +import lombok.Data; + +@Data +public class InviteDecodeException extends RuntimeException{ + private int code; + private String msg; + + public InviteDecodeException(int code, String msg) { + this.code = code; + this.msg = msg; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/bean/InviteMessageInfo.java b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/InviteMessageInfo.java new file mode 100644 index 0000000..beadb69 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/InviteMessageInfo.java @@ -0,0 +1,22 @@ +package com.genersoft.iot.vmp.gb28181.bean; + +import lombok.Data; + +// 从INVITE消息中解析需要的信息 +@Data +public class InviteMessageInfo { + private String requesterId; + private String targetChannelId; + private String sourceChannelId; + private String sessionName; + private String ssrc; + private boolean tcp; + private boolean tcpActive; + private String callId; + private Long startTime; + private Long stopTime; + private String downloadSpeed; + private String ip; + private int port; + +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/bean/InviteStreamCallback.java b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/InviteStreamCallback.java new file mode 100755 index 0000000..42a0519 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/InviteStreamCallback.java @@ -0,0 +1,5 @@ +package com.genersoft.iot.vmp.gb28181.bean; + +public interface InviteStreamCallback { + void call(InviteStreamInfo inviteStreamInfo); +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/bean/InviteStreamInfo.java b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/InviteStreamInfo.java new file mode 100755 index 0000000..e1925fa --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/InviteStreamInfo.java @@ -0,0 +1,61 @@ +package com.genersoft.iot.vmp.gb28181.bean; + +import com.alibaba.fastjson2.JSONObject; +import com.genersoft.iot.vmp.media.bean.MediaServer; + +public class InviteStreamInfo { + + public InviteStreamInfo(MediaServer mediaServerItem, JSONObject response, String callId, String app, String stream) { + this.mediaServerItem = mediaServerItem; + this.response = response; + this.callId = callId; + this.app = app; + this.stream = stream; + } + + private MediaServer mediaServerItem; + private JSONObject response; + private String callId; + private String app; + private String stream; + + public MediaServer getMediaServerItem() { + return mediaServerItem; + } + + public void setMediaServerItem(MediaServer mediaServerItem) { + this.mediaServerItem = mediaServerItem; + } + + public JSONObject getResponse() { + return response; + } + + public void setResponse(JSONObject response) { + this.response = response; + } + + public String getCallId() { + return callId; + } + + public void setCallId(String callId) { + this.callId = callId; + } + + public String getApp() { + return app; + } + + public void setApp(String app) { + this.app = app; + } + + public String getStream() { + return stream; + } + + public void setStream(String stream) { + this.stream = stream; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/bean/InviteStreamType.java b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/InviteStreamType.java new file mode 100755 index 0000000..4f62c66 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/InviteStreamType.java @@ -0,0 +1,8 @@ +package com.genersoft.iot.vmp.gb28181.bean; + +public enum InviteStreamType { + + PLAY,PLAYBACK,DOWNLOAD,PUSH,PROXY,CLOUD_RECORD_PUSH,CLOUD_RECORD_PROXY,BROADCAST,TALK + + +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/bean/MessageResponseTask.java b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/MessageResponseTask.java new file mode 100644 index 0000000..cb3dfb0 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/MessageResponseTask.java @@ -0,0 +1,43 @@ +package com.genersoft.iot.vmp.gb28181.bean; + +import lombok.Getter; +import lombok.Setter; +import org.dom4j.Element; +import org.jetbrains.annotations.NotNull; + +import java.util.List; +import java.util.concurrent.Delayed; +import java.util.concurrent.TimeUnit; + +public class MessageResponseTask implements Delayed { + + @Getter + @Setter + private Element element; + + @Getter + @Setter + private List data; + + @Getter + @Setter + private String key; + + + /** + * 超时时间(单位: 毫秒) + */ + @Getter + @Setter + private long delayTime; + + @Override + public long getDelay(@NotNull TimeUnit unit) { + return unit.convert(delayTime - System.currentTimeMillis(), TimeUnit.MILLISECONDS); + } + + @Override + public int compareTo(@NotNull Delayed o) { + return (int) (this.getDelay(TimeUnit.MILLISECONDS) - o.getDelay(TimeUnit.MILLISECONDS)); + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/bean/MobilePosition.java b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/MobilePosition.java new file mode 100755 index 0000000..39804a0 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/MobilePosition.java @@ -0,0 +1,71 @@ +package com.genersoft.iot.vmp.gb28181.bean; + +import lombok.Data; + +/** + * @description: 移动位置bean + * @author: lawrencehj + * @date: 2021年1月23日 + */ + +@Data +public class MobilePosition { + /** + * 设备Id + */ + private String deviceId; + + /** + * 通道Id + */ + private Integer channelId; + + /** + * 通道国标编号 + */ + private String channelDeviceId; + + /** + * 设备名称 + */ + private String deviceName; + + /** + * 通知时间 + */ + private String time; + + /** + * 经度 + */ + private double longitude; + + /** + * 纬度 + */ + private double latitude; + + /** + * 海拔高度 + */ + private double altitude; + + /** + * 速度 + */ + private double speed; + + /** + * 方向 + */ + private double direction; + + /** + * 位置信息上报来源(Mobile Position、GPS Alarm) + */ + private String reportSource; + /** + * 创建时间 + */ + private String createTime; +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/bean/NetworkIdentificationType.java b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/NetworkIdentificationType.java new file mode 100644 index 0000000..ed469e4 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/NetworkIdentificationType.java @@ -0,0 +1,45 @@ +package com.genersoft.iot.vmp.gb28181.bean; + +import org.jetbrains.annotations.NotNull; + +public class NetworkIdentificationType implements Comparable{ + + /** + * 接入类型码 + */ + private String name; + + /** + * 名称 + */ + private String code; + + public static NetworkIdentificationType getInstance(NetworkIdentificationTypeEnum typeEnum) { + NetworkIdentificationType industryCodeType = new NetworkIdentificationType(); + industryCodeType.setName(typeEnum.getName()); + industryCodeType.setCode(typeEnum.getCode()); + return industryCodeType; + } + + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getCode() { + return code; + } + + public void setCode(String code) { + this.code = code; + } + + @Override + public int compareTo(@NotNull NetworkIdentificationType networkIdentificationType) { + return Integer.compare(Integer.parseInt(this.code), Integer.parseInt(networkIdentificationType.getCode())); + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/bean/NetworkIdentificationTypeEnum.java b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/NetworkIdentificationTypeEnum.java new file mode 100644 index 0000000..1b0ed2a --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/NetworkIdentificationTypeEnum.java @@ -0,0 +1,51 @@ +package com.genersoft.iot.vmp.gb28181.bean; + +/** + * 收录行业编码 + */ +public enum NetworkIdentificationTypeEnum { + PUBLIC_SECURITY_VIDEO_TRANSMISSION_NETWORK("0", "公安视频传输网"), + PUBLIC_SECURITY_VIDEO_TRANSMISSION_NETWORK2("1", "公安视频传输网"), + INDUSTRY_SPECIFIC_NETWORK("2", "行业专网"), + POLITICAL_AND_LEGAL_INFORMATION_NETWORK("3", "政法信息网"), + PUBLIC_SECURITY_MOBILE_INFORMATION_NETWORK("4", "公安移动信息网"), + PUBLIC_SECURITY_INFORMATION_NETWORK("5", "公安信息网"), + ELECTRONIC_GOVERNMENT_EXTRANET("6", "电子政务外网"), + PUBLIC_NETWORKS_SUCH_AS_THE_INTERNET("7", "互联网等公共网络"), + Dedicated_Line("8", "专线"), + RESERVE("9", "预留"), + ; + + /** + * 接入类型码 + */ + private String name; + + /** + * 名称 + */ + private String code; + + + NetworkIdentificationTypeEnum(String code, String name) { + this.name = name; + this.code = code; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getCode() { + return code; + } + + public void setCode(String code) { + this.code = code; + } + +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/bean/NotifyCatalogChannel.java b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/NotifyCatalogChannel.java new file mode 100644 index 0000000..8961677 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/NotifyCatalogChannel.java @@ -0,0 +1,38 @@ +package com.genersoft.iot.vmp.gb28181.bean; + + +public class NotifyCatalogChannel { + + private Type type; + + private DeviceChannel channel; + + + public enum Type { + ADD, DELETE, UPDATE, STATUS_CHANGED + } + + + public static NotifyCatalogChannel getInstance(Type type, DeviceChannel channel) { + NotifyCatalogChannel notifyCatalogChannel = new NotifyCatalogChannel(); + notifyCatalogChannel.setType(type); + notifyCatalogChannel.setChannel(channel); + return notifyCatalogChannel; + } + + public Type getType() { + return type; + } + + public void setType(Type type) { + this.type = type; + } + + public DeviceChannel getChannel() { + return channel; + } + + public void setChannel(DeviceChannel channel) { + this.channel = channel; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/bean/OpenRTPServerResult.java b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/OpenRTPServerResult.java new file mode 100644 index 0000000..aa60444 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/OpenRTPServerResult.java @@ -0,0 +1,12 @@ +package com.genersoft.iot.vmp.gb28181.bean; + +import com.genersoft.iot.vmp.media.event.hook.HookData; +import com.genersoft.iot.vmp.service.bean.SSRCInfo; +import lombok.Data; + +@Data +public class OpenRTPServerResult { + + private SSRCInfo ssrcInfo; + private HookData hookData; +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/bean/Platform.java b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/Platform.java new file mode 100755 index 0000000..13ec2c8 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/Platform.java @@ -0,0 +1,133 @@ +package com.genersoft.iot.vmp.gb28181.bean; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +/** + * @author lin + */ +@Data +@Schema(description = "平台信息") +public class Platform { + + @Schema(description = "ID(数据库中)") + private Integer id; + + @Schema(description = "是否启用") + private boolean enable; + + @Schema(description = "名称") + private String name; + + @Schema(description = "SIP服务国标编码") + private String serverGBId; + + @Schema(description = "SIP服务国标域") + private String serverGBDomain; + + @Schema(description = "SIP服务IP") + private String serverIp; + + @Schema(description = "SIP服务端口") + private int serverPort; + + @Schema(description = "设备国标编号") + private String deviceGBId; + + @Schema(description = "设备ip") + private String deviceIp; + + @Schema(description = "设备端口") + private int devicePort; + + @Schema(description = "SIP认证用户名(默认使用设备国标编号)") + private String username; + + @Schema(description = "SIP认证密码") + private String password; + + @Schema(description = "注册周期 (秒)") + private int expires; + + @Schema(description = "心跳周期(秒)") + private int keepTimeout; + + @Schema(description = "传输协议") + private String transport; + + @Schema(description = "字符集") + private String characterSet; + + @Schema(description = "允许云台控制") + private boolean ptz; + + @Schema(description = "RTCP流保活") + private boolean rtcp; + + @Schema(description = "在线状态") + private boolean status; + + @Schema(description = "通道数量") + private int channelCount; + + @Schema(description = "已被订阅目录信息") + private boolean catalogSubscribe; + + @Schema(description = "已被订阅报警信息") + private boolean alarmSubscribe; + + @Schema(description = "已被订阅移动位置信息") + private boolean mobilePositionSubscribe; + + @Schema(description = "目录分组-每次向上级发送通道信息时单个包携带的通道数量,取值1,2,4,8") + private int catalogGroup; + + @Schema(description = "更新时间") + private String updateTime; + + @Schema(description = "创建时间") + private String createTime; + + @Schema(description = "是否作为消息通道") + private boolean asMessageChannel; + + @Schema(description = "点播回复200OK使用的IP") + private String sendStreamIp; + + @Schema(description = "是否自动推送通道变化") + private Boolean autoPushChannel; + + @Schema(description = "目录信息包含平台信息, 0:关闭,1:打开") + private int catalogWithPlatform; + + @Schema(description = "目录信息包含分组信息, 0:关闭,1:打开") + private int catalogWithGroup; + + @Schema(description = "目录信息包含行政区划, 0:关闭,1:打开") + private int catalogWithRegion; + + @Schema(description = "行政区划") + private String civilCode; + + @Schema(description = "平台厂商") + private String manufacturer; + + @Schema(description = "平台型号") + private String model; + + @Schema(description = "平台安装地址") + private String address; + + @Schema(description = "注册方式(必选)缺省为1; " + + "1-符合IETF RFC 3261标准的认证注册模式;" + + "2-基于口令的双向认证注册模式;" + + "3-基于数字证书的双向认证注册模式(高安全级别要求);" + + "4-基于数字证书的单向认证注册模式(高安全级别要求)") + private int registerWay = 1; + + @Schema(description = "保密属性(必选)缺省为0;0-不涉密,1-涉密") + private int secrecy = 0; + + @Schema(description = "执行注册的服务ID") + private String serverId; +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/bean/PlatformCatalog.java b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/PlatformCatalog.java new file mode 100755 index 0000000..38ba2f0 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/PlatformCatalog.java @@ -0,0 +1,116 @@ +package com.genersoft.iot.vmp.gb28181.bean; + +import io.swagger.v3.oas.annotations.media.Schema; + +/** + * 国标级联-目录 + * @author lin + */ +@Schema(description = "目录信息") +public class PlatformCatalog { + @Schema(description = "ID") + private String id; + + @Schema(description = "名称") + private String name; + + @Schema(description = "平台ID") + private String platformId; + + @Schema(description = "父级目录ID") + private String parentId; + + @Schema(description = "行政区划") + private String civilCode; + + @Schema(description = "目录分组") + private String businessGroupId; + + /** + * 子节点数 + */ + @Schema(description = "子节点数") + private int childrenCount; + + /** + * 0 目录, 1 国标通道, 2 直播流 + */ + @Schema(description = "类型:0 目录, 1 国标通道, 2 直播流") + private int type; + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getPlatformId() { + return platformId; + } + + public void setPlatformId(String platformId) { + this.platformId = platformId; + } + + public String getParentId() { + return parentId; + } + + public void setParentId(String parentId) { + this.parentId = parentId; + } + + public int getChildrenCount() { + return childrenCount; + } + + public void setChildrenCount(int childrenCount) { + this.childrenCount = childrenCount; + } + + public int getType() { + return type; + } + + public void setType(int type) { + this.type = type; + } + + public void setTypeForCatalog() { + this.type = 0; + } + + public void setTypeForGb() { + this.type = 1; + } + + public void setTypeForStream() { + this.type = 2; + } + + public String getCivilCode() { + return civilCode; + } + + public void setCivilCode(String civilCode) { + this.civilCode = civilCode; + } + + public String getBusinessGroupId() { + return businessGroupId; + } + + public void setBusinessGroupId(String businessGroupId) { + this.businessGroupId = businessGroupId; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/bean/PlatformCatch.java b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/PlatformCatch.java new file mode 100755 index 0000000..db7ab98 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/PlatformCatch.java @@ -0,0 +1,24 @@ +package com.genersoft.iot.vmp.gb28181.bean; + +import lombok.Data; + +@Data +public class PlatformCatch { + + private String id; + + /** + * 心跳未回复次数 + */ + private int keepAliveReply; + + // 注册未回复次数 + private int registerAliveReply; + + private String callId; + + private Platform platform; + + private SipTransactionInfo sipTransactionInfo; + +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/bean/PlatformChannel.java b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/PlatformChannel.java new file mode 100644 index 0000000..a9517b3 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/PlatformChannel.java @@ -0,0 +1,208 @@ +package com.genersoft.iot.vmp.gb28181.bean; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; + +@EqualsAndHashCode(callSuper = true) +@Data +public class PlatformChannel extends CommonGBChannel { + + @Schema(description = "Id") + private int id; + + @Schema(description = "平台ID") + private int platformId; + + @Schema(description = "国标-编码") + private String customDeviceId; + + @Schema(description = "国标-名称") + private String customName; + + @Schema(description = "国标-设备厂商") + private String customManufacturer; + + @Schema(description = "国标-设备型号") + private String customModel; + + // 2016 + @Schema(description = "国标-设备归属") + private String customOwner; + + @Schema(description = "国标-行政区域") + private String customCivilCode; + + @Schema(description = "国标-警区") + private String customBlock; + + @Schema(description = "国标-安装地址") + private String customAddress; + + @Schema(description = "国标-是否有子设备") + private Integer customParental; + + @Schema(description = "国标-父节点ID") + private String customParentId; + + // 2016 + @Schema(description = "国标-信令安全模式") + private Integer customSafetyWay; + + @Schema(description = "国标-注册方式") + private Integer customRegisterWay; + + // 2016 + @Schema(description = "国标-证书序列号") + private Integer customCertNum; + + // 2016 + @Schema(description = "国标-证书有效标识") + private Integer customCertifiable; + + // 2016 + @Schema(description = "国标-无效原因码(有证书且证书无效的设备必选)") + private Integer customErrCode; + + // 2016 + @Schema(description = "国标-证书终止有效期(有证书且证书无效的设备必选)") + private Integer customEndTime; + + // 2022 + @Schema(description = "国标-摄像机安全能力等级代码") + private String customSecurityLevelCode; + + @Schema(description = "国标-保密属性(必选)缺省为0;0-不涉密,1-涉密") + private Integer customSecrecy; + + @Schema(description = "国标-设备/系统IPv4/IPv6地址") + private String customIpAddress; + + @Schema(description = "国标-设备/系统端口") + private Integer customPort; + + @Schema(description = "国标-设备口令") + private String customPassword; + + @Schema(description = "国标-设备状态") + private String customStatus; + + @Schema(description = "国标-经度 WGS-84坐标系") + private Double customLongitude; + + @Schema(description = "国标-纬度 WGS-84坐标系") + private Double customLatitude; + + @Schema(description = "国标-虚拟组织所属的业务分组ID") + private String customBusinessGroupId; + + @Schema(description = "国标-摄像机结构类型,标识摄像机类型: 1-球机; 2-半球; 3-固定枪机; 4-遥控枪机;5-遥控半球;6-多目设备的全景/拼接通道;7-多目设备的分割通道") + private Integer customPtzType; + + // 2016 + @Schema(description = "-摄像机位置类型扩展。1-省际检查站、2-党政机关、3-车站码头、4-中心广场、5-体育场馆、6-商业中心、7-宗教场所、" + + "8-校园周边、9-治安复杂区域、10-交通干线。当目录项为摄像机时可选。") + private Integer customPositionType; + + @Schema(description = "国标-摄像机光电成像类型。1-可见光成像;2-热成像;3-雷达成像;4-X光成像;5-深度光场成像;9-其他。可多值,") + private String customPhotoelectricImagingTyp; + + @Schema(description = "国标-摄像机采集部位类型") + private String customCapturePositionType; + + @Schema(description = "国标-摄像机安装位置室外、室内属性。1-室外、2-室内。") + private Integer customRoomType; + + // 2016 + @Schema(description = "国标-用途属性") + private Integer customUseType; + + @Schema(description = "国标-摄像机补光属性。1-无补光;2-红外补光;3-白光补光;4-激光补光;9-其他") + private Integer customSupplyLightType; + + @Schema(description = "国标-摄像机监视方位(光轴方向)属性。1-东(西向东)、2-西(东向西)、3-南(北向南)、4-北(南向北)、" + + "5-东南(西北到东南)、6-东北(西南到东北)、7-西南(东北到西南)、8-西北(东南到西北)") + private Integer customDirectionType; + + @Schema(description = "国标-摄像机支持的分辨率,可多值") + private String customResolution; + + // 2022 + @Schema(description = "国标-摄像机支持的码流编号列表,用于实时点播时指定码流编号(可选)") + private String customStreamNumberList; + + @Schema(description = "国标-下载倍速(可选),可多值") + private String customDownloadSpeed; + + @Schema(description = "国标-空域编码能力,取值0-不支持;1-1级增强(1个增强层);2-2级增强(2个增强层);3-3级增强(3个增强层)") + private Integer customSvcSpaceSupportMod; + + @Schema(description = "国标-时域编码能力,取值0-不支持;1-1级增强;2-2级增强;3-3级增强(可选)") + private Integer customSvcTimeSupportMode; + + // 2022 + @Schema(description = "国标- SSVC增强层与基本层比例能力 ") + private String customSsvcRatioSupportList; + + // 2022 + @Schema(description = "国标-移动采集设备类型(仅移动采集设备适用,必选);1-移动机器人载摄像机;2-执法记录仪;3-移动单兵设备;" + + "4-车载视频记录设备;5-无人机载摄像机;9-其他") + private Integer customMobileDeviceType; + + // 2022 + @Schema(description = "国标-摄像机水平视场角(可选),取值范围大于0度小于等于360度") + private Double customHorizontalFieldAngle; + + // 2022 + @Schema(description = "国标-摄像机竖直视场角(可选),取值范围大于0度小于等于360度 ") + private Double customVerticalFieldAngle; + + // 2022 + @Schema(description = "国标-摄像机可视距离(可选),单位:米") + private Double customMaxViewDistance; + + // 2022 + @Schema(description = "国标-基层组织编码(必选,非基层建设时为“000000”)") + private String customGrassrootsCode; + + // 2022 + @Schema(description = "国标-监控点位类型(当为摄像机时必选),1-一类视频监控点;2-二类视频监控点;3-三类视频监控点;9-其他点位。") + private Integer customPoType; + + // 2022 + @Schema(description = "国标-点位俗称") + private String customPoCommonName; + + // 2022 + @Schema(description = "国标-设备MAC地址(可选),用“XX-XX-XX-XX-XX-XX”格式表达") + private String customMac; + + // 2022 + @Schema(description = "国标-摄像机卡口功能类型,01-人脸卡口;02-人员卡口;03-机动车卡口;04-非机动车卡口;05-物品卡口;99-其他") + private String customFunctionType; + + // 2022 + @Schema(description = "国标-摄像机视频编码格式") + private String customEncodeType; + + // 2022 + @Schema(description = "国标-摄像机安装使用时间") + private String customInstallTime; + + // 2022 + @Schema(description = "国标-摄像机所属管理单位名称") + private String customManagementUnit; + + // 2022 + @Schema(description = "国标-摄像机所属管理单位联系人的联系方式(电话号码,可多值,用英文半角“/”分割)") + private String customContactInfo; + + // 2022 + @Schema(description = "国标-录像保存天数(可选)") + private Integer customRecordSaveDays; + + // 2022 + @Schema(description = "国标-国民经济行业分类代码(可选)") + private String customIndustrialClassification; + +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/bean/PlatformGbStream.java b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/PlatformGbStream.java new file mode 100755 index 0000000..f7402b6 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/PlatformGbStream.java @@ -0,0 +1,39 @@ +package com.genersoft.iot.vmp.gb28181.bean; + +import io.swagger.v3.oas.annotations.media.Schema; + +public class PlatformGbStream { + + @Schema(description = "ID") + private int gbStreamId; + + @Schema(description = "平台ID") + private String platformId; + + @Schema(description = "目录ID") + private String catalogId; + + public Integer getGbStreamId() { + return gbStreamId; + } + + public void setGbStreamId(Integer gbStreamId) { + this.gbStreamId = gbStreamId; + } + + public String getPlatformId() { + return platformId; + } + + public void setPlatformId(String platformId) { + this.platformId = platformId; + } + + public String getCatalogId() { + return catalogId; + } + + public void setCatalogId(String catalogId) { + this.catalogId = catalogId; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/bean/PlatformKeepaliveCallback.java b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/PlatformKeepaliveCallback.java new file mode 100644 index 0000000..2e98fa8 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/PlatformKeepaliveCallback.java @@ -0,0 +1,5 @@ +package com.genersoft.iot.vmp.gb28181.bean; + +public interface PlatformKeepaliveCallback { + public void run(String platformServerGbId, int failCount); +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/bean/PlatformRegister.java b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/PlatformRegister.java new file mode 100755 index 0000000..4a45862 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/PlatformRegister.java @@ -0,0 +1,15 @@ +package com.genersoft.iot.vmp.gb28181.bean; + +public class PlatformRegister { + + // 未回复次数 + private int reply; + + public int getReply() { + return reply; + } + + public void setReply(int reply) { + this.reply = reply; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/bean/PlayException.java b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/PlayException.java new file mode 100644 index 0000000..8934394 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/PlayException.java @@ -0,0 +1,14 @@ +package com.genersoft.iot.vmp.gb28181.bean; + +import lombok.Data; + +@Data +public class PlayException extends RuntimeException{ + private int code; + private String msg; + + public PlayException(int code, String msg) { + this.code = code; + this.msg = msg; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/bean/Preset.java b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/Preset.java new file mode 100755 index 0000000..0bb8eec --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/Preset.java @@ -0,0 +1,12 @@ +package com.genersoft.iot.vmp.gb28181.bean; + + +import lombok.Data; + +@Data +public class Preset { + + private String presetId; + + private String presetName; +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/bean/RecordInfo.java b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/RecordInfo.java new file mode 100755 index 0000000..130b768 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/RecordInfo.java @@ -0,0 +1,43 @@ +package com.genersoft.iot.vmp.gb28181.bean; + + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; +import lombok.Setter; + +import java.time.Instant; +import java.util.List; + +/** + * @description:设备录像信息bean + * @author: swwheihei + * @date: 2020年5月8日 下午2:05:56 + */ +@Setter +@Getter +@Schema(description = "设备录像查询结果信息") +public class RecordInfo { + + @Schema(description = "设备编号") + private String deviceId; + + @Schema(description = "通道编号") + private String channelId; + + @Schema(description = "命令序列号") + private String sn; + + @Schema(description = "设备名称") + private String name; + + @Schema(description = "列表总数") + private int sumNum; + + private int count; + + private Instant lastTime; + + @Schema(description = "") + private List recordList; + +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/bean/RecordItem.java b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/RecordItem.java new file mode 100755 index 0000000..bc7630b --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/RecordItem.java @@ -0,0 +1,68 @@ +package com.genersoft.iot.vmp.gb28181.bean; + + +import com.genersoft.iot.vmp.utils.DateUtil; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; +import lombok.Setter; +import org.jetbrains.annotations.NotNull; + +import java.time.Instant; +import java.time.temporal.TemporalAccessor; + +/** + * @description:设备录像bean + * @author: swwheihei + * @date: 2020年5月8日 下午2:06:54 + */ +@Setter +@Getter +@Schema(description = "设备录像详情") +public class RecordItem implements Comparable{ + + @Schema(description = "设备编号") + private String deviceId; + + @Schema(description = "名称") + private String name; + + @Schema(description = "文件路径名 (可选)") + private String filePath; + + @Schema(description = "录像文件大小,单位:Byte(可选)") + private String fileSize; + + @Schema(description = "录像地址(可选)") + private String address; + + @Schema(description = "录像开始时间(可选)") + private String startTime; + + @Schema(description = "录像结束时间(可选)") + private String endTime; + + @Schema(description = "保密属性(必选)缺省为0;0:不涉密,1:涉密") + private int secrecy; + + @Schema(description = "录像产生类型(可选)time或alarm 或 manual") + private String type; + + @Schema(description = "录像触发者ID(可选)") + private String recorderId; + + @Override + public int compareTo(@NotNull RecordItem recordItem) { + TemporalAccessor startTimeNow = DateUtil.formatter.parse(startTime); + TemporalAccessor startTimeParam = DateUtil.formatter.parse(recordItem.getStartTime()); + Instant startTimeParamInstant = Instant.from(startTimeParam); + Instant startTimeNowInstant = Instant.from(startTimeNow); + if (startTimeNowInstant.equals(startTimeParamInstant)) { + return 0; + }else if (Instant.from(startTimeParam).isAfter(Instant.from(startTimeNow)) ) { + return -1; + }else { + return 1; + } + + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/bean/RedisGroupMessage.java b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/RedisGroupMessage.java new file mode 100644 index 0000000..2691dc1 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/RedisGroupMessage.java @@ -0,0 +1,67 @@ +package com.genersoft.iot.vmp.gb28181.bean; + +import lombok.Data; + +@Data +public class RedisGroupMessage { + + /** + * 分组国标ID + */ + private String groupGbId; + + /** + * 分组别名 + */ + private String groupAlias; + + /** + * 分组名称 + */ + private String groupName; + + /** + * 分组所属的行政区划 + */ + private String groupCivilCode; + + /** + * 分组所属父分组国标ID + */ + private String parentGroupGbId; + + /** + * 分组所属父分组别名 + */ + private String parentGAlias; + + /** + * 分组所属业务分组国标ID + */ + private String topGroupGbId; + + /** + * 分组所属业务分组别名 + */ + private String topGroupGAlias; + + /** + * 分组变化消息中的消息类型,取值为 add update delete + */ + private String messageType; + + + @Override + public String toString() { + return "RedisGroupMessage{" + + "groupGbId='" + groupGbId + '\'' + + ", groupAlias='" + groupAlias + '\'' + + ", groupName='" + groupName + '\'' + + ", groupCivilCode='" + groupCivilCode + '\'' + + ", parentGroupGbId='" + parentGroupGbId + '\'' + + ", parentGAlias='" + parentGAlias + '\'' + + ", topGroupGbId='" + topGroupGbId + '\'' + + ", topGroupGAlias='" + topGroupGAlias + '\'' + + '}'; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/bean/Region.java b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/Region.java new file mode 100644 index 0000000..bb4e1fd --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/Region.java @@ -0,0 +1,122 @@ +package com.genersoft.iot.vmp.gb28181.bean; + +import com.genersoft.iot.vmp.common.CivilCodePo; +import com.genersoft.iot.vmp.utils.CivilCodeUtil; +import com.genersoft.iot.vmp.utils.DateUtil; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import org.jetbrains.annotations.NotNull; + +/** + * 区域 + */ +@Data +@Schema(description = "区域") +public class Region implements Comparable{ + /** + * 数据库自增ID + */ + @Schema(description = "数据库自增ID") + private int id; + + /** + * 区域国标编号 + */ + @Schema(description = "区域国标编号") + private String deviceId; + + /** + * 区域名称 + */ + @Schema(description = "区域名称") + private String name; + + /** + * 父区域国标ID + */ + @Schema(description = "父区域ID") + private Integer parentId; + + /** + * 父区域国标ID + */ + @Schema(description = "父区域国标ID") + private String parentDeviceId; + + /** + * 创建时间 + */ + @Schema(description = "创建时间") + private String createTime; + + /** + * 更新时间 + */ + @Schema(description = "更新时间") + private String updateTime; + + public static Region getInstance(String commonRegionDeviceId, String commonRegionName, String commonRegionParentId) { + Region region = new Region(); + region.setDeviceId(commonRegionDeviceId); + region.setName(commonRegionName); + region.setParentDeviceId(commonRegionParentId); + region.setCreateTime(DateUtil.getNow()); + region.setUpdateTime(DateUtil.getNow()); + return region; + } + + public static Region getInstance(CivilCodePo civilCodePo) { + Region region = new Region(); + region.setName(civilCodePo.getName()); + region.setDeviceId(civilCodePo.getCode()); + if (civilCodePo.getCode().length() > 2) { + region.setParentDeviceId(civilCodePo.getParentCode()); + } + region.setCreateTime(DateUtil.getNow()); + region.setUpdateTime(DateUtil.getNow()); + return region; + } + + public static Region getInstance(DeviceChannel channel) { + Region region = new Region(); + region.setName(channel.getName()); + region.setDeviceId(channel.getDeviceId()); + CivilCodePo parentCode = CivilCodeUtil.INSTANCE.getParentCode(channel.getDeviceId()); + if (parentCode != null) { + region.setParentDeviceId(parentCode.getCode()); + } + region.setCreateTime(DateUtil.getNow()); + region.setUpdateTime(DateUtil.getNow()); + return region; + } + + @Override + public int compareTo(@NotNull Region region) { + return Integer.compare(Integer.parseInt(this.deviceId), Integer.parseInt(region.getDeviceId())); + } + + @Override + public boolean equals(Object obj) { + if (obj == null) + return false; + if (this == obj) + return true; + if (obj instanceof Region) { + Region region = (Region) obj; + + // 比较每个属性的值一致时才返回true + if (region.getId() == this.id) { + return true; + } + } + return false; + } + + /** + * 重写hashcode方法,返回的hashCode一样才再去比较每个属性的值 + */ + @Override + public int hashCode() { + return id; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/bean/RegionTree.java b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/RegionTree.java new file mode 100644 index 0000000..10ef1ae --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/RegionTree.java @@ -0,0 +1,26 @@ +package com.genersoft.iot.vmp.gb28181.bean; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 区域 + */ +@EqualsAndHashCode(callSuper = true) +@Data +@Schema(description = "区域树") +public class RegionTree extends Region { + + @Schema(description = "树节点ID") + private String treeId; + + @Schema(description = "是否有子节点") + private boolean isLeaf; + + @Schema(description = "类型, 行政区划:0 摄像头: 1") + private int type; + + @Schema(description = "在线状态") + private String status; +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/bean/SDPInfo.java b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/SDPInfo.java new file mode 100755 index 0000000..39225b5 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/SDPInfo.java @@ -0,0 +1,14 @@ +package com.genersoft.iot.vmp.gb28181.bean; + +import javax.sdp.SessionDescription; + +public class SDPInfo { + private byte[] source; + private SessionDescription sdpSource; + private String sessionName; + private Long startTime; + private Long stopTime; + private String username; + private String address; + private String ssrc; +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/bean/SendRtpInfo.java b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/SendRtpInfo.java new file mode 100755 index 0000000..0cfc21c --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/SendRtpInfo.java @@ -0,0 +1,249 @@ +package com.genersoft.iot.vmp.gb28181.bean; + +import com.genersoft.iot.vmp.common.enums.ChannelDataType; +import com.genersoft.iot.vmp.media.bean.MediaServer; +import com.genersoft.iot.vmp.service.bean.RequestPushStreamMsg; +import lombok.Data; + +@Data +public class SendRtpInfo { + + /** + * 推流ip + */ + private String ip; + + /** + * 推流端口 + */ + private int port; + + /** + * 推流标识 + */ + private String ssrc; + + /** + * 目标平台或设备的编号 + */ + private String targetId; + + /** + * 目标平台或设备的名称 + */ + private String targetName; + + /** + * 是否是发送给上级平台 + */ + private boolean sendToPlatform; + + /** + * 直播流的应用名 + */ + private String app; + + /** + * 通道id + */ + private Integer channelId; + + /** + * 推流状态 + * 0 等待设备推流上来 + * 1 等待上级平台回复ack + * 2 推流中 + */ + private int status = 0; + + + /** + * 设备推流的streamId + */ + private String stream; + + /** + * 是否为tcp + */ + private boolean tcp; + + /** + * 是否为tcp主动模式 + */ + private boolean tcpActive; + + /** + * 自己推流使用的IP + */ + private String localIp; + + /** + * 自己推流使用的端口 + */ + private int localPort; + + /** + * 使用的流媒体 + */ + private String mediaServerId; + + /** + * 使用的服务的ID + */ + private String serverId; + + /** + * invite 的 callId + */ + private String callId; + + /** + * invite 的 fromTag + */ + private String fromTag; + + /** + * invite 的 toTag + */ + private String toTag; + + /** + * 发送时,rtp的pt(uint8_t),不传时默认为96 + */ + private int pt = 96; + + /** + * 发送时,rtp的负载类型。为true时,负载为ps;为false时,为es; + */ + private boolean usePs = true; + + /** + * 当usePs 为false时,有效。为1时,发送音频;为0时,发送视频;不传时默认为0 + */ + private boolean onlyAudio = false; + + /** + * 是否开启rtcp保活 + */ + private boolean rtcp = false; + + + /** + * 播放类型 + */ + private InviteStreamType playType; + + /** + * 发流的同时收流 + */ + private String receiveStream; + + /** + * 上级的点播类型 + */ + private String sessionName; + + public static SendRtpInfo getInstance(RequestPushStreamMsg requestPushStreamMsg) { + SendRtpInfo sendRtpItem = new SendRtpInfo(); + sendRtpItem.setMediaServerId(requestPushStreamMsg.getMediaServerId()); + sendRtpItem.setApp(requestPushStreamMsg.getApp()); + sendRtpItem.setStream(requestPushStreamMsg.getStream()); + sendRtpItem.setIp(requestPushStreamMsg.getIp()); + sendRtpItem.setPort(requestPushStreamMsg.getPort()); + sendRtpItem.setSsrc(requestPushStreamMsg.getSsrc()); + sendRtpItem.setTcp(requestPushStreamMsg.isTcp()); + sendRtpItem.setLocalPort(requestPushStreamMsg.getSrcPort()); + sendRtpItem.setPt(requestPushStreamMsg.getPt()); + sendRtpItem.setUsePs(requestPushStreamMsg.isPs()); + sendRtpItem.setOnlyAudio(requestPushStreamMsg.isOnlyAudio()); + return sendRtpItem; + + } + + public static SendRtpInfo getInstance(String app, String stream, String ssrc, String dstIp, Integer dstPort, boolean tcp, int sendLocalPort, Integer pt) { + SendRtpInfo sendRtpItem = new SendRtpInfo(); + sendRtpItem.setApp(app); + sendRtpItem.setStream(stream); + sendRtpItem.setSsrc(ssrc); + sendRtpItem.setTcp(tcp); + sendRtpItem.setLocalPort(sendLocalPort); + sendRtpItem.setIp(dstIp); + sendRtpItem.setPort(dstPort); + if (pt != null) { + sendRtpItem.setPt(pt); + } + + return sendRtpItem; + } + + public static SendRtpInfo getInstance(Integer localPort, MediaServer mediaServer, String ip, Integer port, String ssrc, + String deviceId, String platformId, Integer channelId, Boolean isTcp, Boolean rtcp, + String serverId) { + if (localPort == 0) { + return null; + } + SendRtpInfo sendRtpItem = new SendRtpInfo(); + sendRtpItem.setIp(ip); + if(port != null) { + sendRtpItem.setPort(port); + } + + sendRtpItem.setSsrc(ssrc); + if (deviceId != null) { + sendRtpItem.setTargetId(deviceId); + sendRtpItem.setSendToPlatform(false); + }else { + sendRtpItem.setTargetId(platformId); + sendRtpItem.setSendToPlatform(true); + } + sendRtpItem.setChannelId(channelId); + sendRtpItem.setTcp(isTcp); + sendRtpItem.setRtcp(rtcp); + sendRtpItem.setApp("rtp"); + sendRtpItem.setLocalPort(localPort); + sendRtpItem.setServerId(serverId); + sendRtpItem.setMediaServerId(mediaServer.getId()); + return sendRtpItem; + } + + @Override + public String toString() { + return "SendRtpItem{" + + "ip='" + ip + '\'' + + ", port=" + port + + ", ssrc='" + ssrc + '\'' + + ", targetId='" + targetId + '\'' + + ", app='" + app + '\'' + + ", channelId='" + channelId + '\'' + + ", status=" + status + + ", stream='" + stream + '\'' + + ", tcp=" + tcp + + ", tcpActive=" + tcpActive + + ", localIp='" + localIp + '\'' + + ", localPort=" + localPort + + ", mediaServerId='" + mediaServerId + '\'' + + ", serverId='" + serverId + '\'' + + ", CallId='" + callId + '\'' + + ", fromTag='" + fromTag + '\'' + + ", toTag='" + toTag + '\'' + + ", pt=" + pt + + ", usePs=" + usePs + + ", onlyAudio=" + onlyAudio + + ", rtcp=" + rtcp + + ", playType=" + playType + + ", receiveStream='" + receiveStream + '\'' + + ", sessionName='" + sessionName + '\'' + + '}'; + } + + + public void setPlayTypeByChannelDataType(Integer dataType, String sessionName) { + if (dataType == ChannelDataType.STREAM_PUSH) { + this.setPlayType(InviteStreamType.PUSH); + }else if (dataType == ChannelDataType.STREAM_PROXY){ + this.setPlayType(InviteStreamType.PROXY); + }else { + this.setPlayType("Play".equalsIgnoreCase(sessionName) ? InviteStreamType.PLAY : InviteStreamType.PLAYBACK); + } + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/bean/SipMsgInfo.java b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/SipMsgInfo.java new file mode 100755 index 0000000..06d43c8 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/SipMsgInfo.java @@ -0,0 +1,56 @@ +package com.genersoft.iot.vmp.gb28181.bean; + +import org.dom4j.Element; + +import javax.sip.RequestEvent; + +public class SipMsgInfo { + private RequestEvent evt; + private Device device; + private Platform platform; + private Element rootElement; + + public SipMsgInfo(RequestEvent evt, Device device, Element rootElement) { + this.evt = evt; + this.device = device; + this.rootElement = rootElement; + } + + public SipMsgInfo(RequestEvent evt, Platform platform, Element rootElement) { + this.evt = evt; + this.platform = platform; + this.rootElement = rootElement; + } + + public RequestEvent getEvt() { + return evt; + } + + public void setEvt(RequestEvent evt) { + this.evt = evt; + } + + public Device getDevice() { + return device; + } + + public void setDevice(Device device) { + this.device = device; + } + + public Platform getPlatform() { + return platform; + } + + public void setPlatform(Platform platform) { + this.platform = platform; + } + + public Element getRootElement() { + return rootElement; + } + + public void setRootElement(Element rootElement) { + this.rootElement = rootElement; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/bean/SipSendFailEvent.java b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/SipSendFailEvent.java new file mode 100644 index 0000000..2e2c54e --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/SipSendFailEvent.java @@ -0,0 +1,19 @@ +package com.genersoft.iot.vmp.gb28181.bean; + +import com.genersoft.iot.vmp.gb28181.event.SipSubscribe; +import lombok.Data; + +@Data +public class SipSendFailEvent extends SipSubscribe.EventResult { + + private String callId; + + private String msg; + + public static SipSendFailEvent getInstance(String callId, String msg){ + SipSendFailEvent sipSendFailEvent = new SipSendFailEvent(); + sipSendFailEvent.setMsg(msg); + sipSendFailEvent.setCallId(callId); + return sipSendFailEvent; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/bean/SipTransactionInfo.java b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/SipTransactionInfo.java new file mode 100755 index 0000000..3e2c40b --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/SipTransactionInfo.java @@ -0,0 +1,49 @@ +package com.genersoft.iot.vmp.gb28181.bean; + +import gov.nist.javax.sip.message.SIPResponse; +import lombok.Data; + +import javax.sip.header.EventHeader; + +@Data +public class SipTransactionInfo { + + private String callId; + private String fromTag; + private String toTag; + private String viaBranch; + private int expires; + private String user; + private String eventId; + + // 自己是否媒体流发送者 + private boolean asSender; + + public SipTransactionInfo(SIPResponse response, boolean asSender) { + this.callId = response.getCallIdHeader().getCallId(); + this.fromTag = response.getFromTag(); + this.toTag = response.getToTag(); + this.viaBranch = response.getTopmostViaHeader().getBranch(); + this.asSender = asSender; + EventHeader header = (EventHeader)response.getHeader(EventHeader.NAME); + if (header != null) { + this.eventId = header.getEventId(); + } + } + + public SipTransactionInfo(SIPResponse response) { + this.callId = response.getCallIdHeader().getCallId(); + this.fromTag = response.getFromTag(); + this.toTag = response.getToTag(); + this.viaBranch = response.getTopmostViaHeader().getBranch(); + this.asSender = false; + EventHeader header = (EventHeader)response.getHeader(EventHeader.NAME); + if (header != null) { + this.eventId = header.getEventId(); + } + } + + public SipTransactionInfo() { + } + +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/bean/SsrcTransaction.java b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/SsrcTransaction.java new file mode 100755 index 0000000..2a051a7 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/SsrcTransaction.java @@ -0,0 +1,91 @@ +package com.genersoft.iot.vmp.gb28181.bean; + +import com.genersoft.iot.vmp.common.InviteSessionType; +import gov.nist.javax.sip.message.SIPResponse; +import lombok.Data; + +@Data +public class SsrcTransaction { + + /** + * 设备编号 + */ + private String deviceId; + + /** + * 上级平台的编号 + */ + private String platformId; + + /** + * 通道的数据库ID + */ + private Integer channelId; + + /** + * 会话的CALL ID + */ + private String callId; + + /** + * 关联的流应用名 + */ + private String app; + + /** + * 关联的流ID + */ + private String stream; + + /** + * 使用的流媒体 + */ + private String mediaServerId; + + /** + * 使用的SSRC + */ + private String ssrc; + + /** + * 事务信息 + */ + private SipTransactionInfo sipTransactionInfo; + + /** + * 类型 + */ + private InviteSessionType type; + + public static SsrcTransaction buildForDevice(String deviceId, Integer channelId, String callId, String app, String stream, + String ssrc, String mediaServerId, SIPResponse response, InviteSessionType type) { + SsrcTransaction ssrcTransaction = new SsrcTransaction(); + ssrcTransaction.setDeviceId(deviceId); + ssrcTransaction.setChannelId(channelId); + ssrcTransaction.setCallId(callId); + ssrcTransaction.setApp(app); + ssrcTransaction.setStream(stream); + ssrcTransaction.setMediaServerId(mediaServerId); + ssrcTransaction.setSsrc(ssrc); + ssrcTransaction.setSipTransactionInfo(new SipTransactionInfo(response)); + ssrcTransaction.setType(type); + return ssrcTransaction; + } + public static SsrcTransaction buildForPlatform(String platformId, Integer channelId, String callId, String app,String stream, + String ssrc, String mediaServerId, SIPResponse response, InviteSessionType type) { + SsrcTransaction ssrcTransaction = new SsrcTransaction(); + ssrcTransaction.setPlatformId(platformId); + ssrcTransaction.setChannelId(channelId); + ssrcTransaction.setCallId(callId); + ssrcTransaction.setStream(stream); + ssrcTransaction.setApp(app); + ssrcTransaction.setMediaServerId(mediaServerId); + ssrcTransaction.setSsrc(ssrc); + ssrcTransaction.setSipTransactionInfo(new SipTransactionInfo(response)); + ssrcTransaction.setType(type); + return ssrcTransaction; + } + + public SsrcTransaction() { + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/bean/SubscribeHolder.java b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/SubscribeHolder.java new file mode 100755 index 0000000..17a5284 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/SubscribeHolder.java @@ -0,0 +1,120 @@ +package com.genersoft.iot.vmp.gb28181.bean; + +import com.genersoft.iot.vmp.conf.DynamicTask; +import com.genersoft.iot.vmp.conf.UserSetting; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Component; + +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; + +/** + * @author lin + */ +@Slf4j +@Component +public class SubscribeHolder { + + @Autowired + private DynamicTask dynamicTask; + + @Autowired + private UserSetting userSetting; + + @Autowired + private RedisTemplate redisTemplate; + + private final String prefix = "VMP_SUBSCRIBE_OVERDUE"; + + public void putCatalogSubscribe(String platformId, SubscribeInfo subscribeInfo) { + log.info("[国标级联] 添加目录订阅,平台: {}, 有效期: {}", platformId, subscribeInfo.getExpires()); + + String key = String.format("%s_%s_%s_%s", prefix, userSetting.getServerId(), "catalog", platformId); + if (subscribeInfo.getExpires() > 0) { + Duration duration = Duration.ofSeconds(subscribeInfo.getExpires()); + redisTemplate.opsForValue().set(key, subscribeInfo, duration); + }else { + redisTemplate.opsForValue().set(key, subscribeInfo); + } + } + + public SubscribeInfo getCatalogSubscribe(String platformId) { + String key = String.format("%s_%s_%s_%s", prefix, userSetting.getServerId(), "catalog", platformId); + return (SubscribeInfo)redisTemplate.opsForValue().get(key); + } + + public void removeCatalogSubscribe(String platformId) { + String key = String.format("%s_%s_%s_%s", prefix, userSetting.getServerId(), "catalog", platformId); + redisTemplate.delete(key); + } + + public void putMobilePositionSubscribe(String platformId, SubscribeInfo subscribeInfo, Runnable gpsTask) { + log.info("[国标级联] 添加移动位置订阅,平台: {}, 有效期: {}s", platformId, subscribeInfo.getExpires()); + String key = String.format("%s_%s_%s_%s", prefix, userSetting.getServerId(), "mobilePosition", platformId); + if (subscribeInfo.getExpires() > 0) { + Duration duration = Duration.ofSeconds(subscribeInfo.getExpires()); + redisTemplate.opsForValue().set(key, subscribeInfo, duration); + }else { + redisTemplate.opsForValue().set(key, subscribeInfo); + } + int cycleForCatalog; + if (subscribeInfo.getGpsInterval() <= 0) { + cycleForCatalog = 5; + }else { + cycleForCatalog = subscribeInfo.getGpsInterval(); + } + dynamicTask.startCron( + key, + () -> { + SubscribeInfo subscribe = getMobilePositionSubscribe(platformId); + if (subscribe != null) { + gpsTask.run(); + }else { + dynamicTask.stop(key); + } + }, + cycleForCatalog * 1000); + + } + + public SubscribeInfo getMobilePositionSubscribe(String platformId) { + String key = String.format("%s_%s_%s_%s", prefix, userSetting.getServerId(), "mobilePosition", platformId); + return (SubscribeInfo)redisTemplate.opsForValue().get(key); + } + + public void removeMobilePositionSubscribe(String platformId) { + String key = String.format("%s_%s_%s_%s", prefix, userSetting.getServerId(), "mobilePosition", platformId); + redisTemplate.delete(key); + } + + public List getAllCatalogSubscribePlatform(List platformList) { + if (platformList == null || platformList.isEmpty()) { + return new ArrayList<>(); + } + List result = new ArrayList<>(); + for (Platform platform : platformList) { + String key = String.format("%s_%s_%s_%s", prefix, userSetting.getServerId(), "catalog", platform.getServerGBId()); + if (redisTemplate.hasKey(key)) { + result.add(platform.getServerGBId()); + } + } + return result; + } + + public List getAllMobilePositionSubscribePlatform(List platformList) { + if (platformList == null || platformList.isEmpty()) { + return new ArrayList<>(); + } + List result = new ArrayList<>(); + for (Platform platform : platformList) { + String key = String.format("%s_%s_%s_%s", prefix, userSetting.getServerId(), "mobilePosition", platform.getServerGBId()); + if (redisTemplate.hasKey(key)) { + result.add(platform.getServerGBId()); + } + } + return result; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/bean/SubscribeInfo.java b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/SubscribeInfo.java new file mode 100755 index 0000000..5820cb6 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/SubscribeInfo.java @@ -0,0 +1,64 @@ +package com.genersoft.iot.vmp.gb28181.bean; + +import gov.nist.javax.sip.message.SIPResponse; +import lombok.Data; + +import javax.sip.header.EventHeader; +import java.util.UUID; + +@Data +public class SubscribeInfo { + + private String id; + private int expires; + private String eventId; + private String eventType; + private SipTransactionInfo transactionInfo; + + /** + * 以下为可选字段 + */ + private String sn; + + private int gpsInterval; + + /** + * 模拟的FromTag + */ + private String simulatedFromTag; + + /** + * 模拟的ToTag + */ + private String simulatedToTag; + + /** + * 模拟的CallID + */ + private String simulatedCallId; + + + public static SubscribeInfo getInstance(SIPResponse response, String id, int expires, EventHeader eventHeader){ + SubscribeInfo subscribeInfo = new SubscribeInfo(); + subscribeInfo.id = id; + subscribeInfo.transactionInfo = new SipTransactionInfo(response); + + subscribeInfo.expires = expires; + subscribeInfo.eventId = eventHeader.getEventId(); + subscribeInfo.eventType = eventHeader.getEventType(); + return subscribeInfo; + } + public static SubscribeInfo buildSimulated(String platFormServerId, String platFormServerIp){ + SubscribeInfo subscribeInfo = new SubscribeInfo(); + subscribeInfo.setId(platFormServerId); + subscribeInfo.setExpires(-1); + subscribeInfo.setEventType("Catalog"); + int random = (int) Math.floor(Math.random() * 10000); + subscribeInfo.setEventId(random + ""); + subscribeInfo.setSimulatedCallId(UUID.randomUUID().toString().replace("-", "") + "@" + platFormServerIp); + subscribeInfo.setSimulatedFromTag(UUID.randomUUID().toString().replace("-", "")); + subscribeInfo.setSimulatedToTag(UUID.randomUUID().toString().replace("-", "")); + return subscribeInfo; + } + +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/bean/SyncStatus.java b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/SyncStatus.java new file mode 100755 index 0000000..074a7a1 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/SyncStatus.java @@ -0,0 +1,31 @@ +package com.genersoft.iot.vmp.gb28181.bean; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.Instant; + +/** + * 摄像机同步状态 + * @author lin + */ +@Data +@Schema(description = "摄像机同步状态") +public class SyncStatus { + + @Schema(description = "总数") + private Integer total; + + @Schema(description = "当前更新多少") + private Integer current; + + @Schema(description = "错误描述") + private String errorMsg; + + @Schema(description = "是否同步中") + private Boolean syncIng; + + @Schema(description = "时间") + private Instant time; + +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/bean/VectorTileSource.java b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/VectorTileSource.java new file mode 100644 index 0000000..770a3a5 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/VectorTileSource.java @@ -0,0 +1,48 @@ +package com.genersoft.iot.vmp.gb28181.bean; + +import lombok.Getter; +import lombok.Setter; +import org.jetbrains.annotations.NotNull; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.Delayed; +import java.util.concurrent.TimeUnit; + +@Getter +@Setter +public class VectorTileSource implements Delayed { + + /** + * 抽稀的图层数据 + */ + private Map vectorTileMap = new ConcurrentHashMap<>(); + + /** + * 抽稀的原始数据 + */ + private List channelList = new ArrayList<>(); + + private String id; + + /** + * 创建时间, 大于6小时后删除 + */ + private long time; + + public VectorTileSource() { + this.time = System.currentTimeMillis(); + } + + @Override + public long getDelay(@NotNull TimeUnit unit) { + return unit.convert(time + 6 * 60 * 60 * 1000 - System.currentTimeMillis(), TimeUnit.MILLISECONDS); + } + + @Override + public int compareTo(@NotNull Delayed o) { + return (int) (this.getDelay(TimeUnit.MILLISECONDS) - o.getDelay(TimeUnit.MILLISECONDS)); + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/conf/DefaultProperties.java b/src/main/java/com/genersoft/iot/vmp/gb28181/conf/DefaultProperties.java new file mode 100755 index 0000000..73e271d --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/conf/DefaultProperties.java @@ -0,0 +1,70 @@ +package com.genersoft.iot.vmp.gb28181.conf; + +import com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.notify.cmd.AlarmNotifyMessageHandler; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Properties; + +/** + * 获取sip默认配置 + * @author lin + */ +public class DefaultProperties { + + public static Properties getProperties(String name, boolean sipLog, boolean sipCacheServerConnections) { + Properties properties = new Properties(); + properties.setProperty("javax.sip.STACK_NAME", name); +// properties.setProperty("javax.sip.IP_ADDRESS", ip); + // 关闭自动会话 + properties.setProperty("javax.sip.AUTOMATIC_DIALOG_SUPPORT", "off"); + + /** + * 完整配置参考 gov.nist.javax.sip.SipStackImpl,需要下载源码 + * gov/nist/javax/sip/SipStackImpl.class + * sip消息的解析在 gov.nist.javax.sip.stack.UDPMessageChannel的processIncomingDataPacket方法 + */ + + // 接收所有notify请求,即使没有订阅 + properties.setProperty("gov.nist.javax.sip.DELIVER_UNSOLICITED_NOTIFY", "true"); + properties.setProperty("gov.nist.javax.sip.AUTOMATIC_DIALOG_ERROR_HANDLING", "false"); + properties.setProperty("gov.nist.javax.sip.CANCEL_CLIENT_TRANSACTION_CHECKED", "true"); + // 为_NULL _对话框传递_终止的_事件 + properties.setProperty("gov.nist.javax.sip.DELIVER_TERMINATED_EVENT_FOR_NULL_DIALOG", "true"); + // 是否自动计算content length的实际长度,默认不计算 + properties.setProperty("gov.nist.javax.sip.COMPUTE_CONTENT_LENGTH_FROM_MESSAGE_BODY", "true"); + // 会话清理策略 + properties.setProperty("gov.nist.javax.sip.RELEASE_REFERENCES_STRATEGY", "Normal"); + // 处理由该服务器处理的基于底层TCP的保持生存超时 + properties.setProperty("gov.nist.javax.sip.RELIABLE_CONNECTION_KEEP_ALIVE_TIMEOUT", "60"); + // 获取实际内容长度,不使用header中的长度信息 + properties.setProperty("gov.nist.javax.sip.COMPUTE_CONTENT_LENGTH_FROM_MESSAGE_BODY", "true"); + // 线程可重入 + properties.setProperty("gov.nist.javax.sip.REENTRANT_LISTENER", "true"); + // 定义应用程序打算多久审计一次 SIP 堆栈,了解其内部线程的健康状况(该属性指定连续审计之间的时间(以毫秒为单位)) + properties.setProperty("gov.nist.javax.sip.THREAD_AUDIT_INTERVAL_IN_MILLISECS", "30000"); + + // 部分设备会在短时间内发送大量注册, 导致协议栈内存溢出, 开启此项可以防止这部分设备注册, 避免服务崩溃,但是会降低系统性能, 描述如下 + // 默认值为 true。 + // 将此设置为 false 会使 Stack 在 Server Transaction 进入 TERMINATED 状态后关闭服务器套接字。 + // 这允许服务器防止客户端发起的基于 TCP 的拒绝服务攻击(即发起数百个客户端事务)。 + // 如果为 true(默认作),则堆栈将保持套接字打开,以便以牺牲线程和内存资源为代价来最大化性能 - 使自身容易受到 DOS 攻击。 + properties.setProperty("gov.nist.javax.sip.CACHE_SERVER_CONNECTIONS", String.valueOf(sipCacheServerConnections)); + + properties.setProperty("gov.nist.javax.sip.MESSAGE_PROCESSOR_FACTORY", "gov.nist.javax.sip.stack.NioMessageProcessorFactory"); + + /** + * sip_server_log.log 和 sip_debug_log.log ERROR, INFO, WARNING, OFF, DEBUG, TRACE + */ + Logger log = LoggerFactory.getLogger(AlarmNotifyMessageHandler.class); + if (sipLog) { + properties.setProperty("gov.nist.javax.sip.STACK_LOGGER", "com.genersoft.iot.vmp.gb28181.conf.StackLoggerImpl"); + properties.setProperty("gov.nist.javax.sip.SERVER_LOGGER", "com.genersoft.iot.vmp.gb28181.conf.ServerLoggerImpl"); + properties.setProperty("gov.nist.javax.sip.LOG_MESSAGE_CONTENT", "true"); + log.info("[SIP日志]已开启"); + }else { + log.info("[SIP日志]已关闭"); + } + return properties; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/conf/ServerLoggerImpl.java b/src/main/java/com/genersoft/iot/vmp/gb28181/conf/ServerLoggerImpl.java new file mode 100755 index 0000000..c7b1f6e --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/conf/ServerLoggerImpl.java @@ -0,0 +1,91 @@ +package com.genersoft.iot.vmp.gb28181.conf; + +import gov.nist.core.CommonLogger; +import gov.nist.core.ServerLogger; +import gov.nist.core.StackLogger; +import gov.nist.javax.sip.message.SIPMessage; +import gov.nist.javax.sip.stack.SIPTransactionStack; + +import javax.sip.SipStack; +import java.util.Properties; + +public class ServerLoggerImpl implements ServerLogger { + + private boolean showLog = true; + + private SIPTransactionStack sipStack; + + protected StackLogger stackLogger; + + @Override + public void closeLogFile() { + + } + + @Override + public void logMessage(SIPMessage message, String from, String to, boolean sender, long time) { + if (!showLog) { + return; + } + StringBuilder stringBuilder = new StringBuilder(); + stringBuilder.append(sender? "发送:目标--->" + from:"接收:来自--->" + to) + .append("\r\n") + .append(message); + this.stackLogger.logInfo(stringBuilder.toString()); + + } + + @Override + public void logMessage(SIPMessage message, String from, String to, String status, boolean sender, long time) { + if (!showLog) { + return; + } + StringBuilder stringBuilder = new StringBuilder(); + stringBuilder.append(sender? "发送: 目标->" + from :"接收:来自->" + to) + .append("\r\n") + .append(message); + this.stackLogger.logInfo(stringBuilder.toString()); + } + + @Override + public void logMessage(SIPMessage message, String from, String to, String status, boolean sender) { + if (!showLog) { + return; + } + StringBuilder stringBuilder = new StringBuilder(); + stringBuilder.append(sender? "发送: 目标->" + from :"接收:来自->" + to) + .append("\r\n") + .append(message); + this.stackLogger.logInfo(stringBuilder.toString()); + } + + @Override + public void logException(Exception ex) { + if (!showLog) { + return; + } + this.stackLogger.logException(ex); + } + + @Override + public void setStackProperties(Properties stackProperties) { + if (!showLog) { + return; + } + String TRACE_LEVEL = stackProperties.getProperty("gov.nist.javax.sip.TRACE_LEVEL"); + if (TRACE_LEVEL != null) { + showLog = true; + } + } + + @Override + public void setSipStack(SipStack sipStack) { + if (!showLog) { + return; + } + if(sipStack instanceof SIPTransactionStack) { + this.sipStack = (SIPTransactionStack)sipStack; + this.stackLogger = CommonLogger.getLogger(SIPTransactionStack.class); + } + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/conf/StackLoggerImpl.java b/src/main/java/com/genersoft/iot/vmp/gb28181/conf/StackLoggerImpl.java new file mode 100755 index 0000000..bab0285 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/conf/StackLoggerImpl.java @@ -0,0 +1,141 @@ +package com.genersoft.iot.vmp.gb28181.conf; + +import gov.nist.core.StackLogger; +import org.slf4j.LoggerFactory; +import org.slf4j.spi.LocationAwareLogger; +import org.springframework.stereotype.Component; + +import java.util.Properties; + +@Component +public class StackLoggerImpl implements StackLogger { + + /** + * 完全限定类名(Fully Qualified Class Name),用于定位日志位置 + */ + private static final String FQCN = StackLoggerImpl.class.getName(); + + /** + * 获取栈中类信息(以便底层日志记录系统能够提取正确的位置信息(方法名、行号)) + * @return LocationAwareLogger + */ + private static LocationAwareLogger getLocationAwareLogger() { + return (LocationAwareLogger) LoggerFactory.getLogger(new Throwable().getStackTrace()[4].getClassName()); + } + + + /** + * 封装打印日志的位置信息 + * @param level 日志级别 + * @param message 日志事件的消息 + */ + private static void log(int level, String message) { + LocationAwareLogger locationAwareLogger = getLocationAwareLogger(); + locationAwareLogger.log(null, FQCN, level, message, null, null); + } + + /** + * 封装打印日志的位置信息 + * @param level 日志级别 + * @param message 日志事件的消息 + */ + private static void log(int level, String message, Throwable throwable) { + LocationAwareLogger locationAwareLogger = getLocationAwareLogger(); + locationAwareLogger.log(null, FQCN, level, message, null, throwable); + } + + @Override + public void logStackTrace() { + + } + + @Override + public void logStackTrace(int traceLevel) { + System.out.println("traceLevel: " + traceLevel); + } + + @Override + public int getLineCount() { + return 0; + } + + @Override + public void logException(Throwable ex) { + + } + + @Override + public void logDebug(String message) { + log(LocationAwareLogger.INFO_INT, message); + } + + @Override + public void logDebug(String message, Exception ex) { + log(LocationAwareLogger.INFO_INT, message, ex); + } + + @Override + public void logTrace(String message) { + log(LocationAwareLogger.INFO_INT, message); + } + + @Override + public void logFatalError(String message) { + log(LocationAwareLogger.INFO_INT, message); + } + + @Override + public void logError(String message) { + log(LocationAwareLogger.INFO_INT, message); + } + + @Override + public boolean isLoggingEnabled() { + return true; + } + + @Override + public boolean isLoggingEnabled(int logLevel) { + return true; + } + + @Override + public void logError(String message, Exception ex) { + log(LocationAwareLogger.INFO_INT, message, ex); + } + + @Override + public void logWarning(String message) { + log(LocationAwareLogger.INFO_INT, message); + } + + @Override + public void logInfo(String message) { + log(LocationAwareLogger.INFO_INT, message); + } + + @Override + public void disableLogging() { + + } + + @Override + public void enableLogging() { + + } + + @Override + public void setBuildTimeStamp(String buildTimeStamp) { + + } + + @Override + public void setStackProperties(Properties stackProperties) { + + } + + @Override + public String getLoggerName() { + return null; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/controller/AlarmController.java b/src/main/java/com/genersoft/iot/vmp/gb28181/controller/AlarmController.java new file mode 100755 index 0000000..c219d96 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/controller/AlarmController.java @@ -0,0 +1,194 @@ +package com.genersoft.iot.vmp.gb28181.controller; + +import com.genersoft.iot.vmp.conf.exception.ControllerException; +import com.genersoft.iot.vmp.conf.security.JwtUtils; +import com.genersoft.iot.vmp.gb28181.bean.Device; +import com.genersoft.iot.vmp.gb28181.bean.DeviceAlarm; +import com.genersoft.iot.vmp.gb28181.bean.Platform; +import com.genersoft.iot.vmp.gb28181.service.IDeviceAlarmService; +import com.genersoft.iot.vmp.gb28181.service.IDeviceService; +import com.genersoft.iot.vmp.gb28181.service.IPlatformService; +import com.genersoft.iot.vmp.gb28181.transmit.cmd.ISIPCommander; +import com.genersoft.iot.vmp.gb28181.transmit.cmd.ISIPCommanderForPlatform; +import com.genersoft.iot.vmp.utils.DateUtil; +import com.genersoft.iot.vmp.vmanager.bean.ErrorCode; +import com.github.pagehelper.PageInfo; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.util.ObjectUtils; +import org.springframework.web.bind.annotation.*; + +import javax.sip.InvalidArgumentException; +import javax.sip.SipException; +import java.text.ParseException; +import java.util.Arrays; +import java.util.List; + +@Tag(name = "报警信息管理") +@Slf4j +@RestController +@RequestMapping("/api/alarm") +public class AlarmController { + + @Autowired + private IDeviceAlarmService deviceAlarmService; + + @Autowired + private ISIPCommander commander; + + @Autowired + private ISIPCommanderForPlatform commanderForPlatform; + + @Autowired + private IPlatformService platformService; + + @Autowired + private IDeviceService deviceService; + + + /** + * 删除报警 + * + * @param id 报警id + * @param deviceIds 多个设备id,逗号分隔 + * @param time 结束时间(这个时间之前的报警会被删除) + * @return + */ + @DeleteMapping("/delete") + @Operation(summary = "删除报警", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "id", description = "ID") + @Parameter(name = "deviceIds", description = "多个设备id,逗号分隔") + @Parameter(name = "time", description = "结束时间") + public Integer delete( + @RequestParam(required = false) Integer id, + @RequestParam(required = false) String deviceIds, + @RequestParam(required = false) String time + ) { + if (ObjectUtils.isEmpty(id)) { + id = null; + } + if (ObjectUtils.isEmpty(deviceIds)) { + deviceIds = null; + } + + if (ObjectUtils.isEmpty(time)) { + time = null; + }else if (!DateUtil.verification(time, DateUtil.formatter) ){ + throw new ControllerException(ErrorCode.ERROR400.getCode(), "time格式为" + DateUtil.PATTERN); + } + List deviceIdList = null; + if (deviceIds != null) { + String[] deviceIdArray = deviceIds.split(","); + deviceIdList = Arrays.asList(deviceIdArray); + } + + return deviceAlarmService.clearAlarmBeforeTime(id, deviceIdList, time); + } + + /** + * 测试向上级/设备发送模拟报警通知 + * + * @param deviceId 报警id + * @return + */ + @GetMapping("/test/notify/alarm") + @Operation(summary = "测试向上级/设备发送模拟报警通知", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "deviceId", description = "设备国标编号") + public void delete(@RequestParam String deviceId) { + Device device = deviceService.getDeviceByDeviceId(deviceId); + Platform platform = platformService.queryPlatformByServerGBId(deviceId); + DeviceAlarm deviceAlarm = new DeviceAlarm(); + deviceAlarm.setChannelId(deviceId); + deviceAlarm.setAlarmDescription("test"); + deviceAlarm.setAlarmMethod("1"); + deviceAlarm.setAlarmPriority("1"); + deviceAlarm.setAlarmTime(DateUtil.getNow()); + deviceAlarm.setAlarmType("1"); + deviceAlarm.setLongitude(115.33333); + deviceAlarm.setLatitude(39.33333); + + if (device != null && platform == null) { + + try { + commander.sendAlarmMessage(device, deviceAlarm); + } catch (InvalidArgumentException | SipException | ParseException e) { + + } + }else if (device == null && platform != null){ + try { + commanderForPlatform.sendAlarmMessage(platform, deviceAlarm); + } catch (SipException | InvalidArgumentException | ParseException e) { + log.error("[命令发送失败] 国标级联 发送BYE: {}", e.getMessage()); + throw new ControllerException(ErrorCode.ERROR100.getCode(), "命令发送失败: " + e.getMessage()); + } + }else { + throw new ControllerException(ErrorCode.ERROR100.getCode(),"无法确定" + deviceId + "是平台还是设备"); + } + + } + + /** + * 分页查询报警 + * + * @param deviceId 设备id + * @param page 当前页 + * @param count 每页查询数量 + * @param alarmPriority 报警级别 + * @param alarmMethod 报警方式 + * @param alarmType 报警类型 + * @param startTime 开始时间 + * @param endTime 结束时间 + * @return + */ + @Operation(summary = "分页查询报警", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "page",description = "当前页",required = true) + @Parameter(name = "count",description = "每页查询数量",required = true) + @Parameter(name = "deviceId",description = "设备id") + @Parameter(name = "channelId",description = "通道id") + @Parameter(name = "alarmPriority",description = "查询内容") + @Parameter(name = "alarmMethod",description = "查询内容") + @Parameter(name = "alarmType",description = "每页查询数量") + @Parameter(name = "startTime",description = "开始时间") + @Parameter(name = "endTime",description = "结束时间") + @GetMapping("/all") + public PageInfo getAll( + @RequestParam int page, + @RequestParam int count, + @RequestParam(required = false) String deviceId, + @RequestParam(required = false) String channelId, + @RequestParam(required = false) String alarmPriority, + @RequestParam(required = false) String alarmMethod, + @RequestParam(required = false) String alarmType, + @RequestParam(required = false) String startTime, + @RequestParam(required = false) String endTime + ) { + if (ObjectUtils.isEmpty(alarmPriority)) { + alarmPriority = null; + } + if (ObjectUtils.isEmpty(alarmMethod)) { + alarmMethod = null; + } + if (ObjectUtils.isEmpty(alarmType)) { + alarmType = null; + } + + if (ObjectUtils.isEmpty(startTime)) { + startTime = null; + }else if (!DateUtil.verification(startTime, DateUtil.formatter) ){ + throw new ControllerException(ErrorCode.ERROR400.getCode(), "startTime格式为" + DateUtil.PATTERN); + } + + if (ObjectUtils.isEmpty(endTime)) { + endTime = null; + }else if (!DateUtil.verification(endTime, DateUtil.formatter) ){ + throw new ControllerException(ErrorCode.ERROR400.getCode(), "endTime格式为" + DateUtil.PATTERN); + } + + return deviceAlarmService.getAllAlarm(page, count, deviceId, channelId, alarmPriority, alarmMethod, + alarmType, startTime, endTime); + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/controller/ChannelController.java b/src/main/java/com/genersoft/iot/vmp/gb28181/controller/ChannelController.java new file mode 100755 index 0000000..20a92a8 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/controller/ChannelController.java @@ -0,0 +1,593 @@ +package com.genersoft.iot.vmp.gb28181.controller; + +import com.genersoft.iot.vmp.common.StreamInfo; +import com.genersoft.iot.vmp.conf.UserSetting; +import com.genersoft.iot.vmp.conf.exception.ControllerException; +import com.genersoft.iot.vmp.conf.security.JwtUtils; +import com.genersoft.iot.vmp.gb28181.bean.*; +import com.genersoft.iot.vmp.gb28181.controller.bean.*; +import com.genersoft.iot.vmp.gb28181.service.IGbChannelPlayService; +import com.genersoft.iot.vmp.gb28181.service.IGbChannelService; +import com.genersoft.iot.vmp.gb28181.utils.VectorTileCatch; +import com.genersoft.iot.vmp.service.bean.ErrorCallback; +import com.genersoft.iot.vmp.service.bean.InviteErrorCode; +import com.genersoft.iot.vmp.utils.DateUtil; +import com.genersoft.iot.vmp.vmanager.bean.ErrorCode; +import com.genersoft.iot.vmp.vmanager.bean.StreamContent; +import com.genersoft.iot.vmp.vmanager.bean.WVPResult; +import com.github.pagehelper.PageInfo; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.servlet.http.HttpServletRequest; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.BeanWrapperImpl; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.context.request.async.DeferredResult; + +import java.beans.PropertyDescriptor; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.List; +import java.util.concurrent.TimeUnit; + + +@Tag(name = "全局通道管理") +@RestController +@Slf4j +@RequestMapping(value = "/api/common/channel") +public class ChannelController { + + @Autowired + private RedisTemplate redisTemplate; + + @Autowired + private IGbChannelService channelService; + + @Autowired + private IGbChannelPlayService channelPlayService; + + @Autowired + private UserSetting userSetting; + + @Autowired + private VectorTileCatch vectorTileCatch; + + + @Operation(summary = "查询通道信息", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "id", description = "通道的数据库自增Id", required = true) + @GetMapping(value = "/one") + public CommonGBChannel getOne(int id){ + return channelService.getOne(id); + } + + @Operation(summary = "获取行业编码列表", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @GetMapping("/industry/list") + public List getIndustryCodeList(){ + return channelService.getIndustryCodeList(); + } + + @Operation(summary = "获取编码列表", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @GetMapping("/type/list") + public List getDeviceTypeList(){ + return channelService.getDeviceTypeList(); + } + + @Operation(summary = "获取编码列表", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @GetMapping("/network/identification/list") + public List getNetworkIdentificationTypeList(){ + return channelService.getNetworkIdentificationTypeList(); + } + + @Operation(summary = "更新通道", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @PostMapping("/update") + public void update(@RequestBody CommonGBChannel channel){ + BeanWrapperImpl wrapper = new BeanWrapperImpl(channel); + int count = 0; + for (PropertyDescriptor pd : wrapper.getPropertyDescriptors()) { + String name = pd.getName(); + if ("class".equals(name)) continue; + if (pd.getReadMethod() == null) continue; + Object val = wrapper.getPropertyValue(name); + if (val != null) count++; + } + Assert.isTrue(count > 1, "未进行任何修改"); + channelService.update(channel); + } + + + @Operation(summary = "重置国标通道", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @PostMapping("/reset") + public void reset(@RequestBody ResetParam param){ + Assert.notNull(param.getId(), "通道ID不能为空"); + Assert.notEmpty(param.getChanelFields(), "待重置字段不可以空"); + channelService.reset(param.getId(), param.getChanelFields()); + } + + @Operation(summary = "增加通道", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @PostMapping("/add") + public CommonGBChannel add(@RequestBody CommonGBChannel channel){ + channelService.add(channel); + return channel; + } + + @Operation(summary = "获取通道列表", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "page", description = "当前页", required = true) + @Parameter(name = "count", description = "每页查询数量", required = true) + @Parameter(name = "query", description = "查询内容") + @Parameter(name = "online", description = "是否在线") + @Parameter(name = "hasRecordPlan", description = "是否已设置录制计划") + @Parameter(name = "channelType", description = "通道类型, 0:国标设备,1:推流设备,2:拉流代理") + @Parameter(name = "civilCode", description = "行政区划") + @Parameter(name = "parentDeviceId", description = "父节点编码") + @GetMapping("/list") + public PageInfo queryList(int page, int count, + @RequestParam(required = false) String query, + @RequestParam(required = false) Boolean online, + @RequestParam(required = false) Boolean hasRecordPlan, + @RequestParam(required = false) Integer channelType, + @RequestParam(required = false) String civilCode, + @RequestParam(required = false) String parentDeviceId){ + if (ObjectUtils.isEmpty(query)){ + query = null; + } + if (ObjectUtils.isEmpty(civilCode)){ + civilCode = null; + } + if (ObjectUtils.isEmpty(parentDeviceId)){ + parentDeviceId = null; + } + return channelService.queryList(page, count, query, online, hasRecordPlan, channelType, civilCode, parentDeviceId); + } + + @Operation(summary = "获取关联行政区划通道列表", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "page", description = "当前页", required = true) + @Parameter(name = "count", description = "每页查询数量", required = true) + @Parameter(name = "query", description = "查询内容") + @Parameter(name = "online", description = "是否在线") + @Parameter(name = "channelType", description = "通道类型, 0:国标设备,1:推流设备,2:拉流代理") + @Parameter(name = "civilCode", description = "行政区划") + @GetMapping("/civilcode/list") + public PageInfo queryListByCivilCode(int page, int count, + @RequestParam(required = false) String query, + @RequestParam(required = false) Boolean online, + @RequestParam(required = false) Integer channelType, + @RequestParam(required = false) String civilCode){ + if (ObjectUtils.isEmpty(query)){ + query = null; + } + return channelService.queryListByCivilCode(page, count, query, online, channelType, civilCode); + } + + + @Operation(summary = "存在行政区划但无法挂载的通道列表", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "page", description = "当前页", required = true) + @Parameter(name = "count", description = "每页查询数量", required = true) + @Parameter(name = "query", description = "查询内容") + @Parameter(name = "online", description = "是否在线") + @Parameter(name = "channelType", description = "通道类型, 0:国标设备,1:推流设备,2:拉流代理") + @GetMapping("/civilCode/unusual/list") + public PageInfo queryListByCivilCodeForUnusual(int page, int count, + @RequestParam(required = false) String query, + @RequestParam(required = false) Boolean online, + @RequestParam(required = false) Integer channelType){ + if (ObjectUtils.isEmpty(query)){ + query = null; + } + return channelService.queryListByCivilCodeForUnusual(page, count, query, online, channelType); + } + + + @Operation(summary = "存在父节点编号但无法挂载的通道列表", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "page", description = "当前页", required = true) + @Parameter(name = "count", description = "每页查询数量", required = true) + @Parameter(name = "query", description = "查询内容") + @Parameter(name = "online", description = "是否在线") + @Parameter(name = "channelType", description = "通道类型, 0:国标设备,1:推流设备,2:拉流代理") + @GetMapping("/parent/unusual/list") + public PageInfo queryListByParentForUnusual(int page, int count, + @RequestParam(required = false) String query, + @RequestParam(required = false) Boolean online, + @RequestParam(required = false) Integer channelType){ + if (ObjectUtils.isEmpty(query)){ + query = null; + } + return channelService.queryListByParentForUnusual(page, count, query, online, channelType); + } + + @Operation(summary = "清除存在行政区划但无法挂载的通道列表", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "param", description = "清理参数, all为true清理所有异常数据。 否则按照传入的设备Id清理", required = true) + @PostMapping("/civilCode/unusual/clear") + public void clearChannelCivilCode(@RequestBody ChannelToRegionParam param){ + channelService.clearChannelCivilCode(param.getAll(), param.getChannelIds()); + } + + @Operation(summary = "清除存在分组节点但无法挂载的通道列表", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "param", description = "清理参数, all为true清理所有异常数据。 否则按照传入的设备Id清理", required = true) + @PostMapping("/parent/unusual/clear") + public void clearChannelParent(@RequestBody ChannelToRegionParam param){ + channelService.clearChannelParent(param.getAll(), param.getChannelIds()); + } + + @Operation(summary = "获取关联业务分组通道列表", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "page", description = "当前页", required = true) + @Parameter(name = "count", description = "每页查询数量", required = true) + @Parameter(name = "query", description = "查询内容") + @Parameter(name = "online", description = "是否在线") + @Parameter(name = "channelType", description = "通道类型, 0:国标设备,1:推流设备,2:拉流代理") + @Parameter(name = "groupDeviceId", description = "业务分组下的父节点ID") + @GetMapping("/parent/list") + public PageInfo queryListByParentId(int page, int count, + @RequestParam(required = false) String query, + @RequestParam(required = false) Boolean online, + @RequestParam(required = false) Integer channelType, + @RequestParam(required = false) String groupDeviceId){ + if (ObjectUtils.isEmpty(query)){ + query = null; + } + return channelService.queryListByParentId(page, count, query, online, channelType, groupDeviceId); + } + + @Operation(summary = "通道设置行政区划", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @PostMapping("/region/add") + public void addChannelToRegion(@RequestBody ChannelToRegionParam param){ + Assert.notEmpty(param.getChannelIds(),"通道ID不可为空"); + Assert.hasLength(param.getCivilCode(),"未添加行政区划"); + channelService.addChannelToRegion(param.getCivilCode(), param.getChannelIds()); + } + + @Operation(summary = "通道删除行政区划", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @PostMapping("/region/delete") + public void deleteChannelToRegion(@RequestBody ChannelToRegionParam param){ + Assert.isTrue(!param.getChannelIds().isEmpty() || !ObjectUtils.isEmpty(param.getCivilCode()),"参数异常"); + channelService.deleteChannelToRegion(param.getCivilCode(), param.getChannelIds()); + } + + @Operation(summary = "通道设置行政区划-根据国标设备", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @PostMapping("/region/device/add") + public void addChannelToRegionByGbDevice(@RequestBody ChannelToRegionByGbDeviceParam param){ + Assert.notEmpty(param.getDeviceIds(),"参数异常"); + Assert.hasLength(param.getCivilCode(),"未添加行政区划"); + channelService.addChannelToRegionByGbDevice(param.getCivilCode(), param.getDeviceIds()); + } + + @Operation(summary = "通道删除行政区划-根据国标设备", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @PostMapping("/region/device/delete") + public void deleteChannelToRegionByGbDevice(@RequestBody ChannelToRegionByGbDeviceParam param){ + Assert.notEmpty(param.getDeviceIds(),"参数异常"); + channelService.deleteChannelToRegionByGbDevice(param.getDeviceIds()); + } + + @Operation(summary = "通道设置业务分组", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @PostMapping("/group/add") + public void addChannelToGroup(@RequestBody ChannelToGroupParam param){ + Assert.notEmpty(param.getChannelIds(),"通道ID不可为空"); + Assert.hasLength(param.getParentId(),"未添加上级分组编号"); + Assert.hasLength(param.getBusinessGroup(),"未添加业务分组"); + channelService.addChannelToGroup(param.getParentId(), param.getBusinessGroup(), param.getChannelIds()); + } + + @Operation(summary = "通道删除业务分组", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @PostMapping("/group/delete") + public void deleteChannelToGroup(@RequestBody ChannelToGroupParam param){ + Assert.isTrue(!param.getChannelIds().isEmpty() + || (!ObjectUtils.isEmpty(param.getParentId()) && !ObjectUtils.isEmpty(param.getBusinessGroup())), + "参数异常"); + channelService.deleteChannelToGroup(param.getParentId(), param.getBusinessGroup(), param.getChannelIds()); + } + + @Operation(summary = "通道设置业务分组-根据国标设备", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @PostMapping("/group/device/add") + public void addChannelToGroupByGbDevice(@RequestBody ChannelToGroupByGbDeviceParam param){ + Assert.notEmpty(param.getDeviceIds(),"参数异常"); + Assert.hasLength(param.getParentId(),"未添加上级分组编号"); + Assert.hasLength(param.getBusinessGroup(),"未添加业务分组"); + channelService.addChannelToGroupByGbDevice(param.getParentId(), param.getBusinessGroup(), param.getDeviceIds()); + } + + @Operation(summary = "通道删除业务分组-根据国标设备", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @PostMapping("/group/device/delete") + public void deleteChannelToGroupByGbDevice(@RequestBody ChannelToGroupByGbDeviceParam param){ + Assert.notEmpty(param.getDeviceIds(),"参数异常"); + channelService.deleteChannelToGroupByGbDevice(param.getDeviceIds()); + } + + @Operation(summary = "播放通道", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @GetMapping("/play") + public DeferredResult> play(HttpServletRequest request, Integer channelId){ + Assert.notNull(channelId,"参数异常"); + CommonGBChannel channel = channelService.getOne(channelId); + Assert.notNull(channel, "通道不存在"); + + DeferredResult> result = new DeferredResult<>(userSetting.getPlayTimeout().longValue()); + + ErrorCallback callback = (code, msg, streamInfo) -> { + if (code == InviteErrorCode.SUCCESS.getCode()) { + WVPResult wvpResult = WVPResult.success(); + if (streamInfo != null) { + if (userSetting.getUseSourceIpAsStreamIp()) { + streamInfo=streamInfo.clone();//深拷贝 + String host; + try { + URL url=new URL(request.getRequestURL().toString()); + host=url.getHost(); + } catch (MalformedURLException e) { + host=request.getLocalAddr(); + } + streamInfo.changeStreamIp(host); + } + if (!ObjectUtils.isEmpty(streamInfo.getMediaServer().getTranscodeSuffix()) + && !"null".equalsIgnoreCase(streamInfo.getMediaServer().getTranscodeSuffix())) { + streamInfo.setStream(streamInfo.getStream() + "_" + streamInfo.getMediaServer().getTranscodeSuffix()); + } + wvpResult.setData(new StreamContent(streamInfo)); + }else { + wvpResult.setCode(code); + wvpResult.setMsg(msg); + } + result.setResult(wvpResult); + }else { + result.setResult(WVPResult.fail(code, msg)); + } + }; + channelPlayService.play(channel, null, userSetting.getRecordSip(), callback); + return result; + } + + @Operation(summary = "停止播放通道", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @GetMapping("/play/stop") + public void stopPlay(Integer channelId){ + Assert.notNull(channelId,"参数异常"); + CommonGBChannel channel = channelService.getOne(channelId); + Assert.notNull(channel, "通道不存在"); + channelPlayService.stopPlay(channel); + } + + @Operation(summary = "录像查询", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "channelId", description = "通道ID", required = true) + @Parameter(name = "startTime", description = "开始时间", required = true) + @Parameter(name = "endTime", description = "结束时间", required = true) + @GetMapping("/playback/query") + public DeferredResult>> queryRecord(Integer channelId, String startTime, String endTime){ + + DeferredResult>> result = new DeferredResult<>(Long.valueOf(userSetting.getRecordInfoTimeout()), TimeUnit.MILLISECONDS); + if (!DateUtil.verification(startTime, DateUtil.formatter)){ + throw new ControllerException(ErrorCode.ERROR100.getCode(), "startTime格式为" + DateUtil.PATTERN); + } + if (!DateUtil.verification(endTime, DateUtil.formatter)){ + throw new ControllerException(ErrorCode.ERROR100.getCode(), "endTime格式为" + DateUtil.PATTERN); + } + CommonGBChannel channel = channelService.getOne(channelId); + Assert.notNull(channel, "通道不存在"); + + channelPlayService.queryRecord(channel, startTime, endTime, (code, msg, data) -> { + WVPResult> wvpResult = new WVPResult<>(); + wvpResult.setCode(code); + wvpResult.setMsg(msg); + wvpResult.setData(data); + result.setResult(wvpResult); + }); + result.onTimeout(()->{ + WVPResult> wvpResult = new WVPResult<>(); + wvpResult.setCode(ErrorCode.ERROR100.getCode()); + wvpResult.setMsg("timeout"); + result.setResult(wvpResult); + }); + return result; + } + + @Operation(summary = "录像回放", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "channelId", description = "通道ID", required = true) + @Parameter(name = "startTime", description = "开始时间", required = true) + @Parameter(name = "endTime", description = "结束时间", required = true) + @GetMapping("/playback") + public DeferredResult> playback(HttpServletRequest request, Integer channelId, String startTime, String endTime){ + Assert.notNull(channelId,"参数异常"); + CommonGBChannel channel = channelService.getOne(channelId); + Assert.notNull(channel, "通道不存在"); + + DeferredResult> result = new DeferredResult<>(userSetting.getPlayTimeout().longValue()); + + ErrorCallback callback = (code, msg, streamInfo) -> { + if (code == InviteErrorCode.SUCCESS.getCode()) { + WVPResult wvpResult = WVPResult.success(); + if (streamInfo != null) { + if (userSetting.getUseSourceIpAsStreamIp()) { + streamInfo=streamInfo.clone();//深拷贝 + String host; + try { + URL url=new URL(request.getRequestURL().toString()); + host=url.getHost(); + } catch (MalformedURLException e) { + host=request.getLocalAddr(); + } + streamInfo.changeStreamIp(host); + } + if (!ObjectUtils.isEmpty(streamInfo.getMediaServer().getTranscodeSuffix()) + && !"null".equalsIgnoreCase(streamInfo.getMediaServer().getTranscodeSuffix())) { + streamInfo.setStream(streamInfo.getStream() + "_" + streamInfo.getMediaServer().getTranscodeSuffix()); + } + wvpResult.setData(new StreamContent(streamInfo)); + }else { + wvpResult.setCode(code); + wvpResult.setMsg(msg); + } + + result.setResult(wvpResult); + }else { + result.setResult(WVPResult.fail(code, msg)); + } + }; + channelPlayService.playback(channel, DateUtil.yyyy_MM_dd_HH_mm_ssToTimestamp(startTime), + DateUtil.yyyy_MM_dd_HH_mm_ssToTimestamp(endTime), callback); + return result; + } + + @Operation(summary = "停止录像回放", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "channelId", description = "通道ID", required = true) + @Parameter(name = "stream", description = "流ID", required = true) + @GetMapping("/playback/stop") + public void stopPlayback(Integer channelId, String stream){ + Assert.notNull(channelId,"参数异常"); + CommonGBChannel channel = channelService.getOne(channelId); + Assert.notNull(channel, "通道不存在"); + channelPlayService.stopPlayback(channel, stream); + } + + @Operation(summary = "暂停录像回放", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "channelId", description = "通道ID", required = true) + @Parameter(name = "stream", description = "流ID", required = true) + @GetMapping("/playback/pause") + public void pausePlayback(Integer channelId, String stream){ + Assert.notNull(channelId,"参数异常"); + CommonGBChannel channel = channelService.getOne(channelId); + Assert.notNull(channel, "通道不存在"); + channelPlayService.playbackPause(channel, stream); + } + + @Operation(summary = "恢复录像回放", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "channelId", description = "通道ID", required = true) + @Parameter(name = "stream", description = "流ID", required = true) + @GetMapping("/playback/resume") + public void resumePlayback(Integer channelId, String stream){ + Assert.notNull(channelId,"参数异常"); + CommonGBChannel channel = channelService.getOne(channelId); + Assert.notNull(channel, "通道不存在"); + channelPlayService.playbackResume(channel, stream); + } + + @Operation(summary = "拖动录像回放", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "channelId", description = "通道ID", required = true) + @Parameter(name = "stream", description = "流ID", required = true) + @Parameter(name = "seekTime", description = "将要播放的时间", required = true) + @GetMapping("/playback/seek") + public void seekPlayback(Integer channelId, String stream, Long seekTime){ + Assert.notNull(channelId,"参数异常"); + Assert.notNull(seekTime,"参数异常"); + CommonGBChannel channel = channelService.getOne(channelId); + Assert.notNull(channel, "通道不存在"); + channelPlayService.playbackSeek(channel, stream, seekTime); + } + + @Operation(summary = "拖动录像回放", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "channelId", description = "通道ID", required = true) + @Parameter(name = "stream", description = "流ID", required = true) + @Parameter(name = "speed", description = "倍速", required = true) + @GetMapping("/playback/speed") + public void seekPlayback(Integer channelId, String stream, Double speed){ + Assert.notNull(channelId,"参数异常"); + Assert.notNull(speed,"参数异常"); + CommonGBChannel channel = channelService.getOne(channelId); + Assert.notNull(channel, "通道不存在"); + channelPlayService.playbackSpeed(channel, stream, speed); + } + + @Operation(summary = "为地图获取通道列表", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "query", description = "查询内容") + @Parameter(name = "online", description = "是否在线") + @Parameter(name = "hasRecordPlan", description = "是否已设置录制计划") + @Parameter(name = "channelType", description = "通道类型, 0:国标设备,1:推流设备,2:拉流代理") + @Parameter(name = "geoCoordSys", description = "地理坐标系, WGS84/GCJ02") + @GetMapping("/map/list") + public List queryListForMap( + @RequestParam(required = false) String query, + @RequestParam(required = false) Boolean online, + @RequestParam(required = false) Boolean hasRecordPlan, + @RequestParam(required = false) Integer channelType){ + if (ObjectUtils.isEmpty(query)){ + query = null; + } + return channelService.queryListForMap(query, online, hasRecordPlan, channelType); + } + + @Operation(summary = "为地图去除抽稀结果", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @PostMapping("/map/reset-level") + public void resetLevel(){ + channelService.resetLevel(); + } + + @Operation(summary = "执行抽稀", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @PostMapping("/map/thin/draw") + public String drawThin(@RequestBody DrawThinParam param){ + if(param == null || param.getZoomParam() == null || param.getZoomParam().isEmpty()) { + throw new ControllerException(ErrorCode.ERROR400); + } + return channelService.drawThin(param.getZoomParam(), param.getExtent(), param.getGeoCoordSys()); + } + + @Operation(summary = "清除未保存的抽稀结果", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "id", description = "抽稀ID", required = true) + @GetMapping("/map/thin/clear") + public void clearThin(String id){ + vectorTileCatch.remove(id); + } + + @Operation(summary = "保存的抽稀结果", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "id", description = "抽稀ID", required = true) + @GetMapping("/map/thin/save") + public void saveThin(String id){ + channelService.saveThin(id); + } + + @Operation(summary = "获取抽稀执行的进度", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "id", description = "抽稀ID", required = true) + @GetMapping("/map/thin/progress") + public DrawThinProcess thinProgress(String id){ + return channelService.thinProgress(id); + } + + @Operation(summary = "为地图提供标准mvt图层", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @GetMapping(value = "/map/tile/{z}/{x}/{y}", produces = "application/x-protobuf") + @Parameter(name = "geoCoordSys", description = "地理坐标系, WGS84/GCJ02") + public ResponseEntity getTile(@PathVariable int z, @PathVariable int x, @PathVariable int y, String geoCoordSys){ + + try { + byte[] mvt = channelService.getTile(z, x, y, geoCoordSys); + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.parseMediaType("application/x-protobuf")); + if (mvt == null) { + headers.setContentLength(0); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(null); + } + headers.setContentLength(mvt.length); + return new ResponseEntity<>(mvt, headers, HttpStatus.OK); + } catch (Exception e) { + log.error("构建矢量瓦片失败: z: {}, x: {}, y:{}", z, x, y, e); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(null); + } + } + + @Operation(summary = "为地图提供经过抽稀的标准mvt图层", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @GetMapping(value = "/map/thin/tile/{z}/{x}/{y}", produces = "application/x-protobuf") + @Parameter(name = "geoCoordSys", description = "地理坐标系, WGS84/GCJ02") + @Parameter(name = "thinId", description = "抽稀结果ID") + public ResponseEntity getThinTile(@PathVariable int z, @PathVariable int x, @PathVariable int y, + String geoCoordSys, @RequestParam(required = false) String thinId){ + + if (ObjectUtils.isEmpty(thinId)) { + thinId = "DEFAULT"; + } + String catchKey = z + "_" + x + "_" + y + "_" + geoCoordSys.toUpperCase(); + byte[] mvt = vectorTileCatch.getVectorTile(thinId, catchKey); + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.parseMediaType("application/x-protobuf")); + if (mvt == null) { + headers.setContentLength(0); + return ResponseEntity.status(HttpStatus.OK).body(null); + } + + headers.setContentLength(mvt.length); + return new ResponseEntity<>(mvt, headers, HttpStatus.OK); + } + + +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/controller/ChannelFrontEndController.java b/src/main/java/com/genersoft/iot/vmp/gb28181/controller/ChannelFrontEndController.java new file mode 100755 index 0000000..8659cf2 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/controller/ChannelFrontEndController.java @@ -0,0 +1,601 @@ +package com.genersoft.iot.vmp.gb28181.controller; + +import com.genersoft.iot.vmp.conf.exception.ControllerException; +import com.genersoft.iot.vmp.conf.security.JwtUtils; +import com.genersoft.iot.vmp.gb28181.bean.*; +import com.genersoft.iot.vmp.gb28181.service.IGbChannelControlService; +import com.genersoft.iot.vmp.gb28181.service.IGbChannelService; +import com.genersoft.iot.vmp.service.bean.ErrorCallback; +import com.genersoft.iot.vmp.vmanager.bean.ErrorCode; +import com.genersoft.iot.vmp.vmanager.bean.WVPResult; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.util.Assert; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.context.request.async.DeferredResult; + +import java.util.List; + + +@Tag(name = "全局通道前端控制") +@RestController +@Slf4j +@RequestMapping(value = "/api/common/channel/front-end") +public class ChannelFrontEndController { + + @Autowired + private IGbChannelService channelService; + + @Autowired + private IGbChannelControlService channelControlService; + + + @Operation(summary = "云台控制", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "channelId", description = "通道ID", required = true) + @Parameter(name = "command", description = "控制指令,允许值: left, right, up, down, upleft, upright, downleft, downright, zoomin, zoomout, stop", required = true) + @Parameter(name = "panSpeed", description = "水平速度(0-100)", required = true) + @Parameter(name = "tiltSpeed", description = "垂直速度(0-100)", required = true) + @Parameter(name = "zoomSpeed", description = "缩放速度(0-100)", required = true) + @GetMapping("/ptz") + public DeferredResult> ptz(Integer channelId, String command, Integer panSpeed, Integer tiltSpeed, Integer zoomSpeed){ + + if (log.isDebugEnabled()) { + log.debug("[通用通道]云台控制 API调用,channelId:{} ,command:{} ,panSpeed:{} ,tiltSpeed:{} ,zoomSpeed:{}",channelId, command, panSpeed, tiltSpeed, zoomSpeed); + } + + CommonGBChannel channel = channelService.getOne(channelId); + Assert.notNull(channel, "通道不存在"); + + if (panSpeed == null) { + panSpeed = 50; + }else if (panSpeed < 0 || panSpeed > 100) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "panSpeed 为 0-100的数字"); + } + if (tiltSpeed == null) { + tiltSpeed = 50; + }else if (tiltSpeed < 0 || tiltSpeed > 100) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "tiltSpeed 为 0-100的数字"); + } + if (zoomSpeed == null) { + zoomSpeed = 50; + }else if (zoomSpeed < 0 || zoomSpeed > 100) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "zoomSpeed 为 0-100的数字"); + } + + FrontEndControlCodeForPTZ controlCode = new FrontEndControlCodeForPTZ(); + controlCode.setPanSpeed(panSpeed); + controlCode.setTiltSpeed(tiltSpeed); + controlCode.setZoomSpeed(zoomSpeed); + switch (command){ + case "left": + controlCode.setPan(0); + break; + case "right": + controlCode.setPan(1); + break; + case "up": + controlCode.setTilt(0); + break; + case "down": + controlCode.setTilt(1); + break; + case "upleft": + controlCode.setPan(0); + controlCode.setTilt(0); + break; + case "upright": + controlCode.setTilt(0); + controlCode.setPan(1); + break; + case "downleft": + controlCode.setPan(0); + controlCode.setTilt(1); + break; + case "downright": + controlCode.setTilt(1); + controlCode.setPan(1); + break; + case "zoomin": + controlCode.setZoom(1); + break; + case "zoomout": + controlCode.setZoom(0); + break; + default: + break; + } + + DeferredResult> result = new DeferredResult<>(); + + result.onTimeout(()->{ + WVPResult wvpResult = WVPResult.fail(ErrorCode.ERROR100.getCode(), "请求超时"); + result.setResult(wvpResult); + }); + + channelControlService.ptz(channel, controlCode, (code, msg, data) -> { + WVPResult wvpResult = new WVPResult<>(); + wvpResult.setCode(code); + wvpResult.setMsg(msg); + wvpResult.setData(data); + result.setResult(wvpResult); + }); + return result; + } + + + @Operation(summary = "光圈控制", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "channelId", description = "通道国标编号", required = true) + @Parameter(name = "command", description = "控制指令,允许值: in, out, stop", required = true) + @Parameter(name = "speed", description = "光圈速度(0-100)", required = true) + @GetMapping("/fi/iris") + public DeferredResult> iris(Integer channelId, String command, Integer speed){ + + if (log.isDebugEnabled()) { + log.debug("[通用通道]光圈控制 API调用,channelId:{} ,command:{} ,speed:{} ",channelId, command, speed); + } + + CommonGBChannel channel = channelService.getOne(channelId); + Assert.notNull(channel, "通道不存在"); + + if (speed == null) { + speed = 50; + }else if (speed < 0 || speed > 100) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "speed 为 0-100的数字"); + } + + FrontEndControlCodeForFI controlCode = new FrontEndControlCodeForFI(); + controlCode.setIrisSpeed(speed); + + switch (command){ + case "in": + controlCode.setIris(1); + break; + case "out": + controlCode.setIris(0); + break; + default: + break; + } + + DeferredResult> result = new DeferredResult<>(); + + result.onTimeout(()->{ + WVPResult wvpResult = WVPResult.fail(ErrorCode.ERROR100.getCode(), "请求超时"); + result.setResult(wvpResult); + }); + + ErrorCallback callback = (code, msg, data) -> { + WVPResult wvpResult = new WVPResult<>(); + wvpResult.setCode(code); + wvpResult.setMsg(msg); + wvpResult.setData(data); + result.setResult(wvpResult); + }; + + channelControlService.fi(channel, controlCode, callback); + + return result; + } + + @Operation(summary = "聚焦控制", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "channelId", description = "通道国标编号", required = true) + @Parameter(name = "command", description = "控制指令,允许值: near, far, stop", required = true) + @Parameter(name = "speed", description = "聚焦速度(0-100)", required = true) + @GetMapping("/fi/focus") + public DeferredResult> focus(Integer channelId, String command, Integer speed){ + + if (log.isDebugEnabled()) { + log.debug("[通用通道]聚焦控制 API调用,channelId:{} ,command:{} ,speed:{} ", channelId, command, speed); + } + + if (speed == null) { + speed = 50; + }else if (speed < 0 || speed > 100) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "speed 为 0-100的数字"); + } + + CommonGBChannel channel = channelService.getOne(channelId); + Assert.notNull(channel, "通道不存在"); + + FrontEndControlCodeForFI controlCode = new FrontEndControlCodeForFI(); + controlCode.setFocusSpeed(speed); + switch (command){ + case "near": + controlCode.setFocus(0); + break; + case "far": + controlCode.setFocus(1); + break; + default: + break; + } + + DeferredResult> result = new DeferredResult<>(); + + result.onTimeout(()->{ + WVPResult wvpResult = WVPResult.fail(ErrorCode.ERROR100.getCode(), "请求超时"); + result.setResult(wvpResult); + }); + + ErrorCallback callback = (code, msg, data) -> { + WVPResult wvpResult = new WVPResult<>(); + wvpResult.setCode(code); + wvpResult.setMsg(msg); + wvpResult.setData(data); + result.setResult(wvpResult); + }; + + channelControlService.fi(channel, controlCode, callback); + return result; + } + + @Operation(summary = "查询预置位", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "channelId", description = "通道国标编号", required = true) + @GetMapping("/preset/query") + public DeferredResult>> queryPreset(Integer channelId) { + if (log.isDebugEnabled()) { + log.debug("[通用通道] 预置位查询API调用, {}", channelId); + } + + CommonGBChannel channel = channelService.getOne(channelId); + Assert.notNull(channel, "通道不存在"); + + DeferredResult>> result = new DeferredResult<>(); + + result.onTimeout(()->{ + WVPResult> wvpResult = WVPResult.fail(ErrorCode.ERROR100.getCode(), "请求超时"); + result.setResult(wvpResult); + }); + + ErrorCallback> callback = (code, msg, data) -> { + WVPResult> wvpResult = new WVPResult<>(); + wvpResult.setCode(code); + wvpResult.setMsg(msg); + wvpResult.setData(data); + result.setResult(wvpResult); + }; + + channelControlService.queryPreset(channel, callback); + + return result; + } + + private DeferredResult> controlPreset(Integer channelId, FrontEndControlCodeForPreset controlCode) { + CommonGBChannel channel = channelService.getOne(channelId); + Assert.notNull(channel, "通道不存在"); + + + DeferredResult> result = new DeferredResult<>(); + + result.onTimeout(()->{ + WVPResult wvpResult = WVPResult.fail(ErrorCode.ERROR100.getCode(), "请求超时"); + result.setResult(wvpResult); + }); + + ErrorCallback callback = (code, msg, data) -> { + WVPResult wvpResult = new WVPResult<>(); + wvpResult.setCode(code); + wvpResult.setMsg(msg); + wvpResult.setData(data); + result.setResult(wvpResult); + }; + + channelControlService.preset(channel, controlCode, callback); + return result; + } + + @Operation(summary = "预置位指令-设置预置位", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "channelId", description = "通道国标编号", required = true) + @Parameter(name = "presetId", description = "预置位编号", required = true) + @Parameter(name = "presetName", description = "预置位名称", required = true) + @GetMapping("/preset/add") + public DeferredResult> addPreset(Integer channelId, Integer presetId, String presetName) { + FrontEndControlCodeForPreset controlCode = new FrontEndControlCodeForPreset(); + controlCode.setCode(1); + controlCode.setPresetId(presetId); + controlCode.setPresetName(presetName); + + return controlPreset(channelId, controlCode); + } + + @Operation(summary = "预置位指令-调用预置位", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "channelId", description = "通道国标编号", required = true) + @Parameter(name = "presetId", description = "预置位编号(1-100)", required = true) + @GetMapping("/preset/call") + public DeferredResult> callPreset(Integer channelId, Integer presetId) { + FrontEndControlCodeForPreset controlCode = new FrontEndControlCodeForPreset(); + controlCode.setCode(2); + controlCode.setPresetId(presetId); + + return controlPreset(channelId, controlCode); + } + + @Operation(summary = "预置位指令-删除预置位", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "channelId", description = "通道国标编号", required = true) + @Parameter(name = "presetId", description = "预置位编号(1-100)", required = true) + @GetMapping("/preset/delete") + public DeferredResult> deletePreset(Integer channelId, Integer presetId) { + + FrontEndControlCodeForPreset controlCode = new FrontEndControlCodeForPreset(); + controlCode.setCode(3); + controlCode.setPresetId(presetId); + + return controlPreset(channelId, controlCode); + } + + private DeferredResult> tourControl(Integer channelId, FrontEndControlCodeForTour controlCode) { + CommonGBChannel channel = channelService.getOne(channelId); + Assert.notNull(channel, "通道不存在"); + + DeferredResult> result = new DeferredResult<>(); + + result.onTimeout(()->{ + WVPResult wvpResult = WVPResult.fail(ErrorCode.ERROR100.getCode(), "请求超时"); + result.setResult(wvpResult); + }); + + ErrorCallback callback = (code, msg, data) -> { + WVPResult wvpResult = new WVPResult<>(); + wvpResult.setCode(code); + wvpResult.setMsg(msg); + wvpResult.setData(data); + result.setResult(wvpResult); + }; + + channelControlService.tour(channel, controlCode, callback); + return result; + } + + @Operation(summary = "巡航指令-加入巡航点", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "channelId", description = "通道国标编号", required = true) + @Parameter(name = "tourId", description = "巡航组号", required = true) + @Parameter(name = "presetId", description = "预置位编号", required = true) + @GetMapping("/tour/point/add") + public DeferredResult> addTourPoint(Integer channelId, Integer tourId, Integer presetId) { + + FrontEndControlCodeForTour controlCode = new FrontEndControlCodeForTour(); + controlCode.setCode(1); + controlCode.setPresetId(presetId); + controlCode.setTourId(tourId); + + return tourControl(channelId, controlCode); + } + + @Operation(summary = "巡航指令-删除一个巡航点", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "channelId", description = "通道国标编号", required = true) + @Parameter(name = "tourId", description = "巡航组号(1-100)", required = true) + @Parameter(name = "presetId", description = "预置位编号(0-100, 为0时删除整个巡航)", required = true) + @GetMapping("/tour/point/delete") + public DeferredResult> deleteCruisePoint(Integer channelId, Integer tourId, Integer presetId) { + FrontEndControlCodeForTour controlCode = new FrontEndControlCodeForTour(); + controlCode.setCode(2); + controlCode.setPresetId(presetId); + controlCode.setTourId(tourId); + + return tourControl(channelId, controlCode); + } + + @Operation(summary = "巡航指令-设置巡航速度", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "channelId", description = "通道国标编号", required = true) + @Parameter(name = "tourId", description = "巡航组号(0-100)", required = true) + @Parameter(name = "speed", description = "巡航速度(1-4095)", required = true) + @Parameter(name = "presetId", description = "预置位编号", required = true) + @GetMapping("/tour/speed") + public DeferredResult> setCruiseSpeed(Integer channelId, Integer tourId, Integer speed, Integer presetId) { + FrontEndControlCodeForTour controlCode = new FrontEndControlCodeForTour(); + controlCode.setCode(3); + controlCode.setTourSpeed(speed); + controlCode.setTourId(tourId); + controlCode.setPresetId(presetId); + return tourControl(channelId, controlCode); + } + + @Operation(summary = "巡航指令-设置巡航停留时间", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "channelId", description = "通道国标编号", required = true) + @Parameter(name = "tourId", description = "巡航组号", required = true) + @Parameter(name = "time", description = "巡航停留时间(1-4095)", required = true) + @Parameter(name = "presetId", description = "预置位编号", required = true) + @GetMapping("/tour/time") + public DeferredResult> setCruiseTime(Integer channelId, Integer tourId, Integer time, Integer presetId) { + FrontEndControlCodeForTour controlCode = new FrontEndControlCodeForTour(); + controlCode.setCode(4); + controlCode.setTourTime(time); + controlCode.setTourId(tourId); + controlCode.setPresetId(presetId); + return tourControl(channelId, controlCode); + } + + @Operation(summary = "巡航指令-开始巡航", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "channelId", description = "通道国标编号", required = true) + @Parameter(name = "tourId", description = "巡航组号)", required = true) + @GetMapping("/tour/start") + public DeferredResult> startCruise(Integer channelId, Integer tourId) { + FrontEndControlCodeForTour controlCode = new FrontEndControlCodeForTour(); + controlCode.setCode(5); + controlCode.setTourId(tourId); + return tourControl(channelId, controlCode); + } + + @Operation(summary = "巡航指令-停止巡航", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "channelId", description = "通道国标编号", required = true) + @Parameter(name = "tourId", description = "巡航组号", required = true) + @GetMapping("/tour/stop") + public DeferredResult> stopCruise(Integer channelId, Integer tourId) { + FrontEndControlCodeForTour controlCode = new FrontEndControlCodeForTour(); + controlCode.setCode(6); + controlCode.setTourId(tourId); + return tourControl(channelId, controlCode); + } + + private DeferredResult> scanControl(Integer channelId, FrontEndControlCodeForScan controlCode) { + + CommonGBChannel channel = channelService.getOne(channelId); + Assert.notNull(channel, "通道不存在"); + DeferredResult> result = new DeferredResult<>(); + + result.onTimeout(()->{ + WVPResult wvpResult = WVPResult.fail(ErrorCode.ERROR100.getCode(), "请求超时"); + result.setResult(wvpResult); + }); + + ErrorCallback callback = (code, msg, data) -> { + WVPResult wvpResult = new WVPResult<>(); + wvpResult.setCode(code); + wvpResult.setMsg(msg); + wvpResult.setData(data); + result.setResult(wvpResult); + }; + channelControlService.scan(channel, controlCode, callback); + + return result; + + } + + @Operation(summary = "扫描指令-开始自动扫描", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "channelId", description = "通道国标编号", required = true) + @Parameter(name = "scanId", description = "扫描组号(0-100)", required = true) + @GetMapping("/scan/start") + public DeferredResult> startScan(Integer channelId, Integer scanId) { + FrontEndControlCodeForScan controlCode = new FrontEndControlCodeForScan(); + controlCode.setCode(1); + controlCode.setScanId(scanId); + return scanControl(channelId, controlCode); + + } + + @Operation(summary = "扫描指令-停止自动扫描", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "channelId", description = "通道国标编号", required = true) + @Parameter(name = "scanId", description = "扫描组号(0-100)", required = true) + @GetMapping("/scan/stop") + public DeferredResult> stopScan(Integer channelId, Integer scanId) { + FrontEndControlCodeForScan controlCode = new FrontEndControlCodeForScan(); + controlCode.setCode(5); + controlCode.setScanId(scanId); + return scanControl(channelId, controlCode); + } + + @Operation(summary = "扫描指令-设置自动扫描左边界", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "channelId", description = "通道国标编号", required = true) + @Parameter(name = "scanId", description = "扫描组号(0-100)", required = true) + @GetMapping("/scan/set/left") + public DeferredResult> setScanLeft(Integer channelId, Integer scanId) { + FrontEndControlCodeForScan controlCode = new FrontEndControlCodeForScan(); + controlCode.setCode(2); + controlCode.setScanId(scanId); + return scanControl(channelId, controlCode); + } + + @Operation(summary = "扫描指令-设置自动扫描右边界", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "channelId", description = "通道国标编号", required = true) + @Parameter(name = "scanId", description = "扫描组号(0-100)", required = true) + @GetMapping("/scan/set/right") + public DeferredResult> setScanRight(Integer channelId, Integer scanId) { + FrontEndControlCodeForScan controlCode = new FrontEndControlCodeForScan(); + controlCode.setCode(3); + controlCode.setScanId(scanId); + return scanControl(channelId, controlCode); + } + + + @Operation(summary = "扫描指令-设置自动扫描速度", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "channelId", description = "通道国标编号", required = true) + @Parameter(name = "scanId", description = "扫描组号(0-100)", required = true) + @Parameter(name = "speed", description = "自动扫描速度(1-4095)", required = true) + @GetMapping("/scan/set/speed") + public DeferredResult> setScanSpeed(Integer channelId, Integer scanId, Integer speed) { + FrontEndControlCodeForScan controlCode = new FrontEndControlCodeForScan(); + controlCode.setCode(4); + controlCode.setScanId(scanId); + controlCode.setScanSpeed(speed); + return scanControl(channelId, controlCode); + } + + + @Operation(summary = "辅助开关控制指令-雨刷控制", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "channelId", description = "通道国标编号", required = true) + @Parameter(name = "command", description = "控制指令,允许值: on, off", required = true) + @GetMapping("/wiper") + public DeferredResult> wiper(Integer channelId, String command){ + + CommonGBChannel channel = channelService.getOne(channelId); + Assert.notNull(channel, "通道不存在"); + + FrontEndControlCodeForWiper controlCode = new FrontEndControlCodeForWiper(); + + switch (command){ + case "on": + controlCode.setCode(1); + break; + case "off": + controlCode.setCode(2); + break; + default: + break; + } + DeferredResult> result = new DeferredResult<>(); + + result.onTimeout(()->{ + WVPResult wvpResult = WVPResult.fail(ErrorCode.ERROR100.getCode(), "请求超时"); + result.setResult(wvpResult); + }); + + ErrorCallback callback = (code, msg, data) -> { + WVPResult wvpResult = new WVPResult<>(); + wvpResult.setCode(code); + wvpResult.setMsg(msg); + wvpResult.setData(data); + result.setResult(wvpResult); + }; + + channelControlService.wiper(channel, controlCode, callback); + + return result; + } + + @Operation(summary = "辅助开关控制指令", security = @SecurityRequirement(name = JwtUtils.HEADER)) + + @Parameter(name = "channelId", description = "通道国标编号", required = true) + @Parameter(name = "command", description = "控制指令,允许值: on, off", required = true) + @Parameter(name = "auxiliaryId", description = "开关编号", required = true) + @GetMapping("/auxiliary") + public DeferredResult> auxiliarySwitch(Integer channelId, String command, Integer auxiliaryId){ + + CommonGBChannel channel = channelService.getOne(channelId); + Assert.notNull(channel, "通道不存在"); + + FrontEndControlCodeForAuxiliary controlCode = new FrontEndControlCodeForAuxiliary(); + controlCode.setAuxiliaryId(auxiliaryId); + switch (command){ + case "on": + controlCode.setCode(1); + break; + case "off": + controlCode.setCode(2); + break; + default: + break; + } + DeferredResult> result = new DeferredResult<>(); + + result.onTimeout(()->{ + WVPResult wvpResult = WVPResult.fail(ErrorCode.ERROR100.getCode(), "请求超时"); + result.setResult(wvpResult); + }); + + ErrorCallback callback = (code, msg, data) -> { + WVPResult wvpResult = new WVPResult<>(); + wvpResult.setCode(code); + wvpResult.setMsg(msg); + wvpResult.setData(data); + result.setResult(wvpResult); + }; + channelControlService.auxiliary(channel, controlCode, callback); + return result; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/controller/DeviceConfig.java b/src/main/java/com/genersoft/iot/vmp/gb28181/controller/DeviceConfig.java new file mode 100755 index 0000000..f2402cf --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/controller/DeviceConfig.java @@ -0,0 +1,91 @@ +/** + * 设备设置命令API接口 + * + * @author lawrencehj + * @date 2021年2月2日 + */ + +package com.genersoft.iot.vmp.gb28181.controller; + +import com.genersoft.iot.vmp.conf.security.JwtUtils; +import com.genersoft.iot.vmp.gb28181.bean.BasicParam; +import com.genersoft.iot.vmp.gb28181.bean.Device; +import com.genersoft.iot.vmp.gb28181.service.IDeviceService; +import com.genersoft.iot.vmp.vmanager.bean.ErrorCode; +import com.genersoft.iot.vmp.vmanager.bean.WVPResult; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.util.Assert; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.context.request.async.DeferredResult; + +@Slf4j +@Tag(name = "国标设备配置") +@RestController +@RequestMapping("/api/device/config") +public class DeviceConfig { + + @Autowired + private IDeviceService deviceService; + + @GetMapping("/basicParam") + @Operation(summary = "基本配置设置命令", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "basicParam", description = "基础配置参数", required = true) + public DeferredResult> homePositionApi(BasicParam basicParam) { + if (log.isDebugEnabled()) { + log.debug("基本配置设置命令API调用"); + } + Assert.notNull(basicParam.getDeviceId(), "设备ID必须存在"); + + Device device = deviceService.getDeviceByDeviceId(basicParam.getDeviceId()); + Assert.notNull(device, "设备不存在"); + + DeferredResult> deferredResult = new DeferredResult<>(); + deviceService.deviceBasicConfig(device, basicParam, (code, msg, data) -> { + deferredResult.setResult(new WVPResult<>(code, msg, data)); + }); + + deferredResult.onTimeout(() -> { + log.warn("[设备配置] 超时, {}", device.getDeviceId()); + deferredResult.setResult(WVPResult.fail(ErrorCode.ERROR100.getCode(), "超时")); + }); + return deferredResult; + + } + + @Operation(summary = "设备配置查询", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "deviceId", description = "设备国标编号", required = true) + @Parameter(name = "channelId", description = "通道国标编号", required = true) + @Parameter(name = "configType", description = "配置类型, 可选值," + + "基本参数配置:BasicParam," + + "视频参数范围:VideoParamOpt, " + + "SVAC编码配置:SVACEncodeConfig, " + + "SVAC解码配置:SVACDecodeConfig。" + + "可同时查询多个配置类型,各类型以“/”分隔,") + @GetMapping("/query") + public DeferredResult> configDownloadApi(String deviceId,String configType, + @RequestParam(required = false) String channelId) { + if (log.isDebugEnabled()) { + log.debug("设备配置查询请求API调用"); + } + Device device = deviceService.getDeviceByDeviceId(deviceId); + Assert.notNull(device, "设备不存在"); + + DeferredResult> deferredResult = new DeferredResult<>(); + + deviceService.deviceConfigQuery(device, channelId, configType, (code, msg, data) -> { + deferredResult.setResult(new WVPResult<>(code, msg, data)); + }); + + deferredResult.onTimeout(() -> { + log.warn("[获取设备配置] 超时, {}", device.getDeviceId()); + deferredResult.setResult(WVPResult.fail(ErrorCode.ERROR100.getCode(), "超时")); + }); + return deferredResult; + } + +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/controller/DeviceControl.java b/src/main/java/com/genersoft/iot/vmp/gb28181/controller/DeviceControl.java new file mode 100755 index 0000000..6170cb1 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/controller/DeviceControl.java @@ -0,0 +1,234 @@ +/** + * 设备控制命令API接口 + * + * @author lawrencehj + * @date 2021年2月1日 + */ + +package com.genersoft.iot.vmp.gb28181.controller; + +import com.genersoft.iot.vmp.conf.security.JwtUtils; +import com.genersoft.iot.vmp.gb28181.bean.Device; +import com.genersoft.iot.vmp.gb28181.service.IDeviceService; +import com.genersoft.iot.vmp.vmanager.bean.ErrorCode; +import com.genersoft.iot.vmp.vmanager.bean.WVPResult; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.util.Assert; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.context.request.async.DeferredResult; + +@Tag(name = "国标设备控制") +@Slf4j +@RestController +@RequestMapping("/api/device/control") +public class DeviceControl { + + @Autowired + private IDeviceService deviceService; + + + @Operation(summary = "远程启动", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "deviceId", description = "设备国标编号", required = true) + @GetMapping("/teleboot/{deviceId}") + public void teleBootApi(@PathVariable String deviceId) { + if (log.isDebugEnabled()) { + log.debug("设备远程启动API调用"); + } + Device device = deviceService.getDeviceByDeviceId(deviceId); + Assert.notNull(device, "设备不存在"); + deviceService.teleboot(device); + } + + + @Operation(summary = "录像控制", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "deviceId", description = "设备国标编号", required = true) + @Parameter(name = "channelId", description = "通道国标编号", required = true) + @Parameter(name = "recordCmdStr", description = "命令, 可选值:Record(手动录像),StopRecord(停止手动录像)", required = true) + @GetMapping("/record") + public DeferredResult> recordApi(String deviceId, String recordCmdStr, String channelId) { + if (log.isDebugEnabled()) { + log.debug("开始/停止录像API调用"); + } + Device device = deviceService.getDeviceByDeviceId(deviceId); + Assert.notNull(device, "设备不存在"); + DeferredResult> deferredResult = new DeferredResult<>(); + + deviceService.record(device, channelId, recordCmdStr, (code, msg, data) -> { + deferredResult.setResult(new WVPResult<>(code, msg, data)); + }); + deferredResult.onTimeout(() -> { + log.warn("[开始/停止录像] 操作超时, 设备未返回应答指令, {}", deviceId); + deferredResult.setResult(WVPResult.fail(ErrorCode.ERROR100.getCode(), "操作超时, 设备未应答")); + }); + return deferredResult; + } + + @Operation(summary = "布防/撤防", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "deviceId", description = "设备国标编号", required = true) + @Parameter(name = "guardCmd", description = "命令, 可选值:SetGuard(布防),ResetGuard(撤防)", required = true) + @GetMapping("/guard") + public DeferredResult> guardApi(String deviceId, String guardCmd) { + if (log.isDebugEnabled()) { + log.debug("布防/撤防API调用"); + } + Device device = deviceService.getDeviceByDeviceId(deviceId); + Assert.notNull(device, "设备不存在"); + DeferredResult> result = new DeferredResult<>(); + deviceService.guard(device, guardCmd, (code, msg, data) -> { + result.setResult(new WVPResult<>(code, msg, data)); + }); + result.onTimeout(() -> { + log.warn("[布防/撤防] 操作超时, 设备未返回应答指令, {}", deviceId); + result.setResult(WVPResult.fail(ErrorCode.ERROR100.getCode(), "操作超时, 设备未应答")); + }); + return result; + } + + @Operation(summary = "报警复位", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "deviceId", description = "设备国标编号", required = true) + @Parameter(name = "channelId", description = "通道国标编号", required = true) + @Parameter(name = "alarmMethod", description = "报警方式, 报警方式条件(可选),取值0为全部,1为电话报警,2为设备报警,3为短信报警,4为\n" + + "GPS报警,5为视频报警,6为设备故障报警,7其他报警;可以为直接组合如12为电话报警或设备报警") + @Parameter(name = "alarmType", description = "报警类型, " + + "报警类型。" + + "报警方式为2时,不携带 AlarmType为默认的报警设备报警," + + "携带 AlarmType取值及对应报警类型如下:" + + "1-视频丢失报警;2-设备防拆报警;3-存储设备磁盘满报警;4-设备高温报警;5-设备低温报警。" + + "报警方式为5时,取值如下:" + + "1-人工视频报警;2-运动目标检测报警;3-遗留物检测报警;4-物体移除检测报警;5-绊线检测报警;" + + "6-入侵检测报警;7-逆行检测报警;8-徘徊检测报警;9-流量统计报警;10-密度检测报警;" + + "11-视频异常检测报警;12-快速移动报警。" + + "报警方式为6时,取值如下:" + + "1-存储设备磁盘故障报警;2-存储设备风扇故障报警") + @GetMapping("/reset_alarm") + public DeferredResult> resetAlarm(String deviceId, String channelId, + @RequestParam(required = false) String alarmMethod, + @RequestParam(required = false) String alarmType) { + if (log.isDebugEnabled()) { + log.debug("报警复位API调用"); + } + Device device = deviceService.getDeviceByDeviceId(deviceId); + Assert.notNull(device, "设备不存在"); + DeferredResult> result = new DeferredResult<>(); + deviceService.resetAlarm(device, channelId, alarmMethod, alarmType, (code, msg, data) -> { + result.setResult(new WVPResult<>(code, msg, data)); + }); + result.onTimeout(() -> { + log.warn("[布防/撤防] 操作超时, 设备未返回应答指令, {}", deviceId); + result.setResult(WVPResult.fail(ErrorCode.ERROR100.getCode(), "操作超时, 设备未应答")); + }); + return result; + } + + @Operation(summary = "强制关键帧", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "deviceId", description = "设备国标编号", required = true) + @Parameter(name = "channelId", description = "通道国标编号") + @GetMapping("/i_frame") + public void iFrame(String deviceId, @RequestParam(required = false) String channelId) { + if (log.isDebugEnabled()) { + log.debug("强制关键帧API调用"); + } + Device device = deviceService.getDeviceByDeviceId(deviceId); + Assert.notNull(device, "设备不存在"); + deviceService.iFrame(device, channelId); + } + + @Operation(summary = "看守位控制", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "deviceId", description = "设备国标编号", required = true) + @Parameter(name = "channelId", description = "通道国标编号", required = true) + @Parameter(name = "enabled", description = "是否开启看守位", required = true) + @Parameter(name = "presetIndex", description = "调用预置位编号") + @Parameter(name = "resetTime", description = "自动归位时间间隔 单位:秒") + @GetMapping("/home_position") + public DeferredResult> homePositionApi(String deviceId, String channelId, Boolean enabled, + @RequestParam(required = false) Integer resetTime, + @RequestParam(required = false) Integer presetIndex) { + if (log.isDebugEnabled()) { + log.debug("看守位控制API调用"); + } + Device device = deviceService.getDeviceByDeviceId(deviceId); + Assert.notNull(device, "设备不存在"); + DeferredResult> result = new DeferredResult<>(); + deviceService.homePosition(device, channelId, enabled, resetTime, presetIndex, (code, msg, data) -> { + result.setResult(new WVPResult<>(code, msg, data)); + }); + result.onTimeout(() -> { + log.warn("[看守位控制] 操作超时, 设备未返回应答指令, {}", deviceId); + result.setResult(WVPResult.fail(ErrorCode.ERROR100.getCode(), "操作超时, 设备未应答")); + }); + return result; + } + + @Operation(summary = "拉框放大", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "deviceId", description = "设备国标编号", required = true) + @Parameter(name = "channelId", description = "通道国标编号", required = true) + @Parameter(name = "length", description = "播放窗口长度像素值", required = true) + @Parameter(name = "width", description = "播放窗口宽度像素值", required = true) + @Parameter(name = "midpointx", description = "拉框中心的横轴坐标像素值", required = true) + @Parameter(name = "midpointy", description = "拉框中心的纵轴坐标像素值", required = true) + @Parameter(name = "lengthx", description = "拉框长度像素值", required = true) + @Parameter(name = "lengthy", description = "拉框宽度像素值", required = true) + @GetMapping("drag_zoom/zoom_in") + public DeferredResult> dragZoomIn(@RequestParam String deviceId, String channelId, + @RequestParam int length, + @RequestParam int width, + @RequestParam int midpointx, + @RequestParam int midpointy, + @RequestParam int lengthx, + @RequestParam int lengthy) { + if (log.isDebugEnabled()) { + log.debug(String.format("设备拉框放大 API调用,deviceId:%s ,channelId:%s ,length:%d ,width:%d ,midpointx:%d ,midpointy:%d ,lengthx:%d ,lengthy:%d",deviceId, channelId, length, width, midpointx, midpointy,lengthx, lengthy)); + } + Device device = deviceService.getDeviceByDeviceId(deviceId); + Assert.notNull(device, "设备不存在"); + DeferredResult> result = new DeferredResult<>(); + deviceService.dragZoomIn(device, channelId, length, width, midpointx, midpointy, lengthx,lengthy, (code, msg, data) -> { + result.setResult(new WVPResult<>(code, msg, data)); + }); + result.onTimeout(() -> { + log.warn("[设备拉框放大] 操作超时, 设备未返回应答指令, {}", deviceId); + result.setResult(WVPResult.fail(ErrorCode.ERROR100.getCode(), "操作超时, 设备未应答")); + }); + return result; + } + + @Operation(summary = "拉框缩小", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "deviceId", description = "设备国标编号", required = true) + @Parameter(name = "channelId", description = "通道国标编号") + @Parameter(name = "length", description = "播放窗口长度像素值", required = true) + @Parameter(name = "width", description = "拉框中心的横轴坐标像素值", required = true) + @Parameter(name = "midpointx", description = "拉框中心的横轴坐标像素值", required = true) + @Parameter(name = "midpointy", description = "拉框中心的纵轴坐标像素值", required = true) + @Parameter(name = "lengthx", description = "拉框长度像素值", required = true) + @Parameter(name = "lengthy", description = "拉框宽度像素值", required = true) + @GetMapping("/drag_zoom/zoom_out") + public DeferredResult> dragZoomOut(@RequestParam String deviceId, + @RequestParam(required = false) String channelId, + @RequestParam int length, + @RequestParam int width, + @RequestParam int midpointx, + @RequestParam int midpointy, + @RequestParam int lengthx, + @RequestParam int lengthy){ + + if (log.isDebugEnabled()) { + log.debug(String.format("设备拉框缩小 API调用,deviceId:%s ,channelId:%s ,length:%d ,width:%d ,midpointx:%d ,midpointy:%d ,lengthx:%d ,lengthy:%d",deviceId, channelId, length, width, midpointx, midpointy,lengthx, lengthy)); + } + Device device = deviceService.getDeviceByDeviceId(deviceId); + Assert.notNull(device, "设备不存在"); + DeferredResult> result = new DeferredResult<>(); + deviceService.dragZoomOut(device, channelId, length, width, midpointx, midpointy, lengthx,lengthy, (code, msg, data) -> { + result.setResult(new WVPResult<>(code, msg, data)); + }); + result.onTimeout(() -> { + log.warn("[设备拉框放大] 操作超时, 设备未返回应答指令, {}", deviceId); + result.setResult(WVPResult.fail(ErrorCode.ERROR100.getCode(), "操作超时, 设备未应答")); + }); + return result; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/controller/DeviceQuery.java b/src/main/java/com/genersoft/iot/vmp/gb28181/controller/DeviceQuery.java new file mode 100755 index 0000000..6954101 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/controller/DeviceQuery.java @@ -0,0 +1,401 @@ +package com.genersoft.iot.vmp.gb28181.controller; + +import com.alibaba.fastjson2.JSONObject; +import com.genersoft.iot.vmp.conf.DynamicTask; +import com.genersoft.iot.vmp.conf.UserSetting; +import com.genersoft.iot.vmp.conf.exception.ControllerException; +import com.genersoft.iot.vmp.conf.security.JwtUtils; +import com.genersoft.iot.vmp.gb28181.bean.Device; +import com.genersoft.iot.vmp.gb28181.bean.DeviceChannel; +import com.genersoft.iot.vmp.gb28181.bean.SyncStatus; +import com.genersoft.iot.vmp.gb28181.service.IDeviceChannelService; +import com.genersoft.iot.vmp.gb28181.service.IDeviceService; +import com.genersoft.iot.vmp.gb28181.service.IInviteStreamService; +import com.genersoft.iot.vmp.gb28181.transmit.callback.DeferredResultHolder; +import com.genersoft.iot.vmp.gb28181.transmit.cmd.ISIPCommander; +import com.genersoft.iot.vmp.service.redisMsg.IRedisRpcService; +import com.genersoft.iot.vmp.vmanager.bean.ErrorCode; +import com.genersoft.iot.vmp.vmanager.bean.WVPResult; +import com.github.pagehelper.PageInfo; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.compress.utils.IOUtils; +import org.apache.ibatis.annotations.Options; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.MediaType; +import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.context.request.async.DeferredResult; + +import jakarta.servlet.ServletOutputStream; +import jakarta.servlet.http.HttpServletResponse; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; + +@Tag(name = "国标设备查询", description = "国标设备查询") +@SuppressWarnings("rawtypes") +@Slf4j +@RestController +@RequestMapping("/api/device/query") +public class DeviceQuery { + + @Autowired + private IDeviceChannelService deviceChannelService; + + @Autowired + private IInviteStreamService inviteStreamService; + + @Autowired + private IDeviceService deviceService; + + @Autowired + private ISIPCommander cmder; + + @Autowired + private DeferredResultHolder resultHolder; + + @Autowired + private UserSetting userSetting; + + @Autowired + private DynamicTask dynamicTask; + + @Autowired + private IRedisRpcService redisRpcService; + + @Operation(summary = "查询国标设备", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "deviceId", description = "设备国标编号", required = true) + @GetMapping("/devices/{deviceId}") + public Device devices(@PathVariable String deviceId){ + + return deviceService.getDeviceByDeviceId(deviceId); + } + + + @Operation(summary = "分页查询国标设备", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "page", description = "当前页", required = true) + @Parameter(name = "count", description = "每页查询数量", required = true) + @Parameter(name = "query", description = "搜索", required = false) + @Parameter(name = "status", description = "状态", required = false) + @GetMapping("/devices") + @Options() + public PageInfo devices(int page, int count, String query, Boolean status){ + if (ObjectUtils.isEmpty(query)){ + query = null; + } + return deviceService.getAll(page, count, query, status); + } + + + @GetMapping("/devices/{deviceId}/channels") + @Operation(summary = "分页查询通道", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "deviceId", description = "设备国标编号", required = true) + @Parameter(name = "page", description = "当前页", required = true) + @Parameter(name = "count", description = "每页查询数量", required = true) + @Parameter(name = "query", description = "查询内容") + @Parameter(name = "online", description = "是否在线") + @Parameter(name = "channelType", description = "设备/子目录-> false/true") + public PageInfo channels(@PathVariable String deviceId, + int page, int count, + @RequestParam(required = false) String query, + @RequestParam(required = false) Boolean online, + @RequestParam(required = false) Boolean channelType) { + if (ObjectUtils.isEmpty(query)) { + query = null; + } + + return deviceChannelService.queryChannelsByDeviceId(deviceId, query, channelType, online, page, count); + } + + @GetMapping("/streams") + @Operation(summary = "分页查询存在流的通道", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "page", description = "当前页", required = true) + @Parameter(name = "count", description = "每页查询数量", required = true) + @Parameter(name = "query", description = "查询内容") + public PageInfo streamChannels(int page, int count, + @RequestParam(required = false) String query) { + if (ObjectUtils.isEmpty(query)) { + query = null; + } + + return deviceChannelService.queryChannels(query, true, null, null, true, page, count); + } + + @Operation(summary = "同步设备通道", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "deviceId", description = "设备国标编号", required = true) + @GetMapping("/devices/{deviceId}/sync") + public WVPResult devicesSync(@PathVariable String deviceId){ + + if (log.isDebugEnabled()) { + log.debug("设备通道信息同步API调用,deviceId:" + deviceId); + } + Device device = deviceService.getDeviceByDeviceId(deviceId); + if (device.getRegisterTime() == null) { + WVPResult wvpResult = new WVPResult<>(); + wvpResult.setCode(ErrorCode.ERROR100.getCode()); + wvpResult.setMsg("设备尚未注册过"); + return wvpResult; + } + return deviceService.devicesSync(device); + + } + + @Operation(summary = "移除设备", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "deviceId", description = "设备国标编号", required = true) + @DeleteMapping("/devices/{deviceId}/delete") + public String delete(@PathVariable String deviceId){ + + if (log.isDebugEnabled()) { + log.debug("设备信息删除API调用,deviceId:" + deviceId); + } + + // 清除redis记录 + deviceService.delete(deviceId); + JSONObject json = new JSONObject(); + json.put("deviceId", deviceId); + return json.toString(); + } + + @Operation(summary = "分页查询子目录通道", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "deviceId", description = "设备国标编号", required = true) + @Parameter(name = "channelId", description = "通道国标编号", required = true) + @Parameter(name = "page", description = "当前页", required = true) + @Parameter(name = "count", description = "每页查询数量", required = true) + @Parameter(name = "query", description = "查询内容") + @Parameter(name = "online", description = "是否在线") + @Parameter(name = "channelType", description = "设备/子目录-> false/true") + @GetMapping("/sub_channels/{deviceId}/{channelId}/channels") + public PageInfo subChannels(@PathVariable String deviceId, + @PathVariable String channelId, + int page, + int count, + @RequestParam(required = false) String query, + @RequestParam(required = false) Boolean online, + @RequestParam(required = false) Boolean channelType){ + + DeviceChannel deviceChannel = deviceChannelService.getOne(deviceId,channelId); + if (deviceChannel == null) { + PageInfo deviceChannelPageResult = new PageInfo<>(); + return deviceChannelPageResult; + } + + return deviceChannelService.getSubChannels(deviceChannel.getDataDeviceId(), channelId, query, channelType, online, page, count); + } + + @Operation(summary = "开启/关闭通道的音频", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "channelId", description = "通道的数据库ID", required = true) + @Parameter(name = "audio", description = "开启/关闭音频", required = true) + @PostMapping("/channel/audio") + public void changeAudio(Integer channelId, Boolean audio){ + Assert.notNull(channelId, "通道的数据库ID不可为NULL"); + Assert.notNull(audio, "开启/关闭音频不可为NULL"); + deviceChannelService.changeAudio(channelId, audio); + } + + @Operation(summary = "修改通道的码流类型", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @PostMapping("/channel/stream/identification/update/") + public void updateChannelStreamIdentification(DeviceChannel channel){ + deviceChannelService.updateChannelStreamIdentification(channel); + } + @Operation(summary = "获取单个通道详情", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "deviceId", description = "设备的国标编码", required = true) + @Parameter(name = "channelDeviceId", description = "通道的国标编码", required = true) + @GetMapping("/channel/one") + public DeviceChannel getChannel(String deviceId, String channelDeviceId){ + return deviceChannelService.getOne(deviceId, channelDeviceId); + } + + + @Operation(summary = "修改数据流传输模式", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "deviceId", description = "设备国标编号", required = true) + @Parameter(name = "streamMode", description = "数据流传输模式, 取值:" + + "UDP(udp传输),TCP-ACTIVE(tcp主动模式),TCP-PASSIVE(tcp被动模式)", required = true) + @PostMapping("/transport/{deviceId}/{streamMode}") + public void updateTransport(@PathVariable String deviceId, @PathVariable String streamMode){ + Assert.isTrue(streamMode.equalsIgnoreCase("UDP") + || streamMode.equalsIgnoreCase("TCP-ACTIVE") + || streamMode.equalsIgnoreCase("TCP-PASSIVE"), "数据流传输模式, 取值:UDP/TCP-ACTIVE/TCP-PASSIVE"); + Device device = deviceService.getDeviceByDeviceId(deviceId); + device.setStreamMode(streamMode.toUpperCase()); + deviceService.updateCustomDevice(device); + } + + + @Operation(summary = "添加设备信息", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "device", description = "设备", required = true) + @PostMapping("/device/add") + public void addDevice(@RequestBody Device device){ + + if (device == null || device.getDeviceId() == null) { + throw new ControllerException(ErrorCode.ERROR400); + } + + // 查看deviceId是否存在 + boolean exist = deviceService.isExist(device.getDeviceId()); + if (exist) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "设备编号已存在"); + } + deviceService.addCustomDevice(device); + } + + + @Operation(summary = "更新设备信息", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "device", description = "设备", required = true) + @PostMapping("/device/update") + public void updateDevice(@RequestBody Device device){ + if (device == null || device.getDeviceId() == null || device.getId() <= 0) { + throw new ControllerException(ErrorCode.ERROR400); + } + deviceService.updateCustomDevice(device); + } + + @Operation(summary = "设备状态查询", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "deviceId", description = "设备国标编号", required = true) + @GetMapping("/devices/{deviceId}/status") + public DeferredResult> deviceStatusApi(@PathVariable String deviceId) { + if (log.isDebugEnabled()) { + log.debug("设备状态查询API调用"); + } + Device device = deviceService.getDeviceByDeviceId(deviceId); + Assert.notNull(device, "设备不存在"); + DeferredResult> result = new DeferredResult<>(); + deviceService.deviceStatus(device, (code, msg, data) -> { + result.setResult(new WVPResult<>(code, msg, data)); + }); + result.onTimeout(() -> { + log.warn("[设备状态查询] 操作超时, 设备未返回应答指令, {}", deviceId); + result.setResult(WVPResult.fail(ErrorCode.ERROR100.getCode(), "操作超时, 设备未应答")); + }); + return result; + } + + @Operation(summary = "设备报警查询", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "deviceId", description = "设备国标编号", required = true) + @Parameter(name = "startPriority", description = "报警起始级别, 0为全部,1为一级警情,2为二级警情,3为三级警情,4为四级警情") + @Parameter(name = "endPriority", description = "报警终止级别, ,0为全部,1为一级警情,2为二级警情,3为三级警情,4为四级警情") + @Parameter(name = "alarmMethod", description = "报警方式条件,取值0为全部,1为电话报警,2为设备报警,3为短信报警,4为GPS报警," + + "5为视频报警,6为设备故障报警,7其他报警;可以为直接组合如12为电话报警或设备报警") + @Parameter(name = "alarmType", description = "报警类型") + @Parameter(name = "startTime", description = "报警发生起始时间") + @Parameter(name = "endTime", description = "报警发生终止时间") + @GetMapping("/alarm") + public DeferredResult> alarmApi(String deviceId, + @RequestParam(required = false) String startPriority, + @RequestParam(required = false) String endPriority, + @RequestParam(required = false) String alarmMethod, + @RequestParam(required = false) String alarmType, + @RequestParam(required = false) String startTime, + @RequestParam(required = false) String endTime) { + if (log.isDebugEnabled()) { + log.debug("设备报警查询API调用"); + } + Device device = deviceService.getDeviceByDeviceId(deviceId); + Assert.notNull(device, "设备不存在"); + DeferredResult> result = new DeferredResult<>(); + deviceService.alarm(device, startPriority,endPriority ,alarmMethod ,alarmType ,startTime ,endTime, (code, msg, data) -> { + result.setResult(new WVPResult<>(code, msg, data)); + }); + result.onTimeout(() -> { + log.warn("[设备报警查询] 操作超时, 设备未返回应答指令, {}", deviceId); + result.setResult(WVPResult.fail(ErrorCode.ERROR100.getCode(), "操作超时, 设备未应答")); + }); + return result; + } + + @Operation(summary = "设备信息查询", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "deviceId", description = "设备国标编号", required = true) + @GetMapping("/info") + public DeferredResult> deviceInfo(String deviceId) { + if (log.isDebugEnabled()) { + log.debug("设备信息查询API调用"); + } + Device device = deviceService.getDeviceByDeviceId(deviceId); + Assert.notNull(device, "设备不存在"); + DeferredResult> result = new DeferredResult<>(); + deviceService.deviceInfo(device, (code, msg, data) -> { + result.setResult(new WVPResult<>(code, msg, data)); + }); + result.onTimeout(() -> { + log.warn("[设备信息查询] 操作超时, 设备未返回应答指令, {}", deviceId); + result.setResult(WVPResult.fail(ErrorCode.ERROR100.getCode(), "操作超时, 设备未应答")); + }); + return result; + } + + + @GetMapping("/{deviceId}/sync_status") + @Operation(summary = "获取通道同步进度", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "deviceId", description = "设备国标编号", required = true) + public WVPResult getSyncStatus(@PathVariable String deviceId) { + SyncStatus channelSyncStatus = deviceService.getChannelSyncStatus(deviceId); + WVPResult wvpResult = new WVPResult<>(); + if (channelSyncStatus == null) { + wvpResult.setCode(ErrorCode.ERROR100.getCode()); + wvpResult.setMsg("同步不存在"); + }else if (channelSyncStatus.getErrorMsg() != null) { + wvpResult.setCode(ErrorCode.ERROR100.getCode()); + wvpResult.setMsg(channelSyncStatus.getErrorMsg()); + }else if (channelSyncStatus.getTotal() == null){ + wvpResult.setCode(ErrorCode.SUCCESS.getCode()); + wvpResult.setMsg("等待通道信息..."); + }else if (channelSyncStatus.getTotal() == 0){ + wvpResult.setCode(ErrorCode.SUCCESS.getCode()); + wvpResult.setMsg(ErrorCode.SUCCESS.getMsg()); + wvpResult.setData(channelSyncStatus); + }else { + wvpResult.setCode(ErrorCode.SUCCESS.getCode()); + wvpResult.setMsg(ErrorCode.SUCCESS.getMsg()); + wvpResult.setData(channelSyncStatus); + } + return wvpResult; + } + + @GetMapping("/snap/{deviceId}/{channelId}") + @Operation(summary = "请求截图") + @Parameter(name = "deviceId", description = "设备国标编号", required = true) + @Parameter(name = "channelId", description = "通道国标编号", required = true) + @Parameter(name = "mark", description = "标识", required = false) + public void getSnap(HttpServletResponse resp, @PathVariable String deviceId, @PathVariable String channelId, @RequestParam(required = false) String mark) { + + try { + final InputStream in = Files.newInputStream(new File("snap" + File.separator + deviceId + "_" + channelId + (mark == null? ".jpg": ("_" + mark + ".jpg"))).toPath()); + resp.setContentType(MediaType.IMAGE_PNG_VALUE); + ServletOutputStream outputStream = resp.getOutputStream(); + IOUtils.copy(in, resp.getOutputStream()); + in.close(); + outputStream.close(); + } catch (IOException e) { + resp.setStatus(HttpServletResponse.SC_NO_CONTENT); + } + } + + @GetMapping("/channel/raw") + @Operation(summary = "国标通道编辑时的数据回显") + @Parameter(name = "id", description = "通道的Id", required = true) + public DeviceChannel getRawChannel(int id) { + return deviceChannelService.getRawChannel(id); + } + + @GetMapping("/subscribe/catalog") + @Operation(summary = "开启/关闭目录订阅") + @Parameter(name = "id", description = "通道的Id", required = true) + @Parameter(name = "cycle", description = "订阅周期", required = true) + public void subscribeCatalog(int id, int cycle) { + deviceService.subscribeCatalog(id, cycle); + } + + @GetMapping("/subscribe/mobile-position") + @Operation(summary = "开启/关闭移动位置订阅") + @Parameter(name = "id", description = "通道的Id", required = true) + @Parameter(name = "cycle", description = "订阅周期", required = true) + @Parameter(name = "interval", description = "报送间隔", required = true) + public void subscribeMobilePosition(int id, int cycle, int interval) { + deviceService.subscribeMobilePosition(id, cycle, interval); + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/controller/GBRecordController.java b/src/main/java/com/genersoft/iot/vmp/gb28181/controller/GBRecordController.java new file mode 100755 index 0000000..368538f --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/controller/GBRecordController.java @@ -0,0 +1,214 @@ +package com.genersoft.iot.vmp.gb28181.controller; + +import com.genersoft.iot.vmp.common.InviteSessionType; +import com.genersoft.iot.vmp.common.StreamInfo; +import com.genersoft.iot.vmp.conf.UserSetting; +import com.genersoft.iot.vmp.conf.exception.ControllerException; +import com.genersoft.iot.vmp.conf.security.JwtUtils; +import com.genersoft.iot.vmp.gb28181.bean.Device; +import com.genersoft.iot.vmp.gb28181.bean.DeviceChannel; +import com.genersoft.iot.vmp.gb28181.bean.RecordInfo; +import com.genersoft.iot.vmp.gb28181.service.IDeviceChannelService; +import com.genersoft.iot.vmp.gb28181.service.IDeviceService; +import com.genersoft.iot.vmp.gb28181.service.IPlayService; +import com.genersoft.iot.vmp.gb28181.transmit.callback.DeferredResultHolder; +import com.genersoft.iot.vmp.gb28181.transmit.callback.RequestMessage; +import com.genersoft.iot.vmp.gb28181.transmit.cmd.impl.SIPCommander; +import com.genersoft.iot.vmp.service.bean.InviteErrorCode; +import com.genersoft.iot.vmp.utils.DateUtil; +import com.genersoft.iot.vmp.vmanager.bean.ErrorCode; +import com.genersoft.iot.vmp.vmanager.bean.StreamContent; +import com.genersoft.iot.vmp.vmanager.bean.WVPResult; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.context.request.async.DeferredResult; + +import jakarta.servlet.http.HttpServletRequest; +import java.util.UUID; +import java.util.concurrent.TimeUnit; + +@Tag(name = "国标录像") +@Slf4j +@RestController +@RequestMapping("/api/gb_record") +public class GBRecordController { + + @Autowired + private SIPCommander cmder; + + @Autowired + private DeferredResultHolder resultHolder; + + @Autowired + private IPlayService playService; + + @Autowired + private IDeviceChannelService channelService; + + @Autowired + private IDeviceService deviceService; + + @Autowired + private UserSetting userSetting; + + @Operation(summary = "录像查询", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "deviceId", description = "设备国标编号", required = true) + @Parameter(name = "channelId", description = "通道国标编号", required = true) + @Parameter(name = "startTime", description = "开始时间", required = true) + @Parameter(name = "endTime", description = "结束时间", required = true) + @GetMapping("/query/{deviceId}/{channelId}") + public DeferredResult> recordinfo(@PathVariable String deviceId, @PathVariable String channelId, String startTime, String endTime){ + + if (log.isDebugEnabled()) { + log.debug(String.format("录像信息查询 API调用,deviceId:%s ,startTime:%s, endTime:%s",deviceId, startTime, endTime)); + } + DeferredResult> result = new DeferredResult<>(Long.valueOf(userSetting.getRecordInfoTimeout()), TimeUnit.MILLISECONDS); + if (!DateUtil.verification(startTime, DateUtil.formatter)){ + throw new ControllerException(ErrorCode.ERROR100.getCode(), "startTime格式为" + DateUtil.PATTERN); + } + if (!DateUtil.verification(endTime, DateUtil.formatter)){ + throw new ControllerException(ErrorCode.ERROR100.getCode(), "endTime格式为" + DateUtil.PATTERN); + } + + Device device = deviceService.getDeviceByDeviceId(deviceId); + if (device == null) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), deviceId + " 不存在"); + } + DeviceChannel channel = channelService.getOneForSource(device.getId(), channelId); + if (channel == null) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), channelId + " 不存在"); + } + channelService.queryRecordInfo(device, channel, startTime, endTime, (code, msg, data)->{ + WVPResult wvpResult = new WVPResult<>(); + wvpResult.setCode(code); + wvpResult.setMsg(msg); + wvpResult.setData(data); + result.setResult(wvpResult); + }); + result.onTimeout(()->{ + WVPResult wvpResult = new WVPResult<>(); + wvpResult.setCode(ErrorCode.ERROR100.getCode()); + wvpResult.setMsg("timeout"); + result.setResult(wvpResult); + }); + return result; + } + + + @Operation(summary = "开始历史媒体下载", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "deviceId", description = "设备国标编号", required = true) + @Parameter(name = "channelId", description = "通道国标编号", required = true) + @Parameter(name = "startTime", description = "开始时间", required = true) + @Parameter(name = "endTime", description = "结束时间", required = true) + @Parameter(name = "downloadSpeed", description = "下载倍速", required = true) + @GetMapping("/download/start/{deviceId}/{channelId}") + public DeferredResult> download(HttpServletRequest request, @PathVariable String deviceId, @PathVariable String channelId, + String startTime, String endTime, String downloadSpeed) { + + if (log.isDebugEnabled()) { + log.debug(String.format("历史媒体下载 API调用,deviceId:%s,channelId:%s,downloadSpeed:%s", deviceId, channelId, downloadSpeed)); + } + + String uuid = UUID.randomUUID().toString(); + String key = DeferredResultHolder.CALLBACK_CMD_DOWNLOAD + deviceId + channelId; + DeferredResult> result = new DeferredResult<>(30000L); + resultHolder.put(key, uuid, result); + RequestMessage requestMessage = new RequestMessage(); + requestMessage.setId(uuid); + requestMessage.setKey(key); + + Device device = deviceService.getDeviceByDeviceId(deviceId); + if (device == null) { + log.warn("[开始历史媒体下载] 未找到设备 deviceId: {},channelId:{}", deviceId, channelId); + throw new ControllerException(ErrorCode.ERROR100.getCode(), "未找到设备:" + deviceId); + } + + DeviceChannel channel = channelService.getOne(deviceId, channelId); + if (channel == null) { + log.warn("[开始历史媒体下载] 未找到通道 deviceId: {},channelId:{}", deviceId, channelId); + throw new ControllerException(ErrorCode.ERROR100.getCode(), "未找到通道:" + channelId); + } + playService.download(device, channel, startTime, endTime, Integer.parseInt(downloadSpeed), + (code, msg, data)->{ + + WVPResult wvpResult = new WVPResult<>(); + if (code == InviteErrorCode.SUCCESS.getCode()) { + wvpResult.setCode(ErrorCode.SUCCESS.getCode()); + wvpResult.setMsg(ErrorCode.SUCCESS.getMsg()); + + if (data != null) { + StreamInfo streamInfo = (StreamInfo)data; + if (userSetting.getUseSourceIpAsStreamIp()) { + streamInfo.changeStreamIp(request.getLocalAddr()); + } + wvpResult.setData(new StreamContent(streamInfo)); + } + }else { + wvpResult.setCode(code); + wvpResult.setMsg(msg); + } + requestMessage.setData(wvpResult); + resultHolder.invokeResult(requestMessage); + }); + + return result; + } + + @Operation(summary = "停止历史媒体下载", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "deviceId", description = "设备国标编号", required = true) + @Parameter(name = "channelId", description = "通道国标编号", required = true) + @Parameter(name = "stream", description = "流ID", required = true) + @GetMapping("/download/stop/{deviceId}/{channelId}/{stream}") + public void downloadStop(@PathVariable String deviceId, @PathVariable String channelId, @PathVariable String stream) { + + if (log.isDebugEnabled()) { + log.debug(String.format("设备历史媒体下载停止 API调用,deviceId/channelId:%s_%s", deviceId, channelId)); + } + + if (deviceId == null || channelId == null) { + throw new ControllerException(ErrorCode.ERROR400); + } + + Device device = deviceService.getDeviceByDeviceId(deviceId); + if (device == null) { + throw new ControllerException(ErrorCode.ERROR400.getCode(), "设备:" + deviceId + " 未找到"); + } + DeviceChannel deviceChannel = channelService.getOneForSource(deviceId, channelId); + if (deviceChannel == null) { + throw new ControllerException(ErrorCode.ERROR400.getCode(), "通道:" + channelId + " 未找到"); + } + playService.stop(InviteSessionType.DOWNLOAD, device, deviceChannel, stream); + } + + @Operation(summary = "获取历史媒体下载进度", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "deviceId", description = "设备国标编号", required = true) + @Parameter(name = "channelId", description = "通道国标编号", required = true) + @Parameter(name = "stream", description = "流ID", required = true) + @GetMapping("/download/progress/{deviceId}/{channelId}/{stream}") + public StreamContent getProgress(@PathVariable String deviceId, @PathVariable String channelId, @PathVariable String stream) { + Device device = deviceService.getDeviceByDeviceId(deviceId); + if (device == null) { + log.warn("[获取历史媒体下载进度] 未找到设备 deviceId: {},channelId:{}", deviceId, channelId); + throw new ControllerException(ErrorCode.ERROR100.getCode(), "未找到设备:" + deviceId); + } + + DeviceChannel channel = channelService.getOne(deviceId, channelId); + if (channel == null) { + log.warn("[获取历史媒体下载进度] 未找到通道 deviceId: {},channelId:{}", deviceId, channelId); + throw new ControllerException(ErrorCode.ERROR100.getCode(), "未找到通道:" + channelId); + } + StreamInfo downLoadInfo = playService.getDownLoadInfo(device, channel, stream); + if (downLoadInfo == null) { + throw new ControllerException(ErrorCode.ERROR404); + } + return new StreamContent(downLoadInfo); + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/controller/GroupController.java b/src/main/java/com/genersoft/iot/vmp/gb28181/controller/GroupController.java new file mode 100644 index 0000000..7a4578a --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/controller/GroupController.java @@ -0,0 +1,116 @@ +package com.genersoft.iot.vmp.gb28181.controller; + +import com.genersoft.iot.vmp.conf.exception.ControllerException; +import com.genersoft.iot.vmp.gb28181.bean.Group; +import com.genersoft.iot.vmp.gb28181.bean.GroupTree; +import com.genersoft.iot.vmp.gb28181.service.IGroupService; +import com.genersoft.iot.vmp.vmanager.bean.ErrorCode; +import com.github.pagehelper.PageInfo; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@Slf4j +@Tag(name = "分组管理") +@RestController +@RequestMapping("/api/group") +public class GroupController { + + @Autowired + private IGroupService groupService; + + @Operation(summary = "添加分组") + @Parameter(name = "group", description = "group", required = true) + @ResponseBody + @PostMapping("/add") + public void add(@RequestBody Group group){ + groupService.add(group); + } + + @Operation(summary = "查询分组节点") + @Parameter(name = "query", description = "要搜索的内容", required = true) + @Parameter(name = "parent", description = "所属分组编号", required = true) + @ResponseBody + @GetMapping("/tree/list") + public List queryForTree( + @RequestParam(required = false) String query, + @RequestParam(required = false) Integer parent, + @RequestParam(required = false) Boolean hasChannel + ){ + if (ObjectUtils.isEmpty(query)) { + query = null; + } + return groupService.queryForTree(query, parent, hasChannel); + } + + @Operation(summary = "查询分组") + @Parameter(name = "query", description = "要搜索的内容", required = true) + @Parameter(name = "channel", description = "true为查询通道,false为查询节点", required = true) + @ResponseBody + @GetMapping("/tree/query") + public PageInfo queryTree(Integer page, Integer count, + @RequestParam(required = true) String query + ){ + return groupService.queryList(page, count, query); + } + + @Operation(summary = "更新分组") + @Parameter(name = "group", description = "Group", required = true) + @ResponseBody + @PostMapping("/update") + public void update(@RequestBody Group group){ + groupService.update(group); + } + + @Operation(summary = "删除分组") + @Parameter(name = "id", description = "分组id", required = true) + @ResponseBody + @DeleteMapping("/delete") + public void delete(Integer id){ + Assert.notNull(id, "分组id(deviceId)不需要存在"); + boolean result = groupService.delete(id); + if (!result) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "移除失败"); + } + } + + @Operation(summary = "获取所属的行政区划下的行政区划") + @Parameter(name = "deviceId", description = "当前的行政区划", required = false) + @ResponseBody + @GetMapping("/path") + public List getPath(String deviceId, String businessGroup){ + return groupService.getPath(deviceId, businessGroup); + } + + @Operation(summary = "从第三方同步组织结构") + @ResponseBody + @GetMapping("/sync") + public void sync(){ + groupService.sync(); + } + +// @Operation(summary = "根据分组Id查询分组") +// @Parameter(name = "groupDeviceId", description = "分组节点编号", required = true) +// @ResponseBody +// @GetMapping("/one") +// public Group queryGroupByDeviceId( +// @RequestParam(required = true) String deviceId +// ){ +// Assert.hasLength(deviceId, ""); +// return groupService.queryGroupByDeviceId(deviceId); +// } + +// @Operation(summary = "从通道中同步分组") +// @ResponseBody +// @GetMapping("/sync") +// public void sync(){ +// groupService.syncFromChannel(); +// } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/controller/MediaController.java b/src/main/java/com/genersoft/iot/vmp/gb28181/controller/MediaController.java new file mode 100755 index 0000000..ef07950 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/controller/MediaController.java @@ -0,0 +1,155 @@ +package com.genersoft.iot.vmp.gb28181.controller; + +import com.genersoft.iot.vmp.common.StreamInfo; +import com.genersoft.iot.vmp.conf.exception.ControllerException; +import com.genersoft.iot.vmp.conf.security.JwtUtils; +import com.genersoft.iot.vmp.conf.security.SecurityUtils; +import com.genersoft.iot.vmp.conf.security.dto.LoginUser; +import com.genersoft.iot.vmp.media.service.IMediaServerService; +import com.genersoft.iot.vmp.media.zlm.dto.StreamAuthorityInfo; +import com.genersoft.iot.vmp.service.bean.ErrorCallback; +import com.genersoft.iot.vmp.service.bean.InviteErrorCode; +import com.genersoft.iot.vmp.storager.IRedisCatchStorage; +import com.genersoft.iot.vmp.streamProxy.service.IStreamProxyService; +import com.genersoft.iot.vmp.vmanager.bean.ErrorCode; +import com.genersoft.iot.vmp.vmanager.bean.StreamContent; +import com.genersoft.iot.vmp.vmanager.bean.WVPResult; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.servlet.http.HttpServletRequest; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.util.ObjectUtils; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.context.request.async.DeferredResult; + +import java.net.MalformedURLException; +import java.net.URL; + + +@Tag(name = "媒体流相关") +@RestController +@Slf4j +@RequestMapping(value = "/api/media") +public class MediaController { + + @Autowired + private IRedisCatchStorage redisCatchStorage; + + @Autowired + private IStreamProxyService streamProxyService; + + @Autowired + private IMediaServerService mediaServerService; + + + /** + * 根据应用名和流id获取播放地址 + * @param app 应用名 + * @param stream 流id + * @return + */ + @Operation(summary = "根据应用名和流id获取播放地址", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "app", description = "应用名", required = true) + @Parameter(name = "stream", description = "流id", required = true) + @Parameter(name = "mediaServerId", description = "媒体服务器id") + @Parameter(name = "callId", description = "推流时携带的自定义鉴权ID") + @Parameter(name = "useSourceIpAsStreamIp", description = "是否使用请求IP作为返回的地址IP") + @GetMapping(value = "/stream_info_by_app_and_stream") + @ResponseBody + public DeferredResult> getStreamInfoByAppAndStream(HttpServletRequest request, @RequestParam String app, + @RequestParam String stream, + @RequestParam(required = false) String mediaServerId, + @RequestParam(required = false) String callId, + @RequestParam(required = false) Boolean useSourceIpAsStreamIp){ + DeferredResult> result = new DeferredResult<>(); + boolean authority = false; + if (callId != null) { + // 权限校验 + StreamAuthorityInfo streamAuthorityInfo = redisCatchStorage.getStreamAuthorityInfo(app, stream); + if (streamAuthorityInfo != null + && streamAuthorityInfo.getCallId() != null + && streamAuthorityInfo.getCallId().equals(callId)) { + authority = true; + }else { + throw new ControllerException(ErrorCode.ERROR400.getCode(), "获取播放地址鉴权失败"); + } + }else { + // 是否登陆用户, 登陆用户返回完整信息 + LoginUser userInfo = SecurityUtils.getUserInfo(); + if (userInfo!= null) { + authority = true; + } + } + StreamInfo streamInfo; + if (useSourceIpAsStreamIp != null && useSourceIpAsStreamIp) { + String host = request.getHeader("Host"); + String localAddr = host.split(":")[0]; + log.info("使用{}作为返回流的ip", localAddr); + streamInfo = mediaServerService.getStreamInfoByAppAndStreamWithCheck(app, stream, mediaServerId, localAddr, authority); + }else { + streamInfo = mediaServerService.getStreamInfoByAppAndStreamWithCheck(app, stream, mediaServerId, authority); + } + + if (streamInfo != null){ + WVPResult wvpResult = WVPResult.success(); + wvpResult.setData(new StreamContent(streamInfo)); + result.setResult(wvpResult); + }else { + ErrorCallback callback = (code, msg, streamInfoStoStart) -> { + if (code == InviteErrorCode.SUCCESS.getCode()) { + WVPResult wvpResult = WVPResult.success(); + if (useSourceIpAsStreamIp != null && useSourceIpAsStreamIp) { + String host; + try { + URL url=new URL(request.getRequestURL().toString()); + host=url.getHost(); + } catch (MalformedURLException e) { + host=request.getLocalAddr(); + } + streamInfoStoStart.changeStreamIp(host); + } + if (!ObjectUtils.isEmpty(streamInfoStoStart.getMediaServer().getTranscodeSuffix()) + && !"null".equalsIgnoreCase(streamInfoStoStart.getMediaServer().getTranscodeSuffix())) { + streamInfoStoStart.setStream(streamInfoStoStart.getStream() + "_" + streamInfoStoStart.getMediaServer().getTranscodeSuffix()); + } + wvpResult.setData(new StreamContent(streamInfoStoStart)); + result.setResult(wvpResult); + }else { + result.setResult(WVPResult.fail(code, msg)); + } + }; + //获取流失败,重启拉流后重试一次 + streamProxyService.startByAppAndStream(app, stream, callback); + } + return result; + } + /** + * 获取推流播放地址 + * @param app 应用名 + * @param stream 流id + * @return + */ + @GetMapping(value = "/getPlayUrl") + @ResponseBody + @Operation(summary = "获取推流播放地址", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "app", description = "应用名", required = true) + @Parameter(name = "stream", description = "流id", required = true) + @Parameter(name = "mediaServerId", description = "媒体服务器id") + public StreamContent getPlayUrl(@RequestParam String app, @RequestParam String stream, + @RequestParam(required = false) String mediaServerId){ + boolean authority = false; + // 是否登陆用户, 登陆用户返回完整信息 + LoginUser userInfo = SecurityUtils.getUserInfo(); + if (userInfo!= null) { + authority = true; + } + StreamInfo streamInfo = mediaServerService.getStreamInfoByAppAndStreamWithCheck(app, stream, mediaServerId, authority); + if (streamInfo == null){ + throw new ControllerException(ErrorCode.ERROR100.getCode(), "获取播放地址失败"); + } + return new StreamContent(streamInfo); + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/controller/MobilePositionController.java b/src/main/java/com/genersoft/iot/vmp/gb28181/controller/MobilePositionController.java new file mode 100755 index 0000000..7ef9635 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/controller/MobilePositionController.java @@ -0,0 +1,152 @@ +package com.genersoft.iot.vmp.gb28181.controller; + +import com.genersoft.iot.vmp.conf.exception.ControllerException; +import com.genersoft.iot.vmp.conf.security.JwtUtils; +import com.genersoft.iot.vmp.gb28181.bean.Device; +import com.genersoft.iot.vmp.gb28181.bean.MobilePosition; +import com.genersoft.iot.vmp.gb28181.service.IDeviceService; +import com.genersoft.iot.vmp.gb28181.transmit.callback.DeferredResultHolder; +import com.genersoft.iot.vmp.gb28181.transmit.callback.RequestMessage; +import com.genersoft.iot.vmp.gb28181.transmit.cmd.ISIPCommander; +import com.genersoft.iot.vmp.service.IMobilePositionService; +import com.genersoft.iot.vmp.vmanager.bean.ErrorCode; +import com.github.pagehelper.util.StringUtil; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.context.request.async.DeferredResult; + +import javax.sip.InvalidArgumentException; +import javax.sip.SipException; +import java.text.ParseException; +import java.util.List; +import java.util.UUID; + +/** + * 位置信息管理 + */ +@Tag(name = "位置信息管理") +@Slf4j +@RestController +@RequestMapping("/api/position") +public class MobilePositionController { + + @Autowired + private IMobilePositionService mobilePositionService; + + @Autowired + private ISIPCommander cmder; + + @Autowired + private DeferredResultHolder resultHolder; + + @Autowired + private IDeviceService deviceService; + + + /** + * 查询历史轨迹 + * @param deviceId 设备ID + * @param start 开始时间 + * @param end 结束时间 + * @return + */ + @Operation(summary = "查询历史轨迹", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "deviceId", description = "设备国标编号", required = true) + @Parameter(name = "channelId", description = "通道国标编号") + @Parameter(name = "start", description = "开始时间") + @Parameter(name = "end", description = "结束时间") + @GetMapping("/history/{deviceId}") + public List positions(@PathVariable String deviceId, + @RequestParam(required = false) String channelId, + @RequestParam(required = false) String start, + @RequestParam(required = false) String end) { + + if (StringUtil.isEmpty(start)) { + start = null; + } + if (StringUtil.isEmpty(end)) { + end = null; + } + return mobilePositionService.queryMobilePositions(deviceId, channelId, start, end); + } + + /** + * 查询设备最新位置 + * @param deviceId 设备ID + * @return + */ + @Operation(summary = "查询设备最新位置", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "deviceId", description = "设备国标编号", required = true) + @GetMapping("/latest/{deviceId}") + public MobilePosition latestPosition(@PathVariable String deviceId) { + return mobilePositionService.queryLatestPosition(deviceId); + } + + /** + * 获取移动位置信息 + * @param deviceId 设备ID + * @return + */ + @Operation(summary = "获取移动位置信息", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "deviceId", description = "设备国标编号", required = true) + @GetMapping("/realtime/{deviceId}") + public DeferredResult realTimePosition(@PathVariable String deviceId) { + Device device = deviceService.getDeviceByDeviceId(deviceId); + String uuid = UUID.randomUUID().toString(); + String key = DeferredResultHolder.CALLBACK_CMD_MOBILE_POSITION + deviceId; + try { + cmder.mobilePostitionQuery(device, event -> { + RequestMessage msg = new RequestMessage(); + msg.setId(uuid); + msg.setKey(key); + msg.setData(String.format("获取移动位置信息失败,错误码: %s, %s", event.statusCode, event.msg)); + resultHolder.invokeResult(msg); + }); + } catch (InvalidArgumentException | SipException | ParseException e) { + log.error("[命令发送失败] 获取移动位置信息: {}", e.getMessage()); + throw new ControllerException(ErrorCode.ERROR100.getCode(), "命令发送失败: " + e.getMessage()); + } + DeferredResult result = new DeferredResult(5*1000L); + result.onTimeout(()->{ + log.warn(String.format("获取移动位置信息超时")); + // 释放rtpserver + RequestMessage msg = new RequestMessage(); + msg.setId(uuid); + msg.setKey(key); + msg.setData("Timeout"); + resultHolder.invokeResult(msg); + }); + resultHolder.put(key, uuid, result); + return result; + } + + /** + * 订阅位置信息 + * @param deviceId 设备ID + * @param expires 订阅超时时间 + * @param interval 上报时间间隔 + * @return true = 命令发送成功 + */ + @Operation(summary = "订阅位置信息", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "deviceId", description = "设备国标编号", required = true) + @Parameter(name = "expires", description = "订阅超时时间", required = true) + @Parameter(name = "interval", description = "上报时间间隔", required = true) + @GetMapping("/subscribe/{deviceId}") + public void positionSubscribe(@PathVariable String deviceId, + @RequestParam String expires, + @RequestParam String interval) { + + if (StringUtil.isEmpty(interval)) { + interval = "5"; + } + Device device = deviceService.getDeviceByDeviceId(deviceId); + device.setSubscribeCycleForMobilePosition(Integer.parseInt(expires)); + device.setMobilePositionSubmissionInterval(Integer.parseInt(interval)); + deviceService.updateCustomDevice(device); + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/controller/PlatformController.java b/src/main/java/com/genersoft/iot/vmp/gb28181/controller/PlatformController.java new file mode 100755 index 0000000..cba750a --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/controller/PlatformController.java @@ -0,0 +1,285 @@ +package com.genersoft.iot.vmp.gb28181.controller; + +import com.alibaba.fastjson2.JSONObject; +import com.genersoft.iot.vmp.conf.SipConfig; +import com.genersoft.iot.vmp.conf.exception.ControllerException; +import com.genersoft.iot.vmp.conf.security.JwtUtils; +import com.genersoft.iot.vmp.gb28181.bean.Platform; +import com.genersoft.iot.vmp.gb28181.bean.PlatformChannel; +import com.genersoft.iot.vmp.gb28181.bean.SubscribeHolder; +import com.genersoft.iot.vmp.gb28181.controller.bean.UpdateChannelParam; +import com.genersoft.iot.vmp.gb28181.service.IPlatformChannelService; +import com.genersoft.iot.vmp.gb28181.service.IPlatformService; +import com.genersoft.iot.vmp.utils.DateUtil; +import com.genersoft.iot.vmp.vmanager.bean.ErrorCode; +import com.genersoft.iot.vmp.vmanager.bean.WVPResult; +import com.github.pagehelper.PageInfo; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.context.request.async.DeferredResult; + +/** + * 级联平台管理 + */ +@Tag(name = "级联平台管理") +@Slf4j +@RestController +@RequestMapping("/api/platform") +public class PlatformController { + + @Autowired + private IPlatformChannelService platformChannelService; + + @Autowired + private SubscribeHolder subscribeHolder; + + @Autowired + private SipConfig sipConfig; + + @Autowired + private IPlatformService platformService; + + + @Operation(summary = "获取国标服务的配置", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @GetMapping("/server_config") + public JSONObject serverConfig() { + JSONObject result = new JSONObject(); + result.put("deviceIp", sipConfig.getShowIp()); + result.put("devicePort", sipConfig.getPort()); + result.put("username", sipConfig.getId()); + result.put("password", sipConfig.getPassword()); + return result; + } + + @Operation(summary = "获取级联服务器信息", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "id", description = "平台国标编号", required = true) + @GetMapping("/info/{id}") + public Platform getPlatform(@PathVariable String id) { + Platform parentPlatform = platformService.queryPlatformByServerGBId(id); + if (parentPlatform != null) { + return parentPlatform; + } else { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "未查询到此平台"); + } + } + + @GetMapping("/query") + @Operation(summary = "分页查询级联平台", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "page", description = "当前页") + @Parameter(name = "count", description = "每页查询数量") + @Parameter(name = "query", description = "查询内容") + public PageInfo platforms(int page, int count, + @RequestParam(required = false) String query) { + + PageInfo parentPlatformPageInfo = platformService.queryPlatformList(page, count, query); + if (parentPlatformPageInfo != null && !parentPlatformPageInfo.getList().isEmpty()) { + for (Platform platform : parentPlatformPageInfo.getList()) { + platform.setMobilePositionSubscribe(subscribeHolder.getMobilePositionSubscribe(platform.getServerGBId()) != null); + platform.setCatalogSubscribe(subscribeHolder.getCatalogSubscribe(platform.getServerGBId()) != null); + } + } + return parentPlatformPageInfo; + } + + @Operation(summary = "添加上级平台信息", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @PostMapping("/add") + @ResponseBody + public void add(@RequestBody Platform platform) { + + Assert.notNull(platform.getName(), "平台名称不可为空"); + Assert.notNull(platform.getServerGBId(), "上级平台国标编号不可为空"); + Assert.notNull(platform.getServerIp(), "上级平台IP不可为空"); + Assert.isTrue(platform.getServerPort() > 0 && platform.getServerPort() < 65535, "上级平台端口异常"); + Assert.notNull(platform.getDeviceGBId(), "本平台国标编号不可为空"); + + if (ObjectUtils.isEmpty(platform.getServerGBDomain())) { + platform.setServerGBDomain(platform.getServerGBId().substring(0, 6)); + } + + if (platform.getExpires() <= 0) { + platform.setExpires(3600); + } + + if (platform.getKeepTimeout() <= 0) { + platform.setKeepTimeout(60); + } + + if (ObjectUtils.isEmpty(platform.getTransport())) { + platform.setTransport("UDP"); + } + + if (ObjectUtils.isEmpty(platform.getCharacterSet())) { + platform.setCharacterSet("GB2312"); + } + + Platform parentPlatformOld = platformService.queryPlatformByServerGBId(platform.getServerGBId()); + if (parentPlatformOld != null) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "平台 " + platform.getServerGBId() + " 已存在"); + } + platform.setCreateTime(DateUtil.getNow()); + platform.setUpdateTime(DateUtil.getNow()); + boolean updateResult = platformService.add(platform); + + if (!updateResult) { + throw new ControllerException(ErrorCode.ERROR100); + } + } + + @Operation(summary = "更新上级平台信息", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @PostMapping("/update") + @ResponseBody + public void updatePlatform(@RequestBody Platform parentPlatform) { + + if (ObjectUtils.isEmpty(parentPlatform.getName()) + || ObjectUtils.isEmpty(parentPlatform.getServerGBId()) + || ObjectUtils.isEmpty(parentPlatform.getServerGBDomain()) + || ObjectUtils.isEmpty(parentPlatform.getServerIp()) + || ObjectUtils.isEmpty(parentPlatform.getServerPort()) + || ObjectUtils.isEmpty(parentPlatform.getDeviceGBId()) + || ObjectUtils.isEmpty(parentPlatform.getExpires()) + || ObjectUtils.isEmpty(parentPlatform.getKeepTimeout()) + || ObjectUtils.isEmpty(parentPlatform.getTransport()) + || ObjectUtils.isEmpty(parentPlatform.getCharacterSet()) + ) { + throw new ControllerException(ErrorCode.ERROR400); + } + platformService.update(parentPlatform); + } + + @Operation(summary = "删除上级平台", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "id", description = "上级平台ID") + @DeleteMapping("/delete") + @ResponseBody + public DeferredResult> deletePlatform(Integer id) { + + if (log.isDebugEnabled()) { + log.debug("删除上级平台API调用"); + } + DeferredResult> deferredResult = new DeferredResult<>(); + + platformService.delete(id, (object)-> deferredResult.setResult(WVPResult.success())); + return deferredResult; + } + + @Operation(summary = "查询上级平台是否存在", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "serverGBId", description = "上级平台的国标编号") + @GetMapping("/exit/{serverGBId}") + @ResponseBody + public Boolean exitPlatform(@PathVariable String serverGBId) { + Platform platform = platformService.queryPlatformByServerGBId(serverGBId); + return platform != null; + } + + @Operation(summary = "分页查询级联平台的所有所有通道", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "page", description = "当前页", required = true) + @Parameter(name = "count", description = "每页条数", required = true) + @Parameter(name = "platformId", description = "上级平台的数据ID") + @Parameter(name = "channelType", description = "通道类型, 0:国标设备,1:推流设备,2:拉流代理") + @Parameter(name = "query", description = "查询内容") + @Parameter(name = "online", description = "是否在线") + @Parameter(name = "hasShare", description = "是否已经共享") + @GetMapping("/channel/list") + @ResponseBody + public PageInfo queryChannelList(int page, int count, + @RequestParam(required = false) Integer platformId, + @RequestParam(required = false) String query, + @RequestParam(required = false) Integer channelType, + @RequestParam(required = false) Boolean online, + @RequestParam(required = false) Boolean hasShare) { + + Assert.notNull(platformId, "上级平台的数据ID不可为NULL"); + if (ObjectUtils.isEmpty(query)) { + query = null; + } + + return platformChannelService.queryChannelList(page, count, query, channelType, online, platformId, hasShare); + } + + @Operation(summary = "向上级平台添加国标通道", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @PostMapping("/channel/add") + @ResponseBody + public void addChannel(@RequestBody UpdateChannelParam param) { + + if (log.isDebugEnabled()) { + log.debug("给上级平台添加国标通道API调用"); + } + int result = 0; + if (param.getChannelIds() == null || param.getChannelIds().isEmpty()) { + if (param.isAll()) { + log.info("[国标级联]添加所有通道到上级平台, {}", param.getPlatformId()); + result = platformChannelService.addAllChannel(param.getPlatformId()); + } + }else { + result = platformChannelService.addChannels(param.getPlatformId(), param.getChannelIds()); + } + if (result <= 0) { + throw new ControllerException(ErrorCode.ERROR100); + } + } + + @Operation(summary = "从上级平台移除国标通道", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @DeleteMapping("/channel/remove") + @ResponseBody + public void delChannelForGB(@RequestBody UpdateChannelParam param) { + + if (log.isDebugEnabled()) { + log.debug("给上级平台删除国标通道API调用"); + } + int result = 0; + if (param.getChannelIds() == null || param.getChannelIds().isEmpty()) { + if (param.isAll()) { + log.info("[国标级联]移除所有通道,上级平台, {}", param.getPlatformId()); + result = platformChannelService.removeAllChannel(param.getPlatformId()); + } + }else { + result = platformChannelService.removeChannels(param.getPlatformId(), param.getChannelIds()); + } + if (result <= 0) { + throw new ControllerException(ErrorCode.ERROR100); + } + } + + @Operation(summary = "推送通道", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "id", description = "平台ID", required = true) + @GetMapping("/channel/push") + @ResponseBody + public void pushChannel(Integer id) { + Assert.notNull(id, "平台ID不可为空"); + platformChannelService.pushChannel(id); + } + + @Operation(summary = "添加通道-通过设备", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @PostMapping("/channel/device/add") + @ResponseBody + public void addChannelByDevice(@RequestBody UpdateChannelParam param) { + Assert.notNull(param.getPlatformId(), "平台ID不可为空"); + Assert.notNull(param.getDeviceIds(), "设备ID不可为空"); + Assert.notEmpty(param.getDeviceIds(), "设备ID不可为空"); + platformChannelService.addChannelByDevice(param.getPlatformId(), param.getDeviceIds()); + } + + @Operation(summary = "移除通道-通过设备", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @PostMapping("/channel/device/remove") + @ResponseBody + public void removeChannelByDevice(@RequestBody UpdateChannelParam param) { + Assert.notNull(param.getPlatformId(), "平台ID不可为空"); + Assert.notNull(param.getDeviceIds(), "设备ID不可为空"); + Assert.notEmpty(param.getDeviceIds(), "设备ID不可为空"); + platformChannelService.removeChannelByDevice(param.getPlatformId(), param.getDeviceIds()); + } + + @Operation(summary = "自定义共享通道信息", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @PostMapping("/channel/custom/update") + @ResponseBody + public void updateCustomChannel(@RequestBody PlatformChannel channel) { + Assert.isTrue(channel.getId() > 0, "共享通道ID必须存在"); + platformChannelService.updateCustomChannel(channel); + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/controller/PlayController.java b/src/main/java/com/genersoft/iot/vmp/gb28181/controller/PlayController.java new file mode 100755 index 0000000..d4b75bf --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/controller/PlayController.java @@ -0,0 +1,276 @@ +package com.genersoft.iot.vmp.gb28181.controller; + +import com.alibaba.fastjson2.JSONArray; +import com.alibaba.fastjson2.JSONObject; +import com.genersoft.iot.vmp.common.InviteSessionType; +import com.genersoft.iot.vmp.common.StreamInfo; +import com.genersoft.iot.vmp.conf.UserSetting; +import com.genersoft.iot.vmp.conf.exception.ControllerException; +import com.genersoft.iot.vmp.conf.security.JwtUtils; +import com.genersoft.iot.vmp.gb28181.bean.Device; +import com.genersoft.iot.vmp.gb28181.bean.DeviceChannel; +import com.genersoft.iot.vmp.gb28181.bean.SsrcTransaction; +import com.genersoft.iot.vmp.gb28181.service.IDeviceChannelService; +import com.genersoft.iot.vmp.gb28181.service.IDeviceService; +import com.genersoft.iot.vmp.gb28181.service.IInviteStreamService; +import com.genersoft.iot.vmp.gb28181.service.IPlayService; +import com.genersoft.iot.vmp.gb28181.session.SipInviteSessionManager; +import com.genersoft.iot.vmp.gb28181.transmit.callback.DeferredResultHolder; +import com.genersoft.iot.vmp.gb28181.transmit.callback.RequestMessage; +import com.genersoft.iot.vmp.media.bean.MediaServer; +import com.genersoft.iot.vmp.media.service.IMediaServerService; +import com.genersoft.iot.vmp.service.bean.ErrorCallback; +import com.genersoft.iot.vmp.service.bean.InviteErrorCode; +import com.genersoft.iot.vmp.utils.DateUtil; +import com.genersoft.iot.vmp.vmanager.bean.AudioBroadcastResult; +import com.genersoft.iot.vmp.vmanager.bean.ErrorCode; +import com.genersoft.iot.vmp.vmanager.bean.StreamContent; +import com.genersoft.iot.vmp.vmanager.bean.WVPResult; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.context.request.async.DeferredResult; + +import jakarta.servlet.http.HttpServletRequest; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.List; +import java.util.UUID; + + +/** + * @author lin + */ +@Tag(name = "国标设备点播") +@Slf4j +@RestController +@RequestMapping("/api/play") +public class PlayController { + + @Autowired + private SipInviteSessionManager sessionManager; + + @Autowired + private IInviteStreamService inviteStreamService; + + @Autowired + private DeferredResultHolder resultHolder; + + @Autowired + private IPlayService playService; + + @Autowired + private IMediaServerService mediaServerService; + + @Autowired + private UserSetting userSetting; + + @Autowired + private IDeviceService deviceService; + + @Autowired + private IDeviceChannelService deviceChannelService; + + @Operation(summary = "开始点播", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "deviceId", description = "设备国标编号", required = true) + @Parameter(name = "channelId", description = "通道国标编号", required = true) + @GetMapping("/start/{deviceId}/{channelId}") + public DeferredResult> play(HttpServletRequest request, @PathVariable String deviceId, + @PathVariable String channelId) { + + log.info("[开始点播] deviceId:{}, channelId:{}, ", deviceId, channelId); + Assert.notNull(deviceId, "设备国标编号不可为NULL"); + Assert.notNull(channelId, "通道国标编号不可为NULL"); + // 获取可用的zlm + Device device = deviceService.getDeviceByDeviceId(deviceId); + Assert.notNull(deviceId, "设备不存在"); + DeviceChannel channel = deviceChannelService.getOne(deviceId, channelId); + Assert.notNull(channel, "通道不存在"); + + DeferredResult> result = new DeferredResult<>(userSetting.getPlayTimeout().longValue()); + + result.onTimeout(()->{ + log.info("[点播等待超时] deviceId:{}, channelId:{}, ", deviceId, channelId); + // 释放rtpserver + WVPResult wvpResult = new WVPResult<>(); + wvpResult.setCode(ErrorCode.ERROR100.getCode()); + wvpResult.setMsg("点播超时"); + result.setResult(wvpResult); + + inviteStreamService.removeInviteInfoByDeviceAndChannel(InviteSessionType.PLAY, channel.getId()); + deviceChannelService.stopPlay(channel.getId()); + }); + + ErrorCallback callback = (code, msg, streamInfo) -> { + WVPResult wvpResult = new WVPResult<>(); + if (code == InviteErrorCode.SUCCESS.getCode()) { + wvpResult.setCode(ErrorCode.SUCCESS.getCode()); + wvpResult.setMsg(ErrorCode.SUCCESS.getMsg()); + + if (streamInfo != null) { + if (userSetting.getUseSourceIpAsStreamIp()) { + streamInfo=streamInfo.clone();//深拷贝 + String host; + try { + URL url=new URL(request.getRequestURL().toString()); + host=url.getHost(); + } catch (MalformedURLException e) { + host=request.getLocalAddr(); + } + streamInfo.changeStreamIp(host); + } + if (!ObjectUtils.isEmpty(streamInfo.getMediaServer().getTranscodeSuffix()) && !"null".equalsIgnoreCase(streamInfo.getMediaServer().getTranscodeSuffix())) { + streamInfo.setStream(streamInfo.getStream() + "_" + streamInfo.getMediaServer().getTranscodeSuffix()); + } + wvpResult.setData(new StreamContent(streamInfo)); + }else { + wvpResult.setCode(code); + wvpResult.setMsg(msg); + } + }else { + wvpResult.setCode(code); + wvpResult.setMsg(msg); + } + result.setResult(wvpResult); + }; + playService.play(device, channel, callback); + return result; + } + + @Operation(summary = "停止点播", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "deviceId", description = "设备国标编号", required = true) + @Parameter(name = "channelId", description = "通道国标编号", required = true) + @GetMapping("/stop/{deviceId}/{channelId}") + public JSONObject playStop(@PathVariable String deviceId, @PathVariable String channelId) { + + log.debug(String.format("设备预览/回放停止API调用,streamId:%s_%s", deviceId, channelId )); + + if (deviceId == null || channelId == null) { + throw new ControllerException(ErrorCode.ERROR400); + } + + Device device = deviceService.getDeviceByDeviceId(deviceId); + DeviceChannel channel = deviceChannelService.getOneForSource(deviceId, channelId); + Assert.notNull(device, "设备不存在"); + Assert.notNull(channel, "通道不存在"); + String streamId = String.format("%s_%s", device.getDeviceId(), channel.getDeviceId()); + playService.stop(InviteSessionType.PLAY, device, channel, streamId); + JSONObject json = new JSONObject(); + json.put("deviceId", deviceId); + json.put("channelId", channelId); + return json; + } + /** + * 结束转码 + */ + @Operation(summary = "结束转码", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "key", description = "视频流key", required = true) + @Parameter(name = "mediaServerId", description = "流媒体服务ID", required = true) + @PostMapping("/convertStop/{key}") + public void playConvertStop(@PathVariable String key, String mediaServerId) { + if (mediaServerId == null) { + throw new ControllerException(ErrorCode.ERROR400.getCode(), "流媒体:" + mediaServerId + "不存在" ); + } + MediaServer mediaInfo = mediaServerService.getOne(mediaServerId); + if (mediaInfo == null) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "使用的流媒体已经停止运行" ); + }else { + Boolean deleted = mediaServerService.delFFmpegSource(mediaInfo, key); + if (!deleted) { + throw new ControllerException(ErrorCode.ERROR100 ); + } + } + } + + @Operation(summary = "语音广播命令", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "deviceId", description = "设备国标编号", required = true) + @Parameter(name = "deviceId", description = "通道国标编号", required = true) + @Parameter(name = "timeout", description = "推流超时时间(秒)", required = true) + @GetMapping("/broadcast/{deviceId}/{channelId}") + @PostMapping("/broadcast/{deviceId}/{channelId}") + public AudioBroadcastResult broadcastApi(@PathVariable String deviceId, @PathVariable String channelId, Integer timeout, Boolean broadcastMode) { + if (log.isDebugEnabled()) { + log.debug("语音广播API调用"); + } + + return playService.audioBroadcast(deviceId, channelId, broadcastMode); + + } + + @Operation(summary = "停止语音广播") + @Parameter(name = "deviceId", description = "设备Id", required = true) + @Parameter(name = "channelId", description = "通道Id", required = true) + @GetMapping("/broadcast/stop/{deviceId}/{channelId}") + @PostMapping("/broadcast/stop/{deviceId}/{channelId}") + public void stopBroadcast(@PathVariable String deviceId, @PathVariable String channelId) { + if (log.isDebugEnabled()) { + log.debug("停止语音广播API调用"); + } + Device device = deviceService.getDeviceByDeviceId(deviceId); + Assert.notNull(device, "设备不存在"); + DeviceChannel channel = deviceChannelService.getOne(deviceId, channelId); + Assert.notNull(channel, "通道不存在"); + playService.stopAudioBroadcast(device, channel); + } + + @Operation(summary = "获取所有的ssrc", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @GetMapping("/ssrc") + public JSONObject getSSRC() { + if (log.isDebugEnabled()) { + log.debug("获取所有的ssrc"); + } + JSONArray objects = new JSONArray(); + List allSsrc = sessionManager.getAll(); + for (SsrcTransaction transaction : allSsrc) { + JSONObject jsonObject = new JSONObject(); + jsonObject.put("deviceId", transaction.getDeviceId()); + jsonObject.put("channelId", transaction.getChannelId()); + jsonObject.put("ssrc", transaction.getSsrc()); + jsonObject.put("streamId", transaction.getStream()); + objects.add(jsonObject); + } + + JSONObject jsonObject = new JSONObject(); + jsonObject.put("data", objects); + jsonObject.put("count", objects.size()); + return jsonObject; + } + + @Operation(summary = "获取截图", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "deviceId", description = "设备国标编号", required = true) + @Parameter(name = "channelId", description = "通道国标编号", required = true) + @GetMapping("/snap") + public DeferredResult getSnap(String deviceId, String channelId) { + if (log.isDebugEnabled()) { + log.debug("获取截图: {}/{}", deviceId, channelId); + } + + DeferredResult result = new DeferredResult<>(3 * 1000L); + String key = DeferredResultHolder.CALLBACK_CMD_SNAP + deviceId; + String uuid = UUID.randomUUID().toString(); + resultHolder.put(key, uuid, result); + + RequestMessage message = new RequestMessage(); + message.setKey(key); + message.setId(uuid); + + String fileName = deviceId + "_" + channelId + "_" + DateUtil.getNowForUrl() + ".jpg"; + playService.getSnap(deviceId, channelId, fileName, (code, msg, data) -> { + if (code == InviteErrorCode.SUCCESS.getCode()) { + message.setData(data); + }else { + message.setData(WVPResult.fail(code, msg)); + } + resultHolder.invokeResult(message); + }); + return result; + } + +} + diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/controller/PlaybackController.java b/src/main/java/com/genersoft/iot/vmp/gb28181/controller/PlaybackController.java new file mode 100755 index 0000000..d5e47b5 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/controller/PlaybackController.java @@ -0,0 +1,220 @@ +package com.genersoft.iot.vmp.gb28181.controller; + +import com.genersoft.iot.vmp.common.InviteSessionType; +import com.genersoft.iot.vmp.common.StreamInfo; +import com.genersoft.iot.vmp.conf.UserSetting; +import com.genersoft.iot.vmp.conf.exception.ControllerException; +import com.genersoft.iot.vmp.conf.exception.ServiceException; +import com.genersoft.iot.vmp.conf.security.JwtUtils; +import com.genersoft.iot.vmp.gb28181.bean.Device; +import com.genersoft.iot.vmp.gb28181.bean.DeviceChannel; +import com.genersoft.iot.vmp.gb28181.service.IDeviceChannelService; +import com.genersoft.iot.vmp.gb28181.service.IDeviceService; +import com.genersoft.iot.vmp.gb28181.service.IInviteStreamService; +import com.genersoft.iot.vmp.gb28181.service.IPlayService; +import com.genersoft.iot.vmp.gb28181.transmit.callback.DeferredResultHolder; +import com.genersoft.iot.vmp.gb28181.transmit.callback.RequestMessage; +import com.genersoft.iot.vmp.gb28181.transmit.cmd.impl.SIPCommander; +import com.genersoft.iot.vmp.service.bean.InviteErrorCode; +import com.genersoft.iot.vmp.vmanager.bean.ErrorCode; +import com.genersoft.iot.vmp.vmanager.bean.StreamContent; +import com.genersoft.iot.vmp.vmanager.bean.WVPResult; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.context.request.async.DeferredResult; + +import jakarta.servlet.http.HttpServletRequest; +import javax.sip.InvalidArgumentException; +import javax.sip.SipException; +import java.net.MalformedURLException; +import java.net.URL; +import java.text.ParseException; +import java.util.UUID; + +/** + * @author lin + */ +@Tag(name = "视频回放") +@Slf4j +@RestController +@RequestMapping("/api/playback") +public class PlaybackController { + + @Autowired + private SIPCommander cmder; + + @Autowired + private IInviteStreamService inviteStreamService; + + @Autowired + private IPlayService playService; + + @Autowired + private DeferredResultHolder resultHolder; + + @Autowired + private UserSetting userSetting; + + @Autowired + private IDeviceService deviceService; + + @Autowired + private IDeviceChannelService channelService; + + @Operation(summary = "开始视频回放", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "deviceId", description = "设备国标编号", required = true) + @Parameter(name = "channelId", description = "通道国标编号", required = true) + @Parameter(name = "startTime", description = "开始时间", required = true) + @Parameter(name = "endTime", description = "结束时间", required = true) + @GetMapping("/start/{deviceId}/{channelId}") + public DeferredResult> start(HttpServletRequest request, @PathVariable String deviceId, @PathVariable String channelId, + String startTime, String endTime) { + + if (log.isDebugEnabled()) { + log.debug(String.format("设备回放 API调用,deviceId:%s ,channelId:%s", deviceId, channelId)); + } + + String uuid = UUID.randomUUID().toString(); + String key = DeferredResultHolder.CALLBACK_CMD_PLAYBACK + deviceId + channelId; + DeferredResult> result = new DeferredResult<>(userSetting.getPlayTimeout().longValue()); + resultHolder.put(key, uuid, result); + + RequestMessage requestMessage = new RequestMessage(); + requestMessage.setKey(key); + requestMessage.setId(uuid); + Device device = deviceService.getDeviceByDeviceId(deviceId); + if (device == null) { + log.warn("[录像回放] 未找到设备 deviceId: {},channelId:{}", deviceId, channelId); + throw new ControllerException(ErrorCode.ERROR100.getCode(), "未找到设备:" + deviceId); + } + + DeviceChannel channel = channelService.getOne(deviceId, channelId); + if (channel == null) { + log.warn("[录像回放] 未找到通道 deviceId: {},channelId:{}", deviceId, channelId); + throw new ControllerException(ErrorCode.ERROR100.getCode(), "未找到通道:" + channelId); + } + playService.playBack(device, channel, startTime, endTime, + (code, msg, data)->{ + + WVPResult wvpResult = new WVPResult<>(); + if (code == InviteErrorCode.SUCCESS.getCode()) { + wvpResult.setCode(ErrorCode.SUCCESS.getCode()); + wvpResult.setMsg(ErrorCode.SUCCESS.getMsg()); + + if (data != null) { + StreamInfo streamInfo = (StreamInfo)data; + if (userSetting.getUseSourceIpAsStreamIp()) { + streamInfo=streamInfo.clone();//深拷贝 + String host; + try { + URL url=new URL(request.getRequestURL().toString()); + host=url.getHost(); + } catch (MalformedURLException e) { + host=request.getLocalAddr(); + } + streamInfo.changeStreamIp(host); + } + wvpResult.setData(new StreamContent(streamInfo)); + } + }else { + wvpResult.setCode(code); + wvpResult.setMsg(msg); + } + requestMessage.setData(wvpResult); + resultHolder.invokeResult(requestMessage); + }); + + return result; + } + + + @Operation(summary = "停止视频回放", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "deviceId", description = "设备国标编号", required = true) + @Parameter(name = "channelId", description = "通道国标编号", required = true) + @Parameter(name = "stream", description = "流ID", required = true) + @GetMapping("/stop/{deviceId}/{channelId}/{stream}") + public void playStop( + @PathVariable String deviceId, + @PathVariable String channelId, + @PathVariable String stream) { + if (ObjectUtils.isEmpty(deviceId) || ObjectUtils.isEmpty(channelId) || ObjectUtils.isEmpty(stream)) { + throw new ControllerException(ErrorCode.ERROR400); + } + Device device = deviceService.getDeviceByDeviceId(deviceId); + if (device == null) { + throw new ControllerException(ErrorCode.ERROR400.getCode(), "设备:" + deviceId + " 未找到"); + } + DeviceChannel deviceChannel = channelService.getOneForSource(deviceId, channelId); + if (deviceChannel == null) { + throw new ControllerException(ErrorCode.ERROR400.getCode(), "通道:" + channelId + " 未找到"); + } + playService.stop(InviteSessionType.PLAYBACK, device, deviceChannel, stream); + } + + + @Operation(summary = "回放暂停", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "streamId", description = "回放流ID", required = true) + @GetMapping("/pause/{streamId}") + public void playbackPause(@PathVariable String streamId) { + log.info("[回放暂停] streamId: {}", streamId); + try { + playService.playbackPause(streamId); + } catch (ServiceException e) { + throw new ControllerException(ErrorCode.ERROR400.getCode(), e.getMessage()); + } catch (InvalidArgumentException | ParseException | SipException e) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), e.getMessage()); + } + } + + + @Operation(summary = "回放恢复", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "streamId", description = "回放流ID", required = true) + @GetMapping("/resume/{streamId}") + public void playResume(@PathVariable String streamId) { + log.info("playResume: "+streamId); + try { + playService.playbackResume(streamId); + } catch (ServiceException e) { + throw new ControllerException(ErrorCode.ERROR400.getCode(), e.getMessage()); + } catch (InvalidArgumentException | ParseException | SipException e) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), e.getMessage()); + } + } + + @Operation(summary = "回放拖动播放", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "streamId", description = "回放流ID", required = true) + @Parameter(name = "seekTime", description = "拖动偏移量,单位s", required = true) + @GetMapping("/seek/{streamId}/{seekTime}") + public void playbackSeek(@PathVariable String streamId, @PathVariable long seekTime) { + log.info("playSeek: "+streamId+", "+seekTime); + try { + playService.playbackSeek(streamId, seekTime); + } catch (InvalidArgumentException | ParseException | SipException e) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), e.getMessage()); + } + } + + @Operation(summary = "回放倍速播放", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "streamId", description = "回放流ID", required = true) + @Parameter(name = "speed", description = "倍速0.25 0.5 1、2、4、8", required = true) + @GetMapping("/speed/{streamId}/{speed}") + public void playSpeed(@PathVariable String streamId, @PathVariable Double speed) { + Assert.notNull(speed, "倍速不存在"); + log.info("playSpeed: "+streamId+", "+speed); + try { + playService.playbackSpeed(streamId, speed); + } catch (InvalidArgumentException | ParseException | SipException e) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), e.getMessage()); + } + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/controller/PtzController.java b/src/main/java/com/genersoft/iot/vmp/gb28181/controller/PtzController.java new file mode 100755 index 0000000..2f3b3a9 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/controller/PtzController.java @@ -0,0 +1,483 @@ +package com.genersoft.iot.vmp.gb28181.controller; + + +import com.genersoft.iot.vmp.conf.exception.ControllerException; +import com.genersoft.iot.vmp.conf.security.JwtUtils; +import com.genersoft.iot.vmp.gb28181.bean.Device; +import com.genersoft.iot.vmp.gb28181.service.IDeviceService; +import com.genersoft.iot.vmp.gb28181.service.IPTZService; +import com.genersoft.iot.vmp.gb28181.transmit.callback.DeferredResultHolder; +import com.genersoft.iot.vmp.gb28181.transmit.cmd.impl.SIPCommander; +import com.genersoft.iot.vmp.vmanager.bean.ErrorCode; +import com.genersoft.iot.vmp.vmanager.bean.WVPResult; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.util.Assert; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.context.request.async.DeferredResult; + +@Tag(name = "前端设备控制") +@Slf4j +@RestController +@RequestMapping("/api/front-end") +public class PtzController { + + @Autowired + private SIPCommander cmder; + + @Autowired + private IDeviceService deviceService; + + @Autowired + private IPTZService ptzService; + + @Autowired + private DeferredResultHolder resultHolder; + + @Operation(summary = "通用前端控制命令(参考国标文档A.3.1指令格式)", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "deviceId", description = "设备国标编号", required = true) + @Parameter(name = "channelId", description = "通道国标编号", required = true) + @Parameter(name = "cmdCode", description = "指令码(对应国标文档指令格式中的字节4)", required = true) + @Parameter(name = "parameter1", description = "数据一(对应国标文档指令格式中的字节5, 范围0-255)", required = true) + @Parameter(name = "parameter2", description = "数据二(对应国标文档指令格式中的字节6, 范围0-255)", required = true) + @Parameter(name = "combindCode2", description = "组合码二(对应国标文档指令格式中的字节7, 范围0-15)", required = true) + @GetMapping("/common/{deviceId}/{channelId}") + public void frontEndCommand(@PathVariable String deviceId,@PathVariable String channelId,Integer cmdCode, Integer parameter1, Integer parameter2, Integer combindCode2){ + + if (log.isDebugEnabled()) { + log.debug(String.format("设备云台控制 API调用,deviceId:%s ,channelId:%s ,cmdCode:%d parameter1:%d parameter2:%d",deviceId, channelId, cmdCode, parameter1, parameter2)); + } + + if (parameter1 == null || parameter1 < 0 || parameter1 > 255) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "parameter1 为 0-255的数字"); + } + if (parameter2 == null || parameter2 < 0 || parameter2 > 255) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "parameter2 为 0-255的数字"); + } + if (combindCode2 == null || combindCode2 < 0 || combindCode2 > 15) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "combindCode2 为 0-15的数字"); + } + + Device device = deviceService.getDeviceByDeviceId(deviceId); + + Assert.notNull(device, "设备[" + deviceId + "]不存在"); + + ptzService.frontEndCommand(device, channelId, cmdCode, parameter1, parameter2, combindCode2); + } + + @Operation(summary = "云台控制", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "deviceId", description = "设备国标编号", required = true) + @Parameter(name = "channelId", description = "通道国标编号", required = true) + @Parameter(name = "command", description = "控制指令,允许值: left, right, up, down, upleft, upright, downleft, downright, zoomin, zoomout, stop", required = true) + @Parameter(name = "horizonSpeed", description = "水平速度(0-255)", required = true) + @Parameter(name = "verticalSpeed", description = "垂直速度(0-255)", required = true) + @Parameter(name = "zoomSpeed", description = "缩放速度(0-15)", required = true) + @GetMapping("/ptz/{deviceId}/{channelId}") + public void ptz(@PathVariable String deviceId,@PathVariable String channelId, String command, Integer horizonSpeed, Integer verticalSpeed, Integer zoomSpeed){ + + if (log.isDebugEnabled()) { + log.debug(String.format("设备云台控制 API调用,deviceId:%s ,channelId:%s ,command:%s ,horizonSpeed:%d ,verticalSpeed:%d ,zoomSpeed:%d",deviceId, channelId, command, horizonSpeed, verticalSpeed, zoomSpeed)); + } + if (horizonSpeed == null) { + horizonSpeed = 100; + }else if (horizonSpeed < 0 || horizonSpeed > 255) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "horizonSpeed 为 0-255的数字"); + } + if (verticalSpeed == null) { + verticalSpeed = 100; + }else if (verticalSpeed < 0 || verticalSpeed > 255) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "verticalSpeed 为 0-255的数字"); + } + if (zoomSpeed == null) { + zoomSpeed = 16; + }else if (zoomSpeed < 0 || zoomSpeed > 15) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "zoomSpeed 为 0-15的数字"); + } + + int cmdCode = 0; + switch (command){ + case "left": + cmdCode = 2; + break; + case "right": + cmdCode = 1; + break; + case "up": + cmdCode = 8; + break; + case "down": + cmdCode = 4; + break; + case "upleft": + cmdCode = 10; + break; + case "upright": + cmdCode = 9; + break; + case "downleft": + cmdCode = 6; + break; + case "downright": + cmdCode = 5; + break; + case "zoomin": + cmdCode = 16; + break; + case "zoomout": + cmdCode = 32; + break; + case "stop": + horizonSpeed = 0; + verticalSpeed = 0; + zoomSpeed = 0; + break; + default: + break; + } + frontEndCommand(deviceId, channelId, cmdCode, horizonSpeed, verticalSpeed, zoomSpeed); + } + + + @Operation(summary = "光圈控制", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "deviceId", description = "设备国标编号", required = true) + @Parameter(name = "channelId", description = "通道国标编号", required = true) + @Parameter(name = "command", description = "控制指令,允许值: in, out, stop", required = true) + @Parameter(name = "speed", description = "光圈速度(0-255)", required = true) + @GetMapping("/fi/iris/{deviceId}/{channelId}") + public void iris(@PathVariable String deviceId,@PathVariable String channelId, String command, Integer speed){ + + if (log.isDebugEnabled()) { + log.debug("设备光圈控制 API调用,deviceId:{} ,channelId:{} ,command:{} ,speed:{} ",deviceId, channelId, command, speed); + } + + if (speed == null) { + speed = 100; + }else if (speed < 0 || speed > 255) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "speed 为 0-255的数字"); + } + + int cmdCode = 0x40; + switch (command){ + case "in": + cmdCode = 0x44; + break; + case "out": + cmdCode = 0x48; + break; + case "stop": + speed = 0; + break; + default: + break; + } + frontEndCommand(deviceId, channelId, cmdCode, 0, speed, 0); + } + + @Operation(summary = "聚焦控制", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "deviceId", description = "设备国标编号", required = true) + @Parameter(name = "channelId", description = "通道国标编号", required = true) + @Parameter(name = "command", description = "控制指令,允许值: near, far, stop", required = true) + @Parameter(name = "speed", description = "聚焦速度(0-255)", required = true) + @GetMapping("/fi/focus/{deviceId}/{channelId}") + public void focus(@PathVariable String deviceId,@PathVariable String channelId, String command, Integer speed){ + + if (log.isDebugEnabled()) { + log.debug("设备聚焦控制 API调用,deviceId:{} ,channelId:{} ,command:{} ,speed:{} ",deviceId, channelId, command, speed); + } + + if (speed == null) { + speed = 100; + }else if (speed < 0 || speed > 255) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "speed 为 0-255的数字"); + } + + int cmdCode = 0x40; + switch (command){ + case "near": + cmdCode = 0x42; + break; + case "far": + cmdCode = 0x41; + break; + case "stop": + speed = 0; + break; + default: + break; + } + frontEndCommand(deviceId, channelId, cmdCode, speed, 0, 0); + } + + @Operation(summary = "查询预置位", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "deviceId", description = "设备国标编号", required = true) + @Parameter(name = "channelId", description = "通道国标编号", required = true) + @GetMapping("/preset/query/{deviceId}/{channelId}") + public DeferredResult> queryPreset(@PathVariable String deviceId, @PathVariable String channelId) { + if (log.isDebugEnabled()) { + log.debug("设备预置位查询API调用"); + } + Device device = deviceService.getDeviceByDeviceId(deviceId); + Assert.notNull(device, "设备不存在"); + DeferredResult> deferredResult = new DeferredResult<> (3 * 1000L); + deviceService.queryPreset(device, channelId, (code, msg, data) -> { + deferredResult.setResult(new WVPResult<>(code, msg, data)); + }); + + deferredResult.onTimeout(()->{ + log.warn("[获取设备预置位] 超时, {}", device.getDeviceId()); + deferredResult.setResult(WVPResult.fail(ErrorCode.ERROR100.getCode(), "超时")); + }); + return deferredResult; + } + + @Operation(summary = "预置位指令-设置预置位", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "deviceId", description = "设备国标编号", required = true) + @Parameter(name = "channelId", description = "通道国标编号", required = true) + @Parameter(name = "presetId", description = "预置位编号(1-255)", required = true) + @GetMapping("/preset/add/{deviceId}/{channelId}") + public void addPreset(@PathVariable String deviceId, @PathVariable String channelId, Integer presetId) { + if (presetId == null || presetId < 1 || presetId > 255) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "预置位编号必须为1-255之间的数字"); + } + frontEndCommand(deviceId, channelId, 0x81, 1, presetId, 0); + } + + @Operation(summary = "预置位指令-调用预置位", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "deviceId", description = "设备国标编号", required = true) + @Parameter(name = "channelId", description = "通道国标编号", required = true) + @Parameter(name = "presetId", description = "预置位编号(1-255)", required = true) + @GetMapping("/preset/call/{deviceId}/{channelId}") + public void callPreset(@PathVariable String deviceId, @PathVariable String channelId, Integer presetId) { + if (presetId == null || presetId < 1 || presetId > 255) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "预置位编号必须为1-255之间的数字"); + } + frontEndCommand(deviceId, channelId, 0x82, 1, presetId, 0); + } + + @Operation(summary = "预置位指令-删除预置位", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "deviceId", description = "设备国标编号", required = true) + @Parameter(name = "channelId", description = "通道国标编号", required = true) + @Parameter(name = "presetId", description = "预置位编号(1-255)", required = true) + @GetMapping("/preset/delete/{deviceId}/{channelId}") + public void deletePreset(@PathVariable String deviceId, @PathVariable String channelId, Integer presetId) { + if (presetId == null || presetId < 1 || presetId > 255) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "预置位编号必须为1-255之间的数字"); + } + frontEndCommand(deviceId, channelId, 0x83, 1, presetId, 0); + } + + @Operation(summary = "巡航指令-加入巡航点", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "deviceId", description = "设备国标编号", required = true) + @Parameter(name = "channelId", description = "通道国标编号", required = true) + @Parameter(name = "cruiseId", description = "巡航组号(0-255)", required = true) + @Parameter(name = "presetId", description = "预置位编号(1-255)", required = true) + @GetMapping("/cruise/point/add/{deviceId}/{channelId}") + public void addCruisePoint(@PathVariable String deviceId, @PathVariable String channelId, Integer cruiseId, Integer presetId) { + if (presetId == null || cruiseId == null || presetId < 1 || presetId > 255 || cruiseId < 0 || cruiseId > 255) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "编号必须为1-255之间的数字"); + } + frontEndCommand(deviceId, channelId, 0x84, cruiseId, presetId, 0); + } + + @Operation(summary = "巡航指令-删除一个巡航点", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "deviceId", description = "设备国标编号", required = true) + @Parameter(name = "channelId", description = "通道国标编号", required = true) + @Parameter(name = "cruiseId", description = "巡航组号(1-255)", required = true) + @Parameter(name = "presetId", description = "预置位编号(0-255, 为0时删除整个巡航)", required = true) + @GetMapping("/cruise/point/delete/{deviceId}/{channelId}") + public void deleteCruisePoint(@PathVariable String deviceId, @PathVariable String channelId, Integer cruiseId, Integer presetId) { + if (presetId == null || presetId < 0 || presetId > 255) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "预置位编号必须为0-255之间的数字, 为0时删除整个巡航"); + } + if (cruiseId == null || cruiseId < 0 || cruiseId > 255) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "巡航组号必须为0-255之间的数字"); + } + frontEndCommand(deviceId, channelId, 0x85, cruiseId, presetId, 0); + } + + @Operation(summary = "巡航指令-设置巡航速度", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "deviceId", description = "设备国标编号", required = true) + @Parameter(name = "channelId", description = "通道国标编号", required = true) + @Parameter(name = "cruiseId", description = "巡航组号(0-255)", required = true) + @Parameter(name = "speed", description = "巡航速度(1-4095)", required = true) + @GetMapping("/cruise/speed/{deviceId}/{channelId}") + public void setCruiseSpeed(@PathVariable String deviceId, @PathVariable String channelId, Integer cruiseId, Integer speed) { + if (cruiseId == null || cruiseId < 0 || cruiseId > 255) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "巡航组号必须为0-255之间的数字"); + } + if (speed == null || speed < 1 || speed > 4095) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "巡航速度必须为1-4095之间的数字"); + } + int parameter2 = speed & 0xFF; + int combindCode2 = speed >> 8; + frontEndCommand(deviceId, channelId, 0x86, cruiseId, parameter2, combindCode2); + } + + @Operation(summary = "巡航指令-设置巡航停留时间", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "deviceId", description = "设备国标编号", required = true) + @Parameter(name = "channelId", description = "通道国标编号", required = true) + @Parameter(name = "cruiseId", description = "巡航组号", required = true) + @Parameter(name = "time", description = "巡航停留时间(1-4095)", required = true) + @GetMapping("/cruise/time/{deviceId}/{channelId}") + public void setCruiseTime(@PathVariable String deviceId, @PathVariable String channelId, Integer cruiseId, Integer time) { + if (cruiseId == null || cruiseId < 0 || cruiseId > 255) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "巡航组号必须为0-255之间的数字"); + } + if (time == null || time < 1 || time > 4095) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "巡航停留时间必须为1-4095之间的数字"); + } + int parameter2 = time & 0xFF; + int combindCode2 = time >> 8; + frontEndCommand(deviceId, channelId, 0x87, cruiseId, parameter2, combindCode2); + } + + @Operation(summary = "巡航指令-开始巡航", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "deviceId", description = "设备国标编号", required = true) + @Parameter(name = "channelId", description = "通道国标编号", required = true) + @Parameter(name = "cruiseId", description = "巡航组号)", required = true) + @GetMapping("/cruise/start/{deviceId}/{channelId}") + public void startCruise(@PathVariable String deviceId, @PathVariable String channelId, Integer cruiseId) { + if (cruiseId == null || cruiseId < 0 || cruiseId > 255) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "巡航组号必须为0-255之间的数字"); + } + frontEndCommand(deviceId, channelId, 0x88, cruiseId, 0, 0); + } + + @Operation(summary = "巡航指令-停止巡航", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "deviceId", description = "设备国标编号", required = true) + @Parameter(name = "channelId", description = "通道国标编号", required = true) + @Parameter(name = "cruiseId", description = "巡航组号", required = true) + @GetMapping("/cruise/stop/{deviceId}/{channelId}") + public void stopCruise(@PathVariable String deviceId, @PathVariable String channelId, Integer cruiseId) { + if (cruiseId == null || cruiseId < 0 || cruiseId > 255) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "巡航组号必须为0-255之间的数字"); + } + frontEndCommand(deviceId, channelId, 0, 0, 0, 0); + } + + @Operation(summary = "扫描指令-开始自动扫描", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "deviceId", description = "设备国标编号", required = true) + @Parameter(name = "channelId", description = "通道国标编号", required = true) + @Parameter(name = "scanId", description = "扫描组号(0-255)", required = true) + @GetMapping("/scan/start/{deviceId}/{channelId}") + public void startScan(@PathVariable String deviceId, @PathVariable String channelId, Integer scanId) { + if (scanId == null || scanId < 0 || scanId > 255 ) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "扫描组号必须为0-255之间的数字"); + } + frontEndCommand(deviceId, channelId, 0x89, scanId, 0, 0); + } + + @Operation(summary = "扫描指令-停止自动扫描", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "deviceId", description = "设备国标编号", required = true) + @Parameter(name = "channelId", description = "通道国标编号", required = true) + @Parameter(name = "scanId", description = "扫描组号(0-255)", required = true) + @GetMapping("/scan/stop/{deviceId}/{channelId}") + public void stopScan(@PathVariable String deviceId, @PathVariable String channelId, Integer scanId) { + if (scanId == null || scanId < 0 || scanId > 255 ) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "扫描组号必须为0-255之间的数字"); + } + frontEndCommand(deviceId, channelId, 0, 0, 0, 0); + } + + @Operation(summary = "扫描指令-设置自动扫描左边界", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "deviceId", description = "设备国标编号", required = true) + @Parameter(name = "channelId", description = "通道国标编号", required = true) + @Parameter(name = "scanId", description = "扫描组号(0-255)", required = true) + @GetMapping("/scan/set/left/{deviceId}/{channelId}") + public void setScanLeft(@PathVariable String deviceId, @PathVariable String channelId, Integer scanId) { + if (scanId == null || scanId < 0 || scanId > 255 ) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "扫描组号必须为0-255之间的数字"); + } + frontEndCommand(deviceId, channelId, 0x89, scanId, 1, 0); + } + + @Operation(summary = "扫描指令-设置自动扫描右边界", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "deviceId", description = "设备国标编号", required = true) + @Parameter(name = "channelId", description = "通道国标编号", required = true) + @Parameter(name = "scanId", description = "扫描组号(0-255)", required = true) + @GetMapping("/scan/set/right/{deviceId}/{channelId}") + public void setScanRight(@PathVariable String deviceId, @PathVariable String channelId, Integer scanId) { + if (scanId == null || scanId < 0 || scanId > 255 ) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "扫描组号必须为0-255之间的数字"); + } + frontEndCommand(deviceId, channelId, 0x89, scanId, 2, 0); + } + + + @Operation(summary = "扫描指令-设置自动扫描速度", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "deviceId", description = "设备国标编号", required = true) + @Parameter(name = "channelId", description = "通道国标编号", required = true) + @Parameter(name = "scanId", description = "扫描组号(0-255)", required = true) + @Parameter(name = "speed", description = "自动扫描速度(1-4095)", required = true) + @GetMapping("/scan/set/speed/{deviceId}/{channelId}") + public void setScanSpeed(@PathVariable String deviceId, @PathVariable String channelId, Integer scanId, Integer speed) { + if (scanId == null || scanId < 0 || scanId > 255 ) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "扫描组号必须为0-255之间的数字"); + } + if (speed == null || speed < 1 || speed > 4095) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "自动扫描速度必须为1-4095之间的数字"); + } + int parameter2 = speed & 0xFF; + int combindCode2 = speed >> 8; + frontEndCommand(deviceId, channelId, 0x8A, scanId, parameter2, combindCode2); + } + + + @Operation(summary = "辅助开关控制指令-雨刷控制", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "deviceId", description = "设备国标编号", required = true) + @Parameter(name = "channelId", description = "通道国标编号", required = true) + @Parameter(name = "command", description = "控制指令,允许值: on, off", required = true) + @GetMapping("/wiper/{deviceId}/{channelId}") + public void wiper(@PathVariable String deviceId,@PathVariable String channelId, String command){ + + if (log.isDebugEnabled()) { + log.debug("辅助开关控制指令-雨刷控制 API调用,deviceId:{} ,channelId:{} ,command:{}",deviceId, channelId, command); + } + + int cmdCode = 0; + switch (command){ + case "on": + cmdCode = 0x8c; + break; + case "off": + cmdCode = 0x8d; + break; + default: + break; + } + frontEndCommand(deviceId, channelId, cmdCode, 1, 0, 0); + } + + @Operation(summary = "辅助开关控制指令", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "deviceId", description = "设备国标编号", required = true) + @Parameter(name = "channelId", description = "通道国标编号", required = true) + @Parameter(name = "command", description = "控制指令,允许值: on, off", required = true) + @Parameter(name = "switchId", description = "开关编号", required = true) + @GetMapping("/auxiliary/{deviceId}/{channelId}") + public void auxiliarySwitch(@PathVariable String deviceId,@PathVariable String channelId, String command, Integer switchId){ + + if (log.isDebugEnabled()) { + log.debug("辅助开关控制指令-雨刷控制 API调用,deviceId:{} ,channelId:{} ,command:{}, switchId: {}",deviceId, channelId, command, switchId); + } + + int cmdCode = 0; + switch (command){ + case "on": + cmdCode = 0x8c; + break; + case "off": + cmdCode = 0x8d; + break; + default: + break; + } + frontEndCommand(deviceId, channelId, cmdCode, switchId, 0, 0); + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/controller/RegionController.java b/src/main/java/com/genersoft/iot/vmp/gb28181/controller/RegionController.java new file mode 100644 index 0000000..ff415df --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/controller/RegionController.java @@ -0,0 +1,152 @@ +package com.genersoft.iot.vmp.gb28181.controller; + +import com.genersoft.iot.vmp.conf.exception.ControllerException; +import com.genersoft.iot.vmp.gb28181.bean.Region; +import com.genersoft.iot.vmp.gb28181.bean.RegionTree; +import com.genersoft.iot.vmp.gb28181.service.IRegionService; +import com.genersoft.iot.vmp.vmanager.bean.ErrorCode; +import com.github.pagehelper.PageInfo; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@Tag(name = "区域管理") +@RestController +@RequestMapping("/api/region") +public class RegionController { + + private final static Logger logger = LoggerFactory.getLogger(RegionController.class); + + @Autowired + private IRegionService regionService; + + @Operation(summary = "添加区域") + @Parameter(name = "region", description = "Region", required = true) + @ResponseBody + @PostMapping("/add") + public void add(@RequestBody Region region){ + regionService.add(region); + } + + @Operation(summary = "查询区域") + @Parameter(name = "query", description = "要搜索的内容", required = true) + @Parameter(name = "page", description = "当前页", required = true) + @Parameter(name = "count", description = "每页查询数量", required = true) + @ResponseBody + @GetMapping("/page/list") + public PageInfo query( + @RequestParam(required = false) String query, + @RequestParam(required = true) int page, + @RequestParam(required = true) int count + ){ + return regionService.query(query, page, count); + } + + @Operation(summary = "查询区域节点") + @Parameter(name = "query", description = "要搜索的内容", required = true) + @Parameter(name = "parent", description = "所属行政区划编号", required = true) + @Parameter(name = "hasChannel", description = "是否查询通道", required = true) + @ResponseBody + @GetMapping("/tree/list") + public List queryForTree( + @RequestParam(required = false) Integer parent, + @RequestParam(required = false) Boolean hasChannel + ){ + return regionService.queryForTree(parent, hasChannel); + } + + + @Operation(summary = "查询区域") + @Parameter(name = "query", description = "要搜索的内容", required = true) + @Parameter(name = "channel", description = "true为查询通道,false为查询节点", required = true) + @ResponseBody + @GetMapping("/tree/query") + public PageInfo queryTree(Integer page, Integer count, + @RequestParam(required = true) String query + ){ + return regionService.queryList(page, count, query); + } + + @Operation(summary = "更新区域") + @Parameter(name = "region", description = "Region", required = true) + @ResponseBody + @PostMapping("/update") + public void update(@RequestBody Region region){ + regionService.update(region); + } + + @Operation(summary = "删除区域") + @Parameter(name = "id", description = "区域ID", required = true) + @ResponseBody + @DeleteMapping("/delete") + public void delete(Integer id){ + Assert.notNull(id, "区域ID需要存在"); + boolean result = regionService.deleteByDeviceId(id); + if (!result) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "移除失败"); + } + } + + @Operation(summary = "根据区域Id查询区域") + @Parameter(name = "regionDeviceId", description = "行政区划节点编号", required = true) + @ResponseBody + @GetMapping("/one") + public Region queryRegionByDeviceId( + @RequestParam(required = true) String regionDeviceId + ){ + if (ObjectUtils.isEmpty(regionDeviceId.trim())) { + throw new ControllerException(ErrorCode.ERROR400); + } + return regionService.queryRegionByDeviceId(regionDeviceId); + } + + @Operation(summary = "获取所属的行政区划下的行政区划") + @Parameter(name = "parent", description = "所属的行政区划", required = false) + @ResponseBody + @GetMapping("/base/child/list") + public List getAllChild(@RequestParam(required = false) String parent){ + if (ObjectUtils.isEmpty(parent)) { + parent = null; + } + return regionService.getAllChild(parent); + } + + @Operation(summary = "获取所属的行政区划下的行政区划") + @Parameter(name = "deviceId", description = "当前的行政区划", required = false) + @ResponseBody + @GetMapping("/path") + public List getPath(String deviceId){ + return regionService.getPath(deviceId); + } + + @Operation(summary = "从通道中同步行政区划") + @ResponseBody + @GetMapping("/sync") + public void sync(){ + regionService.syncFromChannel(); + } + + @Operation(summary = "根据行政区划编号从文件中查询层级和描述") + @ResponseBody + @GetMapping("/description") + public String getDescription(String civilCode){ + return regionService.getDescription(civilCode); + } + + @Operation(summary = "根据行政区划编号从文件中查询层级并添加") + @ResponseBody + @GetMapping("/addByCivilCode") + public void addByCivilCode(String civilCode){ + regionService.addByCivilCode(civilCode); + } + + +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/controller/SseController.java b/src/main/java/com/genersoft/iot/vmp/gb28181/controller/SseController.java new file mode 100644 index 0000000..8e97295 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/controller/SseController.java @@ -0,0 +1,42 @@ +package com.genersoft.iot.vmp.gb28181.controller; + +import com.genersoft.iot.vmp.gb28181.session.SseSessionManager; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; + +import jakarta.annotation.Resource; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; + + +/** + * SSE 推送. + * + * @author lawrencehj + * @author xiaoQQya + * @since 2021/01/20 + */ +@Tag(name = "SSE 推送") +@RestController +@RequestMapping("/api") +public class SseController { + + @Resource + private SseSessionManager sseSessionManager; + + /** + * SSE 推送. + * + * @param browserId 浏览器ID + */ + @GetMapping("/emit") + public SseEmitter emit(HttpServletResponse response, @RequestParam String browserId) throws IOException, InterruptedException { +// response.setContentType("text/event-stream"); +// response.setCharacterEncoding("utf-8"); + return sseSessionManager.conect(browserId); + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/controller/bean/AudioBroadcastEvent.java b/src/main/java/com/genersoft/iot/vmp/gb28181/controller/bean/AudioBroadcastEvent.java new file mode 100644 index 0000000..3c58934 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/controller/bean/AudioBroadcastEvent.java @@ -0,0 +1,9 @@ +package com.genersoft.iot.vmp.gb28181.controller.bean; + + +/** + * @author lin + */ +public interface AudioBroadcastEvent { + void call(String msg); +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/controller/bean/ChannelForThin.java b/src/main/java/com/genersoft/iot/vmp/gb28181/controller/bean/ChannelForThin.java new file mode 100644 index 0000000..02323b3 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/controller/bean/ChannelForThin.java @@ -0,0 +1,9 @@ +package com.genersoft.iot.vmp.gb28181.controller.bean; + +import lombok.Data; + +@Data +public class ChannelForThin { + private Integer gbId; + private Integer mapLevel; +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/controller/bean/ChannelReduce.java b/src/main/java/com/genersoft/iot/vmp/gb28181/controller/bean/ChannelReduce.java new file mode 100755 index 0000000..b4c1d01 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/controller/bean/ChannelReduce.java @@ -0,0 +1,137 @@ +package com.genersoft.iot.vmp.gb28181.controller.bean; + +import com.genersoft.iot.vmp.gb28181.bean.DeviceChannel; +import io.swagger.v3.oas.annotations.media.Schema; + +/** + * 精简的channel信息展示,主要是选择通道的时候展示列表使用 + */ +@Schema(description = "精简的channel信息展示") +public class ChannelReduce { + + /** + * deviceChannel的数据库自增ID + */ + @Schema(description = "deviceChannel的数据库自增ID") + private int id; + + /** + * 通道id + */ + @Schema(description = "通道国标编号") + private String channelId; + + /** + * 设备id + */ + @Schema(description = "设备国标编号") + private String deviceId; + + /** + * 通道名 + */ + @Schema(description = "通道名") + private String name; + + /** + * 生产厂商 + */ + @Schema(description = "生产厂商") + private String manufacturer; + + /** + * wan地址 + */ + @Schema(description = "wan地址") + private String hostAddress; + + /** + * 子节点数 + */ + @Schema(description = "子节点数") + private int subCount; + + /** + * 平台Id + */ + @Schema(description = "平台上级国标编号") + private String platformId; + + /** + * 目录Id + */ + @Schema(description = "目录国标编号") + private String catalogId; + + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + + public String getChannelId() { + return channelId; + } + + public void setChannelId(String channelId) { + this.channelId = channelId; + } + + public String getDeviceId() { + return deviceId; + } + + public void setDeviceId(String deviceId) { + this.deviceId = deviceId; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getManufacturer() { + return manufacturer; + } + + public void setManufacturer(String manufacturer) { + this.manufacturer = manufacturer; + } + + public String getHostAddress() { + return hostAddress; + } + + public void setHostAddress(String hostAddress) { + this.hostAddress = hostAddress; + } + + public int getSubCount() { + return subCount; + } + + public void setSubCount(int subCount) { + this.subCount = subCount; + } + + public String getPlatformId() { + return platformId; + } + + public void setPlatformId(String platformId) { + this.platformId = platformId; + } + + public String getCatalogId() { + return catalogId; + } + + public void setCatalogId(String catalogId) { + this.catalogId = catalogId; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/controller/bean/ChannelToGroupByGbDeviceParam.java b/src/main/java/com/genersoft/iot/vmp/gb28181/controller/bean/ChannelToGroupByGbDeviceParam.java new file mode 100644 index 0000000..b95a7b8 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/controller/bean/ChannelToGroupByGbDeviceParam.java @@ -0,0 +1,12 @@ +package com.genersoft.iot.vmp.gb28181.controller.bean; + +import lombok.Data; + +import java.util.List; + +@Data +public class ChannelToGroupByGbDeviceParam { + private List deviceIds; + private String parentId; + private String businessGroup; +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/controller/bean/ChannelToGroupParam.java b/src/main/java/com/genersoft/iot/vmp/gb28181/controller/bean/ChannelToGroupParam.java new file mode 100644 index 0000000..5889893 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/controller/bean/ChannelToGroupParam.java @@ -0,0 +1,14 @@ +package com.genersoft.iot.vmp.gb28181.controller.bean; + +import lombok.Data; + +import java.util.List; + +@Data +public class ChannelToGroupParam { + + private String parentId; + private String businessGroup; + private List channelIds; + +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/controller/bean/ChannelToRegionByGbDeviceParam.java b/src/main/java/com/genersoft/iot/vmp/gb28181/controller/bean/ChannelToRegionByGbDeviceParam.java new file mode 100644 index 0000000..6820bb6 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/controller/bean/ChannelToRegionByGbDeviceParam.java @@ -0,0 +1,11 @@ +package com.genersoft.iot.vmp.gb28181.controller.bean; + +import lombok.Data; + +import java.util.List; + +@Data +public class ChannelToRegionByGbDeviceParam { + private List deviceIds; + private String civilCode; +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/controller/bean/ChannelToRegionParam.java b/src/main/java/com/genersoft/iot/vmp/gb28181/controller/bean/ChannelToRegionParam.java new file mode 100644 index 0000000..7f74004 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/controller/bean/ChannelToRegionParam.java @@ -0,0 +1,21 @@ +package com.genersoft.iot.vmp.gb28181.controller.bean; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.util.List; + +@Data +@Schema(description="提交行政区划关联多个通道的参数") +public class ChannelToRegionParam { + + @Schema(description = "行政区划编号") + private String civilCode; + + @Schema(description = "选择的通道, 和all参数二选一") + private List channelIds; + + @Schema(description = "所有通道, 和channelIds参数二选一") + private Boolean all; + +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/controller/bean/DrawThinParam.java b/src/main/java/com/genersoft/iot/vmp/gb28181/controller/bean/DrawThinParam.java new file mode 100644 index 0000000..bd43b66 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/controller/bean/DrawThinParam.java @@ -0,0 +1,18 @@ +package com.genersoft.iot.vmp.gb28181.controller.bean; + +import lombok.Getter; +import lombok.Setter; + +import java.util.Map; + +@Getter +@Setter +public class DrawThinParam { + private Map zoomParam; + private Extent extent; + + /** + * 地理坐标系, WGS84/GCJ02, 用来标识 extent 参数的坐标系 + */ + private String geoCoordSys; +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/controller/bean/Extent.java b/src/main/java/com/genersoft/iot/vmp/gb28181/controller/bean/Extent.java new file mode 100644 index 0000000..33c923e --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/controller/bean/Extent.java @@ -0,0 +1,13 @@ +package com.genersoft.iot.vmp.gb28181.controller.bean; + +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class Extent { + private Double minLng; + private Double maxLng; + private Double minLat; + private Double maxLat; +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/controller/bean/PlayResult.java b/src/main/java/com/genersoft/iot/vmp/gb28181/controller/bean/PlayResult.java new file mode 100755 index 0000000..630eb66 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/controller/bean/PlayResult.java @@ -0,0 +1,38 @@ +package com.genersoft.iot.vmp.gb28181.controller.bean; + +import com.genersoft.iot.vmp.common.StreamInfo; +import com.genersoft.iot.vmp.gb28181.bean.Device; +import com.genersoft.iot.vmp.vmanager.bean.WVPResult; +import org.springframework.web.context.request.async.DeferredResult; + +public class PlayResult { + + private DeferredResult> result; + private String uuid; + + private Device device; + + public DeferredResult> getResult() { + return result; + } + + public void setResult(DeferredResult> result) { + this.result = result; + } + + public String getUuid() { + return uuid; + } + + public void setUuid(String uuid) { + this.uuid = uuid; + } + + public Device getDevice() { + return device; + } + + public void setDevice(Device device) { + this.device = device; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/controller/bean/ResetParam.java b/src/main/java/com/genersoft/iot/vmp/gb28181/controller/bean/ResetParam.java new file mode 100644 index 0000000..bcb144b --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/controller/bean/ResetParam.java @@ -0,0 +1,15 @@ +package com.genersoft.iot.vmp.gb28181.controller.bean; + +import lombok.Getter; +import lombok.Setter; + +import java.util.List; + +@Getter +@Setter +public class ResetParam { + + private Integer id; + private List chanelFields; + +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/controller/bean/UpdateChannelParam.java b/src/main/java/com/genersoft/iot/vmp/gb28181/controller/bean/UpdateChannelParam.java new file mode 100755 index 0000000..5f36002 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/controller/bean/UpdateChannelParam.java @@ -0,0 +1,24 @@ +package com.genersoft.iot.vmp.gb28181.controller.bean; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.util.List; + +@Data +@Schema(description = "通道关联参数") +public class UpdateChannelParam { + + @Schema(description = "上级平台的数据库ID") + private Integer platformId; + + + @Schema(description = "关联所有通道") + private boolean all; + + @Schema(description = "待关联的通道ID") + List channelIds; + + @Schema(description = "待关联的设备ID") + List deviceIds; +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/dao/CommonGBChannelMapper.java b/src/main/java/com/genersoft/iot/vmp/gb28181/dao/CommonGBChannelMapper.java new file mode 100644 index 0000000..1e93f07 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/dao/CommonGBChannelMapper.java @@ -0,0 +1,698 @@ +package com.genersoft.iot.vmp.gb28181.dao; + +import com.genersoft.iot.vmp.gb28181.bean.*; +import com.genersoft.iot.vmp.gb28181.controller.bean.Extent; +import com.genersoft.iot.vmp.gb28181.dao.provider.ChannelProvider; +import com.genersoft.iot.vmp.service.bean.GPSMsgInfo; +import com.genersoft.iot.vmp.streamPush.bean.StreamPush; +import com.genersoft.iot.vmp.web.custom.bean.CameraChannel; +import com.genersoft.iot.vmp.web.custom.bean.CameraGroup; +import com.genersoft.iot.vmp.web.custom.bean.Point; +import org.apache.ibatis.annotations.*; +import org.springframework.stereotype.Repository; + +import java.util.Collection; +import java.util.List; + +@Mapper +@Repository +public interface CommonGBChannelMapper { + + + @SelectProvider(type = ChannelProvider.class, method = "queryByDeviceId") + List queryByDeviceId(@Param("gbDeviceId") String gbDeviceId); + + @Insert(" ") + @Options(useGeneratedKeys = true, keyProperty = "gbId", keyColumn = "id") + int insert(CommonGBChannel commonGBChannel); + + @SelectProvider(type = ChannelProvider.class, method = "queryById") + CommonGBChannel queryById(@Param("gbId") int gbId); + + @Delete(value = {"delete from wvp_device_channel where id = #{gbId} "}) + void delete(int gbId); + + @Update(value = {" "}) + int update(CommonGBChannel commonGBChannel); + + @Update(value = {" "}) + int updateStatusById(@Param("gbId") int gbId, @Param("status") String status); + + @Update("") + int updateStatusForListById(List commonGBChannels, @Param("status") String status); + + @SelectProvider(type = ChannelProvider.class, method = "queryInListByStatus") + List queryInListByStatus(List commonGBChannelList, @Param("status") String status); + + + @Insert(" ") + int batchAdd(List commonGBChannels); + + @Update("") + int updateStatus(List commonGBChannels); + + @Update(value = {" "}) + void reset(@Param("id") int id, List fields, @Param("updateTime") String updateTime); + + + @SelectProvider(type = ChannelProvider.class, method = "queryByIds") + List queryByIds(Collection ids); + + @Delete(value = {" "}) + void batchDelete(List channelListInDb); + + @SelectProvider(type = ChannelProvider.class, method = "queryListByCivilCode") + List queryListByCivilCode(@Param("query") String query, @Param("online") Boolean online, + @Param("dataType") Integer dataType, @Param("civilCode") String civilCode); + + + + @SelectProvider(type = ChannelProvider.class, method = "queryListByParentId") + List queryListByParentId(@Param("query") String query, @Param("online") Boolean online, + @Param("dataType") Integer dataType, @Param("groupDeviceId") String groupDeviceId); + + + + @Select("") + List queryForRegionTreeByCivilCode(@Param("parentDeviceId") String parentDeviceId); + + @Update(value = {" "}) + int removeCivilCode(List allChildren); + + + @Update(value = {" "}) + int updateRegion(@Param("civilCode") String civilCode, @Param("channelList") List channelList); + + @SelectProvider(type = ChannelProvider.class, method = "queryByIdsOrCivilCode") + List queryByIdsOrCivilCode(@Param("civilCode") String civilCode, @Param("ids") List ids); + + @Update(value = {" "}) + int removeCivilCodeByChannels(List channelList); + + @Update(value = {" "}) + int removeCivilCodeByChannelIds(List channelIdList); + + @SelectProvider(type = ChannelProvider.class, method = "queryByCivilCode") + List queryByCivilCode(@Param("civilCode") String civilCode); + + @SelectProvider(type = ChannelProvider.class, method = "queryByDataTypeAndDeviceIds") + List queryByDataTypeAndDeviceIds(@Param("dataType") Integer dataType, List deviceIds); + + @SelectProvider(type = ChannelProvider.class, method = "queryByGbDeviceIds") + List queryByGbDeviceIds(List deviceIds); + + @Select(value = {" "}) + List queryByGbDeviceIdsForIds(@Param("dataType") Integer dataType, List deviceIds); + + @SelectProvider(type = ChannelProvider.class, method = "queryByGroupList") + List queryByGroupList(List groupList); + + @Update(value = {" "}) + int removeParentIdByChannels(List channelList); + + @SelectProvider(type = ChannelProvider.class, method = "queryByBusinessGroup") + List queryByBusinessGroup(@Param("businessGroup") String businessGroup); + + @SelectProvider(type = ChannelProvider.class, method = "queryByParentId") + List queryByParentId(@Param("parentId") String parentId); + + @Update(value = {" "}) + int updateBusinessGroupByChannelList(@Param("businessGroup") String businessGroup, List channelList); + + @Update(value = {" "}) + int updateParentIdByChannelList(@Param("parentId") String parentId, List channelList); + + @Select("") + List queryForGroupTreeByParentId(@Param("query") String query, @Param("parent") String parent); + + @Update(value = {" "}) + int updateGroup(@Param("parentId") String parentId, @Param("businessGroup") String businessGroup, + List channelList); + + @Update({""}) + int batchUpdate(List commonGBChannels); + + @SelectProvider(type = ChannelProvider.class, method = "queryWithPlatform") + List queryWithPlatform(@Param("platformId") Integer platformId); + + @SelectProvider(type = ChannelProvider.class, method = "queryShareChannelByParentId") + List queryShareChannelByParentId(@Param("parentId") String parentId, @Param("platformId") Integer platformId); + + @SelectProvider(type = ChannelProvider.class, method = "queryShareChannelByCivilCode") + List queryShareChannelByCivilCode(@Param("civilCode") String civilCode, @Param("platformId") Integer platformId); + + @Update(value = {" "}) + int updateCivilCodeByChannelList(@Param("civilCode") String civilCode, List channelList); + + @SelectProvider(type = ChannelProvider.class, method = "queryListByStreamPushList") + List queryListByStreamPushList(@Param("dataType") Integer dataType, List streamPushList); + + @Update(value = {" "}) + void updateGpsByDeviceId(List gpsMsgInfoList); + + @SelectProvider(type = ChannelProvider.class, method = "queryList") + List queryList(@Param("query") String query, @Param("online") Boolean online, + @Param("hasRecordPlan") Boolean hasRecordPlan, @Param("dataType") Integer dataType, + @Param("civilCode") String civilCode, @Param("parentDeviceId") String parentDeviceId); + + @Update(value = {" "}) + void removeRecordPlan(List channelIds); + + @Update(value = {" "}) + void addRecordPlan(List channelIds, @Param("planId") Integer planId); + + @Update(value = {" "}) + void addRecordPlanForAll(@Param("planId") Integer planId); + + @Update(value = {" "}) + void removeRecordPlanByPlanId( @Param("planId") Integer planId); + + + @Select("") + List queryForRecordPlanForWebList(@Param("planId") Integer planId, @Param("query") String query, + @Param("dataType") Integer dataType, @Param("online") Boolean online, + @Param("hasLink") Boolean hasLink); + + @SelectProvider(type = ChannelProvider.class, method = "queryByDataId") + CommonGBChannel queryByDataId(@Param("dataType") Integer dataType, @Param("dataDeviceId") Integer dataDeviceId); + + @SelectProvider(type = ChannelProvider.class, method = "queryListByCivilCodeForUnusual") + List queryListByCivilCodeForUnusual(@Param("query") String query, @Param("online") Boolean online, @Param("dataType")Integer dataType); + + @SelectProvider(type = ChannelProvider.class, method = "queryAllForUnusualCivilCode") + List queryAllForUnusualCivilCode(); + + @SelectProvider(type = ChannelProvider.class, method = "queryListByParentForUnusual") + List queryListByParentForUnusual(@Param("query") String query, @Param("online") Boolean online, @Param("dataType")Integer dataType); + + @SelectProvider(type = ChannelProvider.class, method = "queryAllForUnusualParent") + List queryAllForUnusualParent(); + + @Update(value = {" "}) + void removeParentIdByChannelIds(List channelIdsForClear); + + + @SelectProvider(type = ChannelProvider.class, method = "queryOnlineListsByGbDeviceId") + List queryOnlineListsByGbDeviceId(@Param("deviceId") int deviceId); + + @SelectProvider(type = ChannelProvider.class, method = "queryCommonChannelByDeviceChannel") + CommonGBChannel queryCommonChannelByDeviceChannel(@Param("dataType") Integer dataType, @Param("dataDeviceId") Integer dataDeviceId, @Param("deviceId") String deviceId); + + @Update("UPDATE wvp_device_channel SET stream_id = #{stream} where id = #{gbId}") + void updateStream(int gbId, String stream); + + @Update("") + void updateGps(List commonGBChannels); + + + @SelectProvider(type = ChannelProvider.class, method = "queryListForSy") + List queryListForSy(@Param("groupDeviceId") String groupDeviceId, @Param("online") Boolean online); + + + @SelectProvider(type = ChannelProvider.class, method = "queryGbChannelByChannelDeviceIdAndGbDeviceId") + List queryGbChannelByChannelDeviceIdAndGbDeviceId(@Param("channelDeviceId") String channelDeviceId, @Param("gbDeviceId") String gbDeviceId); + + @SelectProvider(type = ChannelProvider.class, method = "queryListByDeviceIds") + List queryListByDeviceIds(List deviceIds); + + @SelectProvider(type = ChannelProvider.class, method = "queryListWithChildForSy") + List queryListWithChildForSy(@Param("query") String query, @Param("sortName") String sortName, @Param("order") Boolean order, @Param("groupList") List groupList, @Param("online") Boolean online); + + @SelectProvider(type = ChannelProvider.class, method = "queryListByAddressAndDirectionType") + List queryListByAddressAndDirectionType(@Param("address") String address, @Param("directionType") Integer directionType); + + @SelectProvider(type = ChannelProvider.class, method = "queryListInBox") + List queryListInBox(@Param("minLongitude") Double minLongitude, @Param("maxLongitude") Double maxLongitude, + @Param("minLatitude") Double minLatitude, @Param("maxLatitude") Double maxLatitude, + @Param("level") Integer level, @Param("groupList") List groupList); + + @SelectProvider(type = ChannelProvider.class, method = "queryListInCircleForMysql", databaseId = "mysql") + @SelectProvider(type = ChannelProvider.class, method = "queryListInCircleForH2", databaseId = "h2") + @SelectProvider(type = ChannelProvider.class, method = "queryListInCircleForKingBase", databaseId = "kingbase") + @SelectProvider(type = ChannelProvider.class, method = "queryListInCircleForKingBase", databaseId = "postgresql") + List queryListInCircle(@Param("centerLongitude") Double centerLongitude, @Param("centerLatitude") Double centerLatitude, + @Param("radius") Double radius, @Param("level") Integer level, @Param("groupList") List groupList); + + @SelectProvider(type = ChannelProvider.class, method = "queryListInPolygonForMysql", databaseId = "mysql") + @SelectProvider(type = ChannelProvider.class, method = "queryListInPolygonForH2", databaseId = "h2") + @SelectProvider(type = ChannelProvider.class, method = "queryListInPolygonForKingBase", databaseId = "kingbase") + @SelectProvider(type = ChannelProvider.class, method = "queryListInPolygonForKingBase", databaseId = "postgresql") + List queryListInPolygon(@Param("pointList") List pointList, @Param("level") Integer level, @Param("groupList") List groupList); + + @SelectProvider(type = ChannelProvider.class, method = "queryListForSyMobile") + List queryListForSyMobile(@Param("business") String business); + + @SelectProvider(type = ChannelProvider.class, method = "queryCameraChannelById") + CameraChannel queryCameraChannelById(@Param("gbId") Integer id); + + @Update("") + void saveLevel(List channels); + + @SelectProvider(type = ChannelProvider.class, method = "queryCameraChannelByIds") + List queryCameraChannelByIds(List channelList); + + @SelectProvider(type = ChannelProvider.class, method = "queryOldChanelListByChannels") + List queryOldChanelListByChannels(List channelList); + + @SelectProvider(type = ChannelProvider.class, method = "queryMeetingChannelList") + List queryMeetingChannelList(@Param("business") String business); + + @Update("UPDATE wvp_device_channel SET map_level=null") + int resetLevel(); + + @SelectProvider(type = ChannelProvider.class, method = "queryCameraChannelInBox") + List queryCameraChannelInBox(@Param("minLon") double minLon, @Param("maxLon") double maxLon, + @Param("minLat") double minLat, @Param("maxLat") double maxLat); + + @Select("select " + + "MAX(coalesce(gb_longitude, longitude)) as maxLng, " + + "MIN(coalesce(gb_longitude, longitude)) as minLng, " + + "MAX(coalesce(gb_latitude, latitude)) as maxLat, " + + "MIN(coalesce(gb_latitude, latitude)) as minLat " + + " from wvp_device_channel " + + " where channel_type = 0") + Extent queryExtent(); + + @SelectProvider(type = ChannelProvider.class, method = "queryAllWithPosition") + List queryAllWithPosition(); + + @SelectProvider(type = ChannelProvider.class, method = "queryListInExtent") + List queryListInExtent(@Param("minLng") double minLng, @Param("maxLng") double maxLng, + @Param("minLat") double minLat, @Param("maxLat") double maxLat); + + @SelectProvider(type = ChannelProvider.class, method = "queryListOutExtent") + List queryListOutExtent(@Param("minLng") double minLng, @Param("maxLng") double maxLng, + @Param("minLat") double minLat, @Param("maxLat") double maxLat); +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/dao/DeviceAlarmMapper.java b/src/main/java/com/genersoft/iot/vmp/gb28181/dao/DeviceAlarmMapper.java new file mode 100755 index 0000000..c380e59 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/dao/DeviceAlarmMapper.java @@ -0,0 +1,51 @@ +package com.genersoft.iot.vmp.gb28181.dao; + +import com.genersoft.iot.vmp.gb28181.bean.DeviceAlarm; +import org.apache.ibatis.annotations.Delete; +import org.apache.ibatis.annotations.Insert; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.annotations.Select; +import org.springframework.stereotype.Repository; + +import java.util.List; + +/** + * 用于存储设备的报警信息 + */ +@Mapper +@Repository +public interface DeviceAlarmMapper { + + @Insert("INSERT INTO wvp_device_alarm (device_id, channel_id, alarm_priority, alarm_method, alarm_time, alarm_description, longitude, latitude, alarm_type , create_time ) " + + "VALUES (#{deviceId}, #{channelId}, #{alarmPriority}, #{alarmMethod}, #{alarmTime}, #{alarmDescription}, #{longitude}, #{latitude}, #{alarmType}, #{createTime})") + int add(DeviceAlarm alarm); + + + @Select( value = {" "}) + List query(@Param("deviceId") String deviceId, @Param("channelId") String channelId, @Param("alarmPriority") String alarmPriority, @Param("alarmMethod") String alarmMethod, + @Param("alarmType") String alarmType, @Param("startTime") String startTime, @Param("endTime") String endTime); + + + @Delete(" " + ) + int clearAlarmBeforeTime(@Param("id") Integer id, @Param("deviceIdList") List deviceIdList, @Param("time") String time); +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/dao/DeviceChannelMapper.java b/src/main/java/com/genersoft/iot/vmp/gb28181/dao/DeviceChannelMapper.java new file mode 100755 index 0000000..b561d8d --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/dao/DeviceChannelMapper.java @@ -0,0 +1,618 @@ +package com.genersoft.iot.vmp.gb28181.dao; + +import com.genersoft.iot.vmp.gb28181.bean.Device; +import com.genersoft.iot.vmp.gb28181.bean.DeviceChannel; +import com.genersoft.iot.vmp.gb28181.controller.bean.ChannelReduce; +import com.genersoft.iot.vmp.gb28181.dao.provider.DeviceChannelProvider; +import com.genersoft.iot.vmp.web.gb28181.dto.DeviceChannelExtend; +import org.apache.ibatis.annotations.*; +import org.springframework.stereotype.Repository; + +import java.util.List; + +/** + * 用于存储设备通道信息 + */ +@Mapper +@Repository +public interface DeviceChannelMapper { + + + @Insert("") + @Options(useGeneratedKeys = true, keyProperty = "id", keyColumn = "id") + int add(DeviceChannel channel); + + @Update(value = {" "}) + int update(DeviceChannel channel); + + @SelectProvider(type = DeviceChannelProvider.class, method = "queryChannels") + List queryChannels(@Param("dataDeviceId") Integer dataDeviceId, @Param("civilCode") String civilCode, + @Param("businessGroupId") String businessGroupId, @Param("parentChannelId") String parentChannelId, + @Param("query") String query, @Param("queryParent") Boolean queryParent, + @Param("hasSubChannel") Boolean hasSubChannel, @Param("online") Boolean online, + @Param("channelIds") List channelIds, @Param("hasStream") Boolean hasStream); + + @SelectProvider(type = DeviceChannelProvider.class, method = "queryChannelsByDeviceDbId") + List queryChannelsByDeviceDbId(@Param("dataDeviceId") int dataDeviceId); + + @Select("") + List queryChaneIdListByDeviceDbIds(List deviceDbIds); + + @Delete("DELETE FROM wvp_device_channel WHERE data_type =1 and data_device_id=#{dataDeviceId}") + int cleanChannelsByDeviceId(@Param("dataDeviceId") int dataDeviceId); + + @Delete("DELETE FROM wvp_device_channel WHERE data_type=#{dataType} and data_device_id=#{dataDeviceId} AND device_id=#{deviceId}") + int deleteForNotify(DeviceChannel channel); + + @Select(value = {" "}) + List queryChannelsWithDeviceInfo( @Param("deviceId") String deviceId, @Param("parentChannelId") String parentChannelId, @Param("query") String query, @Param("hasSubChannel") Boolean hasSubChannel, @Param("online") Boolean online, @Param("channelIds") List channelIds); + + @Update(value = {"UPDATE wvp_device_channel SET stream_id=#{streamId} WHERE id=#{channelId}"}) + void startPlay(@Param("channelId") Integer channelId, @Param("streamId") String streamId); + + + @Select(value = {" "}) + List queryChannelListInAll(@Param("query") String query, @Param("online") Boolean online, @Param("hasSubChannel") Boolean hasSubChannel, @Param("platformId") String platformId, @Param("catalogId") String catalogId); + + + @Update(value = {"UPDATE wvp_device_channel SET status='OFF' WHERE id=#{id}"}) + void offline(@Param("id") int id); + + @Insert("") + int batchAdd(@Param("addChannels") List addChannels); + + + @Update(value = {"UPDATE wvp_device_channel SET status='ON' WHERE id=#{id}"}) + void online(@Param("id") int id); + + @Update({""}) + int batchUpdate(List updateChannels); + + @Update(value = {" "}) + int updatePosition(DeviceChannel deviceChannel); + + @Select("select " + + " id,\n" + + " data_device_id,\n" + + " create_time,\n" + + " update_time,\n" + + " sub_count,\n" + + " stream_id,\n" + + " has_audio,\n" + + " gps_time,\n" + + " stream_identification,\n" + + " channel_type,\n" + + " device_id,\n" + + " name,\n" + + " manufacturer,\n" + + " model,\n" + + " owner,\n" + + " civil_code,\n" + + " block,\n" + + " address,\n" + + " parental,\n" + + " parent_id,\n" + + " safety_way,\n" + + " register_way,\n" + + " cert_num,\n" + + " certifiable,\n" + + " err_code,\n" + + " end_time,\n" + + " secrecy,\n" + + " ip_address,\n" + + " port,\n" + + " password,\n" + + " status,\n" + + " longitude,\n" + + " latitude,\n" + + " gb_longitude,\n" + + " gb_latitude,\n" + + " ptz_type,\n" + + " position_type,\n" + + " room_type,\n" + + " use_type,\n" + + " supply_light_type,\n" + + " direction_type,\n" + + " resolution,\n" + + " business_group_id,\n" + + " download_speed,\n" + + " svc_space_support_mod,\n" + + " svc_time_support_mode\n" + + " from wvp_device_channel where data_type = 1 and data_device_id = #{dataDeviceId}") + List queryAllChannelsForRefresh(@Param("dataDeviceId") int dataDeviceId); + + @Select("select de.* from wvp_device de left join wvp_device_channel dc on de.device_id = dc.device_id where dc.data_type = 1 and dc.device_id=#{channelId}") + List getDeviceByChannelDeviceId(@Param("channelId") String channelId); + + + @Delete({""}) + int batchDel(List deleteChannelList); + + @Update({""}) + int batchUpdateStatus(List channels); + + @Select("select count(1) from wvp_device_channel where status = 'ON'") + int getOnlineCount(); + + @Select("select count(1) from wvp_device_channel") + int getAllChannelCount(); + + @Update("") + void updateChannelStreamIdentification(DeviceChannel channel); + + @Update("") + void updateAllChannelStreamIdentification(@Param("streamIdentification") String streamIdentification); + + @Update({""}) + void batchUpdatePosition(List channelList); + + @SelectProvider(type = DeviceChannelProvider.class, method = "getOne") + DeviceChannel getOne(@Param("id") int id); + + @Select(value = {" "}) + DeviceChannel getOneForSource(@Param("id") int id); + + @SelectProvider(type = DeviceChannelProvider.class, method = "getOneByDeviceId") + DeviceChannel getOneByDeviceId(@Param("dataDeviceId") int dataDeviceId, @Param("channelId") String channelId); + + + @Select(value = {" "}) + DeviceChannel getOneByDeviceIdForSource(@Param("dataDeviceId") int dataDeviceId, @Param("channelId") String channelId); + + + @Update(value = {"UPDATE wvp_device_channel SET stream_id=null WHERE id=#{channelId}"}) + void stopPlayById(@Param("channelId") Integer channelId); + + @Update(value = {" "}) + void changeAudio(@Param("channelId") int channelId, @Param("audio") boolean audio); + + @Update("UPDATE wvp_device_channel SET status=#{status} WHERE data_type=#{dataType} and data_device_id=#{dataDeviceId} AND device_id=#{deviceId}") + @Options(useGeneratedKeys = true, keyProperty = "id", keyColumn = "id") + void updateStatus(DeviceChannel channel); + + @Update({""}) + void updateChannelForNotify(DeviceChannel channel); + + + @Select(value = {" "}) + DeviceChannel getOneBySourceChannelId(@Param("dataDeviceId") int dataDeviceId, @Param("channelId") String channelId); + + @Update(value = {"UPDATE wvp_device_channel SET status = 'OFF' WHERE data_type = 1 and data_device_id=#{deviceId}"}) + void offlineByDeviceId(@Param("deviceId") int deviceId); + + +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/dao/DeviceMapper.java b/src/main/java/com/genersoft/iot/vmp/gb28181/dao/DeviceMapper.java new file mode 100755 index 0000000..ded0297 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/dao/DeviceMapper.java @@ -0,0 +1,505 @@ +package com.genersoft.iot.vmp.gb28181.dao; + +import com.genersoft.iot.vmp.gb28181.bean.Device; +import com.genersoft.iot.vmp.gb28181.bean.DeviceChannel; +import org.apache.ibatis.annotations.*; +import org.springframework.stereotype.Repository; + +import java.util.List; + +/** + * 用于存储设备信息 + */ +@Mapper +@Repository +public interface DeviceMapper { + + @Select("SELECT " + + "id, " + + "device_id, " + + "coalesce(custom_name, name) as name, " + + "password, " + + "manufacturer, " + + "model, " + + "firmware, " + + "transport," + + "stream_mode," + + "ip," + + "sdp_ip," + + "local_ip," + + "port," + + "host_address," + + "expires," + + "register_time," + + "keepalive_time," + + "create_time," + + "update_time," + + "charset," + + "subscribe_cycle_for_catalog," + + "subscribe_cycle_for_mobile_position," + + "mobile_position_submission_interval," + + "subscribe_cycle_for_alarm," + + "ssrc_check," + + "as_message_channel," + + "geo_coord_sys," + + "on_line," + + "server_id,"+ + "media_server_id," + + "broadcast_push_after_ack," + + "(SELECT count(0) FROM wvp_device_channel dc WHERE dc.data_type = 1 and dc.data_device_id= de.id) as channel_count "+ + " FROM wvp_device de WHERE de.device_id = #{deviceId}") + Device getDeviceByDeviceId( @Param("deviceId") String deviceId); + + @Insert("INSERT INTO wvp_device (" + + "device_id, " + + "name, " + + "manufacturer, " + + "model, " + + "firmware, " + + "transport," + + "stream_mode," + + "media_server_id," + + "ip," + + "sdp_ip," + + "local_ip," + + "port," + + "host_address," + + "expires," + + "register_time," + + "keepalive_time," + + "heart_beat_interval," + + "heart_beat_count," + + "position_capability," + + "create_time," + + "update_time," + + "charset," + + "subscribe_cycle_for_catalog," + + "subscribe_cycle_for_mobile_position,"+ + "mobile_position_submission_interval,"+ + "subscribe_cycle_for_alarm,"+ + "ssrc_check,"+ + "as_message_channel,"+ + "broadcast_push_after_ack,"+ + "geo_coord_sys,"+ + "server_id,"+ + "on_line"+ + ") VALUES (" + + "#{deviceId}," + + "#{name}," + + "#{manufacturer}," + + "#{model}," + + "#{firmware}," + + "#{transport}," + + "#{streamMode}," + + "#{mediaServerId}," + + "#{ip}," + + "#{sdpIp}," + + "#{localIp}," + + "#{port}," + + "#{hostAddress}," + + "#{expires}," + + "#{registerTime}," + + "#{keepaliveTime}," + + "#{heartBeatInterval}," + + "#{heartBeatCount}," + + "#{positionCapability}," + + "#{createTime}," + + "#{updateTime}," + + "#{charset}," + + "#{subscribeCycleForCatalog}," + + "#{subscribeCycleForMobilePosition}," + + "#{mobilePositionSubmissionInterval}," + + "#{subscribeCycleForAlarm}," + + "#{ssrcCheck}," + + "#{asMessageChannel}," + + "#{broadcastPushAfterAck}," + + "#{geoCoordSys}," + + "#{serverId}," + + "#{onLine}" + + ")") + @Options(useGeneratedKeys = true, keyProperty = "id", keyColumn = "id") + int add(Device device); + + @Update(value = {" "}) + int update(Device device); + + @Select( + " " + ) + List getDevices(@Param("dataType") Integer dataType, @Param("online") Boolean online); + + @Delete("DELETE FROM wvp_device WHERE device_id=#{deviceId}") + int del(String deviceId); + + @Select("SELECT " + + "id, " + + "device_id, " + + "coalesce(custom_name, name) as name, " + + "password, " + + "manufacturer, " + + "model, " + + "firmware, " + + "transport," + + "stream_mode," + + "ip," + + "sdp_ip,"+ + "local_ip,"+ + "port,"+ + "host_address,"+ + "expires,"+ + "register_time,"+ + "keepalive_time,"+ + "create_time,"+ + "update_time,"+ + "charset,"+ + "subscribe_cycle_for_catalog,"+ + "subscribe_cycle_for_mobile_position,"+ + "mobile_position_submission_interval,"+ + "subscribe_cycle_for_alarm,"+ + "ssrc_check,"+ + "as_message_channel,"+ + "broadcast_push_after_ack,"+ + "geo_coord_sys,"+ + "server_id,"+ + "on_line"+ + " FROM wvp_device WHERE on_line = true") + List getOnlineDevices(); + + @Select("SELECT " + + "id, " + + "device_id, " + + "coalesce(custom_name, name) as name, " + + "password, " + + "manufacturer, " + + "model, " + + "firmware, " + + "transport," + + "stream_mode," + + "ip," + + "sdp_ip,"+ + "local_ip,"+ + "port,"+ + "host_address,"+ + "expires,"+ + "register_time,"+ + "keepalive_time,"+ + "create_time,"+ + "update_time,"+ + "charset,"+ + "subscribe_cycle_for_catalog,"+ + "subscribe_cycle_for_mobile_position,"+ + "mobile_position_submission_interval,"+ + "subscribe_cycle_for_alarm,"+ + "ssrc_check,"+ + "media_server_id,"+ + "as_message_channel,"+ + "broadcast_push_after_ack,"+ + "geo_coord_sys,"+ + "server_id,"+ + "on_line"+ + " FROM wvp_device WHERE on_line = true and server_id = #{serverId}") + List getOnlineDevicesByServerId(@Param("serverId") String serverId); + + @Select("SELECT " + + "id,"+ + "device_id,"+ + "coalesce(custom_name,name)as name,"+ + "password,"+ + "manufacturer,"+ + "model,"+ + "firmware,"+ + "transport,"+ + "stream_mode,"+ + "ip,"+ + "sdp_ip,"+ + "local_ip,"+ + "port,"+ + "host_address,"+ + "expires,"+ + "register_time,"+ + "keepalive_time,"+ + "create_time,"+ + "update_time,"+ + "charset,"+ + "subscribe_cycle_for_catalog,"+ + "subscribe_cycle_for_mobile_position,"+ + "mobile_position_submission_interval,"+ + "subscribe_cycle_for_alarm,"+ + "ssrc_check,"+ + "as_message_channel,"+ + "broadcast_push_after_ack,"+ + "geo_coord_sys,"+ + "on_line"+ + " FROM wvp_device WHERE ip = #{host} AND port=#{port}") + Device getDeviceByHostAndPort(@Param("host") String host, @Param("port") int port); + + @Update(value = {" "}) + void updateCustom(Device device); + + @Insert("INSERT INTO wvp_device (" + + "device_id,"+ + "custom_name,"+ + "password,"+ + "sdp_ip,"+ + "create_time,"+ + "update_time,"+ + "charset,"+ + "ssrc_check,"+ + "as_message_channel,"+ + "broadcast_push_after_ack,"+ + "geo_coord_sys,"+ + "on_line,"+ + "stream_mode," + + "server_id," + + "media_server_id"+ + ") VALUES (" + + "#{deviceId}," + + "#{name}," + + "#{password}," + + "#{sdpIp}," + + "#{createTime}," + + "#{updateTime}," + + "#{charset}," + + "#{ssrcCheck}," + + "#{asMessageChannel}," + + "#{broadcastPushAfterAck}," + + "#{geoCoordSys}," + + "#{onLine}," + + "#{streamMode}," + + "#{serverId}," + + "#{mediaServerId}" + + ")") + void addCustomDevice(Device device); + + @Select("select * FROM wvp_device") + List getAll(); + + @Select("select * FROM wvp_device where as_message_channel = true") + List queryDeviceWithAsMessageChannel(); + + @Select(" ") + List getDeviceList(@Param("dataType") Integer dataType, @Param("query") String query, @Param("status") Boolean status); + + @Select("select * from wvp_device_channel where id = #{id}") + DeviceChannel getRawChannel(@Param("id") int id); + + @Select("select * from wvp_device where id = #{id}") + Device query(@Param("id") Integer id); + + @Select("select wd.* from wvp_device wd left join wvp_device_channel wdc on wdc.data_type = #{dataType} and wd.id = wdc.data_device_id where wdc.id = #{channelId}") + Device queryByChannelId(@Param("dataType") Integer dataType, @Param("channelId") Integer channelId); + + @Select("select wd.* from wvp_device wd left join wvp_device_channel wdc on wdc.data_type = #{dataType} and wd.id = wdc.data_device_id where wdc.device_id = #{channelDeviceId}") + Device getDeviceBySourceChannelDeviceId(@Param("dataType") Integer dataType, @Param("channelDeviceId") String channelDeviceId); + + @Update(value = {" "}) + void updateSubscribeCatalog(Device device); + + @Update(value = {" "}) + void updateSubscribeMobilePosition(Device device); + + @Update(value = {" "}) + void offlineByList(List offlineDevices); + + + @Update({""}) + void batchUpdate(List devices); + + + @Select(value = {" "}) + List queryByDeviceIds(List deviceIds); +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/dao/DeviceMobilePositionMapper.java b/src/main/java/com/genersoft/iot/vmp/gb28181/dao/DeviceMobilePositionMapper.java new file mode 100755 index 0000000..3e09df8 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/dao/DeviceMobilePositionMapper.java @@ -0,0 +1,49 @@ +package com.genersoft.iot.vmp.gb28181.dao; + +import com.genersoft.iot.vmp.gb28181.bean.MobilePosition; +import org.apache.ibatis.annotations.Delete; +import org.apache.ibatis.annotations.Insert; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.annotations.Select; + +import java.util.List; + +@Mapper +public interface DeviceMobilePositionMapper { + + @Insert("INSERT INTO wvp_device_mobile_position (device_id,channel_id, device_name,time,longitude,latitude,altitude,speed,direction,report_source,create_time)"+ + "VALUES (#{deviceId}, #{channelId}, #{deviceName}, #{time}, #{longitude}, #{latitude}, #{altitude}, #{speed}, #{direction}, #{reportSource}, #{createTime})") + int insertNewPosition(MobilePosition mobilePosition); + + @Select(value = {" "}) + List queryPositionByDeviceIdAndTime(@Param("deviceId") String deviceId, @Param("channelId") String channelId, @Param("startTime") String startTime, @Param("endTime") String endTime); + + @Select("SELECT * FROM wvp_device_mobile_position WHERE device_id = #{deviceId}" + + " ORDER BY time DESC LIMIT 1") + MobilePosition queryLatestPositionByDevice(String deviceId); + + @Delete("DELETE FROM wvp_device_mobile_position WHERE device_id = #{deviceId}") + int clearMobilePositionsByDeviceId(String deviceId); + + @Insert("") + void batchadd(List mobilePositions); + +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/dao/GroupMapper.java b/src/main/java/com/genersoft/iot/vmp/gb28181/dao/GroupMapper.java new file mode 100644 index 0000000..1edee01 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/dao/GroupMapper.java @@ -0,0 +1,323 @@ +package com.genersoft.iot.vmp.gb28181.dao; + +import com.genersoft.iot.vmp.gb28181.bean.CommonGBChannel; +import com.genersoft.iot.vmp.gb28181.bean.Group; +import com.genersoft.iot.vmp.gb28181.bean.GroupTree; +import com.genersoft.iot.vmp.gb28181.bean.Platform; +import com.genersoft.iot.vmp.web.custom.bean.CameraCount; +import com.genersoft.iot.vmp.web.custom.bean.CameraGroup; +import org.apache.ibatis.annotations.*; + +import java.util.List; +import java.util.Map; +import java.util.Set; + +@Mapper +public interface GroupMapper { + + @Insert("INSERT INTO wvp_common_group (device_id, name, parent_id, parent_device_id, business_group, create_time, update_time, civil_code, alias) " + + "VALUES (#{deviceId}, #{name}, #{parentId}, #{parentDeviceId}, #{businessGroup}, #{createTime}, #{updateTime}, #{civilCode}, #{alias})") + @Options(useGeneratedKeys = true, keyProperty = "id", keyColumn = "id") + int add(Group group); + + @Insert("INSERT INTO wvp_common_group (device_id, name, business_group, create_time, update_time, civil_code, alias) " + + "VALUES (#{deviceId}, #{name}, #{businessGroup}, #{createTime}, #{updateTime}, #{civilCode}, #{alias})") + @Options(useGeneratedKeys = true, keyProperty = "id", keyColumn = "id") + int addBusinessGroup(Group group); + + @Delete("DELETE FROM wvp_common_group WHERE id=#{id}") + int delete(@Param("id") int id); + + @Update(" UPDATE wvp_common_group " + + " SET update_time=#{updateTime}, device_id=#{deviceId}, name=#{name}, parent_id=#{parentId}, " + + " parent_device_id=#{parentDeviceId}, business_group=#{businessGroup}, civil_code=#{civilCode}, " + + " alias=#{alias}" + + " WHERE id = #{id}") + int update(Group group); + + @Select(value = {" "}) + List query(@Param("query") String query, @Param("parentId") String parentId, @Param("businessGroup") String businessGroup); + + @Select("SELECT * from wvp_common_group WHERE parent_id = #{parentId} ") + List getChildren(@Param("parentId") int parentId); + + @Select("SELECT * from wvp_common_group WHERE id = #{id} ") + Group queryOne(@Param("id") int id); + + + @Insert(" ") + @Options(useGeneratedKeys = true, keyProperty = "id", keyColumn = "id") + int batchAdd(List groupList); + + @Select(" ") + List queryForTree(@Param("query") String query, @Param("parentId") Integer parentId); + + @Select(" ") + List queryForTreeByBusinessGroup(@Param("query") String query, + @Param("businessGroup") String businessGroup); + + @Select(" ") + List queryBusinessGroupForTree(@Param("query") String query); + + @Select("SELECT * from wvp_common_group WHERE device_id = #{deviceId} and business_group = #{businessGroup}") + Group queryOneByDeviceId(@Param("deviceId") String deviceId, @Param("businessGroup") String businessGroup); + + @Select("SELECT * from wvp_common_group WHERE device_id = #{deviceId}") + Group queryOneByOnlyDeviceId(@Param("deviceId") String deviceId); + + @Delete("") + int batchDelete(List allChildren); + + @Select("SELECT * from wvp_common_group WHERE device_id = #{businessGroup} and business_group = #{businessGroup} ") + Group queryBusinessGroup(@Param("businessGroup") String businessGroup); + + @Select("SELECT * from wvp_common_group WHERE business_group = #{businessGroup} ") + List queryByBusinessGroup(@Param("businessGroup") String businessGroup); + + @Select("SELECT * from wvp_common_group WHERE business_group = #{businessGroup}") + @MapKey("id") + Map queryByBusinessGroupForMap(@Param("businessGroup") String businessGroup); + + @Delete("DELETE FROM wvp_common_group WHERE business_group = #{businessGroup}") + int deleteByBusinessGroup(@Param("businessGroup") String businessGroup); + + @Update(" UPDATE wvp_common_group " + + " SET parent_device_id=#{group.deviceId}, business_group = #{group.businessGroup}" + + " WHERE parent_id = #{parentId}") + int updateChild(@Param("parentId") Integer parentId, Group group); + + @Select(" ") + List queryInGroupListByDeviceId(List groupList); + + @Select(" ") + Set queryInChannelList(List channelList); + + @Select(" ") + Set queryParentInChannelList(Set groupSet); + + @Select(" ") + List queryForPlatform(@Param("platformId") Integer platformId); + + @Select(" ") + Set queryNotShareGroupForPlatformByChannelList(List channelList, @Param("platformId") Integer platformId); + + @Select(" ") + Set queryNotShareGroupForPlatformByGroupList(Set allGroup, @Param("platformId") Integer platformId); + + + @Select(" ") + Set queryByChannelList(List channelList); + + @Update(value = " ", databaseId = "mysql") + @Update(value = " ", databaseId = "h2") + @Update( value = " ", databaseId = "postgresql") + @Update( value = " ", databaseId = "kingbase") + void updateParentId(List groupListForAdd); + + @Update(value = " ", databaseId = "mysql") + @Update(value = " ", databaseId = "h2") + @Update( value = " ", databaseId = "kingbase") + @Update( value = " ", databaseId = "postgresql") + void updateParentIdWithBusinessGroup(List groupListForAdd); + + @Select(" ") + List queryForPlatformByGroupId(@Param("groupId") int groupId); + + @Delete("DELETE FROM wvp_platform_group WHERE group_id = #{groupId}") + void deletePlatformGroup(@Param("groupId") int groupId); + + @Select("SELECT * from wvp_common_group WHERE alias = #{alias} ") + CameraGroup queryGroupByAlias(@Param("alias") String alias); + + @Select("SELECT * from wvp_common_group WHERE alias = #{alias} and business_group = #{businessGroup}") + Group queryGroupByAliasAndBusinessGroup(@Param("alias") String alias, @Param("deviceId") String businessGroup); + + + @Select("") + List queryCountWithChild(List groupList); +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/dao/PlatformChannelMapper.java b/src/main/java/com/genersoft/iot/vmp/gb28181/dao/PlatformChannelMapper.java new file mode 100755 index 0000000..8d44f8d --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/dao/PlatformChannelMapper.java @@ -0,0 +1,551 @@ +package com.genersoft.iot.vmp.gb28181.dao; + +import com.genersoft.iot.vmp.gb28181.bean.*; +import org.apache.ibatis.annotations.*; +import org.springframework.stereotype.Repository; + +import java.util.Collection; +import java.util.List; +import java.util.Set; + +@Mapper +@Repository +public interface PlatformChannelMapper { + + + @Insert("") + int addChannels(@Param("platformId") Integer platformId, @Param("channelList") List channelList); + + @Delete("") + int delChannelForDeviceId(String deviceId); + + @Select("select d.*\n" + + "from wvp_platform_channel pgc\n" + + " left join wvp_device_channel dc on dc.id = pgc.device_channel_id\n" + + " left join wvp_device d on dc.device_id = d.device_id\n" + + "where dc.channel_type = 0 and dc.channel_id = #{channelId} and pgc.platform_id=#{platformId}") + List queryDeviceByPlatformIdAndChannelId(@Param("platformId") String platformId, @Param("channelId") String channelId); + + @Select(" ") + List queryPlatFormListForGBWithGBId(@Param("channelId") Integer channelId, List platforms); + + @Select("select dc.channel_id, dc.device_id,dc.name,d.manufacturer,d.model,d.firmware\n" + + "from wvp_platform_channel pgc\n" + + " left join wvp_device_channel dc on dc.id = pgc.device_channel_id\n" + + " left join wvp_device d on dc.device_id = d.device_id\n" + + "where dc.channel_type = 0 and dc.channel_id = #{channelId} and pgc.platform_id=#{platformId}") + List queryDeviceInfoByPlatformIdAndChannelId(@Param("platformId") String platformId, @Param("channelId") String channelId); + + @Select(" SELECT wp.* from wvp_platform_channel pgc " + + " left join wvp_device_channel dc on dc.id = pgc.device_channel_id " + + " left join wvp_platform wp on wp.id = pgc.platform_id" + + " WHERE dc.channel_type = 0 and dc.device_id=#{channelId}") + List queryParentPlatformByChannelId(@Param("channelId") String channelId); + + @Select("") + List queryForPlatformForWebList(@Param("platformId") Integer platformId, @Param("query") String query, + @Param("dataType") Integer dataType, @Param("online") Boolean online, + @Param("hasShare") Boolean hasShare); + + @Select("select\n" + + " wdc.id as gb_id,\n" + + " wdc.data_type,\n" + + " wdc.data_device_id,\n" + + " wdc.create_time,\n" + + " wdc.update_time,\n" + + " coalesce(wpgc.custom_device_id, wdc.gb_device_id, wdc.device_id) as gb_device_id,\n" + + " coalesce(wpgc.custom_name, wdc.gb_name, wdc.name) as gb_name,\n" + + " coalesce(wpgc.custom_manufacturer, wdc.gb_manufacturer, wdc.manufacturer) as gb_manufacturer,\n" + + " coalesce(wpgc.custom_model, wdc.gb_model, wdc.model) as gb_model,\n" + + " coalesce(wpgc.custom_owner, wdc.gb_owner, wdc.owner) as gb_owner,\n" + + " coalesce(wpgc.custom_civil_code, wdc.gb_civil_code, wdc.civil_code) as gb_civil_code,\n" + + " coalesce(wpgc.custom_block, wdc.gb_block, wdc.block) as gb_block,\n" + + " coalesce(wpgc.custom_address, wdc.gb_address, wdc.address) as gb_address,\n" + + " coalesce(wpgc.custom_parental, wdc.gb_parental, wdc.parental) as gb_parental,\n" + + " coalesce(wpgc.custom_parent_id, wdc.gb_parent_id, wdc.parent_id) as gb_parent_id,\n" + + " coalesce(wpgc.custom_safety_way, wdc.gb_safety_way, wdc.safety_way) as gb_safety_way,\n" + + " coalesce(wpgc.custom_register_way, wdc.gb_register_way, wdc.register_way) as gb_register_way,\n" + + " coalesce(wpgc.custom_cert_num, wdc.gb_cert_num, wdc.cert_num) as gb_cert_num,\n" + + " coalesce(wpgc.custom_certifiable, wdc.gb_certifiable, wdc.certifiable) as gb_certifiable,\n" + + " coalesce(wpgc.custom_err_code, wdc.gb_err_code, wdc.err_code) as gb_err_code,\n" + + " coalesce(wpgc.custom_end_time, wdc.gb_end_time, wdc.end_time) as gb_end_time,\n" + + " coalesce(wpgc.custom_secrecy, wdc.gb_secrecy, wdc.secrecy) as gb_secrecy,\n" + + " coalesce(wpgc.custom_ip_address, wdc.gb_ip_address, wdc.ip_address) as gb_ip_address,\n" + + " coalesce(wpgc.custom_port, wdc.gb_port, wdc.port) as gb_port,\n" + + " coalesce(wpgc.custom_password, wdc.gb_password, wdc.password) as gb_password,\n" + + " coalesce(wpgc.custom_status, wdc.gb_status, wdc.status) as gb_status,\n" + + " coalesce(wpgc.custom_longitude, wdc.gb_longitude, wdc.longitude) as gb_longitude,\n" + + " coalesce(wpgc.custom_latitude, wdc.gb_latitude, wdc.latitude) as gb_latitude,\n" + + " coalesce(wpgc.custom_ptz_type, wdc.gb_ptz_type, wdc.ptz_type) as gb_ptz_type,\n" + + " coalesce(wpgc.custom_position_type, wdc.gb_position_type, wdc.position_type) as gb_position_type,\n" + + " coalesce(wpgc.custom_room_type, wdc.gb_room_type, wdc.room_type) as gb_room_type,\n" + + " coalesce(wpgc.custom_use_type, wdc.gb_use_type, wdc.use_type) as gb_use_type,\n" + + " coalesce(wpgc.custom_supply_light_type, wdc.gb_supply_light_type, wdc.supply_light_type) as gb_supply_light_type,\n" + + " coalesce(wpgc.custom_direction_type, wdc.gb_direction_type, wdc.direction_type) as gb_direction_type,\n" + + " coalesce(wpgc.custom_resolution, wdc.gb_resolution, wdc.resolution) as gb_resolution,\n" + + " coalesce(wpgc.custom_business_group_id, wdc.gb_business_group_id, wdc.business_group_id) as gb_business_group_id,\n" + + " coalesce(wpgc.custom_download_speed, wdc.gb_download_speed, wdc.download_speed) as gb_download_speed,\n" + + " coalesce(wpgc.custom_svc_space_support_mod, wdc.gb_svc_space_support_mod, wdc.svc_space_support_mod) as gb_svc_space_support_mod,\n" + + " coalesce(wpgc.custom_svc_time_support_mode, wdc.gb_svc_time_support_mode, wdc.svc_time_support_mode) as gb_svc_time_support_mode\n" + + " from wvp_device_channel wdc" + + " left join wvp_platform_channel wpgc on wdc.id = wpgc.device_channel_id" + + " where wdc.channel_type = 0 and wpgc.platform_id = #{platformId} and coalesce(wpgc.custom_device_id, wdc.gb_device_id, wdc.device_id) = #{channelDeviceId} order by wdc.id " + + ) + List queryOneWithPlatform(@Param("platformId") Integer platformId, @Param("channelDeviceId") String channelDeviceId); + + + @Select("") + List queryNotShare(@Param("platformId") Integer platformId, List channelIds); + + @Select("") + List queryShare(@Param("platformId") Integer platformId, List channelIds); + + @Delete("") + int removeChannelsWithPlatform(@Param("platformId") Integer platformId, List channelList); + + @Delete("") + int removeChannels(List channelList); + + @Insert("") + int addPlatformGroup(Collection groupListNotShare, @Param("platformId") Integer platformId); + + @Insert("") + int addPlatformRegion(List regionListNotShare, @Param("platformId") Integer platformId); + + @Delete("") + int removePlatformGroup(List groupList, @Param("platformId") Integer platformId); + + @Delete("") + void removePlatformGroupById(@Param("id") int id, @Param("platformId") Integer platformId); + + @Delete("") + void removePlatformRegionById(@Param("id") int id, @Param("platformId") Integer platformId); + + @Select(" ") + Set queryShareChildrenGroup(@Param("parentId") Integer parentId, @Param("platformId") Integer platformId); + + @Select(" ") + Set queryShareChildrenRegion(@Param("parentId") String parentId, @Param("platformId") Integer platformId); + + @Select(" ") + Set queryShareParentGroupByGroupSet(Set groupSet, @Param("platformId") Integer platformId); + + @Select(" ") + Set queryShareParentRegionByRegionSet(Set regionSet, @Param("platformId") Integer platformId); + + @Select(" ") + List queryPlatFormListByChannelList(Collection ids); + + @Select(" ") + List queryPlatFormListByChannelId(@Param("channelId") int channelId); + + @Delete("") + void removeChannelsByPlatformId(@Param("platformId") Integer platformId); + + @Delete("") + void removePlatformGroupsByPlatformId(@Param("platformId") Integer platformId); + + @Delete("") + void removePlatformRegionByPlatformId(@Param("platformId") Integer platformId); + + @Update(value = {" "}) + void updateCustomChannel(PlatformChannel channel); + + + @Select("") + CommonGBChannel queryShareChannel(@Param("platformId") int platformId, @Param("gbId") int gbId); + + + @Select(" ") + Set queryShareGroup(@Param("platformId") Integer platformId); + + @Select(" ") + Set queryShareRegion(Integer id); +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/dao/PlatformMapper.java b/src/main/java/com/genersoft/iot/vmp/gb28181/dao/PlatformMapper.java new file mode 100755 index 0000000..bb3c0f0 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/dao/PlatformMapper.java @@ -0,0 +1,110 @@ +package com.genersoft.iot.vmp.gb28181.dao; + +import com.genersoft.iot.vmp.gb28181.bean.Platform; +import org.apache.ibatis.annotations.*; +import org.springframework.stereotype.Repository; + +import java.util.List; + +/** + * 用于存储上级平台 + */ +@Mapper +@Repository +public interface PlatformMapper { + + @Insert("INSERT INTO wvp_platform (enable, name, server_gb_id, server_gb_domain, server_ip, server_port,device_gb_id,device_ip,"+ + " device_port,username,password,expires,keep_timeout,transport,character_set,ptz,rtcp,status,catalog_group, update_time," + + " create_time, as_message_channel, send_stream_ip, auto_push_channel, catalog_with_platform,catalog_with_group,catalog_with_region, "+ + " civil_code,manufacturer,model,address,register_way,secrecy,server_id) " + + " VALUES (#{enable}, #{name}, #{serverGBId}, #{serverGBDomain}, #{serverIp}, #{serverPort}, #{deviceGBId}, #{deviceIp}, " + + " #{devicePort}, #{username}, #{password}, #{expires}, #{keepTimeout}, #{transport}, #{characterSet}, #{ptz}, #{rtcp}, #{status}, #{catalogGroup},#{updateTime}," + + " #{createTime}, #{asMessageChannel}, #{sendStreamIp}, #{autoPushChannel}, #{catalogWithPlatform}, #{catalogWithGroup},#{catalogWithRegion}, " + + " #{civilCode}, #{manufacturer}, #{model}, #{address}, #{registerWay}, #{secrecy}, #{serverId})") + int add(Platform parentPlatform); + + @Update("UPDATE wvp_platform " + + "SET update_time = #{updateTime}," + + " enable=#{enable}, " + + " name=#{name}," + + " server_gb_id=#{serverGBId}, " + + " server_gb_domain=#{serverGBDomain}, " + + " server_ip=#{serverIp}," + + " server_port=#{serverPort}, " + + " device_gb_id=#{deviceGBId}," + + " device_ip=#{deviceIp}, " + + " device_port=#{devicePort}, " + + " username=#{username}, " + + " password=#{password}, " + + " expires=#{expires}, " + + " keep_timeout=#{keepTimeout}, " + + " transport=#{transport}, " + + " character_set=#{characterSet}, " + + " ptz=#{ptz}, " + + " rtcp=#{rtcp}, " + + " status=#{status}, " + + " catalog_group=#{catalogGroup}, " + + " as_message_channel=#{asMessageChannel}, " + + " send_stream_ip=#{sendStreamIp}, " + + " auto_push_channel=#{autoPushChannel}, " + + " catalog_with_platform=#{catalogWithPlatform}, " + + " catalog_with_group=#{catalogWithGroup}, " + + " catalog_with_region=#{catalogWithRegion}, " + + " civil_code=#{civilCode}, " + + " manufacturer=#{manufacturer}, " + + " model=#{model}, " + + " address=#{address}, " + + " register_way=#{registerWay}, " + + " server_id=#{serverId}, " + + " secrecy=#{secrecy} " + + "WHERE id=#{id}") + int update(Platform parentPlatform); + + @Delete("DELETE FROM wvp_platform WHERE id=#{id}") + int delete(@Param("id") Integer id); + + @Select(" ") + List queryList(@Param("query") String query); + + @Select("SELECT * FROM wvp_platform WHERE server_id=#{serverId} and enable=#{enable} ") + List queryEnableParentPlatformListByServerId(@Param("serverId") String serverId, @Param("enable") boolean enable); + + @Select("SELECT * FROM wvp_platform WHERE enable=true and as_message_channel=true") + List queryEnablePlatformListWithAsMessageChannel(); + + @Select("SELECT * FROM wvp_platform WHERE server_gb_id=#{platformGbId}") + Platform getParentPlatByServerGBId(String platformGbId); + + @Select("SELECT * FROM wvp_platform WHERE id=#{id}") + Platform query(int id); + + @Update("UPDATE wvp_platform SET status=#{online}, server_id = #{serverId} WHERE id=#{id}" ) + int updateStatus(@Param("id") int id, @Param("online") boolean online, @Param("serverId") String serverId); + + @Select("SELECT server_id FROM wvp_platform WHERE enable=true and server_id != #{serverId} group by server_id") + List queryServerIdsWithEnableAndNotInServer(@Param("serverId") String serverId); + + @Select("SELECT * FROM wvp_platform WHERE server_id = #{serverId}") + List queryByServerId(@Param("serverId") String serverId); + + @Select("SELECT * FROM wvp_platform ") + List queryAll(); + + @Select("SELECT * FROM wvp_platform WHERE enable=true and server_id = #{serverId}") + List queryServerIdsWithEnableAndServer(@Param("serverId") String serverId); + + @Update("UPDATE wvp_platform SET status=false where server_id = #{serverId}" ) + void offlineAll(@Param("serverId") String serverId); + +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/dao/RegionMapper.java b/src/main/java/com/genersoft/iot/vmp/gb28181/dao/RegionMapper.java new file mode 100644 index 0000000..3d67be0 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/dao/RegionMapper.java @@ -0,0 +1,197 @@ +package com.genersoft.iot.vmp.gb28181.dao; + +import com.genersoft.iot.vmp.common.CivilCodePo; +import com.genersoft.iot.vmp.gb28181.bean.CommonGBChannel; +import com.genersoft.iot.vmp.gb28181.bean.Region; +import com.genersoft.iot.vmp.gb28181.bean.RegionTree; +import org.apache.ibatis.annotations.*; + +import java.util.List; +import java.util.Set; + +@Mapper +public interface RegionMapper { + + @Insert("INSERT INTO wvp_common_region (device_id, name, parent_id, parent_device_id, create_time, update_time) " + + "VALUES (#{deviceId}, #{name}, #{parentId}, #{parentDeviceId}, #{createTime}, #{updateTime})") + @Options(useGeneratedKeys = true, keyProperty = "id", keyColumn = "id") + void add(Region region); + + @Delete("DELETE FROM wvp_common_region WHERE id=#{id}") + int delete(@Param("id") int id); + + @Update(" UPDATE wvp_common_region " + + " SET update_time=#{updateTime}, device_id=#{deviceId}, name=#{name}, parent_id=#{parentId}, parent_device_id=#{parentDeviceId}" + + " WHERE id = #{id}") + int update(Region region); + + @Select(value = {" "}) + List query(@Param("query") String query, @Param("parentId") String parentId); + + @Select("SELECT * from wvp_common_region WHERE parent_id = #{parentId} ORDER BY id ") + List getChildren(@Param("parentId") Integer parentId); + + @Select("SELECT * from wvp_common_region WHERE id = #{id} ") + Region queryOne(@Param("id") int id); + + @Select(" select dc.civil_code as civil_code " + + " from wvp_device_channel dc " + + " where dc.civil_code not in " + + " (select device_id from wvp_common_region)") + List getUninitializedCivilCode(); + + @Select(" ") + List queryInList(Set codes); + + + @Insert(" ") + @Options(useGeneratedKeys = true, keyProperty = "id", keyColumn = "id") + int batchAdd(List regionList); + + @Select(" ") + List queryForTree(@Param("parentId") Integer parentId); + + @Delete("") + void batchDelete(List allChildren); + + @Select(" ") + List queryInRegionListByDeviceId(List regionList); + + @Select(" ") + List queryByPlatform(@Param("platformId") Integer platformId); + + + @Update(value = " ", databaseId = "mysql") + @Update(value = " ", databaseId = "h2") + @Update( value = " ", databaseId = "kingbase") + @Update( value = " ", databaseId = "postgresql") + void updateParentId(List regionListForAdd); + + @Update(" ") + void updateChild(@Param("parentId") int parentId, @Param("parentDeviceId") String parentDeviceId); + + @Select("SELECT * from wvp_common_region WHERE device_id = #{deviceId} ") + Region queryByDeviceId(@Param("deviceId") String deviceId); + + @Select(" ") + Set queryParentInChannelList(Set regionSet); + + @Select(" ") + Set queryByChannelList(List channelList); + + @Select(" ") + Set queryNotShareRegionForPlatformByChannelList(List channelList, @Param("platformId") Integer platformId); + + @Select(" ") + Set queryNotShareRegionForPlatformByRegionList(Set allRegion, @Param("platformId") Integer platformId); + + + @Select(" ") + Set queryInCivilCodePoList(List civilCodePoList); +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/dao/provider/ChannelProvider.java b/src/main/java/com/genersoft/iot/vmp/gb28181/dao/provider/ChannelProvider.java new file mode 100644 index 0000000..96f3cba --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/dao/provider/ChannelProvider.java @@ -0,0 +1,955 @@ +package com.genersoft.iot.vmp.gb28181.dao.provider; + +import com.genersoft.iot.vmp.gb28181.bean.CommonGBChannel; +import com.genersoft.iot.vmp.gb28181.bean.Group; +import com.genersoft.iot.vmp.streamPush.bean.StreamPush; +import com.genersoft.iot.vmp.web.custom.bean.CameraGroup; +import com.genersoft.iot.vmp.web.custom.bean.Point; + +import java.util.Collection; +import java.util.List; +import java.util.Map; + +public class ChannelProvider { + + public final static String BASE_SQL = "select\n" + + " id as gb_id,\n" + + " data_type,\n" + + " data_device_id,\n" + + " create_time,\n" + + " update_time,\n" + + " stream_id,\n" + + " record_plan_id,\n" + + " enable_broadcast,\n" + + " map_level,\n" + + " coalesce(gb_device_id, device_id) as gb_device_id,\n" + + " coalesce(gb_name, name) as gb_name,\n" + + " coalesce(gb_manufacturer, manufacturer) as gb_manufacturer,\n" + + " coalesce(gb_model, model) as gb_model,\n" + + " coalesce(gb_owner, owner) as gb_owner,\n" + + " coalesce(gb_civil_code, civil_code) as gb_civil_code,\n" + + " coalesce(gb_block, block) as gb_block,\n" + + " coalesce(gb_address, address) as gb_address,\n" + + " coalesce(gb_parental, parental) as gb_parental,\n" + + " coalesce(gb_parent_id, parent_id) as gb_parent_id,\n" + + " coalesce(gb_safety_way, safety_way) as gb_safety_way,\n" + + " coalesce(gb_register_way, register_way) as gb_register_way,\n" + + " coalesce(gb_cert_num, cert_num) as gb_cert_num,\n" + + " coalesce(gb_certifiable, certifiable) as gb_certifiable,\n" + + " coalesce(gb_err_code, err_code) as gb_err_code,\n" + + " coalesce(gb_end_time, end_time) as gb_end_time,\n" + + " coalesce(gb_secrecy, secrecy) as gb_secrecy,\n" + + " coalesce(gb_ip_address, ip_address) as gb_ip_address,\n" + + " coalesce(gb_port, port) as gb_port,\n" + + " coalesce(gb_password, password) as gb_password,\n" + + " coalesce(gb_status, status) as gb_status,\n" + + " coalesce(gb_longitude, longitude) as gb_longitude,\n" + + " coalesce(gb_latitude, latitude) as gb_latitude,\n" + + " coalesce(gb_ptz_type, ptz_type) as gb_ptz_type,\n" + + " coalesce(gb_position_type, position_type) as gb_position_type,\n" + + " coalesce(gb_room_type, room_type) as gb_room_type,\n" + + " coalesce(gb_use_type, use_type) as gb_use_type,\n" + + " coalesce(gb_supply_light_type, supply_light_type) as gb_supply_light_type,\n" + + " coalesce(gb_direction_type, direction_type) as gb_direction_type,\n" + + " coalesce(gb_resolution, resolution) as gb_resolution,\n" + + " coalesce(gb_business_group_id, business_group_id) as gb_business_group_id,\n" + + " coalesce(gb_download_speed, download_speed) as gb_download_speed,\n" + + " coalesce(gb_svc_space_support_mod, svc_space_support_mod) as gb_svc_space_support_mod,\n" + + " coalesce(gb_svc_time_support_mode,svc_time_support_mode) as gb_svc_time_support_mode\n" + + " from wvp_device_channel\n" + ; + + public final static String BASE_SQL_TABLE_NAME = "select\n" + + " wdc.id as gb_id,\n" + + " wdc.data_type,\n" + + " wdc.data_device_id,\n" + + " wdc.create_time,\n" + + " wdc.update_time,\n" + + " wdc.stream_id,\n" + + " wdc.record_plan_id,\n" + + " wdc.enable_broadcast,\n" + + " coalesce(wdc.gb_device_id, wdc.device_id) as gb_device_id,\n" + + " coalesce(wdc.gb_name, wdc.name) as gb_name,\n" + + " coalesce(wdc.gb_manufacturer, wdc.manufacturer) as gb_manufacturer,\n" + + " coalesce(wdc.gb_model, wdc.model) as gb_model,\n" + + " coalesce(wdc.gb_owner, wdc.owner) as gb_owner,\n" + + " coalesce(wdc.gb_civil_code, wdc.civil_code) as gb_civil_code,\n" + + " coalesce(wdc.gb_block, wdc.block) as gb_block,\n" + + " coalesce(wdc.gb_address, wdc.address) as gb_address,\n" + + " coalesce(wdc.gb_parental, wdc.parental) as gb_parental,\n" + + " coalesce(wdc.gb_parent_id, wdc.parent_id) as gb_parent_id,\n" + + " coalesce(wdc.gb_safety_way, wdc.safety_way) as gb_safety_way,\n" + + " coalesce(wdc.gb_register_way, wdc.register_way) as gb_register_way,\n" + + " coalesce(wdc.gb_cert_num, wdc.cert_num) as gb_cert_num,\n" + + " coalesce(wdc.gb_certifiable, wdc.certifiable) as gb_certifiable,\n" + + " coalesce(wdc.gb_err_code, wdc.err_code) as gb_err_code,\n" + + " coalesce(wdc.gb_end_time, wdc.end_time) as gb_end_time,\n" + + " coalesce(wdc.gb_secrecy, wdc.secrecy) as gb_secrecy,\n" + + " coalesce(wdc.gb_ip_address, wdc.ip_address) as gb_ip_address,\n" + + " coalesce(wdc.gb_port, wdc.port) as gb_port,\n" + + " coalesce(wdc.gb_password, wdc.password) as gb_password,\n" + + " coalesce(wdc.gb_status, wdc.status) as gb_status,\n" + + " coalesce(wdc.gb_longitude, wdc.longitude) as gb_longitude,\n" + + " coalesce(wdc.gb_latitude, wdc.latitude) as gb_latitude,\n" + + " coalesce(wdc.gb_ptz_type, wdc.ptz_type) as gb_ptz_type,\n" + + " coalesce(wdc.gb_position_type, wdc.position_type) as gb_position_type,\n" + + " coalesce(wdc.gb_room_type, wdc.room_type) as gb_room_type,\n" + + " coalesce(wdc.gb_use_type, wdc.use_type) as gb_use_type,\n" + + " coalesce(wdc.gb_supply_light_type, wdc.supply_light_type) as gb_supply_light_type,\n" + + " coalesce(wdc.gb_direction_type, wdc.direction_type) as gb_direction_type,\n" + + " coalesce(wdc.gb_resolution, wdc.resolution) as gb_resolution,\n" + + " coalesce(wdc.gb_business_group_id, wdc.business_group_id) as gb_business_group_id,\n" + + " coalesce(wdc.gb_download_speed, wdc.download_speed) as gb_download_speed,\n" + + " coalesce(wdc.gb_svc_space_support_mod, wdc.svc_space_support_mod) as gb_svc_space_support_mod,\n" + + " coalesce(wdc.gb_svc_time_support_mode, wdc.svc_time_support_mode) as gb_svc_time_support_mode\n" + + " from wvp_device_channel wdc\n" + ; + + private final static String BASE_SQL_FOR_PLATFORM = + "select\n" + + " wdc.id as gb_id,\n" + + " wdc.data_type,\n" + + " wdc.data_device_id,\n" + + " wdc.create_time,\n" + + " wdc.update_time,\n" + + " wdc.enable_broadcast,\n" + + " coalesce(wpgc.custom_device_id, wdc.gb_device_id, wdc.device_id) as gb_device_id,\n" + + " coalesce(wpgc.custom_name, wdc.gb_name, wdc.name) as gb_name,\n" + + " coalesce(wpgc.custom_manufacturer, wdc.gb_manufacturer, wdc.manufacturer) as gb_manufacturer,\n" + + " coalesce(wpgc.custom_model, wdc.gb_model, wdc.model) as gb_model,\n" + + " coalesce(wpgc.custom_owner, wdc.gb_owner, wdc.owner) as gb_owner,\n" + + " coalesce(wpgc.custom_civil_code, wdc.gb_civil_code, wdc.civil_code) as gb_civil_code,\n" + + " coalesce(wpgc.custom_block, wdc.gb_block, wdc.block) as gb_block,\n" + + " coalesce(wpgc.custom_address, wdc.gb_address, wdc.address) as gb_address,\n" + + " coalesce(wpgc.custom_parental, wdc.gb_parental, wdc.parental) as gb_parental,\n" + + " coalesce(wpgc.custom_parent_id, wdc.gb_parent_id, wdc.parent_id) as gb_parent_id,\n" + + " coalesce(wpgc.custom_safety_way, wdc.gb_safety_way, wdc.safety_way) as gb_safety_way,\n" + + " coalesce(wpgc.custom_register_way, wdc.gb_register_way, wdc.register_way) as gb_register_way,\n" + + " coalesce(wpgc.custom_cert_num, wdc.gb_cert_num, wdc.cert_num) as gb_cert_num,\n" + + " coalesce(wpgc.custom_certifiable, wdc.gb_certifiable, wdc.certifiable) as gb_certifiable,\n" + + " coalesce(wpgc.custom_err_code, wdc.gb_err_code, wdc.err_code) as gb_err_code,\n" + + " coalesce(wpgc.custom_end_time, wdc.gb_end_time, wdc.end_time) as gb_end_time,\n" + + " coalesce(wpgc.custom_secrecy, wdc.gb_secrecy, wdc.secrecy) as gb_secrecy,\n" + + " coalesce(wpgc.custom_ip_address, wdc.gb_ip_address, wdc.ip_address) as gb_ip_address,\n" + + " coalesce(wpgc.custom_port, wdc.gb_port, wdc.port) as gb_port,\n" + + " coalesce(wpgc.custom_password, wdc.gb_password, wdc.password) as gb_password,\n" + + " coalesce(wpgc.custom_status, wdc.gb_status, wdc.status) as gb_status,\n" + + " coalesce(wpgc.custom_longitude, wdc.gb_longitude, wdc.longitude) as gb_longitude,\n" + + " coalesce(wpgc.custom_latitude, wdc.gb_latitude, wdc.latitude) as gb_latitude,\n" + + " coalesce(wpgc.custom_ptz_type, wdc.gb_ptz_type, wdc.ptz_type) as gb_ptz_type,\n" + + " coalesce(wpgc.custom_position_type, wdc.gb_position_type, wdc.position_type) as gb_position_type,\n" + + " coalesce(wpgc.custom_room_type, wdc.gb_room_type, wdc.room_type) as gb_room_type,\n" + + " coalesce(wpgc.custom_use_type, wdc.gb_use_type, wdc.use_type) as gb_use_type,\n" + + " coalesce(wpgc.custom_supply_light_type, wdc.gb_supply_light_type, wdc.supply_light_type) as gb_supply_light_type,\n" + + " coalesce(wpgc.custom_direction_type, wdc.gb_direction_type, wdc.direction_type) as gb_direction_type,\n" + + " coalesce(wpgc.custom_resolution, wdc.gb_resolution, wdc.resolution) as gb_resolution,\n" + + " coalesce(wpgc.custom_business_group_id, wdc.gb_business_group_id, wdc.business_group_id) as gb_business_group_id,\n" + + " coalesce(wpgc.custom_download_speed, wdc.gb_download_speed, wdc.download_speed) as gb_download_speed,\n" + + " coalesce(wpgc.custom_svc_space_support_mod, wdc.gb_svc_space_support_mod, wdc.svc_space_support_mod) as gb_svc_space_support_mod,\n" + + " coalesce(wpgc.custom_svc_time_support_mode, wdc.gb_svc_time_support_mode, wdc.svc_time_support_mode) as gb_svc_time_support_mode\n" + + " from wvp_device_channel wdc" + + " left join wvp_platform_channel wpgc on wdc.id = wpgc.device_channel_id" + ; + + private final static String BASE_SQL_FOR_CAMERA_DEVICE = + "select\n" + + " wdc.id as gb_id,\n" + + " wdc.data_type,\n" + + " wdc.data_device_id,\n" + + " wdc.create_time,\n" + + " wdc.update_time,\n" + + " wdc.stream_id,\n" + + " wdc.record_plan_id,\n" + + " wdc.enable_broadcast,\n" + + " wd.device_id as deviceCode,\n" + + " wcg.alias as groupAlias,\n" + + " wcg2.alias as topGroupGAlias,\n" + + " coalesce(wdc.gb_device_id, wdc.device_id) as gb_device_id,\n" + + " coalesce(wdc.gb_name, wdc.name) as gb_name,\n" + + " coalesce(wdc.gb_manufacturer, wdc.manufacturer) as gb_manufacturer,\n" + + " coalesce(wdc.gb_model, wdc.model) as gb_model,\n" + + " coalesce(wdc.gb_owner, wdc.owner) as gb_owner,\n" + + " coalesce(wdc.gb_civil_code, wdc.civil_code) as gb_civil_code,\n" + + " coalesce(wdc.gb_block, wdc.block) as gb_block,\n" + + " coalesce(wdc.gb_address, wdc.address) as gb_address,\n" + + " coalesce(wdc.gb_parental, wdc.parental) as gb_parental,\n" + + " coalesce(wdc.gb_parent_id, wdc.parent_id) as gb_parent_id,\n" + + " coalesce(wdc.gb_safety_way, wdc.safety_way) as gb_safety_way,\n" + + " coalesce(wdc.gb_register_way, wdc.register_way) as gb_register_way,\n" + + " coalesce(wdc.gb_cert_num, wdc.cert_num) as gb_cert_num,\n" + + " coalesce(wdc.gb_certifiable, wdc.certifiable) as gb_certifiable,\n" + + " coalesce(wdc.gb_err_code, wdc.err_code) as gb_err_code,\n" + + " coalesce(wdc.gb_end_time, wdc.end_time) as gb_end_time,\n" + + " coalesce(wdc.gb_secrecy, wdc.secrecy) as gb_secrecy,\n" + + " coalesce(wdc.gb_ip_address, wdc.ip_address) as gb_ip_address,\n" + + " coalesce(wdc.gb_port, wdc.port) as gb_port,\n" + + " coalesce(wdc.gb_password, wdc.password) as gb_password,\n" + + " coalesce(wdc.gb_status, wdc.status) as gb_status,\n" + + " coalesce(wdc.gb_longitude, wdc.longitude) as gb_longitude,\n" + + " coalesce(wdc.gb_latitude, wdc.latitude) as gb_latitude,\n" + + " coalesce(wdc.gb_ptz_type, wdc.ptz_type) as gb_ptz_type,\n" + + " coalesce(wdc.gb_position_type, wdc.position_type) as gb_position_type,\n" + + " coalesce(wdc.gb_room_type, wdc.room_type) as gb_room_type,\n" + + " coalesce(wdc.gb_use_type, wdc.use_type) as gb_use_type,\n" + + " coalesce(wdc.gb_supply_light_type, wdc.supply_light_type) as gb_supply_light_type,\n" + + " coalesce(wdc.gb_direction_type, wdc.direction_type) as gb_direction_type,\n" + + " coalesce(wdc.gb_resolution, wdc.resolution) as gb_resolution,\n" + + " coalesce(wdc.gb_business_group_id, wdc.business_group_id) as gb_business_group_id,\n" + + " coalesce(wdc.gb_download_speed, wdc.download_speed) as gb_download_speed,\n" + + " coalesce(wdc.gb_svc_space_support_mod, wdc.svc_space_support_mod) as gb_svc_space_support_mod,\n" + + " coalesce(wdc.gb_svc_time_support_mode, wdc.svc_time_support_mode) as gb_svc_time_support_mode\n" + + " from wvp_device_channel wdc\n" + + " left join wvp_device wd on wdc.data_type = 1 AND wd.id = wdc.data_device_id" + + " left join wvp_common_group wcg on wcg.device_id = coalesce(wdc.gb_parent_id, wdc.parent_id)" + + " left join wvp_common_group wcg2 on wcg2.device_id = wcg.business_group" + ; + + public String queryByDeviceId(Map params ){ + return BASE_SQL + " where channel_type = 0 and coalesce(gb_device_id, device_id) = #{gbDeviceId}"; + } + + public String queryById(Map params ){ + return BASE_SQL + " where channel_type = 0 and id = #{gbId}"; + } + + public String queryByDataId(Map params ){ + return BASE_SQL + " where channel_type = 0 and data_type = #{dataType} and data_device_id = #{dataDeviceId}"; + } + + public String queryListByCivilCode(Map params ){ + StringBuilder sqlBuild = new StringBuilder(); + sqlBuild.append(BASE_SQL); + sqlBuild.append(" where channel_type = 0 "); + if (params.get("query") != null) { + sqlBuild.append(" AND (coalesce(gb_device_id, device_id) LIKE concat('%',#{query},'%') escape '/'" + + " OR coalesce(gb_name, name) LIKE concat('%',#{query},'%') escape '/' )") + ; + } + if (params.get("online") != null && (Boolean)params.get("online")) { + sqlBuild.append(" AND coalesce(gb_status, status) = 'ON'"); + } + if (params.get("online") != null && !(Boolean)params.get("online")) { + sqlBuild.append(" AND coalesce(gb_status, status) = 'OFF'"); + } + if (params.get("civilCode") != null) { + sqlBuild.append(" AND coalesce(gb_civil_code, civil_code) = #{civilCode}"); + }else { + sqlBuild.append(" AND coalesce(gb_civil_code, civil_code) is null"); + } + if (params.get("dataType") != null) { + sqlBuild.append(" AND data_type = #{dataType}"); + } + return sqlBuild.toString(); + } + + public String queryListByParentId(Map params ){ + StringBuilder sqlBuild = new StringBuilder(); + sqlBuild.append(BASE_SQL); + sqlBuild.append(" where channel_type = 0 "); + if (params.get("query") != null) { + sqlBuild.append(" AND (coalesce(gb_device_id, device_id) LIKE concat('%',#{query},'%') escape '/'" + + " OR coalesce(gb_name, name) LIKE concat('%',#{query},'%') escape '/' )") + ; + } + if (params.get("online") != null && (Boolean)params.get("online")) { + sqlBuild.append(" AND coalesce(gb_status, status) = 'ON'"); + } + if (params.get("online") != null && !(Boolean)params.get("online")) { + sqlBuild.append(" AND coalesce(gb_status, status) = 'OFF'"); + } + if (params.get("groupDeviceId") != null) { + sqlBuild.append(" AND coalesce(gb_parent_id, parent_id) = #{groupDeviceId}"); + }else { + sqlBuild.append(" AND coalesce(gb_parent_id, parent_id) is null"); + } + if (params.get("dataType") != null) { + sqlBuild.append(" AND data_type = #{dataType}"); + } + return sqlBuild.toString(); + } + + public String queryList(Map params ){ + StringBuilder sqlBuild = new StringBuilder(); + sqlBuild.append(BASE_SQL); + sqlBuild.append(" where channel_type = 0 "); + if (params.get("query") != null) { + sqlBuild.append(" AND (coalesce(gb_device_id, device_id) LIKE concat('%',#{query},'%') escape '/'" + + " OR coalesce(gb_name, name) LIKE concat('%',#{query},'%') escape '/' )") + ; + } + if (params.get("online") != null && (Boolean)params.get("online")) { + sqlBuild.append(" AND coalesce(gb_status, status) = 'ON'"); + } + if (params.get("online") != null && !(Boolean)params.get("online")) { + sqlBuild.append(" AND coalesce(gb_status, status) = 'OFF'"); + } + if (params.get("hasRecordPlan") != null && (Boolean)params.get("hasRecordPlan")) { + sqlBuild.append(" AND record_plan_id > 0"); + } + if (params.get("dataType") != null) { + sqlBuild.append(" AND data_type = #{dataType}"); + } + if (params.get("civilCode") != null) { + sqlBuild.append(" AND coalesce(gb_civil_code, civil_code) = #{civilCode}"); + } + if (params.get("parentDeviceId") != null) { + sqlBuild.append(" AND coalesce(gb_parent_id, parent_id) = #{parentDeviceId}"); + } + return sqlBuild.toString(); + } + + public String queryInListByStatus(Map params ){ + StringBuilder sqlBuild = new StringBuilder(); + sqlBuild.append(BASE_SQL); + sqlBuild.append("where channel_type = 0 and gb_status=#{status} and id in ( "); + + List commonGBChannelList = (List)params.get("commonGBChannelList"); + boolean first = true; + for (CommonGBChannel channel : commonGBChannelList) { + if (!first) { + sqlBuild.append(","); + } + sqlBuild.append(channel.getGbId()); + first = false; + } + sqlBuild.append(" )"); + return sqlBuild.toString() ; + } + + public String queryByIds(Map params ){ + StringBuilder sqlBuild = new StringBuilder(); + sqlBuild.append(BASE_SQL); + sqlBuild.append("where channel_type = 0 and id in ( "); + + Collection ids = (Collection)params.get("ids"); + boolean first = true; + for (Integer id : ids) { + if (!first) { + sqlBuild.append(","); + } + sqlBuild.append(id); + first = false; + } + sqlBuild.append(" )"); + return sqlBuild.toString() ; + } + + public String queryByDataTypeAndDeviceIds(Map params ){ + StringBuilder sqlBuild = new StringBuilder(); + sqlBuild.append(BASE_SQL); + sqlBuild.append("where channel_type = 0 and data_type = #{dataType} and data_device_id in ( "); + + Collection ids = (Collection)params.get("deviceIds"); + boolean first = true; + for (Integer id : ids) { + if (!first) { + sqlBuild.append(","); + } + sqlBuild.append(id); + first = false; + } + sqlBuild.append(" )"); + return sqlBuild.toString() ; + } + + public String queryByGbDeviceIds(Map params ){ + StringBuilder sqlBuild = new StringBuilder(); + sqlBuild.append(BASE_SQL); + sqlBuild.append("where channel_type = 0 and coalesce(gb_device_id, device_id) in ( "); + + Collection ids = (Collection)params.get("deviceIds"); + boolean first = true; + for (String id : ids) { + if (!first) { + sqlBuild.append(","); + } + sqlBuild.append("'"); + sqlBuild.append(id); + sqlBuild.append("'"); + first = false; + } + sqlBuild.append(" )"); + return sqlBuild.toString() ; + } + + public String queryByDeviceIds(Map params ){ + StringBuilder sqlBuild = new StringBuilder(); + sqlBuild.append(BASE_SQL); + sqlBuild.append("where channel_type = 0 and id in ( "); + + Collection ids = (Collection)params.get("deviceIds"); + boolean first = true; + for (Integer id : ids) { + if (!first) { + sqlBuild.append(","); + } + sqlBuild.append(id); + first = false; + } + sqlBuild.append(" )"); + return sqlBuild.toString() ; + } + + public String queryByIdsOrCivilCode(Map params ){ + StringBuilder sqlBuild = new StringBuilder(); + sqlBuild.append(BASE_SQL); + sqlBuild.append("where channel_type = 0 and "); + if (params.get("civilCode") != null) { + sqlBuild.append(" coalesce(gb_civil_code, civil_code) = #{civilCode} "); + if (params.get("ids") != null) { + sqlBuild.append(" OR "); + } + } + if (params.get("ids") != null) { + sqlBuild.append(" id in ( "); + Collection ids = (Collection)params.get("ids"); + boolean first = true; + for (Integer id : ids) { + if (!first) { + sqlBuild.append(","); + } + sqlBuild.append(id); + first = false; + } + sqlBuild.append(" )"); + } + return sqlBuild.toString() ; + } + + public String queryByCivilCode(Map params ){ + StringBuilder sqlBuild = new StringBuilder(); + sqlBuild.append(BASE_SQL); + sqlBuild.append("where channel_type = 0 and coalesce(gb_civil_code, civil_code) = #{civilCode} "); + return sqlBuild.toString(); + } + + public String queryByBusinessGroup(Map params ){ + StringBuilder sqlBuild = new StringBuilder(); + sqlBuild.append(BASE_SQL); + sqlBuild.append("where channel_type = 0 and coalesce(gb_business_group_id, business_group_id) = #{businessGroup} "); + return sqlBuild.toString() ; + } + + public String queryByParentId(Map params ){ + StringBuilder sqlBuild = new StringBuilder(); + sqlBuild.append(BASE_SQL); + sqlBuild.append("where channel_type = 0 and gb_parent_id = #{parentId} "); + return sqlBuild.toString() ; + } + + public String queryByGroupList(Map params ){ + StringBuilder sqlBuild = new StringBuilder(); + sqlBuild.append(BASE_SQL); + + sqlBuild.append(" where channel_type = 0 and gb_parent_id in ( "); + Collection ids = (Collection)params.get("groupList"); + boolean first = true; + for (Group group : ids) { + if (!first) { + sqlBuild.append(","); + } + sqlBuild.append(group.getDeviceId()); + first = false; + } + sqlBuild.append(" )"); + + return sqlBuild.toString() ; + } + + public String queryListByStreamPushList(Map params ){ + StringBuilder sqlBuild = new StringBuilder(); + sqlBuild.append(BASE_SQL); + + sqlBuild.append(" where channel_type = 0 and data_type = #{dataType} and data_device_id in ( "); + Collection ids = (Collection)params.get("streamPushList"); + boolean first = true; + for (StreamPush streamPush : ids) { + if (!first) { + sqlBuild.append(","); + } + sqlBuild.append(streamPush.getId()); + first = false; + } + sqlBuild.append(" )"); + + return sqlBuild.toString() ; + } + + public String queryWithPlatform(Map params ){ + StringBuilder sqlBuild = new StringBuilder(); + sqlBuild.append(BASE_SQL_FOR_PLATFORM); + sqlBuild.append(" where wpgc.platform_id = #{platformId}"); + return sqlBuild.toString() ; + } + + public String queryShareChannelByParentId(Map params ){ + StringBuilder sqlBuild = new StringBuilder(); + sqlBuild.append(BASE_SQL_FOR_PLATFORM); + sqlBuild.append(" where wpgc.platform_id = #{platformId} and coalesce(wpgc.custom_parent_id, wdc.gb_parent_id, wdc.parent_id) = #{parentId}"); + return sqlBuild.toString() ; + } + + public String queryShareChannelByCivilCode(Map params ){ + StringBuilder sqlBuild = new StringBuilder(); + sqlBuild.append(BASE_SQL_FOR_PLATFORM); + sqlBuild.append(" where wpgc.platform_id = #{platformId} and coalesce(wpgc.custom_civil_code, wdc.gb_civil_code, wdc.civil_code) = #{civilCode}"); + return sqlBuild.toString() ; + } + + public String queryListByCivilCodeForUnusual(Map params ){ + StringBuilder sqlBuild = new StringBuilder(); + sqlBuild.append(BASE_SQL_TABLE_NAME); + sqlBuild.append(" left join (select wcr.device_id from wvp_common_region wcr) temp on temp.device_id = coalesce(wdc.gb_civil_code, wdc.civil_code)" + + " where coalesce(wdc.gb_civil_code, wdc.civil_code) is not null and temp.device_id is null "); + sqlBuild.append(" AND wdc.channel_type = 0 "); + if (params.get("query") != null) { + sqlBuild.append(" AND (coalesce(wdc.gb_device_id, wdc.device_id) LIKE concat('%',#{query},'%') escape '/'" + + " OR coalesce(wdc.gb_name, wdc.name) LIKE concat('%',#{query},'%') escape '/' )") + ; + } + if (params.get("online") != null && (Boolean)params.get("online")) { + sqlBuild.append(" AND coalesce(wdc.gb_status, wdc.status) = 'ON'"); + } + if (params.get("online") != null && !(Boolean)params.get("online")) { + sqlBuild.append(" AND coalesce(wdc.gb_status, wdc.status) = 'OFF'"); + } + if (params.get("dataType") != null) { + sqlBuild.append(" AND wdc.data_type = #{dataType}"); + } + return sqlBuild.toString(); + } + + public String queryListByParentForUnusual(Map params ){ + StringBuilder sqlBuild = new StringBuilder(); + sqlBuild.append(BASE_SQL_TABLE_NAME); + sqlBuild.append(" left join (select wcg.device_id from wvp_common_group wcg) temp on temp.device_id = coalesce(wdc.gb_parent_id, wdc.parent_id)" + + " where coalesce(wdc.gb_parent_id, wdc.parent_id) is not null and temp.device_id is null "); + sqlBuild.append(" AND wdc.channel_type = 0 "); + if (params.get("query") != null) { + sqlBuild.append(" AND (coalesce(wdc.gb_device_id, wdc.device_id) LIKE concat('%',#{query},'%') escape '/'" + + " OR coalesce(wdc.gb_name, wdc.name) LIKE concat('%',#{query},'%') escape '/' )") + ; + } + if (params.get("online") != null && (Boolean)params.get("online")) { + sqlBuild.append(" AND coalesce(wdc.gb_status, wdc.status) = 'ON'"); + } + if (params.get("online") != null && !(Boolean)params.get("online")) { + sqlBuild.append(" AND coalesce(wdc.gb_status, wdc.status) = 'OFF'"); + } + if (params.get("dataType") != null) { + sqlBuild.append(" AND wdc.data_type = #{dataType}"); + } + return sqlBuild.toString(); + } + + public String queryCommonChannelByDeviceChannel(Map params ){ + return BASE_SQL + + " where data_type=#{dataType} and data_device_id=#{dataDeviceId} AND device_id=#{deviceId}"; + } + + public String queryCameraChannelInBox(Map params ){ + StringBuilder sqlBuild = new StringBuilder(); + sqlBuild.append(BASE_SQL_TABLE_NAME); + sqlBuild.append(" where wdc.channel_type = 0 AND coalesce(wdc.gb_longitude, wdc.longitude) > #{minLon} " + + "AND coalesce(wdc.gb_longitude, wdc.longitude) <= #{maxLon} " + + "AND coalesce(wdc.gb_latitude, wdc.latitude) > #{minLat} " + + "AND coalesce(wdc.gb_latitude, wdc.latitude) <= #{maxLat}"); + return sqlBuild.toString(); + } + + public String queryOldChanelListByChannels(Map params ){ + StringBuilder sqlBuild = new StringBuilder(); + sqlBuild.append(BASE_SQL); + sqlBuild.append(" where id in ( "); + + List channelList = (List)params.get("channelList"); + boolean first = true; + for (CommonGBChannel channel : channelList) { + if (!first) { + sqlBuild.append(","); + } + sqlBuild.append(channel.getGbId()); + first = false; + } + sqlBuild.append(" )"); + return sqlBuild.toString() ; + } + + public String queryAllForUnusualCivilCode(Map params ){ + StringBuilder sqlBuild = new StringBuilder(); + sqlBuild.append("select wdc.id from wvp_device_channel wdc "); + sqlBuild.append(" left join (select wcr.device_id from wvp_common_region wcr) temp on temp.device_id = coalesce(wdc.gb_civil_code, wdc.civil_code)" + + " where coalesce(wdc.gb_civil_code, wdc.civil_code) is not null and temp.device_id is null "); + sqlBuild.append(" AND wdc.channel_type = 0 "); + return sqlBuild.toString(); + } + + public String queryAllForUnusualParent(Map params ){ + StringBuilder sqlBuild = new StringBuilder(); + sqlBuild.append("select wdc.id from wvp_device_channel wdc "); + sqlBuild.append(" left join (select wcg.device_id from wvp_common_group wcg) temp on temp.device_id = coalesce(wdc.gb_parent_id, wdc.parent_id)" + + " where coalesce(wdc.gb_parent_id, wdc.parent_id) is not null and temp.device_id is null "); + sqlBuild.append(" AND wdc.channel_type = 0 "); + return sqlBuild.toString(); + } + + public String queryOnlineListsByGbDeviceId(Map params ){ + StringBuilder sqlBuild = new StringBuilder(); + sqlBuild.append(BASE_SQL_TABLE_NAME); + sqlBuild.append(" where wdc.channel_type = 0 AND coalesce(wdc.gb_status, wdc.status) = 'ON' AND wdc.data_type = 1 AND data_device_id = #{deviceId}"); + return sqlBuild.toString(); + } + + public String queryListForSy(Map params ){ + StringBuilder sqlBuild = new StringBuilder(); + sqlBuild.append(BASE_SQL_FOR_CAMERA_DEVICE); + sqlBuild.append(" where wdc.channel_type = 0 AND wdc.data_type != 2 AND (wdc.gb_ptz_type is null or ( wdc.gb_ptz_type != 98 AND wdc.gb_ptz_type != 99)) AND coalesce(wdc.gb_parent_id, wdc.parent_id) = #{groupDeviceId}"); + if (params.get("online") != null && (Boolean)params.get("online")) { + sqlBuild.append(" AND coalesce(wdc.gb_status, wdc.status) = 'ON'"); + } + if (params.get("online") != null && !(Boolean)params.get("online")) { + sqlBuild.append(" AND coalesce(wdc.gb_status, wdc.status) = 'OFF'"); + } + sqlBuild.append(" order by coalesce(wdc.gb_status, wdc.status) desc"); + + return sqlBuild.toString(); + } + + public String queryListWithChildForSy(Map params ){ + StringBuilder sqlBuild = new StringBuilder(); + sqlBuild.append(BASE_SQL_FOR_CAMERA_DEVICE); + sqlBuild.append(" where wdc.channel_type = 0 AND wdc.data_type != 2 AND (wdc.gb_ptz_type is null or ( wdc.gb_ptz_type != 98 AND wdc.gb_ptz_type != 99)) "); + + + List groupList = (List)params.get("groupList"); + if (groupList != null && !groupList.isEmpty()) { + sqlBuild.append(" AND coalesce(wdc.gb_parent_id, wdc.parent_id) in ("); + boolean first = true; + for (CameraGroup group : groupList) { + if (!first) { + sqlBuild.append(","); + } + sqlBuild.append("'" + group.getDeviceId() + "'"); + first = false; + } + sqlBuild.append(" )"); + } + if (params.get("query") != null) { + sqlBuild.append(" AND (coalesce(wdc.gb_device_id, wdc.device_id) LIKE concat('%',#{query},'%') escape '/'" + + " OR coalesce(wdc.gb_name, wdc.name) LIKE concat('%',#{query},'%') escape '/' )") + ; + } + + if (params.get("online") != null && (Boolean)params.get("online")) { + sqlBuild.append(" AND coalesce(wdc.gb_status, status) = 'ON'"); + } + if (params.get("online") != null && !(Boolean)params.get("online")) { + sqlBuild.append(" AND coalesce(wdc.gb_status, status) = 'OFF'"); + } + + if (params.get("sortName") != null) { + StringBuilder sqlBuildForSort = new StringBuilder(); + sqlBuildForSort.append("select * from ( "); + sqlBuildForSort.append(sqlBuild); + sqlBuildForSort.append(" ) as temp"); + String sortName = (String)params.get("sortName"); + switch (sortName) { + case "gbId": + sqlBuildForSort.append(" order by gb_id "); + break; + case "gbDeviceId": + sqlBuildForSort.append(" order by gb_device_id "); + break; + case "gbName": + sqlBuildForSort.append(" order by gb_name "); + break; + case "gbStatus": + sqlBuildForSort.append(" order by gb_status "); + break; + case "createTime": + sqlBuildForSort.append(" order by create_time "); + break; + case "updateTime": + sqlBuildForSort.append(" order by update_time "); + break; + case "deviceCode": + sqlBuildForSort.append(" order by deviceCode "); + break; + } + + if (params.get("order") != null && (Boolean)params.get("order")) { + sqlBuildForSort.append(" ASC"); + } + if (params.get("order") != null && !(Boolean)params.get("order")) { + sqlBuildForSort.append(" DESC"); + } + return sqlBuildForSort.toString(); + }else { + return sqlBuild.toString(); + } + } + + public String queryListInBox(Map params ){ + StringBuilder sqlBuild = new StringBuilder(); + sqlBuild.append(BASE_SQL_FOR_CAMERA_DEVICE); + sqlBuild.append(" where wdc.channel_type = 0 AND wdc.data_type != 2 AND (wdc.gb_ptz_type is null or ( wdc.gb_ptz_type != 98 AND wdc.gb_ptz_type != 99)) " + + " AND coalesce(wdc.gb_parent_id, wdc.parent_id) in ("); + + sqlBuild.append(" "); + List groupList = (List)params.get("groupList"); + boolean first = true; + for (CameraGroup group : groupList) { + if (!first) { + sqlBuild.append(","); + } + sqlBuild.append("'" + group.getDeviceId() + "'"); + first = false; + } + sqlBuild.append(" )"); + + sqlBuild.append(" AND coalesce(wdc.gb_longitude, wdc.longitude) >= #{minLongitude} AND coalesce(wdc.gb_longitude, wdc.longitude) <= #{maxLongitude}"); + sqlBuild.append(" AND coalesce(wdc.gb_latitude, wdc.latitude) >= #{minLatitude} AND coalesce(wdc.gb_latitude, wdc.latitude) <= #{maxLatitude}"); + + if (params.get("level") != null) { + sqlBuild.append(" AND ( map_level <= #{level} or map_level is null )"); + } + + return sqlBuild.toString(); + } + + public String queryListInCircleForMysql(Map params ){ + StringBuilder sqlBuild = new StringBuilder(); + sqlBuild.append(BASE_SQL_FOR_CAMERA_DEVICE); + sqlBuild.append(" where wdc.channel_type = 0 AND wdc.data_type != 2 AND (wdc.gb_ptz_type is null or ( wdc.gb_ptz_type != 98 AND wdc.gb_ptz_type != 99)) " + + " AND coalesce(wdc.gb_parent_id, wdc.parent_id) in ("); + + sqlBuild.append(" "); + List groupList = (List)params.get("groupList"); + boolean first = true; + for (CameraGroup group : groupList) { + if (!first) { + sqlBuild.append(","); + } + sqlBuild.append("'" + group.getDeviceId() + "'"); + first = false; + } + sqlBuild.append(" )"); + + String geomTextBuilder = "point(" + params.get("centerLongitude") + " " + params.get("centerLatitude") + ")"; + + sqlBuild.append("AND ST_Distance_Sphere(point(coalesce(wdc.gb_longitude, wdc.longitude), coalesce(wdc.gb_latitude, wdc.latitude)), ST_GeomFromText('").append(geomTextBuilder).append("')) < #{radius}"); + + if (params.get("level") != null) { + sqlBuild.append(" AND ( map_level <= #{level} or map_level is null )"); + } + + return sqlBuild.toString(); + } + + public String queryListInCircleForKingBase(Map params ){ + StringBuilder sqlBuild = new StringBuilder(); + sqlBuild.append(BASE_SQL_FOR_CAMERA_DEVICE); + sqlBuild.append(" where wdc.channel_type = 0 AND wdc.data_type != 2 AND (wdc.gb_ptz_type is null or ( wdc.gb_ptz_type != 98 AND wdc.gb_ptz_type != 99)) " + + " AND coalesce(wdc.gb_parent_id, wdc.parent_id) in ("); + + sqlBuild.append(" "); + List groupList = (List)params.get("groupList"); + boolean first = true; + for (CameraGroup group : groupList) { + if (!first) { + sqlBuild.append(","); + } + sqlBuild.append("'" + group.getDeviceId() + "'"); + first = false; + } + sqlBuild.append(" )"); + + String geomTextBuilder = "point(" + params.get("centerLongitude") + " " + params.get("centerLatitude") + ")"; + + sqlBuild.append("AND ST_DistanceSphere(ST_MakePoint(coalesce(wdc.gb_longitude, wdc.longitude), coalesce(wdc.gb_latitude, wdc.latitude)), ST_GeomFromText('").append(geomTextBuilder).append("')) < #{radius}"); + + if (params.get("level") != null) { + sqlBuild.append(" AND ( map_level <= #{level} or map_level is null )"); + } + + return sqlBuild.toString(); + } + + public String queryListInPolygonForMysql(Map params ){ + StringBuilder sqlBuild = new StringBuilder(); + sqlBuild.append(BASE_SQL_FOR_CAMERA_DEVICE); + sqlBuild.append(" where wdc.channel_type = 0 AND wdc.data_type != 2 AND (wdc.gb_ptz_type is null or ( wdc.gb_ptz_type != 98 AND wdc.gb_ptz_type != 99)) " + + " AND coalesce(wdc.gb_parent_id, wdc.parent_id) in ("); + + sqlBuild.append(" "); + List groupList = (List)params.get("groupList"); + boolean first = true; + for (CameraGroup group : groupList) { + if (!first) { + sqlBuild.append(","); + } + sqlBuild.append("'" + group.getDeviceId() + "'"); + first = false; + } + sqlBuild.append(" )"); + + StringBuilder geomTextBuilder = new StringBuilder(); + geomTextBuilder.append("POLYGON(("); + List pointList = (List)params.get("pointList"); + for (int i = 0; i < pointList.size(); i++) { + if (i > 0) { + geomTextBuilder.append(", "); + } + Point point = pointList.get(i); + geomTextBuilder.append(point.getLng()).append(" ").append(point.getLat()); + } + geomTextBuilder.append("))"); + sqlBuild.append("AND ST_Within(point(coalesce(wdc.gb_longitude, wdc.longitude), coalesce(wdc.gb_latitude, wdc.latitude)), ST_GeomFromText('").append(geomTextBuilder).append("'))"); + + if (params.get("level") != null) { + sqlBuild.append(" AND ( map_level <= #{level} or map_level is null )"); + } + + return sqlBuild.toString(); + } + + public String queryListInPolygonForKingBase(Map params ){ + StringBuilder sqlBuild = new StringBuilder(); + sqlBuild.append(BASE_SQL_FOR_CAMERA_DEVICE); + sqlBuild.append(" where wdc.channel_type = 0 AND wdc.data_type != 2 AND (wdc.gb_ptz_type is null or ( wdc.gb_ptz_type != 98 AND wdc.gb_ptz_type != 99)) " + + " AND coalesce(wdc.gb_parent_id, wdc.parent_id) in ("); + + sqlBuild.append(" "); + List groupList = (List)params.get("groupList"); + boolean first = true; + for (CameraGroup group : groupList) { + if (!first) { + sqlBuild.append(","); + } + sqlBuild.append("'" + group.getDeviceId() + "'"); + first = false; + } + sqlBuild.append(" )"); + + StringBuilder geomTextBuilder = new StringBuilder(); + geomTextBuilder.append("POLYGON(("); + List pointList = (List)params.get("pointList"); + for (int i = 0; i < pointList.size(); i++) { + if (i > 0) { + geomTextBuilder.append(", "); + } + Point point = pointList.get(i); + geomTextBuilder.append(point.getLng()).append(" ").append(point.getLat()); + } + geomTextBuilder.append("))"); + sqlBuild.append("AND ST_Within(ST_MakePoint(coalesce(wdc.gb_longitude, wdc.longitude), coalesce(wdc.gb_latitude, wdc.latitude)), ST_GeomFromText('").append(geomTextBuilder).append("'))"); + + if (params.get("level") != null) { + sqlBuild.append(" AND ( map_level <= #{level} or map_level is null )"); + } + + return sqlBuild.toString(); + } + + public String queryGbChannelByChannelDeviceIdAndGbDeviceId(Map params ){ + StringBuilder sqlBuild = new StringBuilder(); + sqlBuild.append(BASE_SQL_FOR_CAMERA_DEVICE); + sqlBuild.append(" where coalesce(wdc.gb_device_id, wdc.device_id) = #{channelDeviceId}"); + if (params.get("gbDeviceId") != null) { + sqlBuild.append(" AND wdc.data_type = 1 and wd.device_id = #{gbDeviceId}"); + } + return sqlBuild.toString(); + } + + public String queryListByAddressAndDirectionType(Map params ){ + StringBuilder sqlBuild = new StringBuilder(); + sqlBuild.append(BASE_SQL_FOR_CAMERA_DEVICE); + sqlBuild.append(" where coalesce(wdc.gb_address, wdc.address) = #{address}"); + if (params.get("directionType") != null) { + sqlBuild.append(" and coalesce(wdc.gb_direction_type, wdc.direction_type) = #{directionType}"); + } + return sqlBuild.toString(); + } + + + public String queryListByDeviceIds(Map params ){ + StringBuilder sqlBuild = new StringBuilder(); + sqlBuild.append(""); + return sqlBuild.toString() ; + } + + public String queryCameraChannelByIds(Map params ){ + StringBuilder sqlBuild = new StringBuilder(); + sqlBuild.append(BASE_SQL_FOR_CAMERA_DEVICE); + sqlBuild.append(" where wdc.id in ( "); + + List channelList = (List)params.get("channelList"); + boolean first = true; + for (CommonGBChannel channel : channelList) { + if (!first) { + sqlBuild.append(","); + } + sqlBuild.append(channel.getGbId()); + first = false; + } + sqlBuild.append(" )"); + return sqlBuild.toString() ; + } + + public String queryListForSyMobile(Map params ){ + StringBuilder sqlBuild = new StringBuilder(); + sqlBuild.append(BASE_SQL_FOR_CAMERA_DEVICE); + sqlBuild.append(" WHERE wdc.gb_ptz_type = 99 and wdc.channel_type = 0 AND wdc.data_type != 2 "); + if (params.get("business") != null) { + sqlBuild.append(" AND coalesce(gb_business_group_id, business_group_id) = #{business}"); + } + return sqlBuild.toString(); + } + + public String queryMeetingChannelList(Map params ){ + StringBuilder sqlBuild = new StringBuilder(); + sqlBuild.append(BASE_SQL_FOR_CAMERA_DEVICE); + sqlBuild.append(" WHERE wdc.channel_type = 0 AND wdc.data_type = 3 and wdc.gb_ptz_type = 98 and coalesce(wdc.gb_business_group_id, wdc.business_group_id) = #{business}"); + return sqlBuild.toString(); + } + + + public String queryCameraChannelById(Map params ){ + return BASE_SQL_FOR_CAMERA_DEVICE + " where wdc.id = #{gbId}"; + } + + public String queryAllWithPosition(Map params ){ + return BASE_SQL + " where channel_type = 0 " + + " AND coalesce(gb_longitude, longitude) > 0" + + " AND coalesce(gb_latitude, latitude) > 0 " + + " ORDER BY map_level"; + } + + public String queryListInExtent(Map params ){ + StringBuilder sqlBuild = new StringBuilder(); + sqlBuild.append(BASE_SQL); + sqlBuild.append(" where channel_type = 0 " + + "AND coalesce(gb_longitude, longitude) > #{minLng} " + + "AND coalesce(gb_longitude, longitude) <= #{maxLng} " + + "AND coalesce(gb_latitude, latitude) > #{minLat} " + + "AND coalesce(gb_latitude, latitude) <= #{maxLat}"); + return sqlBuild.toString(); + } + + public String queryListOutExtent(Map params ){ + StringBuilder sqlBuild = new StringBuilder(); + sqlBuild.append(BASE_SQL); + sqlBuild.append(" where channel_type = 0 AND ( " + + "coalesce(gb_longitude, longitude) <= #{minLng} " + + "or coalesce(gb_longitude, longitude) > #{maxLng} " + + "or coalesce(gb_latitude, latitude) <= #{minLat} " + + "or coalesce(gb_latitude, latitude) > #{maxLat}" + + ")"); + return sqlBuild.toString(); + } + +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/dao/provider/DeviceChannelProvider.java b/src/main/java/com/genersoft/iot/vmp/gb28181/dao/provider/DeviceChannelProvider.java new file mode 100644 index 0000000..af5b8e0 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/dao/provider/DeviceChannelProvider.java @@ -0,0 +1,190 @@ +package com.genersoft.iot.vmp.gb28181.dao.provider; + +import com.genersoft.iot.vmp.common.enums.ChannelDataType; +import org.springframework.util.ObjectUtils; + +import java.util.List; +import java.util.Map; + +public class DeviceChannelProvider { + + public String getBaseSelectSql(){ + return "SELECT " + + " dc.id,\n" + + " dc.data_device_id,\n" + + " dc.create_time,\n" + + " dc.update_time,\n" + + " dc.sub_count,\n" + + " dc.stream_id,\n" + + " dc.has_audio,\n" + + " dc.gps_time,\n" + + " dc.stream_identification,\n" + + " dc.channel_type,\n" + + " d.device_id as parent_device_id,\n" + + " coalesce(d.custom_name, d.name) as parent_name,\n" + + " coalesce(dc.gb_device_id, dc.device_id) as device_id,\n" + + " coalesce(dc.gb_name, dc.name) as name,\n" + + " coalesce(dc.gb_manufacturer, dc.manufacturer) as manufacturer,\n" + + " coalesce(dc.gb_model, dc.model) as model,\n" + + " coalesce(dc.gb_owner, dc.owner) as owner,\n" + + " coalesce(dc.gb_civil_code, dc.civil_code) as civil_code,\n" + + " coalesce(dc.gb_block, dc.block) as block,\n" + + " coalesce(dc.gb_address, dc.address) as address,\n" + + " coalesce(dc.gb_parental, dc.parental) as parental,\n" + + " coalesce(dc.gb_parent_id, dc.parent_id) as parent_id,\n" + + " coalesce(dc.gb_safety_way, dc.safety_way) as safety_way,\n" + + " coalesce(dc.gb_register_way, dc.register_way) as register_way,\n" + + " coalesce(dc.gb_cert_num, dc.cert_num) as cert_num,\n" + + " coalesce(dc.gb_certifiable, dc.certifiable) as certifiable,\n" + + " coalesce(dc.gb_err_code, dc.err_code) as err_code,\n" + + " coalesce(dc.gb_end_time, dc.end_time) as end_time,\n" + + " coalesce(dc.gb_secrecy, dc.secrecy) as secrecy,\n" + + " coalesce(dc.gb_ip_address, dc.ip_address) as ip_address,\n" + + " coalesce(dc.gb_port, dc.port) as port,\n" + + " coalesce(dc.gb_password, dc.password) as password,\n" + + " coalesce(dc.gb_status, dc.status) as status,\n" + + " coalesce(dc.gb_longitude, dc.longitude) as longitude,\n" + + " coalesce(dc.gb_latitude, dc.latitude) as latitude,\n" + + " coalesce(dc.gb_ptz_type, dc.ptz_type) as ptz_type,\n" + + " coalesce(dc.gb_position_type, dc.position_type) as position_type,\n" + + " coalesce(dc.gb_room_type, dc.room_type) as room_type,\n" + + " coalesce(dc.gb_use_type, dc.use_type) as use_type,\n" + + " coalesce(dc.gb_supply_light_type, dc.supply_light_type) as supply_light_type,\n" + + " coalesce(dc.gb_direction_type, dc.direction_type) as direction_type,\n" + + " coalesce(dc.gb_resolution, dc.resolution) as resolution,\n" + + " coalesce(dc.gb_business_group_id, dc.business_group_id) as business_group_id,\n" + + " coalesce(dc.gb_download_speed, dc.download_speed) as download_speed,\n" + + " coalesce(dc.gb_svc_space_support_mod, dc.svc_space_support_mod) as svc_space_support_mod,\n" + + " coalesce(dc.gb_svc_time_support_mode,dc.svc_time_support_mode) as svc_time_support_mode\n" + + " from " + + " wvp_device_channel dc " + + " left join wvp_device d on d.id = dc.data_device_id " + ; + } + public String queryChannels(Map params ){ + StringBuilder sqlBuild = new StringBuilder(); + sqlBuild.append(getBaseSelectSql()); + sqlBuild.append(" where data_type = " + ChannelDataType.GB28181); + if (params.get("dataDeviceId") != null) { + sqlBuild.append(" AND dc.data_device_id = #{dataDeviceId} "); + } + if (params.get("businessGroupId") != null ) { + sqlBuild.append(" AND coalesce(dc.gb_business_group_id, dc.business_group_id)=#{businessGroupId} AND coalesce(dc.gb_parent_id, dc.parent_id) is null"); + }else if (params.get("parentChannelId") != null ) { + sqlBuild.append(" AND coalesce(dc.gb_parent_id, dc.parent_id)=#{parentChannelId}"); + } + if (params.get("civilCode") != null ) { + sqlBuild.append(" AND (coalesce(dc.gb_civil_code, dc.civil_code) = #{civilCode} " + + "OR (LENGTH(coalesce(dc.gb_device_id, dc.device_id))=LENGTH(#{civilCode}) + 2) AND coalesce(dc.gb_device_id, dc.device_id) LIKE concat(#{civilCode},'%'))"); + } + if (params.get("query") != null && !ObjectUtils.isEmpty(params.get("query"))) { + sqlBuild.append(" AND (coalesce(dc.gb_device_id, dc.device_id) LIKE concat('%',#{query},'%') escape '/'" + + " OR coalesce(dc.gb_name, dc.name) LIKE concat('%',#{query},'%') escape '/'"); + if (params.get("queryParent") != null && (Boolean) params.get("queryParent")) { + sqlBuild.append(" OR d.device_id LIKE concat('%',#{query},'%') escape '/'"); + sqlBuild.append(" OR coalesce(d.custom_name, d.name) LIKE concat('%',#{query},'%') escape '/'"); + } + sqlBuild.append(")"); + } + if (params.get("hasStream") != null && (Boolean) params.get("hasStream")) { + sqlBuild.append(" AND dc.stream_id IS NOT NULL"); + } + if (params.get("online") != null && (Boolean)params.get("online")) { + sqlBuild.append(" AND coalesce(gb_status, status) = 'ON'"); + } + if (params.get("online") != null && !(Boolean)params.get("online")) { + sqlBuild.append(" AND coalesce(gb_status, status) = 'OFF'"); + } + if (params.get("hasSubChannel") != null && (Boolean)params.get("hasSubChannel")) { + sqlBuild.append(" AND dc.sub_count > 0"); + } + if (params.get("hasSubChannel") != null && !(Boolean)params.get("hasSubChannel")) { + sqlBuild.append(" AND dc.sub_count = 0"); + } + List channelIds = (List)params.get("channelIds"); + if (channelIds != null && !channelIds.isEmpty()) { + sqlBuild.append(" AND dc.device_id in ("); + boolean first = true; + for (String id : channelIds) { + if (!first) { + sqlBuild.append(","); + } + sqlBuild.append(id); + first = false; + } + sqlBuild.append(" )"); + } + sqlBuild.append(" ORDER BY d.device_id, dc.device_id"); + return sqlBuild.toString(); + } + + + public String queryChannelsByDeviceDbId(Map params ){ + StringBuilder sqlBuild = new StringBuilder(); + sqlBuild.append(getBaseSelectSql()); + sqlBuild.append(" where data_type = " + ChannelDataType.GB28181 + " and dc.data_device_id = #{dataDeviceId}"); + return sqlBuild.toString(); + } + + public String queryAllChannels(Map params ){ + StringBuilder sqlBuild = new StringBuilder(); + sqlBuild.append(getBaseSelectSql()); + sqlBuild.append(" where data_type = " + ChannelDataType.GB28181 + " and dc.data_device_id = #{dataDeviceId}"); + return sqlBuild.toString(); + } + + public String getOne(Map params ){ + StringBuilder sqlBuild = new StringBuilder(); + sqlBuild.append(getBaseSelectSql()); + sqlBuild.append(" where dc.id=#{id}"); + return sqlBuild.toString(); + } + + public String getOneByDeviceId(Map params ){ + StringBuilder sqlBuild = new StringBuilder(); + sqlBuild.append(getBaseSelectSql()); + sqlBuild.append(" where data_type = " + ChannelDataType.GB28181 + " and dc.data_device_id=#{dataDeviceId} and coalesce(dc.gb_device_id, dc.device_id) = #{channelId}"); + return sqlBuild.toString(); + } + + + + public String queryByDeviceId(Map params ){ + return getBaseSelectSql() + " where data_type = " + ChannelDataType.GB28181 + " and channel_type = 0 and coalesce(gb_device_id, device_id) = #{gbDeviceId}"; + } + + public String queryById(Map params ){ + return getBaseSelectSql() + " where data_type = " + ChannelDataType.GB28181 + " and channel_type = 0 and id = #{gbId}"; + } + + + public String queryList(Map params ){ + StringBuilder sqlBuild = new StringBuilder(); + sqlBuild.append(getBaseSelectSql()); + sqlBuild.append(" where channel_type = 0 and data_type = " + ChannelDataType.GB28181); + if (params.get("query") != null) { + sqlBuild.append(" AND (coalesce(gb_device_id, device_id) LIKE concat('%',#{query},'%')" + + " OR coalesce(gb_name, name) LIKE concat('%',#{query},'%') )") + ; + } + if (params.get("online") != null && (Boolean)params.get("online")) { + sqlBuild.append(" AND coalesce(gb_status, status) = 'ON'"); + } + if (params.get("online") != null && !(Boolean)params.get("online")) { + sqlBuild.append(" AND coalesce(gb_status, status) = 'OFF'"); + } + if (params.get("hasCivilCode") != null && (Boolean)params.get("hasCivilCode")) { + sqlBuild.append(" AND coalesce(gb_civil_code, civil_code) is not null"); + } + if (params.get("hasCivilCode") != null && !(Boolean)params.get("hasCivilCode")) { + sqlBuild.append(" AND coalesce(gb_civil_code, civil_code) is null"); + } + if (params.get("hasGroup") != null && (Boolean)params.get("hasGroup")) { + sqlBuild.append(" AND coalesce(gb_parent_id, parent_id) is not null"); + } + if (params.get("hasGroup") != null && !(Boolean)params.get("hasGroup")) { + sqlBuild.append(" AND coalesce(gb_parent_id, parent_id) is null"); + } + return sqlBuild.toString(); + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/event/EventPublisher.java b/src/main/java/com/genersoft/iot/vmp/gb28181/event/EventPublisher.java new file mode 100755 index 0000000..df21658 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/event/EventPublisher.java @@ -0,0 +1,120 @@ +package com.genersoft.iot.vmp.gb28181.event; + +import com.genersoft.iot.vmp.conf.UserSetting; +import com.genersoft.iot.vmp.gb28181.bean.CommonGBChannel; +import com.genersoft.iot.vmp.gb28181.bean.DeviceAlarm; +import com.genersoft.iot.vmp.gb28181.bean.MobilePosition; +import com.genersoft.iot.vmp.gb28181.bean.Platform; +import com.genersoft.iot.vmp.gb28181.event.alarm.AlarmEvent; +import com.genersoft.iot.vmp.gb28181.event.channel.ChannelEvent; +import com.genersoft.iot.vmp.gb28181.event.subscribe.catalog.CatalogEvent; +import com.genersoft.iot.vmp.gb28181.event.subscribe.mobilePosition.MobilePositionEvent; +import com.genersoft.iot.vmp.media.bean.MediaServer; +import com.genersoft.iot.vmp.media.event.mediaServer.MediaServerOfflineEvent; +import com.genersoft.iot.vmp.media.event.mediaServer.MediaServerOnlineEvent; +import com.genersoft.iot.vmp.service.redisMsg.IRedisRpcService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.stereotype.Component; + +import java.util.*; + +/** + * @description:Event事件通知推送器,支持推送在线事件、离线事件 + * @author: swwheihei + * @date: 2020年5月6日 上午11:30:50 + */ +@Slf4j +@Component +public class EventPublisher { + + @Autowired + private ApplicationEventPublisher applicationEventPublisher; + + @Autowired + private UserSetting userSetting; + + @Autowired + private IRedisRpcService redisRpcService; + + /** + * 设备报警事件 + * @param deviceAlarm + */ + public void deviceAlarmEventPublish(DeviceAlarm deviceAlarm) { + AlarmEvent alarmEvent = new AlarmEvent(this); + alarmEvent.setAlarmInfo(deviceAlarm); + applicationEventPublisher.publishEvent(alarmEvent); + } + + public void mediaServerOfflineEventPublish(MediaServer mediaServer){ + MediaServerOfflineEvent outEvent = new MediaServerOfflineEvent(this); + outEvent.setMediaServer(mediaServer); + applicationEventPublisher.publishEvent(outEvent); + } + + public void mediaServerOnlineEventPublish(MediaServer mediaServer) { + MediaServerOnlineEvent outEvent = new MediaServerOnlineEvent(this); + outEvent.setMediaServer(mediaServer); + applicationEventPublisher.publishEvent(outEvent); + } + + public void channelEventPublish(CommonGBChannel commonGBChannel, ChannelEvent.ChannelEventMessageType type) { + channelEventPublish(Collections.singletonList(commonGBChannel), type); + } + + public void channelEventPublishForUpdate(CommonGBChannel commonGBChannel, CommonGBChannel deviceChannelForOld) { + ChannelEvent channelEvent = ChannelEvent.getInstanceForUpdate(this, Collections.singletonList(commonGBChannel), Collections.singletonList(deviceChannelForOld)); + applicationEventPublisher.publishEvent(channelEvent); + } + + public void channelEventPublishForUpdate(List channelList, List channelListForOld) { + ChannelEvent channelEvent = ChannelEvent.getInstanceForUpdate(this, channelList, channelListForOld); + applicationEventPublisher.publishEvent(channelEvent); + } + + public void channelEventPublish(List channelList, ChannelEvent.ChannelEventMessageType type) { + ChannelEvent channelEvent = ChannelEvent.getInstance(this, type, channelList); + applicationEventPublisher.publishEvent(channelEvent); + } + + public void catalogEventPublish(Platform platform, CommonGBChannel deviceChannel, String type) { + catalogEventPublish(platform, Collections.singletonList(deviceChannel), type); + } + public void catalogEventPublish(Platform platform, List deviceChannels, String type) { + if (platform != null && !userSetting.getServerId().equals(platform.getServerId())) { + log.info("[国标级联] 目录状态推送, 此上级平台由其他服务处理,消息已经忽略"); + return; + } + CatalogEvent outEvent = new CatalogEvent(this); + List channels = new ArrayList<>(); + if (deviceChannels.size() > 1) { + // 数据去重 + Set gbIdSet = new HashSet<>(); + for (CommonGBChannel deviceChannel : deviceChannels) { + if (deviceChannel != null && deviceChannel.getGbDeviceId() != null && !gbIdSet.contains(deviceChannel.getGbDeviceId())) { + gbIdSet.add(deviceChannel.getGbDeviceId()); + channels.add(deviceChannel); + } + } + }else { + channels = deviceChannels; + } + outEvent.setChannels(channels); + outEvent.setType(type); + if (platform != null) { + outEvent.setPlatform(platform); + } + applicationEventPublisher.publishEvent(outEvent); + + } + + public void mobilePositionEventPublish(MobilePosition mobilePosition) { + MobilePositionEvent event = new MobilePositionEvent(this); + event.setMobilePosition(mobilePosition); + applicationEventPublisher.publishEvent(event); + } + + +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/event/MessageSubscribe.java b/src/main/java/com/genersoft/iot/vmp/gb28181/event/MessageSubscribe.java new file mode 100755 index 0000000..f86c878 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/event/MessageSubscribe.java @@ -0,0 +1,73 @@ +package com.genersoft.iot.vmp.gb28181.event; + +import com.genersoft.iot.vmp.gb28181.event.sip.MessageEvent; +import com.genersoft.iot.vmp.vmanager.bean.ErrorCode; +import lombok.extern.slf4j.Slf4j; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.DelayQueue; + +/** + * @author lin + */ +@Slf4j +@Component +public class MessageSubscribe { + + private final Map> subscribes = new ConcurrentHashMap<>(); + + private final DelayQueue> delayQueue = new DelayQueue<>(); + + @Scheduled(fixedDelay = 200) //每200毫秒执行 + public void execute(){ + while (!delayQueue.isEmpty()) { + try { + MessageEvent take = delayQueue.take(); + // 出现超时异常 + if(take.getCallback() != null) { + take.getCallback().run(ErrorCode.ERROR486.getCode(), "消息超时未回复", null); + } + subscribes.remove(take.getKey()); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + } + + + public void addSubscribe(MessageEvent event) { + MessageEvent messageEvent = subscribes.get(event.getKey()); + if (messageEvent != null) { + subscribes.remove(event.getKey()); + delayQueue.remove(messageEvent); + } + subscribes.put(event.getKey(), event); + delayQueue.offer(event); + } + + public MessageEvent getSubscribe(String key) { + return subscribes.get(key); + } + + public void removeSubscribe(String key) { + if(key == null){ + return; + } + MessageEvent messageEvent = subscribes.get(key); + if (messageEvent != null) { + subscribes.remove(key); + delayQueue.remove(messageEvent); + } + } + + public boolean isEmpty(){ + return subscribes.isEmpty(); + } + + public Integer size() { + return subscribes.size(); + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/event/SipSubscribe.java b/src/main/java/com/genersoft/iot/vmp/gb28181/event/SipSubscribe.java new file mode 100755 index 0000000..1ef18ca --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/event/SipSubscribe.java @@ -0,0 +1,181 @@ +package com.genersoft.iot.vmp.gb28181.event; + +import com.genersoft.iot.vmp.gb28181.bean.DeviceNotFoundEvent; +import com.genersoft.iot.vmp.gb28181.event.sip.SipEvent; +import gov.nist.javax.sip.message.SIPRequest; +import gov.nist.javax.sip.message.SIPResponse; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.ObjectUtils; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +import javax.sip.DialogTerminatedEvent; +import javax.sip.ResponseEvent; +import javax.sip.TimeoutEvent; +import javax.sip.TransactionTerminatedEvent; +import javax.sip.header.WarningHeader; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.DelayQueue; + +/** + * @author lin + */ +@Slf4j +@Component +public class SipSubscribe { + + private final Map subscribes = new ConcurrentHashMap<>(); + + private final DelayQueue delayQueue = new DelayQueue<>(); + + + @Scheduled(fixedDelay = 200) //每200毫秒执行 + public void execute(){ + while (!delayQueue.isEmpty()) { + try { + SipEvent take = delayQueue.take(); + // 出现超时异常 + if(take.getErrorEvent() != null) { + EventResult eventResult = new EventResult<>(); + eventResult.type = EventResultType.timeout; + eventResult.msg = "消息超时未回复"; + eventResult.statusCode = -1024; + take.getErrorEvent().response(eventResult); + } + subscribes.remove(take.getKey()); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + } + + public interface Event { void response(EventResult eventResult); + } + + /** + * + */ + public enum EventResultType{ + // 超时 + timeout, + // 回复 + response, + // 事务已结束 + transactionTerminated, + // 会话已结束 + dialogTerminated, + // 设备未找到 + deviceNotFoundEvent, + // 消息发送失败 + cmdSendFailEvent, + // 消息发送失败 + failedToGetPort, + // 收到失败的回复 + failedResult + } + + public static class EventResult{ + public int statusCode; + public EventResultType type; + public String msg; + public String callId; + public T event; + + public EventResult() { + } + + public EventResult(T event) { + this.event = event; + if (event instanceof ResponseEvent) { + ResponseEvent responseEvent = (ResponseEvent)event; + SIPResponse response = (SIPResponse)responseEvent.getResponse(); + this.type = EventResultType.response; + if (response != null) { + WarningHeader warningHeader = (WarningHeader)response.getHeader(WarningHeader.NAME); + if (warningHeader != null && !ObjectUtils.isEmpty(warningHeader.getText())) { + this.msg = ""; + if (warningHeader.getCode() > 0) { + this.msg += warningHeader.getCode() + ":"; + } + if (warningHeader.getAgent() != null) { + this.msg += warningHeader.getCode() + ":"; + } + if (warningHeader.getText() != null) { + this.msg += warningHeader.getText(); + } + }else { + this.msg = response.getReasonPhrase(); + } + this.statusCode = response.getStatusCode(); + this.callId = response.getCallIdHeader().getCallId(); + } + }else if (event instanceof TimeoutEvent) { + TimeoutEvent timeoutEvent = (TimeoutEvent)event; + this.type = EventResultType.timeout; + this.msg = "消息超时未回复"; + this.statusCode = -1024; + if (timeoutEvent.isServerTransaction()) { + this.callId = ((SIPRequest)timeoutEvent.getServerTransaction().getRequest()).getCallIdHeader().getCallId(); + }else { + this.callId = ((SIPRequest)timeoutEvent.getClientTransaction().getRequest()).getCallIdHeader().getCallId(); + } + }else if (event instanceof TransactionTerminatedEvent) { + TransactionTerminatedEvent transactionTerminatedEvent = (TransactionTerminatedEvent)event; + this.type = EventResultType.transactionTerminated; + this.msg = "事务已结束"; + this.statusCode = -1024; + if (transactionTerminatedEvent.isServerTransaction()) { + this.callId = ((SIPRequest)transactionTerminatedEvent.getServerTransaction().getRequest()).getCallIdHeader().getCallId(); + }else { + this.callId = ((SIPRequest)transactionTerminatedEvent.getClientTransaction().getRequest()).getCallIdHeader().getCallId(); + } + }else if (event instanceof DialogTerminatedEvent) { + DialogTerminatedEvent dialogTerminatedEvent = (DialogTerminatedEvent)event; + this.type = EventResultType.dialogTerminated; + this.msg = "会话已结束"; + this.statusCode = -1024; + this.callId = dialogTerminatedEvent.getDialog().getCallId().getCallId(); + }else if (event instanceof DeviceNotFoundEvent) { + this.type = EventResultType.deviceNotFoundEvent; + this.msg = "设备未找到"; + this.statusCode = -1024; + this.callId = ((DeviceNotFoundEvent) event).getCallId(); + } + } + } + + + public void addSubscribe(String key, SipEvent event) { + SipEvent sipEvent = subscribes.get(key); + if (sipEvent != null) { + subscribes.remove(key); + delayQueue.remove(sipEvent); + } + subscribes.put(key, event); + delayQueue.offer(event); + } + + public SipEvent getSubscribe(String key) { + return subscribes.get(key); + } + + public void removeSubscribe(String key) { + if(key == null){ + return; + } + SipEvent sipEvent = subscribes.get(key); + if (sipEvent != null) { + subscribes.remove(key); + delayQueue.remove(sipEvent); + } + } + + public boolean isEmpty(){ + return subscribes.isEmpty(); + } + + public Integer size() { + return subscribes.size(); + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/event/alarm/AlarmEvent.java b/src/main/java/com/genersoft/iot/vmp/gb28181/event/alarm/AlarmEvent.java new file mode 100755 index 0000000..11ed3a5 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/event/alarm/AlarmEvent.java @@ -0,0 +1,32 @@ +package com.genersoft.iot.vmp.gb28181.event.alarm; + +import com.genersoft.iot.vmp.gb28181.bean.DeviceAlarm; +import org.springframework.context.ApplicationEvent; + +import java.io.Serial; + +/** + * @description: 报警事件 + * @author: lawrencehj + * @data: 2021-01-20 + */ + +public class AlarmEvent extends ApplicationEvent { + + @Serial + private static final long serialVersionUID = 1L; + + public AlarmEvent(Object source) { + super(source); + } + + private DeviceAlarm deviceAlarm; + + public DeviceAlarm getAlarmInfo() { + return deviceAlarm; + } + + public void setAlarmInfo(DeviceAlarm deviceAlarm) { + this.deviceAlarm = deviceAlarm; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/event/alarm/AlarmEventListener.java b/src/main/java/com/genersoft/iot/vmp/gb28181/event/alarm/AlarmEventListener.java new file mode 100755 index 0000000..46c4cc1 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/event/alarm/AlarmEventListener.java @@ -0,0 +1,32 @@ +package com.genersoft.iot.vmp.gb28181.event.alarm; + +import com.genersoft.iot.vmp.gb28181.session.SseSessionManager; +import lombok.extern.slf4j.Slf4j; +import org.jetbrains.annotations.NotNull; +import org.springframework.context.ApplicationListener; +import org.springframework.stereotype.Component; + +import jakarta.annotation.Resource; + +/** + * 报警事件监听器. + * + * @author lawrencehj + * @author xiaoQQya + * @since 2021/01/20 + */ +@Slf4j +@Component +public class AlarmEventListener implements ApplicationListener { + + @Resource + private SseSessionManager sseSessionManager; + + @Override + public void onApplicationEvent(@NotNull AlarmEvent event) { + if (log.isDebugEnabled()) { + log.debug("设备报警事件触发, deviceId: {}, {}", event.getAlarmInfo().getDeviceId(), event.getAlarmInfo().getAlarmDescription()); + } + sseSessionManager.sendForAll("message", event.getAlarmInfo()); + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/event/channel/ChannelEvent.java b/src/main/java/com/genersoft/iot/vmp/gb28181/event/channel/ChannelEvent.java new file mode 100755 index 0000000..534c6ce --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/event/channel/ChannelEvent.java @@ -0,0 +1,53 @@ +package com.genersoft.iot.vmp.gb28181.event.channel; + +import com.genersoft.iot.vmp.gb28181.bean.CommonGBChannel; +import lombok.Getter; +import lombok.Setter; +import org.springframework.context.ApplicationEvent; + +import java.io.Serial; +import java.util.List; + +/** + * 通道事件 + */ + +@Setter +@Getter +public class ChannelEvent extends ApplicationEvent { + + @Serial + private static final long serialVersionUID = 1L; + + public ChannelEvent(Object source) { + super(source); + } + + private List channels; + + private List oldChannels; + + private ChannelEventMessageType messageType; + + + + public enum ChannelEventMessageType { + ADD, UPDATE, DEL, ON, OFF, VLOST, DEFECT + } + + public static ChannelEvent getInstance(Object source, ChannelEventMessageType messageType, List channelList) { + ChannelEvent channelEvent = new ChannelEvent(source); + channelEvent.setMessageType(messageType); + channelEvent.setChannels(channelList); + return channelEvent; + } + + public static ChannelEvent getInstanceForUpdate(Object source, List channelList, List channelListForOld) { + ChannelEvent channelEvent = new ChannelEvent(source); + channelEvent.setMessageType(ChannelEventMessageType.UPDATE); + channelEvent.setChannels(channelList); + channelEvent.setOldChannels(channelListForOld); + return channelEvent; + } + +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/event/record/RecordInfoEndEvent.java b/src/main/java/com/genersoft/iot/vmp/gb28181/event/record/RecordInfoEndEvent.java new file mode 100755 index 0000000..ef2283f --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/event/record/RecordInfoEndEvent.java @@ -0,0 +1,28 @@ +package com.genersoft.iot.vmp.gb28181.event.record; + +import com.genersoft.iot.vmp.gb28181.bean.RecordInfo; +import lombok.Getter; +import lombok.Setter; +import org.springframework.context.ApplicationEvent; + +import java.io.Serial; + +/** + * @description: 录像查询结束时间 + * @author: pan + * @data: 2022-02-23 + */ +@Setter +@Getter +public class RecordInfoEndEvent extends ApplicationEvent { + + @Serial + private static final long serialVersionUID = 1L; + + public RecordInfoEndEvent(Object source) { + super(source); + } + + private RecordInfo recordInfo; + +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/event/record/RecordInfoEvent.java b/src/main/java/com/genersoft/iot/vmp/gb28181/event/record/RecordInfoEvent.java new file mode 100755 index 0000000..e6a9da9 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/event/record/RecordInfoEvent.java @@ -0,0 +1,28 @@ +package com.genersoft.iot.vmp.gb28181.event.record; + +import com.genersoft.iot.vmp.gb28181.bean.RecordInfo; +import lombok.Getter; +import lombok.Setter; +import org.springframework.context.ApplicationEvent; + +import java.io.Serial; + +/** + * @description: 录像查询结束时间 + * @author: pan + * @data: 2022-02-23 + */ + +@Setter +@Getter +public class RecordInfoEvent extends ApplicationEvent { + + @Serial + private static final long serialVersionUID = 1L; + + public RecordInfoEvent(Object source) { + super(source); + } + + private RecordInfo recordInfo; +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/event/record/RecordInfoEventListener.java b/src/main/java/com/genersoft/iot/vmp/gb28181/event/record/RecordInfoEventListener.java new file mode 100755 index 0000000..f2a0986 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/event/record/RecordInfoEventListener.java @@ -0,0 +1,61 @@ +package com.genersoft.iot.vmp.gb28181.event.record; + +import com.genersoft.iot.vmp.gb28181.bean.RecordInfo; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.ApplicationListener; +import org.springframework.stereotype.Component; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * @description: 录像查询结束事件 + * @author: pan + * @data: 2022-02-23 + */ +@Slf4j +@Component +public class RecordInfoEventListener implements ApplicationListener { + + private final Map handlerMap = new ConcurrentHashMap<>(); + public interface RecordEndEventHandler{ + void handler(RecordInfo recordInfo); + } + + @Override + public void onApplicationEvent(RecordInfoEvent event) { + String deviceId = event.getRecordInfo().getDeviceId(); + String channelId = event.getRecordInfo().getChannelId(); + int count = event.getRecordInfo().getCount(); + int sumNum = event.getRecordInfo().getSumNum(); + log.info("录像查询事件触发,deviceId:{}, channelId: {}, 录像数量{}/{}条", event.getRecordInfo().getDeviceId(), + event.getRecordInfo().getChannelId(), count,sumNum); + if (!handlerMap.isEmpty()) { + RecordEndEventHandler handler = handlerMap.get(deviceId + channelId); + log.info("录像查询事件触发, 发送订阅,deviceId:{}, channelId: {}", + event.getRecordInfo().getDeviceId(), event.getRecordInfo().getChannelId()); + if (handler !=null){ + handler.handler(event.getRecordInfo()); + if (count ==sumNum){ + handlerMap.remove(deviceId + channelId); + } + } + } + } + + /** + * 添加 + */ + public void addEndEventHandler(String device, String channelId, RecordEndEventHandler recordEndEventHandler) { + log.info("录像查询事件添加监听,deviceId:{}, channelId: {}", device, channelId); + handlerMap.put(device + channelId, recordEndEventHandler); + } + /** + * 添加 + */ + public void delEndEventHandler(String device, String channelId) { + log.info("录像查询事件移除监听,deviceId:{}, channelId: {}", device, channelId); + handlerMap.remove(device + channelId); + } + +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/event/sip/MessageEvent.java b/src/main/java/com/genersoft/iot/vmp/gb28181/event/sip/MessageEvent.java new file mode 100644 index 0000000..e55fce3 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/event/sip/MessageEvent.java @@ -0,0 +1,56 @@ +package com.genersoft.iot.vmp.gb28181.event.sip; + +import com.genersoft.iot.vmp.service.bean.ErrorCallback; +import lombok.Data; +import org.jetbrains.annotations.NotNull; + +import java.util.concurrent.Delayed; +import java.util.concurrent.TimeUnit; + +@Data +public class MessageEvent implements Delayed { + /** + * 超时时间(单位: 毫秒) + */ + private long delay; + + private String cmdType; + + private String sn; + + private String deviceId; + + private String result; + + private T t; + + private ErrorCallback callback; + + @Override + public long getDelay(@NotNull TimeUnit unit) { + return unit.convert(delay - System.currentTimeMillis(), TimeUnit.MILLISECONDS); + } + + @Override + public int compareTo(@NotNull Delayed o) { + return (int) (this.getDelay(TimeUnit.MILLISECONDS) - o.getDelay(TimeUnit.MILLISECONDS)); + } + + public String getKey(){ + return cmdType + sn; + } + + public static MessageEvent getInstance(String cmdType, String sn, String deviceId, Long delay, ErrorCallback callback){ + MessageEvent messageEvent = new MessageEvent<>(); + messageEvent.cmdType = cmdType; + messageEvent.sn = sn; + messageEvent.deviceId = deviceId; + messageEvent.callback = callback; + if (delay == null) { + messageEvent.delay = System.currentTimeMillis() + 1000; + }else { + messageEvent.delay = System.currentTimeMillis() + delay; + } + return messageEvent; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/event/sip/SipEvent.java b/src/main/java/com/genersoft/iot/vmp/gb28181/event/sip/SipEvent.java new file mode 100644 index 0000000..0bca7cd --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/event/sip/SipEvent.java @@ -0,0 +1,51 @@ +package com.genersoft.iot.vmp.gb28181.event.sip; + +import com.genersoft.iot.vmp.gb28181.bean.SipTransactionInfo; +import com.genersoft.iot.vmp.gb28181.event.SipSubscribe; +import lombok.Data; +import org.jetbrains.annotations.NotNull; + +import java.util.concurrent.Delayed; +import java.util.concurrent.TimeUnit; + +@Data +public class SipEvent implements Delayed { + + private String key; + + /** + * 成功的回调 + */ + private SipSubscribe.Event okEvent; + + /** + * 错误的回调,包括超时 + */ + private SipSubscribe.Event errorEvent; + + /** + * 超时时间(单位: 毫秒) + */ + private long delay; + + private SipTransactionInfo sipTransactionInfo; + + public static SipEvent getInstance(String key, SipSubscribe.Event okEvent, SipSubscribe.Event errorEvent, long delay) { + SipEvent sipEvent = new SipEvent(); + sipEvent.setKey(key); + sipEvent.setOkEvent(okEvent); + sipEvent.setErrorEvent(errorEvent); + sipEvent.setDelay(System.currentTimeMillis() + delay); + return sipEvent; + } + + @Override + public long getDelay(@NotNull TimeUnit unit) { + return unit.convert(delay - System.currentTimeMillis(), TimeUnit.MILLISECONDS); + } + + @Override + public int compareTo(@NotNull Delayed o) { + return (int) (this.getDelay(TimeUnit.MILLISECONDS) - o.getDelay(TimeUnit.MILLISECONDS)); + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/event/subscribe/catalog/CatalogEvent.java b/src/main/java/com/genersoft/iot/vmp/gb28181/event/subscribe/catalog/CatalogEvent.java new file mode 100755 index 0000000..d0d2122 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/event/subscribe/catalog/CatalogEvent.java @@ -0,0 +1,60 @@ +package com.genersoft.iot.vmp.gb28181.event.subscribe.catalog; + +import com.genersoft.iot.vmp.gb28181.bean.CommonGBChannel; +import com.genersoft.iot.vmp.gb28181.bean.Platform; +import lombok.Getter; +import lombok.Setter; +import org.springframework.context.ApplicationEvent; + +import java.util.List; + +@Setter +@Getter +public class CatalogEvent extends ApplicationEvent { + + public CatalogEvent(Object source) { + super(source); + } + + /** + * 上线 + */ + public static final String ON = "ON"; + + /** + * 离线 + */ + public static final String OFF = "OFF"; + + /** + * 视频丢失 + */ + public static final String VLOST = "VLOST"; + + /** + * 故障 + */ + public static final String DEFECT = "DEFECT"; + + /** + * 增加 + */ + public static final String ADD = "ADD"; + + /** + * 删除 + */ + public static final String DEL = "DEL"; + + /** + * 更新 + */ + public static final String UPDATE = "UPDATE"; + + private List channels; + + private String type; + + private Platform platform; + +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/event/subscribe/catalog/CatalogEventLister.java b/src/main/java/com/genersoft/iot/vmp/gb28181/event/subscribe/catalog/CatalogEventLister.java new file mode 100755 index 0000000..2aef0ce --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/event/subscribe/catalog/CatalogEventLister.java @@ -0,0 +1,178 @@ +package com.genersoft.iot.vmp.gb28181.event.subscribe.catalog; + +import com.genersoft.iot.vmp.conf.UserSetting; +import com.genersoft.iot.vmp.gb28181.bean.CommonGBChannel; +import com.genersoft.iot.vmp.gb28181.bean.Platform; +import com.genersoft.iot.vmp.gb28181.bean.SubscribeHolder; +import com.genersoft.iot.vmp.gb28181.bean.SubscribeInfo; +import com.genersoft.iot.vmp.gb28181.service.IPlatformChannelService; +import com.genersoft.iot.vmp.gb28181.service.IPlatformService; +import com.genersoft.iot.vmp.gb28181.transmit.cmd.ISIPCommanderForPlatform; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationListener; + +import javax.sip.InvalidArgumentException; +import javax.sip.SipException; +import java.text.ParseException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * catalog事件 + */ +@Slf4j +//@Component +public class CatalogEventLister implements ApplicationListener { + + @Autowired + private IPlatformService platformService; + + @Autowired + private IPlatformChannelService platformChannelService; + + @Autowired + private ISIPCommanderForPlatform sipCommanderFroPlatform; + + @Autowired + private SubscribeHolder subscribeHolder; + + @Autowired + private UserSetting userSetting; + + @Override + public void onApplicationEvent(CatalogEvent event) { + SubscribeInfo subscribe = null; + Platform parentPlatform = null; + log.info("[Catalog事件: {}]通道数量: {}", event.getType(), event.getChannels().size()); + Map> platformMap = new HashMap<>(); + Map channelMap = new HashMap<>(); + if (event.getPlatform() != null) { + parentPlatform = event.getPlatform(); + if (parentPlatform.getServerGBId() == null) { + log.info("[Catalog事件: {}] 平台服务国标编码未找到", event.getType()); + return; + } + subscribe = subscribeHolder.getCatalogSubscribe(parentPlatform.getServerGBId()); + if (subscribe == null) { + log.info("[Catalog事件: {}] 未订阅目录事件", event.getType()); + return; + } + + }else { + List allPlatform = platformService.queryAll(userSetting.getServerId()); + // 获取所用订阅 + List platforms = subscribeHolder.getAllCatalogSubscribePlatform(allPlatform); + if (event.getChannels() != null) { + if (!platforms.isEmpty()) { + for (CommonGBChannel deviceChannel : event.getChannels()) { + List parentPlatformsForGB = platformChannelService.queryPlatFormListByChannelDeviceId( + deviceChannel.getGbId(), platforms); + platformMap.put(deviceChannel.getGbDeviceId(), parentPlatformsForGB); + channelMap.put(deviceChannel.getGbDeviceId(), deviceChannel); + } + }else { + log.info("[Catalog事件: {}] 未订阅目录事件", event.getType()); + } + }else { + log.info("[Catalog事件: {}] 事件内通道数为0", event.getType()); + } + } + switch (event.getType()) { + case CatalogEvent.ON: + case CatalogEvent.OFF: + case CatalogEvent.DEL: + + if (parentPlatform != null) { + List channels = new ArrayList<>(); + if (event.getChannels() != null) { + channels.addAll(event.getChannels()); + } + if (!channels.isEmpty()) { + log.info("[Catalog事件: {}]平台:{},影响通道{}个", event.getType(), parentPlatform.getServerGBId(), channels.size()); + try { + sipCommanderFroPlatform.sendNotifyForCatalogOther(event.getType(), parentPlatform, channels, subscribe, null); + } catch (InvalidArgumentException | ParseException | NoSuchFieldException | SipException | + IllegalAccessException e) { + log.error("[命令发送失败] 国标级联 Catalog通知: {}", e.getMessage()); + } + } + }else if (!platformMap.keySet().isEmpty()) { + for (String serverGbId : platformMap.keySet()) { + List platformList = platformMap.get(serverGbId); + if (platformList != null && !platformList.isEmpty()) { + for (Platform platform : platformList) { + SubscribeInfo subscribeInfo = subscribeHolder.getCatalogSubscribe(platform.getServerGBId()); + if (subscribeInfo == null) { + continue; + } + log.info("[Catalog事件: {}]平台:{},影响通道{}", event.getType(), platform.getServerGBId(), serverGbId); + List deviceChannelList = new ArrayList<>(); + CommonGBChannel deviceChannel = new CommonGBChannel(); + deviceChannel.setGbDeviceId(serverGbId); + deviceChannelList.add(deviceChannel); + try { + sipCommanderFroPlatform.sendNotifyForCatalogOther(event.getType(), platform, deviceChannelList, subscribeInfo, null); + } catch (InvalidArgumentException | ParseException | NoSuchFieldException | SipException | + IllegalAccessException e) { + log.error("[命令发送失败] 国标级联 Catalog通知: {}", e.getMessage()); + } + } + }else { + log.info("[Catalog事件: {}] 未找到上级平台: {}", event.getType(), serverGbId); + } + } + } + break; + case CatalogEvent.VLOST: + break; + case CatalogEvent.DEFECT: + break; + case CatalogEvent.ADD: + case CatalogEvent.UPDATE: + if (parentPlatform != null) { + List deviceChannelList = new ArrayList<>(); + if (event.getChannels() != null) { + deviceChannelList.addAll(event.getChannels()); + } + if (!deviceChannelList.isEmpty()) { + log.info("[Catalog事件: {}]平台:{},影响通道{}个", event.getType(), parentPlatform.getServerGBId(), deviceChannelList.size()); + try { + sipCommanderFroPlatform.sendNotifyForCatalogAddOrUpdate(event.getType(), parentPlatform, deviceChannelList, subscribe, null); + } catch (InvalidArgumentException | ParseException | NoSuchFieldException | SipException | + IllegalAccessException e) { + log.error("[命令发送失败] 国标级联 Catalog通知: {}", e.getMessage()); + } + } + }else if (!platformMap.keySet().isEmpty()) { + for (String gbId : platformMap.keySet()) { + List parentPlatforms = platformMap.get(gbId); + if (parentPlatforms != null && !parentPlatforms.isEmpty()) { + for (Platform platform : parentPlatforms) { + SubscribeInfo subscribeInfo = subscribeHolder.getCatalogSubscribe(platform.getServerGBId()); + if (subscribeInfo == null) { + continue; + } + log.info("[Catalog事件: {}]平台:{},影响通道{}", event.getType(), platform.getServerGBId(), gbId); + List channelList = new ArrayList<>(); + CommonGBChannel deviceChannel = channelMap.get(gbId); + channelList.add(deviceChannel); + try { + sipCommanderFroPlatform.sendNotifyForCatalogAddOrUpdate(event.getType(), platform, channelList, subscribeInfo, null); + } catch (InvalidArgumentException | ParseException | NoSuchFieldException | + SipException | IllegalAccessException e) { + log.error("[命令发送失败] 国标级联 Catalog通知: {}", e.getMessage()); + } + } + } + } + } + break; + default: + break; + } + } +} + diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/event/subscribe/mobilePosition/MobilePositionEvent.java b/src/main/java/com/genersoft/iot/vmp/gb28181/event/subscribe/mobilePosition/MobilePositionEvent.java new file mode 100755 index 0000000..f6a4ad7 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/event/subscribe/mobilePosition/MobilePositionEvent.java @@ -0,0 +1,17 @@ +package com.genersoft.iot.vmp.gb28181.event.subscribe.mobilePosition; + +import com.genersoft.iot.vmp.gb28181.bean.MobilePosition; +import lombok.Getter; +import lombok.Setter; +import org.springframework.context.ApplicationEvent; + + +public class MobilePositionEvent extends ApplicationEvent { + public MobilePositionEvent(Object source) { + super(source); + } + + @Getter + @Setter + private MobilePosition mobilePosition; +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/event/subscribe/mobilePosition/MobilePositionEventLister.java b/src/main/java/com/genersoft/iot/vmp/gb28181/event/subscribe/mobilePosition/MobilePositionEventLister.java new file mode 100755 index 0000000..7b06f07 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/event/subscribe/mobilePosition/MobilePositionEventLister.java @@ -0,0 +1,75 @@ +package com.genersoft.iot.vmp.gb28181.event.subscribe.mobilePosition; + +import com.genersoft.iot.vmp.conf.UserSetting; +import com.genersoft.iot.vmp.gb28181.bean.CommonGBChannel; +import com.genersoft.iot.vmp.gb28181.bean.Platform; +import com.genersoft.iot.vmp.gb28181.bean.SubscribeHolder; +import com.genersoft.iot.vmp.gb28181.bean.SubscribeInfo; +import com.genersoft.iot.vmp.gb28181.service.IPlatformChannelService; +import com.genersoft.iot.vmp.gb28181.service.IPlatformService; +import com.genersoft.iot.vmp.gb28181.transmit.cmd.impl.SIPCommanderForPlatform; +import com.genersoft.iot.vmp.service.bean.GPSMsgInfo; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationListener; +import org.springframework.stereotype.Component; + +import javax.sip.InvalidArgumentException; +import javax.sip.SipException; +import java.text.ParseException; +import java.util.List; + +/** + * 移动位置通知消息转发 + */ +@Slf4j +@Component +public class MobilePositionEventLister implements ApplicationListener { + + @Autowired + private IPlatformService platformService; + + @Autowired + private IPlatformChannelService platformChannelService; + + @Autowired + private SIPCommanderForPlatform sipCommanderForPlatform; + + @Autowired + private SubscribeHolder subscribeHolder; + + @Autowired + private UserSetting userSetting; + + @Override + public void onApplicationEvent(MobilePositionEvent event) { + if (event.getMobilePosition().getChannelId() == 0) { + return; + } + List allPlatforms = platformService.queryAll(userSetting.getServerId()); + // 获取所用订阅 + List platforms = subscribeHolder.getAllMobilePositionSubscribePlatform(allPlatforms); + if (platforms.isEmpty()) { + return; + } + List platformsForGB = platformChannelService.queryPlatFormListByChannelDeviceId(event.getMobilePosition().getChannelId(), platforms); + + for (Platform platform : platformsForGB) { + if (log.isDebugEnabled()){ + log.debug("[向上级发送MobilePosition] 通道:{},平台:{}, 位置: {}:{}", event.getMobilePosition().getChannelId(), + platform.getServerGBId(), event.getMobilePosition().getLongitude(), event.getMobilePosition().getLatitude()); + } + SubscribeInfo subscribe = subscribeHolder.getMobilePositionSubscribe(platform.getServerGBId()); + try { + GPSMsgInfo gpsMsgInfo = GPSMsgInfo.getInstance(event.getMobilePosition()); + // 获取通道编号 + CommonGBChannel commonGBChannel = platformChannelService.queryChannelByPlatformIdAndChannelId(platform.getId(), event.getMobilePosition().getChannelId()); + sipCommanderForPlatform.sendNotifyMobilePosition(platform, gpsMsgInfo, commonGBChannel, + subscribe); + } catch (InvalidArgumentException | ParseException | NoSuchFieldException | SipException | + IllegalAccessException e) { + log.error("[命令发送失败] 国标级联 Catalog通知: {}", e.getMessage()); + } + } + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/service/IDeviceAlarmService.java b/src/main/java/com/genersoft/iot/vmp/gb28181/service/IDeviceAlarmService.java new file mode 100755 index 0000000..3677188 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/service/IDeviceAlarmService.java @@ -0,0 +1,43 @@ +package com.genersoft.iot.vmp.gb28181.service; + +import com.genersoft.iot.vmp.gb28181.bean.DeviceAlarm; +import com.github.pagehelper.PageInfo; + +import java.util.List; + +/** + * 报警相关业务处理 + */ +public interface IDeviceAlarmService { + + /** + * 根据多个添加获取报警列表 + * @param page 当前页 + * @param count 每页数量 + * @param deviceId 设备id + * @param alarmPriority 报警级别, 1为一级警情, 2为二级警情, 3为三级警情, 4为四级 警情- + * @param alarmMethod 报警方式 , 1为电话报警, 2为设备报警, 3为短信报警, 4为 GPS报警, 5为视频报警, 6为设备故障报警, + * 7其他报警;可以为直接组合如12为电话报警或 设备报警- + * @param alarmType 报警类型 + * @param startTime 开始时间 + * @param endTime 结束时间 + * @return 报警列表 + */ + PageInfo getAllAlarm(int page, int count, String deviceId, String channelId, String alarmPriority, String alarmMethod, + String alarmType, String startTime, String endTime); + + /** + * 添加一个报警 + * @param deviceAlarm 添加报警 + */ + void add(DeviceAlarm deviceAlarm); + + /** + * 清空时间以前的报警 + * @param id 数据库id + * @param deviceIdList 制定需要清理的设备id + * @param time 不写时间则清空所有时间的 + */ + int clearAlarmBeforeTime(Integer id, List deviceIdList, String time); + +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/service/IDeviceChannelService.java b/src/main/java/com/genersoft/iot/vmp/gb28181/service/IDeviceChannelService.java new file mode 100755 index 0000000..48f23a3 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/service/IDeviceChannelService.java @@ -0,0 +1,99 @@ +package com.genersoft.iot.vmp.gb28181.service; + +import com.genersoft.iot.vmp.common.enums.DeviceControlType; +import com.genersoft.iot.vmp.gb28181.bean.*; +import com.genersoft.iot.vmp.service.bean.ErrorCallback; +import com.genersoft.iot.vmp.vmanager.bean.ResourceBaseInfo; +import com.genersoft.iot.vmp.web.gb28181.dto.DeviceChannelExtend; +import com.github.pagehelper.PageInfo; +import jakarta.validation.constraints.NotNull; +import org.dom4j.Element; + +import java.util.List; + +/** + * 国标通道业务类 + * @author lin + */ +public interface IDeviceChannelService { + + /** + * 批量添加设备通道 + */ + int updateChannels(Device device, List channels); + + /** + * 获取统计信息 + * @return + */ + ResourceBaseInfo getOverview(); + + /** + * 获取一个通道 + */ + DeviceChannel getOne(String deviceId, String channelId); + + DeviceChannel getOneForSource(String deviceId, String channelId); + + /** + * 修改通道的码流类型 + */ + void updateChannelStreamIdentification(DeviceChannel channel); + + List queryChaneListByDeviceId(String deviceId); + + void updateChannelGPS(Device device, DeviceChannel deviceChannel, MobilePosition mobilePosition); + + void startPlay(Integer channelId, String stream); + + void stopPlay(Integer channelId); + + void online(DeviceChannel channel); + + void offline(DeviceChannel channel); + + void deleteForNotify(DeviceChannel channel); + + void cleanChannelsForDevice(int deviceId); + + boolean resetChannels(int deviceDbId, List deviceChannels); + + PageInfo getSubChannels(int deviceDbId, String channelId, String query, Boolean channelType, Boolean online, int page, int count); + + List queryChannelExtendsByDeviceId(String deviceId, List channelIds, Boolean online); + + PageInfo queryChannelsByDeviceId(String deviceId, String query, Boolean channelType, Boolean online, int page, int count); + + PageInfo queryChannels(String query, Boolean queryParent, Boolean channelType, Boolean online, Boolean hasStream, int page, int count); + + List queryDeviceWithAsMessageChannel(); + + DeviceChannel getRawChannel(int id); + + DeviceChannel getOneById(Integer channelId); + + DeviceChannel getOneForSourceById(Integer channelId); + + DeviceChannel getBroadcastChannel(int deviceDbId); + + void changeAudio(Integer channelId, Boolean audio); + + void updateChannelStatusForNotify(DeviceChannel channel); + + void addChannel(DeviceChannel channel); + + void updateChannelForNotify(DeviceChannel channel); + + DeviceChannel getOneForSource(int deviceDbId, String channelId); + + DeviceChannel getOneBySourceId(int deviceDbId, String channelId); + + List queryChaneIdListByDeviceDbIds(List deviceDbId); + + void handlePtzCmd(@NotNull Integer dataDeviceId, @NotNull Integer gbId, Element rootElement, DeviceControlType type, ErrorCallback callback); + + void queryRecordInfo(Device device, DeviceChannel channel, String startTime, String endTime, ErrorCallback object); + + void queryRecordInfo(CommonGBChannel channel, String startTime, String endTime, ErrorCallback object); + +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/service/IDeviceService.java b/src/main/java/com/genersoft/iot/vmp/gb28181/service/IDeviceService.java new file mode 100755 index 0000000..2930c4f --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/service/IDeviceService.java @@ -0,0 +1,205 @@ +package com.genersoft.iot.vmp.gb28181.service; + +import com.genersoft.iot.vmp.common.CommonCallback; +import com.genersoft.iot.vmp.gb28181.bean.*; +import com.genersoft.iot.vmp.service.bean.ErrorCallback; +import com.genersoft.iot.vmp.vmanager.bean.ResourceBaseInfo; +import com.genersoft.iot.vmp.vmanager.bean.WVPResult; +import com.github.pagehelper.PageInfo; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +/** + * 设备相关业务处理 + * @author lin + */ +public interface IDeviceService { + + /** + * 设备上线 + * @param device 设备信息 + */ + void online(Device device, SipTransactionInfo sipTransactionInfo); + + /** + * 设备下线 + * @param deviceId 设备编号 + */ + void offline(String deviceId, String reason); + + /** + * 添加目录订阅 + * @param device 设备信息 + * @return 布尔 + */ + boolean addCatalogSubscribe(Device device, SipTransactionInfo transactionInfo); + + /** + * 移除目录订阅 + * @param device 设备信息 + * @return 布尔 + */ + boolean removeCatalogSubscribe(Device device, CommonCallback callback); + + /** + * 添加移动位置订阅 + * @param device 设备信息 + * @return 布尔 + */ + boolean addMobilePositionSubscribe(Device device, SipTransactionInfo transactionInfo); + + /** + * 移除移动位置订阅 + * @param device 设备信息 + * @return 布尔 + */ + boolean removeMobilePositionSubscribe(Device device, CommonCallback callback); + + /** + * 移除移动位置订阅 + * @param deviceId 设备ID + * @return 同步状态 + */ + SyncStatus getChannelSyncStatus(String deviceId); + + /** + * 查看是否仍在同步 + * @param deviceId 设备ID + * @return 布尔 + */ + Boolean isSyncRunning(String deviceId); + + /** + * 通道同步 + * @param device 设备信息 + */ + void sync(Device device); + + /** + * 查询设备信息 + * @param deviceId 设备编号 + * @return 设备信息 + */ + Device getDeviceByDeviceId(String deviceId); + + /** + * 获取所有在线设备 + * @return 设备列表 + */ + List getAllOnlineDevice(String serverId); + + List getAllByStatus(Boolean status); + + /** + * 判断是否注册已经失效 + * @param device 设备信息 + * @return 布尔 + */ + boolean expire(Device device); + + /** + * 检查设备状态 + * @param device 设备信息 + */ + Boolean getDeviceStatus(Device device); + + /** + * 根据IP和端口获取设备信息 + * @param host IP + * @param port 端口 + * @return 设备信息 + */ + Device getDeviceByHostAndPort(String host, int port); + + /** + * 更新设备 + * @param device 设备信息 + */ + void updateDevice(Device device); + + @Transactional + void updateDeviceList(List deviceList); + + /** + * 检查设备编号是否已经存在 + * @param deviceId 设备编号 + * @return + */ + boolean isExist(String deviceId); + + /** + * 添加设备 + * @param device + */ + void addCustomDevice(Device device); + + /** + * 页面表单更新设备信息 + * @param device + */ + void updateCustomDevice(Device device); + + /** + * 删除设备 + * @param deviceId + * @return + */ + boolean delete(String deviceId); + + /** + * 获取统计信息 + * @return + */ + ResourceBaseInfo getOverview(); + + /** + * 获取所有设备 + */ + List getAll(); + + PageInfo getAll(int page, int count, String query, Boolean status); + + Device getDevice(Integer gbDeviceDbId); + + Device getDeviceByChannelId(Integer channelId); + + Device getDeviceBySourceChannelDeviceId(String requesterId); + + void subscribeCatalog(int id, int cycle); + + void subscribeMobilePosition(int id, int cycle, int interval); + + WVPResult devicesSync(Device device); + + void deviceBasicConfig(Device device, BasicParam basicParam, ErrorCallback callback); + + void deviceConfigQuery(Device device, String channelId, String configType, ErrorCallback callback); + + void teleboot(Device device); + + void record(Device device, String channelId, String recordCmdStr, ErrorCallback callback); + + void guard(Device device, String guardCmdStr, ErrorCallback callback); + + void resetAlarm(Device device, String channelId, String alarmMethod, String alarmType, ErrorCallback callback); + + void iFrame(Device device, String channelId); + + void homePosition(Device device, String channelId, Boolean enabled, Integer resetTime, Integer presetIndex, ErrorCallback callback); + + void dragZoomIn(Device device, String channelId, int length, int width, int midpointx, int midpointy, int lengthx, int lengthy, ErrorCallback callback); + + void dragZoomOut(Device device, String channelId, int length, int width, int midpointx, int midpointy, int lengthx, int lengthy, ErrorCallback callback); + + void deviceStatus(Device device, ErrorCallback callback); + + void updateDeviceHeartInfo(Device device); + + void alarm(Device device, String startPriority, String endPriority, String alarmMethod, String alarmType, String startTime, String endTime, ErrorCallback callback); + + void deviceInfo(Device device, ErrorCallback callback); + + void queryPreset(Device device, String channelId, ErrorCallback> callback); + +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/service/IGbChannelControlService.java b/src/main/java/com/genersoft/iot/vmp/gb28181/service/IGbChannelControlService.java new file mode 100644 index 0000000..bc28dbd --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/service/IGbChannelControlService.java @@ -0,0 +1,19 @@ +package com.genersoft.iot.vmp.gb28181.service; + +import com.genersoft.iot.vmp.gb28181.bean.*; +import com.genersoft.iot.vmp.service.bean.ErrorCallback; + +import java.util.List; + +public interface IGbChannelControlService { + + + void ptz(CommonGBChannel channel, FrontEndControlCodeForPTZ frontEndControlCode, ErrorCallback callback); + void fi(CommonGBChannel channel, FrontEndControlCodeForFI frontEndControlCode, ErrorCallback callback); + void preset(CommonGBChannel channel, FrontEndControlCodeForPreset frontEndControlCode, ErrorCallback callback); + void tour(CommonGBChannel channel, FrontEndControlCodeForTour frontEndControlCode, ErrorCallback callback); + void scan(CommonGBChannel channel, FrontEndControlCodeForScan frontEndControlCode, ErrorCallback callback); + void wiper(CommonGBChannel channel, FrontEndControlCodeForWiper controlCode, ErrorCallback callback); + void auxiliary(CommonGBChannel channel, FrontEndControlCodeForAuxiliary frontEndControlCode, ErrorCallback callback); + void queryPreset(CommonGBChannel channel, ErrorCallback> callback); +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/service/IGbChannelPlayService.java b/src/main/java/com/genersoft/iot/vmp/gb28181/service/IGbChannelPlayService.java new file mode 100644 index 0000000..b0653e7 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/service/IGbChannelPlayService.java @@ -0,0 +1,41 @@ +package com.genersoft.iot.vmp.gb28181.service; + +import com.genersoft.iot.vmp.common.InviteSessionType; +import com.genersoft.iot.vmp.common.StreamInfo; +import com.genersoft.iot.vmp.gb28181.bean.CommonGBChannel; +import com.genersoft.iot.vmp.gb28181.bean.CommonRecordInfo; +import com.genersoft.iot.vmp.gb28181.bean.InviteMessageInfo; +import com.genersoft.iot.vmp.gb28181.bean.Platform; +import com.genersoft.iot.vmp.service.bean.ErrorCallback; + +import java.util.List; + +public interface IGbChannelPlayService { + + void startInvite(CommonGBChannel channel, InviteMessageInfo inviteInfo, Platform platform, ErrorCallback callback); + + void stopInvite(InviteSessionType type, CommonGBChannel channel, String stream); + + void playback(CommonGBChannel channel, Long startTime, Long stopTime, ErrorCallback callback); + + void download(CommonGBChannel channel, Long startTime, Long stopTime, Integer downloadSpeed, + ErrorCallback callback); + + void stopPlay(CommonGBChannel channel); + + void play(CommonGBChannel channel, Platform platform, Boolean record, ErrorCallback callback); + + void stopPlayback(CommonGBChannel channel, String stream); + + void stopDownload(CommonGBChannel channel, String stream); + + void playbackPause(CommonGBChannel channel, String streamId); + + void playbackResume(CommonGBChannel channel, String streamId); + + void playbackSeek(CommonGBChannel channel, String stream, long seekTime); + + void playbackSpeed(CommonGBChannel channel, String stream, Double speed); + + void queryRecord(CommonGBChannel channel, String startTime, String endTime, ErrorCallback> callback); +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/service/IGbChannelService.java b/src/main/java/com/genersoft/iot/vmp/gb28181/service/IGbChannelService.java new file mode 100644 index 0000000..6f5b45a --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/service/IGbChannelService.java @@ -0,0 +1,116 @@ +package com.genersoft.iot.vmp.gb28181.service; + +import com.genersoft.iot.vmp.gb28181.bean.*; +import com.genersoft.iot.vmp.gb28181.controller.bean.Extent; +import com.genersoft.iot.vmp.service.bean.GPSMsgInfo; +import com.genersoft.iot.vmp.streamPush.bean.StreamPush; +import com.github.pagehelper.PageInfo; + +import java.util.Collection; +import java.util.List; +import java.util.Map; + +public interface IGbChannelService { + + CommonGBChannel queryByDeviceId(String gbDeviceId); + + int add(CommonGBChannel commonGBChannel); + + int delete(int gbId); + + void delete(Collection ids); + + int update(CommonGBChannel commonGBChannel); + + int offline(CommonGBChannel commonGBChannel); + + int offline(List commonGBChannelList); + + int online(CommonGBChannel commonGBChannel); + + int online(List commonGBChannelList); + + void batchAdd(List commonGBChannels); + + void updateStatus(List channelList); + + CommonGBChannel getOne(int id); + + List getIndustryCodeList(); + + List getDeviceTypeList(); + + List getNetworkIdentificationTypeList(); + + void reset(int id, List chanelFields); + + PageInfo queryListByCivilCode(int page, int count, String query, Boolean online, Integer channelType, String civilCode); + + PageInfo queryListByParentId(int page, int count, String query, Boolean online, Integer channelType, String groupDeviceId); + + void removeCivilCode(List allChildren); + + void addChannelToRegion(String civilCode, List channelIds); + + void deleteChannelToRegion(String civilCode, List channelIds); + + void deleteChannelToRegionByCivilCode(String civilCode); + + void deleteChannelToRegionByChannelIds(List channelIds); + + void addChannelToRegionByGbDevice(String civilCode, List deviceIds); + + void deleteChannelToRegionByGbDevice(List deviceIds); + + void removeParentIdByBusinessGroup(String businessGroup); + + void removeParentIdByGroupList(List groupList); + + void updateBusinessGroup(String oldBusinessGroup, String newBusinessGroup); + + void updateParentIdGroup(String oldParentId, String newParentId); + + void addChannelToGroup(String parentId, String businessGroup, List channelIds); + + void deleteChannelToGroup(String parentId, String businessGroup, List channelIds); + + void addChannelToGroupByGbDevice(String parentId, String businessGroup, List deviceIds); + + void deleteChannelToGroupByGbDevice(List deviceIds); + + void batchUpdate(List commonGBChannels); + + CommonGBChannel queryOneWithPlatform(Integer platformId, String channelDeviceId); + + void updateCivilCode(String oldCivilCode, String newCivilCode); + + List queryListByStreamPushList(List streamPushList); + + PageInfo queryList(int page, int count, String query, Boolean online, Boolean hasRecordPlan, Integer channelType, String civilCode, String parentDeviceId); + + PageInfo queryListByCivilCodeForUnusual(int page, int count, String query, Boolean online, Integer channelType); + + void clearChannelCivilCode(Boolean all, List channelIds); + + PageInfo queryListByParentForUnusual(int page, int count, String query, Boolean online, Integer channelType); + + void clearChannelParent(Boolean all, List channelIds); + + void updateGPSFromGPSMsgInfo(List gpsMsgInfoList); + + void updateGPS(List channelList); + + List queryListForMap(String query, Boolean online, Boolean hasRecordPlan, Integer channelType); + + CommonGBChannel queryCommonChannelByDeviceChannel(DeviceChannel channel); + + void resetLevel(); + + byte[] getTile(int z, int x, int y, String geoCoordSys); + + String drawThin(Map zoomParam, Extent extent, String geoCoordSys); + + DrawThinProcess thinProgress(String id); + + void saveThin(String id); +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/service/IGroupService.java b/src/main/java/com/genersoft/iot/vmp/gb28181/service/IGroupService.java new file mode 100644 index 0000000..7b58256 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/service/IGroupService.java @@ -0,0 +1,34 @@ +package com.genersoft.iot.vmp.gb28181.service; + +import com.genersoft.iot.vmp.gb28181.bean.Group; +import com.genersoft.iot.vmp.gb28181.bean.GroupTree; +import com.github.pagehelper.PageInfo; + +import java.util.List; + + +public interface IGroupService { + + void add(Group group); + + List queryAllChildren(Integer id); + + void update(Group group); + + Group queryGroupByDeviceId(String regionDeviceId); + + List queryForTree(String query, Integer parent, Boolean hasChannel); + + boolean delete(int id); + + boolean batchAdd(List groupList); + + List getPath(String deviceId, String businessGroup); + + PageInfo queryList(Integer page, Integer count, String query); + + Group queryGroupByAlias(String groupAlias); + + void sync(); + +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/service/IInviteStreamService.java b/src/main/java/com/genersoft/iot/vmp/gb28181/service/IInviteStreamService.java new file mode 100755 index 0000000..e8e4d3c --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/service/IInviteStreamService.java @@ -0,0 +1,85 @@ +package com.genersoft.iot.vmp.gb28181.service; + +import com.genersoft.iot.vmp.common.InviteInfo; +import com.genersoft.iot.vmp.common.InviteSessionType; +import com.genersoft.iot.vmp.common.StreamInfo; +import com.genersoft.iot.vmp.service.bean.ErrorCallback; + +import java.util.List; + +/** + * 记录国标点播的状态,包括实时预览,下载,录像回放 + */ +public interface IInviteStreamService { + + /** + * 更新点播的状态信息 + */ + void updateInviteInfo(InviteInfo inviteInfo); + + void updateInviteInfo(InviteInfo inviteInfo, Long time); + + InviteInfo updateInviteInfoForStream(InviteInfo inviteInfo, String stream); + + /** + * 获取点播的状态信息 + */ + InviteInfo getInviteInfo(InviteSessionType type, Integer channelId, String stream); + + /** + * 移除点播的状态信息 + */ + void removeInviteInfo(InviteSessionType type, Integer channelId, String stream); + /** + * 移除点播的状态信息 + */ + void removeInviteInfo(InviteInfo inviteInfo); + /** + * 移除点播的状态信息 + */ + void removeInviteInfoByDeviceAndChannel(InviteSessionType inviteSessionType, Integer channelId); + + List getAllInviteInfo(); + + /** + * 获取点播的状态信息 + */ + InviteInfo getInviteInfoByDeviceAndChannel(InviteSessionType type, Integer channelId); + + /** + * 获取点播的状态信息 + */ + InviteInfo getInviteInfoByStream(InviteSessionType type, String stream); + + + /** + * 添加一个invite回调 + */ + void once(InviteSessionType type, Integer channelId, String stream, ErrorCallback callback); + + /** + * 调用一个invite回调 + */ + void call(InviteSessionType type, Integer channelId, String stream, int code, String msg, StreamInfo data); + + /** + * 清空一个设备的所有invite信息 + */ + void clearInviteInfo(String deviceId); + + /** + * 统计同一个zlm下的国标收流个数 + */ + int getStreamInfoCount(String mediaServerId); + + + /** + * 获取MediaServer下的流信息 + */ + InviteInfo getInviteInfoBySSRC(String ssrc); + + /** + * 更新ssrc + */ + InviteInfo updateInviteInfoForSSRC(InviteInfo inviteInfo, String ssrcInResponse); +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/service/IPTZService.java b/src/main/java/com/genersoft/iot/vmp/gb28181/service/IPTZService.java new file mode 100755 index 0000000..2a871d8 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/service/IPTZService.java @@ -0,0 +1,21 @@ +package com.genersoft.iot.vmp.gb28181.service; + + +import com.genersoft.iot.vmp.gb28181.bean.CommonGBChannel; +import com.genersoft.iot.vmp.gb28181.bean.Device; +import com.genersoft.iot.vmp.gb28181.bean.Preset; +import com.genersoft.iot.vmp.service.bean.ErrorCallback; + +import java.util.List; + +public interface IPTZService { + + void ptz(Device device, String channelId, int cmdCode, int horizonSpeed, int verticalSpeed, int zoomSpeed); + + void frontEndCommand(Device device, String channelId, int cmdCode, int parameter1, int parameter2, int combindCode2); + + void frontEndCommand(CommonGBChannel channel, Integer cmdCode, Integer parameter1, Integer parameter2, Integer combindCode2); + + void queryPresetList(CommonGBChannel channel, ErrorCallback> callback); + +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/service/IPlatformChannelService.java b/src/main/java/com/genersoft/iot/vmp/gb28181/service/IPlatformChannelService.java new file mode 100755 index 0000000..0151b63 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/service/IPlatformChannelService.java @@ -0,0 +1,53 @@ +package com.genersoft.iot.vmp.gb28181.service; + +import com.genersoft.iot.vmp.gb28181.bean.*; +import com.github.pagehelper.PageInfo; + +import java.util.List; + +/** + * 平台关联通道管理 + * @author lin + */ +public interface IPlatformChannelService { + + PageInfo queryChannelList(int page, int count, String query, Integer channelType, Boolean online, Integer platformId, Boolean hasShare); + + int addAllChannel(Integer platformId); + + int removeAllChannel(Integer platformId); + + int addChannels(Integer platformId, List channelIds); + + int removeChannels(Integer platformId, List channelIds); + + void removeChannels(List ids); + + void removeChannel(int gbId); + + List queryByPlatform(Platform platform); + + void pushChannel(Integer platformId); + + void addChannelByDevice(Integer platformId, List deviceIds); + + void removeChannelByDevice(Integer platformId, List deviceIds); + + void updateCustomChannel(PlatformChannel channel); + + void checkGroupRemove(List channelList, List groups); + + void checkGroupAdd(List channelList); + + List queryPlatFormListByChannelDeviceId(Integer channelId, List platforms); + + CommonGBChannel queryChannelByPlatformIdAndChannelId(Integer platformId, Integer channelId); + + List queryChannelByPlatformIdAndChannelIds(Integer platformId, List channelIds); + + void checkRegionAdd(List channelList); + + void checkRegionRemove(List channelList, List regionList); + + List queryByPlatformBySharChannelId(String gbId); +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/service/IPlatformService.java b/src/main/java/com/genersoft/iot/vmp/gb28181/service/IPlatformService.java new file mode 100755 index 0000000..b0bea7d --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/service/IPlatformService.java @@ -0,0 +1,85 @@ +package com.genersoft.iot.vmp.gb28181.service; + +import com.genersoft.iot.vmp.common.CommonCallback; +import com.genersoft.iot.vmp.gb28181.bean.CommonGBChannel; +import com.genersoft.iot.vmp.gb28181.bean.Platform; +import com.genersoft.iot.vmp.gb28181.bean.SipTransactionInfo; +import com.genersoft.iot.vmp.gb28181.event.SipSubscribe; +import com.genersoft.iot.vmp.media.bean.MediaServer; +import com.genersoft.iot.vmp.media.event.hook.HookSubscribe; +import com.genersoft.iot.vmp.service.bean.InviteTimeOutCallback; +import com.github.pagehelper.PageInfo; + +import javax.sip.InvalidArgumentException; +import javax.sip.SipException; +import java.text.ParseException; +import java.util.List; + +/** + * 国标平台的业务类 + * @author lin + */ +public interface IPlatformService { + + Platform queryPlatformByServerGBId(String platformGbId); + + /** + * 分页获取上级平台 + * @param page + * @param count + * @return + */ + PageInfo queryPlatformList(int page, int count, String query); + + /** + * 添加级联平台 + * @param parentPlatform 级联平台 + */ + boolean add(Platform parentPlatform); + + /** + * 添加级联平台 + * @param parentPlatform 级联平台 + */ + boolean update(Platform parentPlatform); + + /** + * 平台上线 + * @param parentPlatform 平台信息 + */ + void online(Platform parentPlatform, SipTransactionInfo sipTransactionInfo); + + /** + * 平台离线 + * @param parentPlatform 平台信息 + */ + void offline(Platform parentPlatform); + + /** + * 向上级平台发送位置订阅 + * @param platformId 平台 + */ + void sendNotifyMobilePosition(String platformId); + + /** + * 向上级发送语音喊话的消息 + */ + void broadcastInvite(Platform platform, CommonGBChannel channel, String sourceId, MediaServer mediaServerItem, HookSubscribe.Event hookEvent, + SipSubscribe.Event errorEvent, InviteTimeOutCallback timeoutCallback) throws InvalidArgumentException, ParseException, SipException; + + /** + * 语音喊话回复BYE + */ + void stopBroadcast(Platform platform, CommonGBChannel channel, String app, String stream, boolean sendBye, MediaServer mediaServerItem); + + void addSimulatedSubscribeInfo(Platform parentPlatform); + + Platform queryOne(Integer platformId); + + List queryEnablePlatformList(String serverId); + + void delete(Integer platformId, CommonCallback callback); + + List queryAll(String serverId); + +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/service/IPlayService.java b/src/main/java/com/genersoft/iot/vmp/gb28181/service/IPlayService.java new file mode 100755 index 0000000..a1f234f --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/service/IPlayService.java @@ -0,0 +1,85 @@ +package com.genersoft.iot.vmp.gb28181.service; + +import com.genersoft.iot.vmp.common.InviteInfo; +import com.genersoft.iot.vmp.common.InviteSessionType; +import com.genersoft.iot.vmp.common.StreamInfo; +import com.genersoft.iot.vmp.conf.exception.ServiceException; +import com.genersoft.iot.vmp.gb28181.bean.*; +import com.genersoft.iot.vmp.gb28181.controller.bean.AudioBroadcastEvent; +import com.genersoft.iot.vmp.media.bean.MediaInfo; +import com.genersoft.iot.vmp.media.bean.MediaServer; +import com.genersoft.iot.vmp.service.bean.ErrorCallback; +import com.genersoft.iot.vmp.service.bean.SSRCInfo; +import com.genersoft.iot.vmp.vmanager.bean.AudioBroadcastResult; +import gov.nist.javax.sip.message.SIPResponse; + +import javax.sip.InvalidArgumentException; +import javax.sip.SipException; +import javax.sip.header.CallIdHeader; +import java.text.ParseException; + +/** + * 点播处理 + */ +public interface IPlayService { + + SSRCInfo play(MediaServer mediaServerItem, String deviceId, String channelId, String ssrc, ErrorCallback callback); + + void play(Device device, DeviceChannel channel, ErrorCallback callback); + + StreamInfo onPublishHandlerForPlay(MediaServer mediaServerItem, MediaInfo mediaInfo, Device device, DeviceChannel channel); + + MediaServer getNewMediaServerItem(Device device); + + void playBack(Device device, DeviceChannel channel, String startTime, String endTime, ErrorCallback callback); + void zlmServerOffline(MediaServer mediaServer); + + void download(Device device, DeviceChannel channel, String startTime, String endTime, int downloadSpeed, ErrorCallback callback); + + StreamInfo getDownLoadInfo(Device device, DeviceChannel channel, String stream); + + void zlmServerOnline(MediaServer mediaServer); + + AudioBroadcastResult audioBroadcast(String deviceId, String channelDeviceId, Boolean broadcastMode); + + boolean audioBroadcastCmd(Device device, DeviceChannel channel, MediaServer mediaServerItem, String app, String stream, int timeout, boolean isFromPlatform, AudioBroadcastEvent event) throws InvalidArgumentException, ParseException, SipException; + + boolean audioBroadcastInUse(Device device, DeviceChannel channel); + + void stopAudioBroadcast(Device device, DeviceChannel channel); + + void playbackPause(String streamId) throws ServiceException, InvalidArgumentException, ParseException, SipException; + + void playbackResume(String streamId) throws ServiceException, InvalidArgumentException, ParseException, SipException; + + void playbackSeek(String streamId, long seekTime) throws InvalidArgumentException, ParseException, SipException; + + void playbackSpeed(String streamId, double speed) throws InvalidArgumentException, ParseException, SipException; + + void startPushStream(SendRtpInfo sendRtpItem, DeviceChannel channel, SIPResponse sipResponse, Platform platform, CallIdHeader callIdHeader); + + void startSendRtpStreamFailHand(SendRtpInfo sendRtpItem, Platform platform, CallIdHeader callIdHeader); + + void talkCmd(Device device, DeviceChannel channel, MediaServer mediaServerItem, String stream, AudioBroadcastEvent event); + + void stopTalk(Device device, DeviceChannel channel, Boolean streamIsReady); + + void getSnap(String deviceId, String channelId, String fileName, ErrorCallback errorCallback); + + void stop(InviteSessionType type, Device device, DeviceChannel channel, String stream); + + void stop(InviteInfo inviteInfo); + + void play(CommonGBChannel channel, Boolean record, ErrorCallback callback); + + void stopPlay(InviteSessionType inviteSessionType, CommonGBChannel channel); + + void stop(InviteSessionType inviteSessionType, CommonGBChannel channel, String stream); + + void playBack(CommonGBChannel channel, Long startTime, Long stopTime, ErrorCallback callback); + + void download(CommonGBChannel channel, Long startTime, Long stopTime, Integer downloadSpeed, ErrorCallback callback); + + + +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/service/IRegionService.java b/src/main/java/com/genersoft/iot/vmp/gb28181/service/IRegionService.java new file mode 100644 index 0000000..c4878fb --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/service/IRegionService.java @@ -0,0 +1,45 @@ +package com.genersoft.iot.vmp.gb28181.service; + +import com.genersoft.iot.vmp.gb28181.bean.Region; +import com.genersoft.iot.vmp.gb28181.bean.RegionTree; +import com.github.pagehelper.PageInfo; + +import java.util.List; + + +public interface IRegionService { + + void add(Region region); + + boolean deleteByDeviceId(Integer regionDeviceId); + + /** + * 查询区划列表 + */ + PageInfo query(String query, int page, int count); + + /** + * 更新区域 + */ + void update(Region region); + + List getAllChild(String parent); + + Region queryRegionByDeviceId(String regionDeviceId); + + List queryForTree(Integer parent, Boolean hasChannel); + + void syncFromChannel(); + + boolean delete(int id); + + boolean batchAdd(List regionList); + + List getPath(String deviceId); + + String getDescription(String civilCode); + + void addByCivilCode(String civilCode); + + PageInfo queryList(int page, int count, String query); +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/service/ISourceDownloadService.java b/src/main/java/com/genersoft/iot/vmp/gb28181/service/ISourceDownloadService.java new file mode 100644 index 0000000..26c4064 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/service/ISourceDownloadService.java @@ -0,0 +1,15 @@ +package com.genersoft.iot.vmp.gb28181.service; + +import com.genersoft.iot.vmp.common.StreamInfo; +import com.genersoft.iot.vmp.gb28181.bean.CommonGBChannel; +import com.genersoft.iot.vmp.service.bean.ErrorCallback; + +/** + * 资源能力接入-录像下载 + */ +public interface ISourceDownloadService { + + void download(CommonGBChannel channel, Long startTime, Long stopTime, Integer downloadSpeed, ErrorCallback callback); + + void stopDownload(CommonGBChannel channel, String stream); +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/service/ISourcePTZService.java b/src/main/java/com/genersoft/iot/vmp/gb28181/service/ISourcePTZService.java new file mode 100644 index 0000000..9679fb5 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/service/ISourcePTZService.java @@ -0,0 +1,28 @@ +package com.genersoft.iot.vmp.gb28181.service; + +import com.genersoft.iot.vmp.gb28181.bean.*; +import com.genersoft.iot.vmp.service.bean.ErrorCallback; + +import java.util.List; + +/** + * 资源能力接入-云台控制 + */ +public interface ISourcePTZService { + + void ptz(CommonGBChannel channel, FrontEndControlCodeForPTZ frontEndControlCode, ErrorCallback callback); + + void preset(CommonGBChannel channel, FrontEndControlCodeForPreset frontEndControlCode, ErrorCallback callback); + + void fi(CommonGBChannel channel, FrontEndControlCodeForFI frontEndControlCode, ErrorCallback callback); + + void tour(CommonGBChannel channel, FrontEndControlCodeForTour frontEndControlCode, ErrorCallback callback); + + void scan(CommonGBChannel channel, FrontEndControlCodeForScan frontEndControlCode, ErrorCallback callback); + + void auxiliary(CommonGBChannel channel, FrontEndControlCodeForAuxiliary frontEndControlCode, ErrorCallback callback); + + void wiper(CommonGBChannel channel, FrontEndControlCodeForWiper frontEndControlCode, ErrorCallback callback); + + void queryPreset(CommonGBChannel channel, ErrorCallback> callback); +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/service/ISourcePlayService.java b/src/main/java/com/genersoft/iot/vmp/gb28181/service/ISourcePlayService.java new file mode 100644 index 0000000..5c125b7 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/service/ISourcePlayService.java @@ -0,0 +1,17 @@ +package com.genersoft.iot.vmp.gb28181.service; + +import com.genersoft.iot.vmp.common.StreamInfo; +import com.genersoft.iot.vmp.gb28181.bean.CommonGBChannel; +import com.genersoft.iot.vmp.gb28181.bean.Platform; +import com.genersoft.iot.vmp.service.bean.ErrorCallback; + +/** + * 资源能力接入-实时录像 + */ +public interface ISourcePlayService { + + void play(CommonGBChannel channel, Platform platform, Boolean record, ErrorCallback callback); + + void stopPlay(CommonGBChannel channel); + +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/service/ISourcePlaybackService.java b/src/main/java/com/genersoft/iot/vmp/gb28181/service/ISourcePlaybackService.java new file mode 100644 index 0000000..b271142 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/service/ISourcePlaybackService.java @@ -0,0 +1,28 @@ +package com.genersoft.iot.vmp.gb28181.service; + +import com.genersoft.iot.vmp.common.StreamInfo; +import com.genersoft.iot.vmp.gb28181.bean.CommonGBChannel; +import com.genersoft.iot.vmp.gb28181.bean.CommonRecordInfo; +import com.genersoft.iot.vmp.service.bean.ErrorCallback; + +import java.util.List; + +/** + * 资源能力接入-录像回放 + */ +public interface ISourcePlaybackService { + + void playback(CommonGBChannel channel, Long startTime, Long stopTime, ErrorCallback callback); + + void stopPlayback(CommonGBChannel channel, String stream); + + void playbackPause(CommonGBChannel channel, String stream); + + void playbackResume(CommonGBChannel channel, String stream); + + void playbackSeek(CommonGBChannel channel, String stream, long seekTime); + + void playbackSpeed(CommonGBChannel channel, String stream, Double speed); + + void queryRecord(CommonGBChannel channel, String startTime, String endTime, ErrorCallback> callback); +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/service/impl/DeviceAlarmServiceImpl.java b/src/main/java/com/genersoft/iot/vmp/gb28181/service/impl/DeviceAlarmServiceImpl.java new file mode 100755 index 0000000..7f73ddf --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/service/impl/DeviceAlarmServiceImpl.java @@ -0,0 +1,35 @@ +package com.genersoft.iot.vmp.gb28181.service.impl; + +import com.genersoft.iot.vmp.gb28181.bean.DeviceAlarm; +import com.genersoft.iot.vmp.gb28181.dao.DeviceAlarmMapper; +import com.genersoft.iot.vmp.gb28181.service.IDeviceAlarmService; +import com.github.pagehelper.PageHelper; +import com.github.pagehelper.PageInfo; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +public class DeviceAlarmServiceImpl implements IDeviceAlarmService { + + @Autowired + private DeviceAlarmMapper deviceAlarmMapper; + + @Override + public PageInfo getAllAlarm(int page, int count, String deviceId, String channelId, String alarmPriority, String alarmMethod, String alarmType, String startTime, String endTime) { + PageHelper.startPage(page, count); + List all = deviceAlarmMapper.query(deviceId, channelId, alarmPriority, alarmMethod, alarmType, startTime, endTime); + return new PageInfo<>(all); + } + + @Override + public void add(DeviceAlarm deviceAlarm) { + deviceAlarmMapper.add(deviceAlarm); + } + + @Override + public int clearAlarmBeforeTime(Integer id, List deviceIdList, String time) { + return deviceAlarmMapper.clearAlarmBeforeTime(id, deviceIdList, time); + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/service/impl/DeviceChannelServiceImpl.java b/src/main/java/com/genersoft/iot/vmp/gb28181/service/impl/DeviceChannelServiceImpl.java new file mode 100755 index 0000000..73e316d --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/service/impl/DeviceChannelServiceImpl.java @@ -0,0 +1,685 @@ +package com.genersoft.iot.vmp.gb28181.service.impl; + +import com.genersoft.iot.vmp.common.InviteInfo; +import com.genersoft.iot.vmp.common.InviteSessionType; +import com.genersoft.iot.vmp.common.enums.ChannelDataType; +import com.genersoft.iot.vmp.common.enums.DeviceControlType; +import com.genersoft.iot.vmp.conf.UserSetting; +import com.genersoft.iot.vmp.conf.exception.ControllerException; +import com.genersoft.iot.vmp.gb28181.bean.*; +import com.genersoft.iot.vmp.gb28181.dao.DeviceChannelMapper; +import com.genersoft.iot.vmp.gb28181.dao.DeviceMapper; +import com.genersoft.iot.vmp.gb28181.dao.DeviceMobilePositionMapper; +import com.genersoft.iot.vmp.gb28181.dao.PlatformChannelMapper; +import com.genersoft.iot.vmp.gb28181.event.EventPublisher; +import com.genersoft.iot.vmp.gb28181.event.record.RecordInfoEndEvent; +import com.genersoft.iot.vmp.gb28181.event.subscribe.catalog.CatalogEvent; +import com.genersoft.iot.vmp.gb28181.service.IDeviceChannelService; +import com.genersoft.iot.vmp.gb28181.service.IInviteStreamService; +import com.genersoft.iot.vmp.gb28181.service.IPlatformChannelService; +import com.genersoft.iot.vmp.gb28181.transmit.cmd.ISIPCommander; +import com.genersoft.iot.vmp.gb28181.utils.SipUtils; +import com.genersoft.iot.vmp.service.bean.ErrorCallback; +import com.genersoft.iot.vmp.service.redisMsg.IRedisRpcPlayService; +import com.genersoft.iot.vmp.storager.IRedisCatchStorage; +import com.genersoft.iot.vmp.utils.Coordtransform; +import com.genersoft.iot.vmp.utils.DateUtil; +import com.genersoft.iot.vmp.vmanager.bean.ErrorCode; +import com.genersoft.iot.vmp.vmanager.bean.ResourceBaseInfo; +import com.genersoft.iot.vmp.web.gb28181.dto.DeviceChannelExtend; +import com.github.pagehelper.PageHelper; +import com.github.pagehelper.PageInfo; +import jakarta.validation.constraints.NotNull; +import lombok.extern.slf4j.Slf4j; +import org.dom4j.Element; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.Assert; +import org.springframework.util.CollectionUtils; +import org.springframework.util.ObjectUtils; + +import javax.sip.InvalidArgumentException; +import javax.sip.SipException; +import javax.sip.message.Response; +import java.text.ParseException; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.SynchronousQueue; +import java.util.concurrent.TimeUnit; + +import static com.genersoft.iot.vmp.gb28181.utils.XmlUtil.getText; + +/** + * @author lin + */ +@Slf4j +@Service +public class DeviceChannelServiceImpl implements IDeviceChannelService { + + @Autowired + private EventPublisher eventPublisher; + + @Autowired + private IInviteStreamService inviteStreamService; + + @Autowired + private DeviceChannelMapper channelMapper; + + @Autowired + private PlatformChannelMapper platformChannelMapper; + + @Autowired + private DeviceMapper deviceMapper; + + @Autowired + private DeviceMobilePositionMapper deviceMobilePositionMapper; + + @Autowired + private UserSetting userSetting; + + @Autowired + private IRedisCatchStorage redisCatchStorage; + + @Autowired + private IPlatformChannelService platformChannelService; + + @Autowired + private IRedisRpcPlayService redisRpcPlayService; + + @Autowired + private ISIPCommander commander; + + // 记录录像查询的结果等待 + private final Map> topicSubscribers = new ConcurrentHashMap<>(); + + /** + * 监听录像查询结束事件 + */ + @Async("taskExecutor") + @org.springframework.context.event.EventListener + public void onApplicationEvent(RecordInfoEndEvent event) { + SynchronousQueue queue = topicSubscribers.get("record" + event.getRecordInfo().getSn()); + if (queue != null) { + queue.offer(event.getRecordInfo()); + } + } + + @Autowired + private ISIPCommander cmder; + + + @Override + public int updateChannels(Device device, List channels) { + if (CollectionUtils.isEmpty(channels)) { + return 0; + } + List addChannels = new ArrayList<>(); + List updateChannels = new ArrayList<>(); + HashMap channelsInStore = new HashMap<>(); + int result = 0; + List channelList = channelMapper.queryChannelsByDeviceDbId(device.getId()); + if (channelList.isEmpty()) { + for (DeviceChannel channel : channels) { + channel.setDataDeviceId(device.getId()); + InviteInfo inviteInfo = inviteStreamService.getInviteInfoByDeviceAndChannel(InviteSessionType.PLAY, channel.getId()); + if (inviteInfo != null && inviteInfo.getStreamInfo() != null) { + channel.setStreamId(inviteInfo.getStreamInfo().getStream()); + } + String now = DateUtil.getNow(); + channel.setUpdateTime(now); + channel.setCreateTime(now); + addChannels.add(channel); + } + }else { + for (DeviceChannel deviceChannel : channelList) { + channelsInStore.put(deviceChannel.getDataDeviceId() + deviceChannel.getDeviceId(), deviceChannel); + } + for (DeviceChannel channel : channels) { + InviteInfo inviteInfo = inviteStreamService.getInviteInfoByDeviceAndChannel(InviteSessionType.PLAY, channel.getId()); + if (inviteInfo != null && inviteInfo.getStreamInfo() != null) { + channel.setStreamId(inviteInfo.getStreamInfo().getStream()); + } + String now = DateUtil.getNow(); + channel.setUpdateTime(now); + DeviceChannel deviceChannelInDb = channelsInStore.get(channel.getDataDeviceId() + channel.getDeviceId()); + if ( deviceChannelInDb != null) { + channel.setId(deviceChannelInDb.getId()); + channel.setUpdateTime(now); + updateChannels.add(channel); + }else { + channel.setCreateTime(now); + channel.setUpdateTime(now); + addChannels.add(channel); + } + } + } + Set channelSet = new HashSet<>(); + // 滤重 + List addChannelList = new ArrayList<>(); + List updateChannelList = new ArrayList<>(); + addChannels.forEach(channel -> { + if (channelSet.add(channel.getDeviceId())) { + addChannelList.add(channel); + } + }); + channelSet.clear(); + updateChannels.forEach(channel -> { + if (channelSet.add(channel.getDeviceId())) { + updateChannelList.add(channel); + } + }); + + int limitCount = 500; + if (!addChannelList.isEmpty()) { + if (addChannelList.size() > limitCount) { + for (int i = 0; i < addChannelList.size(); i += limitCount) { + int toIndex = i + limitCount; + if (i + limitCount > addChannelList.size()) { + toIndex = addChannelList.size(); + } + result += channelMapper.batchAdd(addChannelList.subList(i, toIndex)); + } + }else { + result += channelMapper.batchAdd(addChannelList); + } + } + if (!updateChannelList.isEmpty()) { + if (updateChannelList.size() > limitCount) { + for (int i = 0; i < updateChannelList.size(); i += limitCount) { + int toIndex = i + limitCount; + if (i + limitCount > updateChannelList.size()) { + toIndex = updateChannelList.size(); + } + result += channelMapper.batchUpdate(updateChannelList.subList(i, toIndex)); + } + }else { + result += channelMapper.batchUpdate(updateChannelList); + } + } + return result; + } + + @Override + public ResourceBaseInfo getOverview() { + int online = channelMapper.getOnlineCount(); + int total = channelMapper.getAllChannelCount(); + return new ResourceBaseInfo(total, online); + } + + @Override + public void online(DeviceChannel channel) { + channelMapper.online(channel.getId()); + } + + @Override + public void offline(DeviceChannel channel) { + channelMapper.offline(channel.getId()); + } + + @Override + public void deleteForNotify(DeviceChannel channel) { + channelMapper.deleteForNotify(channel); + } + + @Override + public DeviceChannel getOne(String deviceId, String channelId){ + Device device = deviceMapper.getDeviceByDeviceId(deviceId); + if (device == null) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "未找到设备:" + deviceId); + } + return channelMapper.getOneByDeviceId(device.getId(), channelId); + } + + @Override + public DeviceChannel getOneForSource(String deviceId, String channelId){ + Device device = deviceMapper.getDeviceByDeviceId(deviceId); + if (device == null) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "未找到设备:" + deviceId); + } + return channelMapper.getOneByDeviceIdForSource(device.getId(), channelId); + } + + @Override + public DeviceChannel getOneForSource(int deviceDbId, String channelId) { + return channelMapper.getOneByDeviceIdForSource(deviceDbId, channelId); + } + + @Override + public DeviceChannel getOneBySourceId(int deviceDbId, String channelId) { + return channelMapper.getOneBySourceChannelId(deviceDbId, channelId); + } + + @Override + public void updateChannelStreamIdentification(DeviceChannel channel) { + Assert.hasLength(channel.getStreamIdentification(), "码流标识必须存在"); + if (ObjectUtils.isEmpty(channel.getStreamIdentification())) { + log.info("[重置通道码流类型] 设备: {}, 码流: {}", channel.getDeviceId(), channel.getStreamIdentification()); + }else { + log.info("[更新通道码流类型] 设备: {}, 通道:{}, 码流: {}", channel.getDeviceId(), channel.getDeviceId(), + channel.getStreamIdentification()); + } + if (channel.getId() > 0) { + channelMapper.updateChannelStreamIdentification(channel); + }else { + channelMapper.updateAllChannelStreamIdentification(channel.getStreamIdentification()); + } + } + + @Override + public List queryChaneListByDeviceId(String deviceId) { + Device device = deviceMapper.getDeviceByDeviceId(deviceId); + if (device == null) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "未找到通道:" + deviceId); + } + return channelMapper.queryChannelsByDeviceDbId(device.getId()); + } + + @Override + public List queryChaneIdListByDeviceDbIds(List deviceDbIds) { + return channelMapper.queryChaneIdListByDeviceDbIds(deviceDbIds); + } + + @Override + public void handlePtzCmd(@NotNull Integer dataDeviceId, @NotNull Integer gbId, Element rootElement, DeviceControlType type, ErrorCallback callback) { + + // 根据通道ID,获取所属设备 + Device device = deviceMapper.query(dataDeviceId); + if (device == null) { + // 不存在则回复404 + log.warn("[INFO 消息] 通道所属设备不存在, 设备ID: {}", dataDeviceId); + callback.run(Response.NOT_FOUND, "device not found", null); + return; + } + + DeviceChannel deviceChannel = channelMapper.getOneForSource(gbId); + if (deviceChannel == null) { + log.warn("[deviceControl] 未找到设备原始通道, 设备: {}({}),通道编号:{}", device.getName(), + device.getDeviceId(), gbId); + callback.run(Response.NOT_FOUND, "channel not found", null); + return; + } + log.info("[deviceControl] 命令: {}, 设备: {}({}), 通道{}({}", type, device.getName(), device.getDeviceId(), + deviceChannel.getName(), deviceChannel.getDeviceId()); + String cmdString = getText(rootElement, type.getVal()); + try { + cmder.fronEndCmd(device, deviceChannel.getDeviceId(), cmdString, errorResult->{ + callback.run(errorResult.statusCode, errorResult.msg, null); + }, errorResult->{ + callback.run(errorResult.statusCode, errorResult.msg, null); + }); + } catch (InvalidArgumentException | SipException | ParseException e) { + log.error("[命令发送失败] 云台/前端: {}", e.getMessage()); + } + } + + @Override + public void updateChannelGPS(Device device, DeviceChannel deviceChannel, MobilePosition mobilePosition) { + + if (device.getGeoCoordSys().equalsIgnoreCase("GCJ02")) { + Double[] wgs84Position = Coordtransform.GCJ02ToWGS84(mobilePosition.getLongitude(), mobilePosition.getLatitude()); + mobilePosition.setLongitude(wgs84Position[0]); + mobilePosition.setLatitude(wgs84Position[1]); + + Double[] wgs84PositionForChannel = Coordtransform.GCJ02ToWGS84(deviceChannel.getLongitude(), deviceChannel.getLatitude()); + deviceChannel.setGbLongitude(wgs84PositionForChannel[0]); + deviceChannel.setGbLatitude(wgs84PositionForChannel[1]); + } + + if (userSetting.getSavePositionHistory()) { + deviceMobilePositionMapper.insertNewPosition(mobilePosition); + } + + if (deviceChannel.getDeviceId().equals(device.getDeviceId())) { + deviceChannel.setDeviceId(null); + } + if (deviceChannel.getGpsTime() == null) { + deviceChannel.setGpsTime(DateUtil.getNow()); + } + + int updated = channelMapper.updatePosition(deviceChannel); + if (updated == 0) { + return; + } + + List deviceChannels = new ArrayList<>(); + if (deviceChannel.getDeviceId() == null) { + // 有的设备这里上报的deviceId与通道Id是一样,这种情况更新设备下的全部通道 + List deviceChannelsInDb = queryChaneListByDeviceId(device.getDeviceId()); + deviceChannels.addAll(deviceChannelsInDb); + }else { + deviceChannels.add(deviceChannel); + } + if (deviceChannels.isEmpty()) { + return; + } + if (deviceChannels.size() > 100) { + log.warn("[更新通道位置信息后发送通知] 设备可能是平台,上报的位置信息未标明通道编号," + + "导致所有通道被更新位置, deviceId:{}", device.getDeviceId()); + } + for (DeviceChannel channel : deviceChannels) { + // 向关联了该通道并且开启移动位置订阅的上级平台发送移动位置订阅消息 + mobilePosition.setChannelId(channel.getId()); + mobilePosition.setChannelDeviceId(channel.getDeviceId()); + try { + eventPublisher.mobilePositionEventPublish(mobilePosition); + }catch (Exception e) { + log.error("[向上级转发移动位置失败] ", e); + } + } + } + + @Override + public void startPlay(Integer channelId, String stream) { + channelMapper.startPlay(channelId, stream); + } + + @Override + public void stopPlay(Integer channelId) { + channelMapper.stopPlayById(channelId); + } + + @Override + public void cleanChannelsForDevice(int deviceId) { + channelMapper.cleanChannelsByDeviceId(deviceId); + } + + @Override + @Transactional + public boolean resetChannels(int deviceDbId, List deviceChannelList) { + if (CollectionUtils.isEmpty(deviceChannelList)) { + return false; + } + List allChannels = channelMapper.queryAllChannelsForRefresh(deviceDbId); + Map allChannelMap = new HashMap<>(); + if (!allChannels.isEmpty()) { + for (DeviceChannel deviceChannel : allChannels) { + allChannelMap.put(deviceChannel.getDataDeviceId() + deviceChannel.getDeviceId(), deviceChannel); + } + } + // 数据去重 + List channels = new ArrayList<>(); + + List updateChannels = new ArrayList<>(); + List addChannels = new ArrayList<>(); + List deleteChannels = new ArrayList<>(); + StringBuilder stringBuilder = new StringBuilder(); + Map subContMap = new HashMap<>(); + + for (DeviceChannel deviceChannel : deviceChannelList) { + DeviceChannel channelInDb = allChannelMap.get(deviceChannel.getDataDeviceId() + deviceChannel.getDeviceId()); + if (channelInDb != null) { + deviceChannel.setStreamId(channelInDb.getStreamId()); + deviceChannel.setHasAudio(channelInDb.isHasAudio()); + deviceChannel.setId(channelInDb.getId()); + if (channelInDb.getStatus() != null && !channelInDb.getStatus().equalsIgnoreCase(deviceChannel.getStatus())){ + List platformList = platformChannelMapper.queryParentPlatformByChannelId(deviceChannel.getDeviceId()); + if (!CollectionUtils.isEmpty(platformList)){ + platformList.forEach(platform->{ + eventPublisher.catalogEventPublish(platform, deviceChannel.buildCommonGBChannelForStatus(), deviceChannel.getStatus().equals("ON")? CatalogEvent.ON:CatalogEvent.OFF); + }); + } + } + deviceChannel.setUpdateTime(DateUtil.getNow()); + updateChannels.add(deviceChannel); + }else { + deviceChannel.setCreateTime(DateUtil.getNow()); + deviceChannel.setUpdateTime(DateUtil.getNow()); + addChannels.add(deviceChannel); + } + allChannelMap.remove(deviceChannel.getDataDeviceId() + deviceChannel.getDeviceId()); + channels.add(deviceChannel); + if (!ObjectUtils.isEmpty(deviceChannel.getParentId())) { + if (subContMap.get(deviceChannel.getParentId()) == null) { + subContMap.put(deviceChannel.getParentId(), 1); + }else { + Integer count = subContMap.get(deviceChannel.getParentId()); + subContMap.put(deviceChannel.getParentId(), count++); + } + } + } + deleteChannels.addAll(allChannelMap.values()); + if (!channels.isEmpty()) { + for (DeviceChannel channel : channels) { + if (subContMap.get(channel.getDeviceId()) != null){ + Integer count = subContMap.get(channel.getDeviceId()); + if (count > 0) { + channel.setSubCount(count); + channel.setParental(1); + } + } + } + } + + if (stringBuilder.length() > 0) { + log.info("[目录查询]收到的数据存在重复: {}" , stringBuilder); + } + if(CollectionUtils.isEmpty(channels)){ + log.info("通道重设,数据为空={}" , deviceChannelList); + return false; + } + int limitCount = 500; + if (!addChannels.isEmpty()) { + if (addChannels.size() > limitCount) { + for (int i = 0; i < addChannels.size(); i += limitCount) { + int toIndex = i + limitCount; + if (i + limitCount > addChannels.size()) { + toIndex = addChannels.size(); + } + channelMapper.batchAdd(addChannels.subList(i, toIndex)); + } + }else { + channelMapper.batchAdd(addChannels); + } + } + if (!updateChannels.isEmpty()) { + if (updateChannels.size() > limitCount) { + for (int i = 0; i < updateChannels.size(); i += limitCount) { + int toIndex = i + limitCount; + if (i + limitCount > updateChannels.size()) { + toIndex = updateChannels.size(); + } + channelMapper.batchUpdate(updateChannels.subList(i, toIndex)); + } + }else { + channelMapper.batchUpdate(updateChannels); + } + // 不对收到的通道做比较,已确定是否真的发生变化,所以不发送更新通知 + + } + if (!deleteChannels.isEmpty()) { + try { + // 这些通道可能关联了,上级平台需要删除同时发送消息 + List ids = new ArrayList<>(); + deleteChannels.stream().forEach(deviceChannel -> { + ids.add(deviceChannel.getId()); + }); + platformChannelService.removeChannels(ids); + }catch (Exception e) { + log.error("[移除通道国标级联共享失败]", e); + } + if (deleteChannels.size() > limitCount) { + for (int i = 0; i < deleteChannels.size(); i += limitCount) { + int toIndex = i + limitCount; + if (i + limitCount > deleteChannels.size()) { + toIndex = deleteChannels.size(); + } + channelMapper.batchDel(deleteChannels.subList(i, toIndex)); + } + }else { + channelMapper.batchDel(deleteChannels); + } + } + return true; + + } + + @Override + public PageInfo getSubChannels(int deviceDbId, String channelId, String query, Boolean channelType, Boolean online, int page, int count) { + PageHelper.startPage(page, count); + String civilCode = null; + String parentId = null; + String businessGroupId = null; + if (channelId.length() <= 8) { + civilCode = channelId; + }else { + GbCode decode = GbCode.decode(channelId); + if (Integer.parseInt(decode.getTypeCode()) == 215) { + businessGroupId = channelId; + }else { + parentId = channelId; + } + } + if (query != null) { + query = query.replaceAll("/", "//") + .replaceAll("%", "/%") + .replaceAll("_", "/_"); + } + List all = channelMapper.queryChannels(deviceDbId, civilCode, businessGroupId, parentId, query, false, channelType, online, null, null); + return new PageInfo<>(all); + } + + @Override + public List queryChannelExtendsByDeviceId(String deviceId, List channelIds, Boolean online) { + return channelMapper.queryChannelsWithDeviceInfo(deviceId, null,null, null, online,channelIds); + } + + @Override + public PageInfo queryChannelsByDeviceId(String deviceId, String query, Boolean hasSubChannel, Boolean online, int page, int count) { + Device device = deviceMapper.getDeviceByDeviceId(deviceId); + if (device == null) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "未找到设备:" + deviceId); + } + if (query != null) { + query = query.replaceAll("/", "//") + .replaceAll("%", "/%") + .replaceAll("_", "/_"); + } + PageHelper.startPage(page, count); + List all = channelMapper.queryChannels(device.getId(), null, null, null, query, false, hasSubChannel, online, null, null); + return new PageInfo<>(all); + } + + @Override + public PageInfo queryChannels(String query, Boolean queryParent, Boolean hasSubChannel, Boolean online, Boolean hasStream, int page, int count) { + PageHelper.startPage(page, count); + if (query != null) { + query = query.replaceAll("/", "//") + .replaceAll("%", "/%") + .replaceAll("_", "/_"); + } + List all = channelMapper.queryChannels(null, null, null, null, query, queryParent, hasSubChannel, online, null, hasStream); + return new PageInfo<>(all); + } + + @Override + public List queryDeviceWithAsMessageChannel() { + return deviceMapper.queryDeviceWithAsMessageChannel(); + } + + @Override + public DeviceChannel getRawChannel(int id) { + return deviceMapper.getRawChannel(id); + } + + @Override + public DeviceChannel getOneById(Integer channelId) { + return channelMapper.getOne(channelId); + } + + @Override + public DeviceChannel getOneForSourceById(Integer channelId) { + return channelMapper.getOneForSource(channelId); + } + + @Override + public DeviceChannel getBroadcastChannel(int deviceDbId) { + List channels = channelMapper.queryChannelsByDeviceDbId(deviceDbId); + if (channels.size() == 1) { + return channels.get(0); + } + for (DeviceChannel channel : channels) { + // 获取137类型的 + if (SipUtils.isFrontEnd(channel.getDeviceId())) { + return channel; + } + } + return null; + } + + @Override + public void changeAudio(Integer channelId, Boolean audio) { + channelMapper.changeAudio(channelId, audio); + } + + @Override + public void updateChannelStatusForNotify(DeviceChannel channel) { + channelMapper.updateStatus(channel); + } + + @Override + public void addChannel(DeviceChannel channel) { + channel.setDataType(ChannelDataType.GB28181); + channel.setDataDeviceId(channel.getDataDeviceId()); + channelMapper.add(channel); + } + + @Override + public void updateChannelForNotify(DeviceChannel channel) { + channelMapper.updateChannelForNotify(channel); + } + + @Override + public void queryRecordInfo(Device device, DeviceChannel channel, String startTime, String endTime, ErrorCallback callback) { + if (!userSetting.getServerId().equals(device.getServerId())){ + redisRpcPlayService.queryRecordInfo(device.getServerId(), channel.getId(), startTime, endTime, callback); + return; + } + try { + int sn = (int)((Math.random()*9+1)*100000); + commander.recordInfoQuery(device, channel.getDeviceId(), startTime, endTime, sn, null, null, eventResult -> { + try { + // 消息发送成功, 监听等待数据到来 + SynchronousQueue queue = new SynchronousQueue<>(); + topicSubscribers.put("record" + sn, queue); + RecordInfo recordInfo = queue.poll(userSetting.getRecordInfoTimeout(), TimeUnit.MILLISECONDS); + if (recordInfo != null) { + callback.run(ErrorCode.SUCCESS.getCode(), ErrorCode.SUCCESS.getMsg(), recordInfo); + }else { + callback.run(ErrorCode.ERROR100.getCode(), ErrorCode.ERROR100.getMsg(), recordInfo); + } + } catch (InterruptedException e) { + callback.run(ErrorCode.ERROR100.getCode(), e.getMessage(), null); + } finally { + this.topicSubscribers.remove("record" + sn); + } + + }, (eventResult -> { + callback.run(ErrorCode.ERROR100.getCode(), "查询录像失败, status: " + eventResult.statusCode + ", message: " + eventResult.msg, null); + })); + } catch (InvalidArgumentException | SipException | ParseException e) { + log.error("[命令发送失败] 查询录像: {}", e.getMessage()); + throw new ControllerException(ErrorCode.ERROR100.getCode(), "命令发送失败: " + e.getMessage()); + } + } + + @Override + public void queryRecordInfo(CommonGBChannel channel, String startTime, String endTime, ErrorCallback callback) { + if (channel.getDataType() != ChannelDataType.GB28181){ + // 只支持国标的语音喊话 + log.warn("[INFO 消息] 非国标设备, 通道ID: {}", channel.getGbId()); + callback.run(ErrorCode.ERROR100.getCode(), "非国标设备", null); + return; + } + Device device = deviceMapper.query(channel.getDataDeviceId()); + if (device == null) { + log.warn("[点播] 未找到通道{}的设备信息", channel); + callback.run(ErrorCode.ERROR100.getCode(), "设备不存在", null); + return; + } + DeviceChannel deviceChannel = getOneForSourceById(channel.getGbId()); + queryRecordInfo(device, deviceChannel, startTime, endTime, callback); + + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/service/impl/DeviceServiceImpl.java b/src/main/java/com/genersoft/iot/vmp/gb28181/service/impl/DeviceServiceImpl.java new file mode 100755 index 0000000..f20d9a2 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/service/impl/DeviceServiceImpl.java @@ -0,0 +1,1280 @@ +package com.genersoft.iot.vmp.gb28181.service.impl; + +import com.alibaba.fastjson2.JSON; +import com.genersoft.iot.vmp.common.CommonCallback; +import com.genersoft.iot.vmp.common.enums.ChannelDataType; +import com.genersoft.iot.vmp.conf.UserSetting; +import com.genersoft.iot.vmp.conf.exception.ControllerException; +import com.genersoft.iot.vmp.gb28181.bean.*; +import com.genersoft.iot.vmp.gb28181.dao.CommonGBChannelMapper; +import com.genersoft.iot.vmp.gb28181.dao.DeviceChannelMapper; +import com.genersoft.iot.vmp.gb28181.dao.DeviceMapper; +import com.genersoft.iot.vmp.gb28181.dao.PlatformChannelMapper; +import com.genersoft.iot.vmp.gb28181.event.EventPublisher; +import com.genersoft.iot.vmp.gb28181.event.channel.ChannelEvent; +import com.genersoft.iot.vmp.gb28181.service.IDeviceService; +import com.genersoft.iot.vmp.gb28181.service.IInviteStreamService; +import com.genersoft.iot.vmp.gb28181.session.AudioBroadcastManager; +import com.genersoft.iot.vmp.gb28181.session.SipInviteSessionManager; +import com.genersoft.iot.vmp.gb28181.task.deviceStatus.DeviceStatusTask; +import com.genersoft.iot.vmp.gb28181.task.deviceStatus.DeviceStatusTaskInfo; +import com.genersoft.iot.vmp.gb28181.task.deviceStatus.DeviceStatusTaskRunner; +import com.genersoft.iot.vmp.gb28181.task.deviceSubscribe.SubscribeTask; +import com.genersoft.iot.vmp.gb28181.task.deviceSubscribe.SubscribeTaskInfo; +import com.genersoft.iot.vmp.gb28181.task.deviceSubscribe.SubscribeTaskRunner; +import com.genersoft.iot.vmp.gb28181.task.deviceSubscribe.impl.SubscribeTaskForCatalog; +import com.genersoft.iot.vmp.gb28181.task.deviceSubscribe.impl.SubscribeTaskForMobilPosition; +import com.genersoft.iot.vmp.gb28181.transmit.cmd.ISIPCommander; +import com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.response.cmd.CatalogResponseMessageHandler; +import com.genersoft.iot.vmp.media.bean.MediaServer; +import com.genersoft.iot.vmp.media.service.IMediaServerService; +import com.genersoft.iot.vmp.service.ISendRtpServerService; +import com.genersoft.iot.vmp.service.bean.ErrorCallback; +import com.genersoft.iot.vmp.service.redisMsg.IRedisRpcService; +import com.genersoft.iot.vmp.storager.IRedisCatchStorage; +import com.genersoft.iot.vmp.utils.DateUtil; +import com.genersoft.iot.vmp.vmanager.bean.ErrorCode; +import com.genersoft.iot.vmp.vmanager.bean.ResourceBaseInfo; +import com.genersoft.iot.vmp.vmanager.bean.WVPResult; +import com.github.pagehelper.PageHelper; +import com.github.pagehelper.PageInfo; +import gov.nist.javax.sip.message.SIPResponse; +import jakarta.validation.constraints.NotNull; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.CommandLineRunner; +import org.springframework.core.annotation.Order; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.Assert; + +import javax.sip.InvalidArgumentException; +import javax.sip.ResponseEvent; +import javax.sip.SipException; +import java.text.ParseException; +import java.time.Instant; +import java.util.*; +import java.util.concurrent.SynchronousQueue; +import java.util.concurrent.TimeUnit; + +/** + * 设备业务(目录订阅) + */ +@Slf4j +@Service +@Order(value=16) +public class DeviceServiceImpl implements IDeviceService, CommandLineRunner { + + @Autowired + private ISIPCommander sipCommander; + + @Autowired + private CatalogResponseMessageHandler catalogResponseMessageHandler; + + @Autowired + private IRedisCatchStorage redisCatchStorage; + + @Autowired + private IInviteStreamService inviteStreamService; + + @Autowired + private DeviceMapper deviceMapper; + + @Autowired + private PlatformChannelMapper platformChannelMapper; + + @Autowired + private DeviceChannelMapper deviceChannelMapper; + + @Autowired + private CommonGBChannelMapper commonGBChannelMapper; + + @Autowired + private EventPublisher eventPublisher; + + @Autowired + private ISendRtpServerService sendRtpServerService; + + @Autowired + private UserSetting userSetting; + + @Autowired + private ISIPCommander commander; + + @Autowired + private SipInviteSessionManager sessionManager; + + @Autowired + private IMediaServerService mediaServerService; + + @Autowired + private AudioBroadcastManager audioBroadcastManager; + + @Autowired + private IRedisRpcService redisRpcService; + + @Autowired + private SubscribeTaskRunner subscribeTaskRunner; + + @Autowired + private DeviceStatusTaskRunner deviceStatusTaskRunner; + + private Device getDeviceByDeviceIdFromDb(String deviceId) { + return deviceMapper.getDeviceByDeviceId(deviceId); + } + + @Override + public void run(String... args) throws Exception { + + // 清理数据库不存在但是redis中存在的数据 + List devicesInDb = getAll(); + if (devicesInDb.isEmpty()) { + redisCatchStorage.removeAllDevice(); + }else { + List devicesInRedis = redisCatchStorage.getAllDevices(); + if (!devicesInRedis.isEmpty()) { + Map deviceMapInDb = new HashMap<>(); + devicesInDb.parallelStream().forEach(device -> { + deviceMapInDb.put(device.getDeviceId(), device); + }); + devicesInRedis.parallelStream().forEach(device -> { + if (deviceMapInDb.get(device.getDeviceId()) == null + && userSetting.getServerId().equals(device.getServerId())) { + redisCatchStorage.removeDevice(device.getDeviceId()); + } + }); + } + } + + // 重置cseq计数 + redisCatchStorage.resetAllCSEQ(); + // 处理设备状态 + List allTaskInfo = deviceStatusTaskRunner.getAllTaskInfo(); + List onlineDeviceIds = new ArrayList<>(); + if (!allTaskInfo.isEmpty()) { + for (DeviceStatusTaskInfo taskInfo : allTaskInfo) { + Device device = getDeviceByDeviceId(taskInfo.getDeviceId()); + if (device == null) { + deviceStatusTaskRunner.removeTask(taskInfo.getDeviceId()); + continue; + } + // 恢复定时任务, TCP因为连接已经断开必须等待设备重新连接 + DeviceStatusTask deviceStatusTask = DeviceStatusTask.getInstance(taskInfo.getDeviceId(), + taskInfo.getTransactionInfo(), taskInfo.getExpireTime() + 1000 + System.currentTimeMillis(), this::deviceStatusExpire); + deviceStatusTaskRunner.addTask(deviceStatusTask); + onlineDeviceIds.add(taskInfo.getDeviceId()); + } + // 除了记录的设备以外, 其他设备全部离线 + List onlineDevice = getAllOnlineDevice(userSetting.getServerId()); + if (!onlineDevice.isEmpty()) { + List offlineDevices = new ArrayList<>(); + for (Device device : onlineDevice) { + if (!onlineDeviceIds.contains(device.getDeviceId())) { + // 此设备需要离线 + device.setOnLine(false); + // 清理离线设备的相关缓存 + cleanOfflineDevice(device); + // 更新数据库 + offlineDevices.add(device); + } + } + if (!offlineDevices.isEmpty()) { + offlineByIds(offlineDevices); + } + } + }else { + // 所有设备全部离线 + List onlineDevice = getAllOnlineDevice(userSetting.getServerId()); + for (Device device : onlineDevice) { + // 此设备需要离线 + device.setOnLine(false); + // 清理离线设备的相关缓存 + cleanOfflineDevice(device); + } + offlineByIds(onlineDevice); + } + + // 处理订阅任务 + List taskInfoList = subscribeTaskRunner.getAllTaskInfo(); + if (!taskInfoList.isEmpty()) { + for (SubscribeTaskInfo taskInfo : taskInfoList) { + if (taskInfo == null) { + continue; + } + Device device = getDeviceByDeviceId(taskInfo.getDeviceId()); + if (device == null || !device.isOnLine() || !onlineDeviceIds.contains(taskInfo.getDeviceId())) { + subscribeTaskRunner.removeSubscribe(taskInfo.getKey()); + continue; + } + if (SubscribeTaskForCatalog.name.equals(taskInfo.getName())) { + device.setSubscribeCycleForCatalog((int)taskInfo.getExpireTime()); + SubscribeTask subscribeTask = SubscribeTaskForCatalog.getInstance(device, this::catalogSubscribeExpire, taskInfo.getTransactionInfo()); + if (subscribeTask != null) { + subscribeTaskRunner.addSubscribe(subscribeTask); + } + }else if (SubscribeTaskForMobilPosition.name.equals(taskInfo.getName())) { + device.setSubscribeCycleForMobilePosition((int)taskInfo.getExpireTime()); + SubscribeTask subscribeTask = SubscribeTaskForMobilPosition.getInstance(device, this::mobilPositionSubscribeExpire, taskInfo.getTransactionInfo()); + if (subscribeTask != null) { + subscribeTaskRunner.addSubscribe(subscribeTask); + } + } + } + } + } + + private void offlineByIds(List offlineDevices) { + if (offlineDevices.isEmpty()) { + log.info("[更新多个离线设备信息] 参数为空"); + return; + } + deviceMapper.offlineByList(offlineDevices); + for (Device device : offlineDevices) { + device.setOnLine(false); + redisCatchStorage.updateDevice(device); + } + } + + private void cleanOfflineDevice(Device device) { + if (subscribeTaskRunner.containsKey(SubscribeTaskForCatalog.getKey(device))) { + subscribeTaskRunner.removeSubscribe(SubscribeTaskForCatalog.getKey(device)); + } + if (subscribeTaskRunner.containsKey(SubscribeTaskForMobilPosition.getKey(device))) { + subscribeTaskRunner.removeSubscribe(SubscribeTaskForMobilPosition.getKey(device)); + } + // 离线释放所有ssrc + List ssrcTransactions = sessionManager.getSsrcTransactionByDeviceId(device.getDeviceId()); + if (ssrcTransactions != null && !ssrcTransactions.isEmpty()) { + for (SsrcTransaction ssrcTransaction : ssrcTransactions) { + mediaServerService.releaseSsrc(ssrcTransaction.getMediaServerId(), ssrcTransaction.getSsrc()); + mediaServerService.closeRTPServer(ssrcTransaction.getMediaServerId(), ssrcTransaction.getStream()); + sessionManager.removeByCallId(ssrcTransaction.getCallId()); + } + } + // 移除订阅 + removeCatalogSubscribe(device, null); + removeMobilePositionSubscribe(device, null); + + List audioBroadcastCatches = audioBroadcastManager.getByDeviceId(device.getDeviceId()); + if (!audioBroadcastCatches.isEmpty()) { + for (AudioBroadcastCatch audioBroadcastCatch : audioBroadcastCatches) { + + SendRtpInfo sendRtpItem = sendRtpServerService.queryByChannelId(audioBroadcastCatch.getChannelId(), device.getDeviceId()); + if (sendRtpItem != null) { + sendRtpServerService.delete(sendRtpItem); + MediaServer mediaInfo = mediaServerService.getOne(sendRtpItem.getMediaServerId()); + mediaServerService.stopSendRtp(mediaInfo, sendRtpItem.getApp(), sendRtpItem.getStream(), null); + } + + audioBroadcastManager.del(audioBroadcastCatch.getChannelId()); + } + } + } + + private void deviceStatusExpire(String deviceId, SipTransactionInfo transactionInfo) { + log.info("[设备状态] 到期, 编号: {}", deviceId); + offline(deviceId, "保活到期"); + } + + @Override + public void online(Device device, SipTransactionInfo sipTransactionInfo) { + log.info("[设备上线] deviceId:{}->{}:{}", device.getDeviceId(), device.getIp(), device.getPort()); + Device deviceInRedis = redisCatchStorage.getDevice(device.getDeviceId()); + Device deviceInDb = getDeviceByDeviceIdFromDb(device.getDeviceId()); + + String now = DateUtil.getNow(); + if (deviceInRedis != null && deviceInDb == null) { + // redis 存在脏数据 + inviteStreamService.clearInviteInfo(device.getDeviceId()); + } + device.setUpdateTime(now); + device.setKeepaliveTime(now); + if (device.getHeartBeatCount() == null) { + // 读取设备配置, 获取心跳间隔和心跳超时次数, 在次之前暂时设置为默认值 + device.setHeartBeatCount(3); + device.setHeartBeatInterval(60); + device.setPositionCapability(0); + + } + if (sipTransactionInfo != null) { + device.setSipTransactionInfo(sipTransactionInfo); + }else { + if (deviceInRedis != null) { + device.setSipTransactionInfo(deviceInRedis.getSipTransactionInfo()); + } + } + + // 第一次上线 或则设备之前是离线状态--进行通道同步和设备信息查询 + if (deviceInDb == null) { + device.setOnLine(true); + device.setCreateTime(now); + device.setUpdateTime(now); + log.info("[设备上线,首次注册]: {},查询设备信息以及通道信息", device.getDeviceId()); + if(device.getStreamMode() == null) { + device.setStreamMode("TCP-PASSIVE"); + } + deviceMapper.add(device); + redisCatchStorage.updateDevice(device); + try { + commander.deviceInfoQuery(device, null); + commander.deviceConfigQuery(device, null, "BasicParam", null); + } catch (InvalidArgumentException | SipException | ParseException e) { + log.error("[命令发送失败] 查询设备信息: {}", e.getMessage()); + } + sync(device); + }else { + device.setServerId(userSetting.getServerId()); + if(!deviceInDb.isOnLine()){ + device.setOnLine(true); + device.setCreateTime(now); + deviceMapper.update(device); + redisCatchStorage.updateDevice(device); + if (userSetting.getSyncChannelOnDeviceOnline()) { + log.info("[设备上线,离线状态下重新注册]: {},查询设备信息以及通道信息", device.getDeviceId()); + try { + commander.deviceInfoQuery(device, null); + } catch (InvalidArgumentException | SipException | ParseException e) { + log.error("[命令发送失败] 查询设备信息: {}", e.getMessage()); + } + sync(device); + }else { + if (isDevice(device.getDeviceId())) { + sync(device); + } + } + // 上线添加订阅 + if (device.getSubscribeCycleForCatalog() > 0 && !subscribeTaskRunner.containsKey(SubscribeTaskForCatalog.getKey(device))) { + // 查询在线设备那些开启了订阅,为设备开启定时的目录订阅 + addCatalogSubscribe(device, null); + } + if (device.getSubscribeCycleForMobilePosition() > 0 && !subscribeTaskRunner.containsKey(SubscribeTaskForMobilPosition.getKey(device))) { + addMobilePositionSubscribe(device, null); + } + + if (userSetting.getDeviceStatusNotify()) { + // 发送redis消息 + redisCatchStorage.sendDeviceOrChannelStatus(device.getDeviceId(), null, true); + } + }else { + deviceMapper.update(device); + redisCatchStorage.updateDevice(device); + } + if (deviceChannelMapper.queryChannelsByDeviceDbId(device.getId()).isEmpty()) { + log.info("[设备上线]: {},通道数为0,查询通道信息", device.getDeviceId()); + sync(device); + } + } + long expiresTime = Math.min(device.getExpires(), device.getHeartBeatInterval() * device.getHeartBeatCount()) * 1000L; + if (deviceStatusTaskRunner.containsKey(device.getDeviceId())) { + if (sipTransactionInfo == null) { + deviceStatusTaskRunner.updateDelay(device.getDeviceId(), expiresTime + System.currentTimeMillis()); + }else { + deviceStatusTaskRunner.removeTask(device.getDeviceId()); + DeviceStatusTask task = DeviceStatusTask.getInstance(device.getDeviceId(), sipTransactionInfo, expiresTime + System.currentTimeMillis(), this::deviceStatusExpire); + deviceStatusTaskRunner.addTask(task); + } + }else { + DeviceStatusTask task = DeviceStatusTask.getInstance(device.getDeviceId(), sipTransactionInfo, expiresTime + System.currentTimeMillis(), this::deviceStatusExpire); + deviceStatusTaskRunner.addTask(task); + } + + } + + @Override + @Transactional + public void offline(String deviceId, String reason) { + Device device = getDeviceByDeviceIdFromDb(deviceId); + if (device == null) { + log.warn("[设备不存在] device:{}", deviceId); + return; + } + + // 主动查询设备状态, 没有HostAddress无法发送请求,可能是手动添加的设备 + if (device.getHostAddress() != null) { + Boolean deviceStatus = getDeviceStatus(device); + if (deviceStatus != null && deviceStatus) { + log.info("[设备离线] 主动探测发现设备在线,暂不处理 device:{}", deviceId); + online(device, null); + return; + } + } + log.info("[设备离线] {}, device:{}, 心跳间隔: {},心跳超时次数: {}, 上次心跳时间:{}, 上次注册时间: {}", reason, deviceId, + device.getHeartBeatInterval(), device.getHeartBeatCount(), device.getKeepaliveTime(), device.getRegisterTime()); + device.setOnLine(false); + cleanOfflineDevice(device); + redisCatchStorage.updateDevice(device); + deviceMapper.update(device); + if (userSetting.getDeviceStatusNotify()) { + // 发送redis消息 + redisCatchStorage.sendDeviceOrChannelStatus(device.getDeviceId(), null, false); + } + if (isDevice(deviceId)) { + channelOfflineByDevice(device); + } + } + + private void channelOfflineByDevice(Device device) { + // 进行通道离线 + List channelList = commonGBChannelMapper.queryOnlineListsByGbDeviceId(device.getId()); + if (channelList.isEmpty()) { + return; + } + deviceChannelMapper.offlineByDeviceId(device.getId()); + // 发送通道离线通知 + eventPublisher.channelEventPublish(channelList, ChannelEvent.ChannelEventMessageType.OFF); + } + + private boolean isDevice(String deviceId) { + GbCode decode = GbCode.decode(deviceId); + if (decode == null) { + return true; + } + int code = Integer.parseInt(decode.getTypeCode()); + return code <= 199; + } + + // 订阅丢失检查 + @Scheduled(fixedDelay = 10, timeUnit = TimeUnit.SECONDS) + public void lostCheckForSubscribe(){ + // 获取所有设备 + List deviceList = redisCatchStorage.getAllDevices(); + if (deviceList.isEmpty()) { + return; + } + for (Device device : deviceList) { + if (device == null || !device.isOnLine() || !userSetting.getServerId().equals(device.getServerId())) { + continue; + } + if (device.getSubscribeCycleForCatalog() > 0 && !subscribeTaskRunner.containsKey(SubscribeTaskForCatalog.getKey(device))) { + log.debug("[订阅丢失] 目录订阅, 编号: {}, 重新发起订阅", device.getDeviceId()); + addCatalogSubscribe(device, null); + } + if (device.getSubscribeCycleForMobilePosition() > 0 && !subscribeTaskRunner.containsKey(SubscribeTaskForMobilPosition.getKey(device))) { + log.debug("[订阅丢失] 移动位置订阅, 编号: {}, 重新发起订阅", device.getDeviceId()); + addMobilePositionSubscribe(device, null); + } + } + } + + // 设备状态丢失检查 + @Scheduled(fixedDelay = 30, timeUnit = TimeUnit.SECONDS) + public void lostCheckForStatus(){ + // 获取所有设备 + List deviceList = redisCatchStorage.getAllDevices(); + if (deviceList.isEmpty()) { + return; + } + for (Device device : deviceList) { + if (device == null || !device.isOnLine() || !userSetting.getServerId().equals(device.getServerId())) { + continue; + } + if (!deviceStatusTaskRunner.containsKey(device.getDeviceId())) { + log.debug("[状态丢失] 执行设备离线, 编号: {},", device.getDeviceId()); + offline(device.getDeviceId(), ""); + } + } + } + + private void catalogSubscribeExpire(String deviceId, SipTransactionInfo transactionInfo) { + log.info("[目录订阅] 到期, 编号: {}", deviceId); + Device device = getDeviceByDeviceId(deviceId); + if (device == null) { + log.info("[目录订阅] 到期, 编号: {}, 设备不存在, 忽略", deviceId); + return; + } + if (device.isOnLine() && device.getSubscribeCycleForCatalog() > 0) { + addCatalogSubscribe(device, transactionInfo); + } + } + + private void mobilPositionSubscribeExpire(String deviceId, SipTransactionInfo transactionInfo) { + log.info("[移动位置订阅] 到期, 编号: {}", deviceId); + Device device = getDeviceByDeviceId(deviceId); + if (device == null) { + log.info("[移动位置订阅] 到期, 编号: {}, 设备不存在, 忽略", deviceId); + return; + } + if (device.isOnLine() && device.getSubscribeCycleForMobilePosition() > 0) { + addMobilePositionSubscribe(device, transactionInfo); + } + } + + @Override + public boolean addCatalogSubscribe(@NotNull Device device, SipTransactionInfo transactionInfo) { + if (device == null || device.getSubscribeCycleForCatalog() < 0) { + return false; + } + if (transactionInfo == null) { + log.info("[添加目录订阅] 设备 {}", device.getDeviceId()); + }else { + log.info("[目录订阅续期] 设备 {}", device.getDeviceId()); + } + try { + sipCommander.catalogSubscribe(device, transactionInfo, eventResult -> { + ResponseEvent event = (ResponseEvent) eventResult.event; + // 成功 + log.info("[目录订阅]成功: {}", device.getDeviceId()); + if (!subscribeTaskRunner.containsKey(SubscribeTaskForCatalog.getKey(device))) { + SIPResponse response = (SIPResponse) event.getResponse(); + SipTransactionInfo transactionInfoForResponse = new SipTransactionInfo(response); + SubscribeTask subscribeTask = SubscribeTaskForCatalog.getInstance(device, this::catalogSubscribeExpire, transactionInfoForResponse); + if (subscribeTask != null) { + subscribeTaskRunner.addSubscribe(subscribeTask); + } + }else { + subscribeTaskRunner.updateDelay(SubscribeTaskForCatalog.getKey(device), (device.getSubscribeCycleForCatalog() * 1000L - 500L) + System.currentTimeMillis()); + } + + },eventResult -> { + // 失败 + log.warn("[目录订阅]失败,信令发送失败: {}-{} ", device.getDeviceId(), eventResult.msg); + }); + } catch (InvalidArgumentException | SipException | ParseException e) { + log.error("[命令发送失败] 目录订阅: {}", e.getMessage()); + return false; + } + return true; + } + + @Override + public boolean removeCatalogSubscribe(@NotNull Device device, CommonCallback callback) { + String key = SubscribeTaskForCatalog.getKey(device); + if (subscribeTaskRunner.containsKey(key)) { + log.info("[移除目录订阅]: {}", device.getDeviceId()); + SipTransactionInfo transactionInfo = subscribeTaskRunner.getTransactionInfo(key); + if (transactionInfo == null) { + log.warn("[移除目录订阅] 未找到事务信息,{}", device.getDeviceId()); + } + try { + device.setSubscribeCycleForCatalog(0); + sipCommander.catalogSubscribe(device, transactionInfo, eventResult -> { + // 成功 + log.info("[取消目录订阅]成功: {}", device.getDeviceId()); + subscribeTaskRunner.removeSubscribe(SubscribeTaskForCatalog.getKey(device)); + if (callback != null) { + callback.run(true); + } + },eventResult -> { + // 失败 + log.warn("[取消目录订阅]失败,信令发送失败: {}-{} ", device.getDeviceId(), eventResult.msg); + }); + }catch (Exception e) { + // 失败 + log.warn("[取消目录订阅]失败: {}-{} ", device.getDeviceId(), e.getMessage()); + } + } + return true; + } + + @Override + public boolean addMobilePositionSubscribe(@NotNull Device device, SipTransactionInfo transactionInfo) { + if (transactionInfo == null) { + log.info("[添加移动位置订阅] 设备 {}", device.getDeviceId()); + }else { + log.info("[移动位置订阅续期] 设备 {}", device.getDeviceId()); + } + try { + sipCommander.mobilePositionSubscribe(device, transactionInfo, eventResult -> { + ResponseEvent event = (ResponseEvent) eventResult.event; + // 成功 + log.info("[移动位置订阅]成功: {}", device.getDeviceId()); + if (!subscribeTaskRunner.containsKey(SubscribeTaskForMobilPosition.getKey(device))) { + SIPResponse response = (SIPResponse) event.getResponse(); + SipTransactionInfo transactionInfoForResponse = new SipTransactionInfo(response); + SubscribeTask subscribeTask = SubscribeTaskForMobilPosition.getInstance(device, this::mobilPositionSubscribeExpire, transactionInfoForResponse); + if (subscribeTask != null) { + subscribeTaskRunner.addSubscribe(subscribeTask); + } + }else { + subscribeTaskRunner.updateDelay(SubscribeTaskForMobilPosition.getKey(device), (device.getSubscribeCycleForMobilePosition() * 1000L - 500L) + System.currentTimeMillis()); + } + + },eventResult -> { + // 失败 + log.warn("[移动位置订阅]失败,信令发送失败: {}-{} ", device.getDeviceId(), eventResult.msg); + }); + } catch (InvalidArgumentException | SipException | ParseException e) { + log.error("[命令发送失败] 移动位置订阅: {}", e.getMessage()); + return false; + } + return true; + } + + @Override + public boolean removeMobilePositionSubscribe(Device device, CommonCallback callback) { + + String key = SubscribeTaskForMobilPosition.getKey(device); + if (subscribeTaskRunner.containsKey(key)) { + log.info("[移除移动位置订阅]: {}", device.getDeviceId()); + SipTransactionInfo transactionInfo = subscribeTaskRunner.getTransactionInfo(key); + if (transactionInfo == null) { + log.warn("[移除移动位置订阅] 未找到事务信息,{}", device.getDeviceId()); + } + try { + device.setSubscribeCycleForMobilePosition(0); + sipCommander.mobilePositionSubscribe(device, transactionInfo, eventResult -> { + // 成功 + log.info("[取消移动位置订阅]成功: {}", device.getDeviceId()); + subscribeTaskRunner.removeSubscribe(SubscribeTaskForMobilPosition.getKey(device)); + if (callback != null) { + callback.run(true); + } + },eventResult -> { + // 失败 + log.warn("[取消移动位置订阅]失败,信令发送失败: {}-{} ", device.getDeviceId(), eventResult.msg); + }); + }catch (Exception e) { + // 失败 + log.warn("[取消移动位置订阅]失败: {}-{} ", device.getDeviceId(), e.getMessage()); + } + } + return true; + } + + @Override + public SyncStatus getChannelSyncStatus(String deviceId) { + Device device = deviceMapper.getDeviceByDeviceId(deviceId); + if (device == null) { + throw new ControllerException(ErrorCode.ERROR404.getCode(), "设备不存在"); + } + if (!userSetting.getServerId().equals(device.getServerId())) { + return redisRpcService.getChannelSyncStatus(device.getServerId(), deviceId); + } + return catalogResponseMessageHandler.getChannelSyncProgress(deviceId); + } + + @Override + public Boolean isSyncRunning(String deviceId) { + return catalogResponseMessageHandler.isSyncRunning(deviceId); + } + + @Override + public void sync(Device device) { + if (catalogResponseMessageHandler.isSyncRunning(device.getDeviceId())) { + SyncStatus syncStatus = catalogResponseMessageHandler.getChannelSyncProgress(device.getDeviceId()); + log.info("[同步通道] 同步已存在, 设备: {}, 同步信息: {}", device.getDeviceId(), JSON.toJSON(syncStatus)); + return; + } + int sn = (int)((Math.random()*9+1)*100000); + catalogResponseMessageHandler.setChannelSyncReady(device, sn); + try { + sipCommander.catalogQuery(device, sn, event -> { + String errorMsg = String.format("同步通道失败,错误码: %s, %s", event.statusCode, event.msg); + log.info("[同步通道]失败,编号: {}, 错误码: {}, {}", device.getDeviceId(), event.statusCode, event.msg); + catalogResponseMessageHandler.setChannelSyncEnd(device.getDeviceId(), sn, errorMsg); + }); + } catch (SipException | InvalidArgumentException | ParseException e) { + log.error("[同步通道], 信令发送失败:{}", e.getMessage() ); + String errorMsg = String.format("同步通道失败,信令发送失败: %s", e.getMessage()); + catalogResponseMessageHandler.setChannelSyncEnd(device.getDeviceId(), sn, errorMsg); + } + } + + @Override + public Device getDeviceByDeviceId(String deviceId) { + Device device = redisCatchStorage.getDevice(deviceId); + if (device == null) { + device = getDeviceByDeviceIdFromDb(deviceId); + if (device != null) { + redisCatchStorage.updateDevice(device); + } + } + return device; + } + + @Override + public List getAllOnlineDevice(String serverId) { + return deviceMapper.getOnlineDevicesByServerId(serverId); + } + + @Override + public List getAllByStatus(Boolean status) { + return deviceMapper.getDevices(ChannelDataType.GB28181, status); + } + + @Override + public boolean expire(Device device) { + Instant registerTimeDate = Instant.from(DateUtil.formatter.parse(device.getRegisterTime())); + Instant expireInstant = registerTimeDate.plusMillis(TimeUnit.SECONDS.toMillis(device.getExpires())); + return expireInstant.isBefore(Instant.now()); + } + + @Override + public Boolean getDeviceStatus(@NotNull Device device) { + SynchronousQueue queue = new SynchronousQueue<>(); + try { + sipCommander.deviceStatusQuery(device, ((code, msg, data) -> { + queue.offer(msg); + })); + String data = queue.poll(10, TimeUnit.SECONDS); + if (data != null && "ONLINE".equalsIgnoreCase(data.trim())) { + return Boolean.TRUE; + }else { + return Boolean.FALSE; + } + + } catch (InvalidArgumentException | SipException | ParseException | InterruptedException e) { + log.error("[命令发送失败] 设备状态查询: {}", e.getMessage()); + } + return null; + } + + @Override + public Device getDeviceByHostAndPort(String host, int port) { + return deviceMapper.getDeviceByHostAndPort(host, port); + } + + @Override + public void updateDevice(Device device) { + + device.setCharset(device.getCharset() == null ? "" : device.getCharset().toUpperCase()); + device.setUpdateTime(DateUtil.getNow()); + if (deviceMapper.update(device) > 0) { + redisCatchStorage.updateDevice(device); + } + } + + @Transactional + @Override + public void updateDeviceList(List deviceList) { + if (deviceList.isEmpty()){ + log.info("[批量更新设备] 列表为空,更细失败"); + return; + } + if (deviceList.size() == 1) { + updateDevice(deviceList.get(0)); + }else { + for (Device device : deviceList) { + device.setCharset(device.getCharset() == null ? "" : device.getCharset().toUpperCase()); + device.setUpdateTime(DateUtil.getNow()); + } + int limitCount = 300; + if (!deviceList.isEmpty()) { + if (deviceList.size() > limitCount) { + for (int i = 0; i < deviceList.size(); i += limitCount) { + int toIndex = i + limitCount; + if (i + limitCount > deviceList.size()) { + toIndex = deviceList.size(); + } + deviceMapper.batchUpdate(deviceList.subList(i, toIndex)); + } + }else { + deviceMapper.batchUpdate(deviceList); + } + for (Device device : deviceList) { + redisCatchStorage.updateDevice(device); + } + } + } + } + + @Override + public boolean isExist(String deviceId) { + return getDeviceByDeviceIdFromDb(deviceId) != null; + } + + @Override + public void addCustomDevice(Device device) { + device.setOnLine(false); + device.setCreateTime(DateUtil.getNow()); + device.setUpdateTime(DateUtil.getNow()); + if(device.getStreamMode() == null) { + device.setStreamMode("TCP-PASSIVE"); + } + deviceMapper.addCustomDevice(device); + } + + @Override + public void updateCustomDevice(Device device) { + // 订阅状态的修改使用一个单独方法控制,此处不再进行状态修改 + Device deviceInStore = deviceMapper.query(device.getId()); + if (deviceInStore == null) { + log.warn("更新设备时未找到设备信息"); + return; + } + if (deviceInStore.getGeoCoordSys() != null) { + // 坐标系变化,需要重新计算GCJ02坐标和WGS84坐标 + if (!deviceInStore.getGeoCoordSys().equals(device.getGeoCoordSys())) { + deviceInStore.setGeoCoordSys(device.getGeoCoordSys()); + } + }else { + deviceInStore.setGeoCoordSys("WGS84"); + } + if (device.getCharset() == null) { + deviceInStore.setCharset("GB2312"); + } + + deviceMapper.updateCustom(device); + redisCatchStorage.updateDevice(device); + } + + @Override + @Transactional + public boolean delete(String deviceId) { + Device device = getDeviceByDeviceIdFromDb(deviceId); + Assert.notNull(device, "未找到设备"); + if (subscribeTaskRunner.containsKey(SubscribeTaskForCatalog.getKey(device))) { + removeCatalogSubscribe(device, null); + } + if (subscribeTaskRunner.containsKey(SubscribeTaskForMobilPosition.getKey(device))) { + removeMobilePositionSubscribe(device, null); + } + if (deviceStatusTaskRunner.containsKey(deviceId)) { + deviceStatusTaskRunner.removeTask(deviceId); + } + List commonGBChannels = commonGBChannelMapper.queryByDataTypeAndDeviceIds(1, List.of(device.getId())); + + try { + // 发送catalog + eventPublisher.channelEventPublish(commonGBChannels, ChannelEvent.ChannelEventMessageType.DEL); + } catch (Exception e) { + log.warn("[多个通道删除] 发送失败,数量:{}", commonGBChannels.size(), e); + } + + platformChannelMapper.delChannelForDeviceId(deviceId); + deviceChannelMapper.cleanChannelsByDeviceId(device.getId()); + deviceMapper.del(deviceId); + redisCatchStorage.removeDevice(deviceId); + inviteStreamService.clearInviteInfo(deviceId); + return true; + } + + @Override + public ResourceBaseInfo getOverview() { + List onlineDevices = deviceMapper.getOnlineDevices(); + List all = deviceMapper.getAll(); + return new ResourceBaseInfo(all.size(), onlineDevices.size()); + } + + @Override + public List getAll() { + return deviceMapper.getAll(); + } + + @Override + public PageInfo getAll(int page, int count, String query, Boolean status) { + PageHelper.startPage(page, count); + if (query != null) { + query = query.replaceAll("/", "//") + .replaceAll("%", "/%") + .replaceAll("_", "/_"); + } + List all = deviceMapper.getDeviceList(ChannelDataType.GB28181, query, status); + return new PageInfo<>(all); + } + + @Override + public Device getDevice(Integer id) { + return deviceMapper.query(id); + } + + @Override + public Device getDeviceByChannelId(Integer channelId) { + return deviceMapper.queryByChannelId(ChannelDataType.GB28181,channelId); + } + + @Override + public Device getDeviceBySourceChannelDeviceId(String channelId) { + return deviceMapper.getDeviceBySourceChannelDeviceId(ChannelDataType.GB28181,channelId); + } + + @Override + public void subscribeCatalog(int id, int cycle) { + Device device = deviceMapper.query(id); + Assert.notNull(device, "未找到设备"); + Assert.isTrue(device.isOnLine(), "设备已离线"); + if (device.getSubscribeCycleForCatalog() == cycle) { + return; + } + if (!userSetting.getServerId().equals(device.getServerId())) { + redisRpcService.subscribeCatalog(id, cycle); + return; + } + // 目录订阅相关的信息 + if (device.getSubscribeCycleForCatalog() > 0) { + // 订阅周期不同,则先取消 + removeCatalogSubscribe(device, result->{ + device.setSubscribeCycleForCatalog(cycle); + updateDevice(device); + if (cycle > 0) { + // 开启订阅 + addCatalogSubscribe(device, null); + } + }); + }else { + // 开启订阅 + device.setSubscribeCycleForCatalog(cycle); + updateDevice(device); + addCatalogSubscribe(device, null); + } + } + + @Override + public void subscribeMobilePosition(int id, int cycle, int interval) { + Device device = deviceMapper.query(id); + Assert.notNull(device, "未找到设备"); + if (!device.isOnLine()) { + // 开启订阅 + device.setSubscribeCycleForMobilePosition(cycle); + device.setMobilePositionSubmissionInterval(interval); + updateDevice(device); + if (subscribeTaskRunner.containsKey(SubscribeTaskForMobilPosition.getKey(device))) { + subscribeTaskRunner.removeSubscribe(SubscribeTaskForMobilPosition.getKey(device)); + } + throw new ControllerException(ErrorCode.ERROR100.getCode(), "设备已离线"); + } + + if (device.getSubscribeCycleForMobilePosition() == cycle) { + return; + } + if (!userSetting.getServerId().equals(device.getServerId())) { + redisRpcService.subscribeMobilePosition(id, cycle, interval); + return; + } + // 目录订阅相关的信息 + if (device.getSubscribeCycleForMobilePosition() > 0) { + // 订阅周期已经开启,则先取消 + removeMobilePositionSubscribe(device, result->{ + // 开启订阅 + device.setSubscribeCycleForMobilePosition(cycle); + device.setMobilePositionSubmissionInterval(interval); + updateDevice(device); + if (cycle > 0) { + addMobilePositionSubscribe(device, null); + } + }); + }else { + // 订阅未开启 + device.setSubscribeCycleForMobilePosition(cycle); + device.setMobilePositionSubmissionInterval(interval); + updateDevice(device); + // 开启订阅 + addMobilePositionSubscribe(device, null); + } + } + + @Override + public void updateDeviceHeartInfo(Device device) { + Device deviceInDb = deviceMapper.query(device.getId()); + if (deviceInDb == null) { + return; + } + if (!Objects.equals(deviceInDb.getHeartBeatCount(), device.getHeartBeatCount()) + || !Objects.equals(deviceInDb.getHeartBeatInterval(), device.getHeartBeatInterval())) { + + deviceInDb.setHeartBeatCount(device.getHeartBeatCount()); + deviceInDb.setHeartBeatInterval(device.getHeartBeatInterval()); + deviceInDb.setPositionCapability(device.getPositionCapability()); + updateDevice(deviceInDb); + + long expiresTime = Math.min(device.getExpires(), device.getHeartBeatInterval() * device.getHeartBeatCount()) * 1000L; + if (deviceStatusTaskRunner.containsKey(device.getDeviceId())) { + deviceStatusTaskRunner.updateDelay(device.getDeviceId(), expiresTime + System.currentTimeMillis()); + } + } + } + + @Override + public WVPResult devicesSync(Device device) { + if (device.getServerId() != null && !userSetting.getServerId().equals(device.getServerId())) { + return redisRpcService.devicesSync(device.getServerId(), device.getDeviceId()); + } + // 已存在则返回进度 + if (isSyncRunning(device.getDeviceId())) { + SyncStatus channelSyncStatus = getChannelSyncStatus(device.getDeviceId()); + WVPResult wvpResult = new WVPResult(); + if (channelSyncStatus.getErrorMsg() != null) { + wvpResult.setCode(ErrorCode.ERROR100.getCode()); + wvpResult.setMsg(channelSyncStatus.getErrorMsg()); + }else if (channelSyncStatus.getTotal() == null || channelSyncStatus.getTotal() == 0){ + wvpResult.setCode(ErrorCode.SUCCESS.getCode()); + wvpResult.setMsg("等待通道信息..."); + }else { + wvpResult.setCode(ErrorCode.SUCCESS.getCode()); + wvpResult.setMsg(ErrorCode.SUCCESS.getMsg()); + wvpResult.setData(channelSyncStatus); + } + return wvpResult; + } + sync(device); + WVPResult wvpResult = new WVPResult<>(); + wvpResult.setCode(0); + wvpResult.setMsg("开始同步"); + return wvpResult; + } + + @Override + public void deviceBasicConfig(Device device, BasicParam basicParam, ErrorCallback callback) { + if (!userSetting.getServerId().equals(device.getServerId())) { + WVPResult result = redisRpcService.deviceBasicConfig(device.getServerId(), device, basicParam); + if (result.getCode() == ErrorCode.SUCCESS.getCode()) { + callback.run(result.getCode(), result.getMsg(), result.getData()); + } + return; + } + + try { + sipCommander.deviceBasicConfigCmd(device, basicParam, callback); + } catch (InvalidArgumentException | SipException | ParseException e) { + log.error("[命令发送失败] 设备配置: {}", e.getMessage()); + callback.run(ErrorCode.ERROR100.getCode(), "命令发送: " + e.getMessage(), null); + throw new ControllerException(ErrorCode.ERROR100.getCode(), "命令发送: " + e.getMessage()); + } + } + + @Override + public void deviceConfigQuery(Device device, String channelId, String configType, ErrorCallback callback) { + + if (!userSetting.getServerId().equals(device.getServerId())) { + WVPResult result = redisRpcService.deviceConfigQuery(device.getServerId(), device, channelId, configType); + callback.run(result.getCode(), result.getMsg(), result.getData()); + return; + } + + try { + sipCommander.deviceConfigQuery(device, channelId, configType, callback); + } catch (InvalidArgumentException | SipException | ParseException e) { + log.error("[命令发送失败] 获取设备配置: {}", e.getMessage()); + callback.run(ErrorCode.ERROR100.getCode(), "命令发送: " + e.getMessage(), null); + throw new ControllerException(ErrorCode.ERROR100.getCode(), "命令发送: " + e.getMessage()); + } + } + + @Override + public void teleboot(Device device) { + + if (!userSetting.getServerId().equals(device.getServerId())) { + redisRpcService.teleboot(device.getServerId(), device); + } + try { + sipCommander.teleBootCmd(device); + } catch (InvalidArgumentException | SipException | ParseException e) { + log.error("[命令发送失败] 远程启动: {}", e.getMessage()); + throw new ControllerException(ErrorCode.ERROR100.getCode(), "命令发送失败: " + e.getMessage()); + } + } + + @Override + public void record(Device device, String channelId, String recordCmdStr, ErrorCallback callback) { + + if (!userSetting.getServerId().equals(device.getServerId())) { + WVPResult result = redisRpcService.recordControl(device.getServerId(), device, channelId, recordCmdStr); + callback.run(result.getCode(), result.getMsg(), result.getData()); + return; + } + + try { + sipCommander.recordCmd(device, channelId, recordCmdStr, callback); + } catch (InvalidArgumentException | SipException | ParseException e) { + log.error("[命令发送失败] 开始/停止录像: {}", e.getMessage()); + callback.run(ErrorCode.ERROR100.getCode(), "命令发送: " + e.getMessage(), null); + throw new ControllerException(ErrorCode.ERROR100.getCode(), "命令发送: " + e.getMessage()); + } + } + + @Override + public void guard(Device device, String guardCmdStr, ErrorCallback callback) { + if (!userSetting.getServerId().equals(device.getServerId())) { + WVPResult result = redisRpcService.guard(device.getServerId(), device, guardCmdStr); + callback.run(result.getCode(), result.getMsg(), result.getData()); + return; + } + + try { + sipCommander.guardCmd(device, guardCmdStr, callback); + } catch (InvalidArgumentException | SipException | ParseException e) { + log.error("[命令发送失败] 布防/撤防操作: {}", e.getMessage()); + callback.run(ErrorCode.ERROR100.getCode(), "命令发送: " + e.getMessage(), null); + throw new ControllerException(ErrorCode.ERROR100.getCode(), "命令发送: " + e.getMessage()); + } + } + + @Override + public void resetAlarm(Device device, String channelId, String alarmMethod, String alarmType, ErrorCallback callback) { + if (!userSetting.getServerId().equals(device.getServerId())) { + WVPResult result = redisRpcService.resetAlarm(device.getServerId(), device, channelId, alarmMethod, alarmType); + callback.run(result.getCode(), result.getMsg(), result.getData()); + return; + } + try { + sipCommander.alarmResetCmd(device, alarmMethod, alarmType, callback); + } catch (InvalidArgumentException | SipException | ParseException e) { + log.error("[命令发送失败] 布防/撤防操作: {}", e.getMessage()); + callback.run(ErrorCode.ERROR100.getCode(), "命令发送: " + e.getMessage(), null); + throw new ControllerException(ErrorCode.ERROR100.getCode(), "命令发送: " + e.getMessage()); + } + + } + + @Override + public void iFrame(Device device, String channelId) { + if (!userSetting.getServerId().equals(device.getServerId())) { + redisRpcService.iFrame(device.getServerId(), device, channelId); + return; + } + + try { + sipCommander.iFrameCmd(device, channelId); + } catch (InvalidArgumentException | SipException | ParseException e) { + log.error("[命令发送失败] 强制关键帧操作: {}", e.getMessage()); + throw new ControllerException(ErrorCode.ERROR100.getCode(), "命令发送: " + e.getMessage()); + } + } + + @Override + public void homePosition(Device device, String channelId, Boolean enabled, Integer resetTime, Integer presetIndex, ErrorCallback callback) { + if (!userSetting.getServerId().equals(device.getServerId())) { + WVPResult result = redisRpcService.homePosition(device.getServerId(), device, channelId, enabled, resetTime, presetIndex); + callback.run(result.getCode(), result.getMsg(), result.getData()); + return; + } + + try { + sipCommander.homePositionCmd(device, channelId, enabled, resetTime, presetIndex, callback); + } catch (InvalidArgumentException | SipException | ParseException e) { + log.error("[命令发送失败] 看守位控制: {}", e.getMessage()); + callback.run(ErrorCode.ERROR100.getCode(), "命令发送: " + e.getMessage(), null); + throw new ControllerException(ErrorCode.ERROR100.getCode(), "命令发送失败: " + e.getMessage()); + } + } + + @Override + public void dragZoomIn(Device device, String channelId, int length, int width, int midpointx, int midpointy, int lengthx, int lengthy, ErrorCallback callback) { + if (!userSetting.getServerId().equals(device.getServerId())) { + redisRpcService.dragZoomIn(device.getServerId(), device, channelId, length, width, midpointx, midpointy, lengthx, lengthy); + return; + } + + StringBuffer cmdXml = new StringBuffer(200); + cmdXml.append("\r\n"); + cmdXml.append("" + length+ "\r\n"); + cmdXml.append("" + width+ "\r\n"); + cmdXml.append("" + midpointx+ "\r\n"); + cmdXml.append("" + midpointy+ "\r\n"); + cmdXml.append("" + lengthx+ "\r\n"); + cmdXml.append("" + lengthy+ "\r\n"); + cmdXml.append("\r\n"); + try { + sipCommander.dragZoomCmd(device, channelId, cmdXml.toString(), callback); + } catch (InvalidArgumentException | SipException | ParseException e) { + log.error("[命令发送失败] 拉框放大: {}", e.getMessage()); + callback.run(ErrorCode.ERROR100.getCode(), "命令发送: " + e.getMessage(), null); + throw new ControllerException(ErrorCode.ERROR100.getCode(), "命令发送失败: " + e.getMessage()); + } + } + + @Override + public void dragZoomOut(Device device, String channelId, int length, int width, int midpointx, int midpointy, int lengthx, int lengthy, ErrorCallback callback) { + if (!userSetting.getServerId().equals(device.getServerId())) { + redisRpcService.dragZoomOut(device.getServerId(), device, channelId, length, width, midpointx, midpointy, lengthx, lengthy); + return; + } + + StringBuffer cmdXml = new StringBuffer(200); + cmdXml.append("\r\n"); + cmdXml.append("" + length+ "\r\n"); + cmdXml.append("" + width+ "\r\n"); + cmdXml.append("" + midpointx+ "\r\n"); + cmdXml.append("" + midpointy+ "\r\n"); + cmdXml.append("" + lengthx+ "\r\n"); + cmdXml.append("" + lengthy+ "\r\n"); + cmdXml.append("\r\n"); + try { + sipCommander.dragZoomCmd(device, channelId, cmdXml.toString(), callback); + } catch (InvalidArgumentException | SipException | ParseException e) { + log.error("[命令发送失败] 拉框放大: {}", e.getMessage()); + callback.run(ErrorCode.ERROR100.getCode(), "命令发送: " + e.getMessage(), null); + throw new ControllerException(ErrorCode.ERROR100.getCode(), "命令发送失败: " + e.getMessage()); + } + } + + @Override + public void deviceStatus(Device device, ErrorCallback callback) { + + if (!userSetting.getServerId().equals(device.getServerId())) { + WVPResult result = redisRpcService.deviceStatus(device.getServerId(), device); + callback.run(result.getCode(), result.getMsg(), result.getData()); + return; + } + try { + sipCommander.deviceStatusQuery(device, (code, msg, data) -> { + if ("ONLINE".equalsIgnoreCase(data.trim())) { + online(device, null); + }else { + offline(device.getDeviceId(), "设备状态查询结果:" + data.trim()); + } + if (callback != null) { + callback.run(code, msg, data); + } + }); + } catch (InvalidArgumentException | SipException | ParseException e) { + log.error("[命令发送失败] 获取设备状态: {}", e.getMessage()); + callback.run(ErrorCode.ERROR100.getCode(), "命令发送: " + e.getMessage(), null); + throw new ControllerException(ErrorCode.ERROR100.getCode(), "命令发送失败: " + e.getMessage()); + } + } + + + @Override + public void alarm(Device device, String startPriority, String endPriority, String alarmMethod, String alarmType, String startTime, String endTime, ErrorCallback callback) { + if (!userSetting.getServerId().equals(device.getServerId())) { + WVPResult result = redisRpcService.alarm(device.getServerId(), device, startPriority, endPriority, alarmMethod, alarmType, startTime, endTime); + callback.run(result.getCode(), result.getMsg(), result.getData()); + return; + } + + String startAlarmTime = ""; + if (startTime != null) { + startAlarmTime = DateUtil.yyyy_MM_dd_HH_mm_ssToISO8601(startTime); + } + String endAlarmTime = ""; + if (startTime != null) { + endAlarmTime = DateUtil.yyyy_MM_dd_HH_mm_ssToISO8601(endTime); + } + + try { + sipCommander.alarmInfoQuery(device, startPriority, endPriority, alarmMethod, alarmType, startAlarmTime, endAlarmTime, callback); + } catch (InvalidArgumentException | SipException | ParseException e) { + log.error("[命令发送失败] 获取设备状态: {}", e.getMessage()); + callback.run(ErrorCode.ERROR100.getCode(), "命令发送: " + e.getMessage(), null); + throw new ControllerException(ErrorCode.ERROR100.getCode(), "命令发送失败: " + e.getMessage()); + } + } + + @Override + public void deviceInfo(Device device, ErrorCallback callback) { + if (!userSetting.getServerId().equals(device.getServerId())) { + WVPResult result = redisRpcService.deviceInfo(device.getServerId(), device); + callback.run(result.getCode(), result.getMsg(), result.getData()); + return; + } + + try { + sipCommander.deviceInfoQuery(device, callback); + } catch (InvalidArgumentException | SipException | ParseException e) { + log.error("[命令发送失败] 获取设备信息: {}", e.getMessage()); + callback.run(ErrorCode.ERROR100.getCode(), "命令发送: " + e.getMessage(), null); + throw new ControllerException(ErrorCode.ERROR100.getCode(), "命令发送失败: " + e.getMessage()); + } + } + + @Override + public void queryPreset(Device device, String channelId, ErrorCallback> callback) { + if (!userSetting.getServerId().equals(device.getServerId())) { + WVPResult> result = redisRpcService.queryPreset(device.getServerId(), device, channelId); + callback.run(result.getCode(), result.getMsg(), result.getData()); + return; + } + + try { + sipCommander.presetQuery(device, channelId, callback); + } catch (InvalidArgumentException | SipException | ParseException e) { + log.error("[命令发送失败] 预制位查询: {}", e.getMessage()); + callback.run(ErrorCode.ERROR100.getCode(), "命令发送: " + e.getMessage(), null); + throw new ControllerException(ErrorCode.ERROR100.getCode(), "命令发送失败: " + e.getMessage()); + } + } + + +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/service/impl/GbChannelControlServiceImpl.java b/src/main/java/com/genersoft/iot/vmp/gb28181/service/impl/GbChannelControlServiceImpl.java new file mode 100644 index 0000000..3409a84 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/service/impl/GbChannelControlServiceImpl.java @@ -0,0 +1,128 @@ +package com.genersoft.iot.vmp.gb28181.service.impl; + +import com.genersoft.iot.vmp.common.enums.ChannelDataType; +import com.genersoft.iot.vmp.gb28181.bean.*; +import com.genersoft.iot.vmp.gb28181.service.IGbChannelControlService; +import com.genersoft.iot.vmp.gb28181.service.ISourcePTZService; +import com.genersoft.iot.vmp.service.bean.ErrorCallback; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import javax.sip.message.Response; +import java.util.List; +import java.util.Map; + +@Service +@Slf4j +public class GbChannelControlServiceImpl implements IGbChannelControlService { + + + @Autowired + private Map sourcePTZServiceMap; + + + @Override + public void ptz(CommonGBChannel channel, FrontEndControlCodeForPTZ frontEndControlCode, ErrorCallback callback) { + log.info("[通用通道] 云台控制, 类型: {}, 编号:{}", channel.getDataType(), channel.getGbDeviceId()); + Integer dataType = channel.getDataType(); + ISourcePTZService sourcePTZService = sourcePTZServiceMap.get(ChannelDataType.PTZ_SERVICE + dataType); + if (sourcePTZService == null) { + // 通道数据异常 + log.error("[点播通用通道] 类型: {} 不支持云台控制", dataType); + throw new PlayException(Response.BUSY_HERE, "channel not support"); + } + sourcePTZService.ptz(channel, frontEndControlCode, callback); + } + + @Override + public void preset(CommonGBChannel channel, FrontEndControlCodeForPreset frontEndControlCode, ErrorCallback callback) { + log.info("[通用通道] 预置位控制, 类型: {}, 编号:{}", channel.getDataType(), channel.getGbDeviceId()); + Integer dataType = channel.getDataType(); + ISourcePTZService sourcePTZService = sourcePTZServiceMap.get(ChannelDataType.PTZ_SERVICE + dataType); + if (sourcePTZService == null) { + // 通道数据异常 + log.error("[点播通用通道] 类型: {} 不支持预置位控制", dataType); + throw new PlayException(Response.BUSY_HERE, "channel not support"); + } + sourcePTZService.preset(channel, frontEndControlCode, callback); + } + + @Override + public void fi(CommonGBChannel channel, FrontEndControlCodeForFI frontEndControlCode, ErrorCallback callback) { + log.info("[通用通道] FI指令, 类型: {}, 编号:{}", channel.getDataType(), channel.getGbDeviceId()); + Integer dataType = channel.getDataType(); + ISourcePTZService sourcePTZService = sourcePTZServiceMap.get(ChannelDataType.PTZ_SERVICE + dataType); + if (sourcePTZService == null) { + // 通道数据异常 + log.error("[点播通用通道] 类型: {} 不支持FI指令", dataType); + throw new PlayException(Response.BUSY_HERE, "channel not support"); + } + sourcePTZService.fi(channel, frontEndControlCode, callback); + } + + @Override + public void tour(CommonGBChannel channel, FrontEndControlCodeForTour frontEndControlCode, ErrorCallback callback) { + log.info("[通用通道] 巡航指令, 类型: {}, 编号:{}", channel.getDataType(), channel.getGbDeviceId()); + Integer dataType = channel.getDataType(); + ISourcePTZService sourcePTZService = sourcePTZServiceMap.get(ChannelDataType.PTZ_SERVICE + dataType); + if (sourcePTZService == null) { + // 通道数据异常 + log.error("[点播通用通道] 类型: {} 不支持巡航指令", dataType); + throw new PlayException(Response.BUSY_HERE, "channel not support"); + } + sourcePTZService.tour(channel, frontEndControlCode, callback); + } + + @Override + public void scan(CommonGBChannel channel, FrontEndControlCodeForScan frontEndControlCode, ErrorCallback callback) { + log.info("[通用通道] 扫描指令, 类型: {}, 编号:{}", channel.getDataType(), channel.getGbDeviceId()); + Integer dataType = channel.getDataType(); + ISourcePTZService sourcePTZService = sourcePTZServiceMap.get(ChannelDataType.PTZ_SERVICE + dataType); + if (sourcePTZService == null) { + // 通道数据异常 + log.error("[点播通用通道] 类型: {} 不支持扫描指令", dataType); + throw new PlayException(Response.BUSY_HERE, "channel not support"); + } + sourcePTZService.scan(channel, frontEndControlCode, callback); + } + + @Override + public void auxiliary(CommonGBChannel channel, FrontEndControlCodeForAuxiliary frontEndControlCode, ErrorCallback callback) { + log.info("[通用通道] 辅助开关控制指令, 类型: {}, 编号:{}", channel.getDataType(), channel.getGbDeviceId()); + Integer dataType = channel.getDataType(); + ISourcePTZService sourcePTZService = sourcePTZServiceMap.get(ChannelDataType.PTZ_SERVICE + dataType); + if (sourcePTZService == null) { + // 通道数据异常 + log.error("[点播通用通道] 类型: {} 不支持辅助开关控制指令", dataType); + throw new PlayException(Response.BUSY_HERE, "channel not support"); + } + sourcePTZService.auxiliary(channel, frontEndControlCode, callback); + } + + @Override + public void wiper(CommonGBChannel channel, FrontEndControlCodeForWiper frontEndControlCode, ErrorCallback callback) { + log.info("[通用通道] 雨刷控制, 类型: {}, 编号:{}", channel.getDataType(), channel.getGbDeviceId()); + Integer dataType = channel.getDataType(); + ISourcePTZService sourcePTZService = sourcePTZServiceMap.get(ChannelDataType.PTZ_SERVICE + dataType); + if (sourcePTZService == null) { + // 通道数据异常 + log.error("[点播通用通道] 类型: {} 不支持雨刷控制", dataType); + throw new PlayException(Response.BUSY_HERE, "channel not support"); + } + sourcePTZService.wiper(channel, frontEndControlCode, callback); + } + + @Override + public void queryPreset(CommonGBChannel channel, ErrorCallback> callback) { + log.info("[通用通道] 预置位查询, 类型: {}, 编号:{}", channel.getDataType(), channel.getGbDeviceId()); + Integer dataType = channel.getDataType(); + ISourcePTZService sourcePTZService = sourcePTZServiceMap.get(ChannelDataType.PTZ_SERVICE + dataType); + if (sourcePTZService == null) { + // 通道数据异常 + log.error("[点播通用通道] 类型: {} 不支持预置位查询", dataType); + throw new PlayException(Response.BUSY_HERE, "channel not support"); + } + sourcePTZService.queryPreset(channel, callback); + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/service/impl/GbChannelPlayServiceImpl.java b/src/main/java/com/genersoft/iot/vmp/gb28181/service/impl/GbChannelPlayServiceImpl.java new file mode 100644 index 0000000..5157198 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/service/impl/GbChannelPlayServiceImpl.java @@ -0,0 +1,240 @@ +package com.genersoft.iot.vmp.gb28181.service.impl; + +import com.genersoft.iot.vmp.common.InviteSessionType; +import com.genersoft.iot.vmp.common.StreamInfo; +import com.genersoft.iot.vmp.common.enums.ChannelDataType; +import com.genersoft.iot.vmp.conf.UserSetting; +import com.genersoft.iot.vmp.gb28181.bean.*; +import com.genersoft.iot.vmp.gb28181.dao.CommonGBChannelMapper; +import com.genersoft.iot.vmp.gb28181.service.IGbChannelPlayService; +import com.genersoft.iot.vmp.gb28181.service.ISourceDownloadService; +import com.genersoft.iot.vmp.gb28181.service.ISourcePlayService; +import com.genersoft.iot.vmp.gb28181.service.ISourcePlaybackService; +import com.genersoft.iot.vmp.jt1078.service.Ijt1078PlayService; +import com.genersoft.iot.vmp.service.bean.ErrorCallback; +import com.genersoft.iot.vmp.service.bean.InviteErrorCode; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import javax.sip.message.Response; +import java.util.List; +import java.util.Map; + +@Slf4j +@Service +public class GbChannelPlayServiceImpl implements IGbChannelPlayService { + + @Autowired + private UserSetting userSetting; + + @Autowired + private CommonGBChannelMapper channelMapper; + + @Autowired + private Map sourcePlayServiceMap; + + @Autowired + private Ijt1078PlayService jt1078PlayService; + + @Autowired + private Map sourcePlaybackServiceMap; + + @Autowired + private Map sourceDownloadServiceMap; + + + @Override + public void startInvite(CommonGBChannel channel, InviteMessageInfo inviteInfo, Platform platform, ErrorCallback callback) { + if (channel == null || inviteInfo == null || callback == null || channel.getDataType() == null) { + log.warn("[通用通道点播] 参数异常, channel: {}, inviteInfo: {}, callback: {}", channel != null, inviteInfo != null, callback != null); + throw new PlayException(Response.SERVER_INTERNAL_ERROR, "server internal error"); + } + log.info("[点播通用通道] 类型:{}, 通道: {}({})", inviteInfo.getSessionName(), channel.getGbName(), channel.getGbDeviceId()); + + if ("Play".equalsIgnoreCase(inviteInfo.getSessionName())) { + play(channel, platform, userSetting.getRecordSip(), callback); + }else if ("Playback".equals(inviteInfo.getSessionName())) { + playback(channel, inviteInfo.getStartTime(), inviteInfo.getStopTime(), callback); + }else if ("Download".equals(inviteInfo.getSessionName())) { + Integer downloadSpeed = Integer.parseInt(inviteInfo.getDownloadSpeed()); + // 国标通道 + download(channel, inviteInfo.getStartTime(), inviteInfo.getStopTime(), downloadSpeed, callback); + }else { + // 不支持的点播方式 + log.error("[点播通用通道] 不支持的点播方式:{}, {}({})", inviteInfo.getSessionName(), channel.getGbName(), channel.getGbDeviceId()); + throw new PlayException(Response.BAD_REQUEST, "bad request"); + } + } + + @Override + public void stopInvite(InviteSessionType type, CommonGBChannel channel, String stream) { + switch (type) { + case PLAY: + stopPlay(channel); + break; + case PLAYBACK: + stopPlayback(channel, stream); + break; + case DOWNLOAD: + stopDownload(channel, stream); + break; + default: + // 通道数据异常 + log.error("[点播通用通道] 类型编号: {} 不支持此类型请求", type); + throw new PlayException(Response.BUSY_HERE, "channel not support"); + } + } + + + + @Override + public void play(CommonGBChannel channel, Platform platform, Boolean record, ErrorCallback callback) { + log.info("[通用通道] 播放, 类型: {}, 编号:{}", ChannelDataType.getDateTypeDesc(channel.getDataType()), channel.getGbDeviceId()); + Integer dataType = channel.getDataType(); + ISourcePlayService sourceChannelPlayService = sourcePlayServiceMap.get(ChannelDataType.PLAY_SERVICE + dataType); + if (sourceChannelPlayService == null) { + // 通道数据异常 + log.error("[点播通用通道] 类型编号: {} 不支持实时流预览", ChannelDataType.getDateTypeDesc(channel.getDataType())); + throw new PlayException(Response.BUSY_HERE, "channel not support"); + } + sourceChannelPlayService.play(channel, platform, record, (code, msg, data) -> { + if (code == InviteErrorCode.SUCCESS.getCode()) { + // 将流ID记录到数据库 + if (channel.getDataType() != ChannelDataType.GB28181) { + channelMapper.updateStream(channel.getGbId(), data.getStream()); + } + } + callback.run(code, msg, data); + }); + } + @Override + public void playback(CommonGBChannel channel, Long startTime, Long stopTime, ErrorCallback callback) { + log.info("[通用通道] 回放, 类型: {}, 编号:{}", ChannelDataType.getDateTypeDesc(channel.getDataType()), channel.getGbDeviceId()); + Integer dataType = channel.getDataType(); + ISourcePlaybackService playbackService = sourcePlaybackServiceMap.get(ChannelDataType.PLAYBACK_SERVICE + dataType); + if (playbackService == null) { + // 通道数据异常 + log.error("[点播通用通道] 类型编号: {} 不支持回放", dataType); + throw new PlayException(Response.BUSY_HERE, "channel not support"); + } + playbackService.playback(channel, startTime, stopTime, callback); + } + + @Override + public void download(CommonGBChannel channel, Long startTime, Long stopTime, Integer downloadSpeed, + ErrorCallback callback){ + log.info("[通用通道] 录像下载, 类型: {}, 编号:{}", ChannelDataType.getDateTypeDesc(channel.getDataType()), channel.getGbDeviceId()); + Integer dataType = channel.getDataType(); + ISourceDownloadService downloadService = sourceDownloadServiceMap.get(ChannelDataType.DOWNLOAD_SERVICE + dataType); + if (downloadService == null) { + // 通道数据异常 + log.error("[点播通用通道] 类型编号: {} 不支持录像下载", dataType); + throw new PlayException(Response.BUSY_HERE, "channel not support"); + } + downloadService.download(channel, startTime, stopTime, downloadSpeed, callback); + } + + @Override + public void stopPlay(CommonGBChannel channel) { + Integer dataType = channel.getDataType(); + ISourcePlayService sourceChannelPlayService = sourcePlayServiceMap.get(ChannelDataType.PLAY_SERVICE + dataType); + if (sourceChannelPlayService == null) { + // 通道数据异常 + log.error("[点播通用通道] 类型编号: {} 不支持停止实时流", dataType); + throw new PlayException(Response.BUSY_HERE, "channel not support"); + } + sourceChannelPlayService.stopPlay(channel); + } + + @Override + public void stopPlayback(CommonGBChannel channel, String stream) { + log.info("[通用通道] 停止回放, 类型: {}, 编号:{}", ChannelDataType.getDateTypeDesc(channel.getDataType()), channel.getGbDeviceId()); + Integer dataType = channel.getDataType(); + ISourcePlaybackService playbackService = sourcePlaybackServiceMap.get(ChannelDataType.PLAYBACK_SERVICE + dataType); + if (playbackService == null) { + // 通道数据异常 + log.error("[点播通用通道] 类型编号: {} 不支持回放", dataType); + throw new PlayException(Response.BUSY_HERE, "channel not support"); + } + playbackService.stopPlayback(channel, stream); + } + + @Override + public void stopDownload(CommonGBChannel channel, String stream) { + log.info("[通用通道] 停止录像下载, 类型: {}, 编号:{} stream: {}", ChannelDataType.getDateTypeDesc(channel.getDataType()), channel.getGbDeviceId(), stream); + Integer dataType = channel.getDataType(); + ISourceDownloadService downloadService = sourceDownloadServiceMap.get(ChannelDataType.DOWNLOAD_SERVICE + dataType); + if (downloadService == null) { + // 通道数据异常 + log.error("[点播通用通道] 类型编号: {} 不支持录像下载", dataType); + throw new PlayException(Response.BUSY_HERE, "channel not support"); + } + downloadService.stopDownload(channel, stream); + } + + @Override + public void playbackPause(CommonGBChannel channel, String stream) { + log.info("[通用通道] 回放暂停, 类型: {}, 编号:{} stream:{}", ChannelDataType.getDateTypeDesc(channel.getDataType()), channel.getGbDeviceId(), stream); + Integer dataType = channel.getDataType(); + ISourcePlaybackService playbackService = sourcePlaybackServiceMap.get(ChannelDataType.PLAYBACK_SERVICE + dataType); + if (playbackService == null) { + // 通道数据异常 + log.error("[点播通用通道] 类型编号: {} 不支持回放暂停", dataType); + throw new PlayException(Response.BUSY_HERE, "channel not support"); + } + playbackService.playbackPause(channel, stream); + } + + @Override + public void playbackResume(CommonGBChannel channel, String stream) { + log.info("[通用通道] 回放暂停恢复, 类型: {}, 编号:{} stream:{}", ChannelDataType.getDateTypeDesc(channel.getDataType()), channel.getGbDeviceId(), stream); + Integer dataType = channel.getDataType(); + ISourcePlaybackService playbackService = sourcePlaybackServiceMap.get(ChannelDataType.PLAYBACK_SERVICE + dataType); + if (playbackService == null) { + // 通道数据异常 + log.error("[点播通用通道] 类型编号: {} 不支持回放暂停恢复", dataType); + throw new PlayException(Response.BUSY_HERE, "channel not support"); + } + playbackService.playbackResume(channel, stream); + } + + @Override + public void playbackSeek(CommonGBChannel channel, String stream, long seekTime) { + log.info("[通用通道] 回放拖动播放, 类型: {}, 编号:{} stream:{}", ChannelDataType.getDateTypeDesc(channel.getDataType()), channel.getGbDeviceId(), stream); + Integer dataType = channel.getDataType(); + ISourcePlaybackService playbackService = sourcePlaybackServiceMap.get(ChannelDataType.PLAYBACK_SERVICE + dataType); + if (playbackService == null) { + // 通道数据异常 + log.error("[点播通用通道] 类型编号: {} 不支持回放暂停恢复", dataType); + throw new PlayException(Response.BUSY_HERE, "channel not support"); + } + playbackService.playbackSeek(channel, stream, seekTime); + } + + @Override + public void playbackSpeed(CommonGBChannel channel, String stream, Double speed) { + log.info("[通用通道] 回放倍速播放, 类型: {}, 编号:{} stream:{}", ChannelDataType.getDateTypeDesc(channel.getDataType()), channel.getGbDeviceId(), stream); + Integer dataType = channel.getDataType(); + ISourcePlaybackService playbackService = sourcePlaybackServiceMap.get(ChannelDataType.PLAYBACK_SERVICE + dataType); + if (playbackService == null) { + // 通道数据异常 + log.error("[点播通用通道] 类型编号: {} 不支持回放暂停恢复", dataType); + throw new PlayException(Response.BUSY_HERE, "channel not support"); + } + playbackService.playbackSpeed(channel, stream, speed); + } + + @Override + public void queryRecord(CommonGBChannel channel, String startTime, String endTime, ErrorCallback> callback) { + log.info("[通用通道] 录像查询, 类型: {}, 编号:{}", ChannelDataType.getDateTypeDesc(channel.getDataType()), channel.getGbDeviceId()); + Integer dataType = channel.getDataType(); + ISourcePlaybackService playbackService = sourcePlaybackServiceMap.get(ChannelDataType.PLAYBACK_SERVICE + dataType); + if (playbackService == null) { + // 通道数据异常 + log.error("[点播通用通道] 类型编号: {} 不支持回放暂停恢复", dataType); + throw new PlayException(Response.BUSY_HERE, "channel not support"); + } + playbackService.queryRecord(channel, startTime, endTime, callback); + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/service/impl/GbChannelServiceImpl.java b/src/main/java/com/genersoft/iot/vmp/gb28181/service/impl/GbChannelServiceImpl.java new file mode 100644 index 0000000..984e293 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/service/impl/GbChannelServiceImpl.java @@ -0,0 +1,1193 @@ +package com.genersoft.iot.vmp.gb28181.service.impl; + +import com.alibaba.excel.support.cglib.beans.BeanMap; +import com.alibaba.excel.util.BeanMapUtils; +import com.alibaba.fastjson2.JSONObject; +import com.genersoft.iot.vmp.common.VideoManagerConstants; +import com.genersoft.iot.vmp.common.enums.ChannelDataType; +import com.genersoft.iot.vmp.conf.DynamicTask; +import com.genersoft.iot.vmp.conf.exception.ControllerException; +import com.genersoft.iot.vmp.gb28181.bean.*; +import com.genersoft.iot.vmp.gb28181.controller.bean.Extent; +import com.genersoft.iot.vmp.gb28181.dao.CommonGBChannelMapper; +import com.genersoft.iot.vmp.gb28181.dao.GroupMapper; +import com.genersoft.iot.vmp.gb28181.dao.PlatformChannelMapper; +import com.genersoft.iot.vmp.gb28181.dao.RegionMapper; +import com.genersoft.iot.vmp.gb28181.event.EventPublisher; +import com.genersoft.iot.vmp.gb28181.event.channel.ChannelEvent; +import com.genersoft.iot.vmp.gb28181.service.IGbChannelService; +import com.genersoft.iot.vmp.gb28181.service.IPlatformChannelService; +import com.genersoft.iot.vmp.gb28181.utils.VectorTileCatch; +import com.genersoft.iot.vmp.service.bean.GPSMsgInfo; +import com.genersoft.iot.vmp.streamPush.bean.StreamPush; +import com.genersoft.iot.vmp.utils.Coordtransform; +import com.genersoft.iot.vmp.utils.DateUtil; +import com.genersoft.iot.vmp.utils.TileUtils; +import com.genersoft.iot.vmp.vmanager.bean.ErrorCode; +import com.github.pagehelper.PageHelper; +import com.github.pagehelper.PageInfo; +import com.google.common.base.CaseFormat; +import lombok.extern.slf4j.Slf4j; +import no.ecc.vectortile.VectorTileEncoder; +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.GeometryFactory; +import org.locationtech.jts.geom.Point; +import org.springframework.beans.BeanWrapperImpl; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.CommandLineRunner; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; + +import java.time.Duration; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicReference; + +@Slf4j +@Service +public class GbChannelServiceImpl implements IGbChannelService, CommandLineRunner { + + @Autowired + private EventPublisher eventPublisher; + + @Autowired + private CommonGBChannelMapper commonGBChannelMapper; + + @Autowired + private PlatformChannelMapper platformChannelMapper; + + @Autowired + private IPlatformChannelService platformChannelService; + + @Autowired + private RegionMapper regionMapper; + + @Autowired + private GroupMapper groupMapper; + + @Autowired + private DynamicTask dynamicTask; + + @Autowired + private RedisTemplate redisTemplate; + + @Autowired + private VectorTileCatch vectorTileCatch; + + private final GeometryFactory geometryFactory = new GeometryFactory(); + + + @Override + public void run(String... args) throws Exception { + // 启动时重新发布抽稀图层 + List channelList = commonGBChannelMapper.queryAllWithPosition(); + Map> zoomCameraMap = new ConcurrentHashMap<>(); + + channelList.stream().forEach(commonGBChannel -> { + if (commonGBChannel.getMapLevel() == null) { + return; + } + List channelListForZoom = zoomCameraMap.computeIfAbsent(commonGBChannel.getMapLevel(), k -> new ArrayList<>()); + channelListForZoom.add(commonGBChannel); + }); + + String id = "DEFAULT"; + List beforeData = new ArrayList<>(); + for (Integer zoom : zoomCameraMap.keySet()) { + beforeData.addAll(zoomCameraMap.get(zoom)); + log.info("[抽稀-发布mvt矢量瓦片] ID:{},当前层级: {}, ", id, zoom); + // 按照 z/x/y 数据组织数据, 矢量数据暂时保存在内存中 + // 按照范围生成 x y范围, + saveTile(id, zoom, "WGS84", beforeData); + saveTile(id, zoom, "GCJ02", beforeData); + } + } + + @Override + public CommonGBChannel queryByDeviceId(String gbDeviceId) { + List commonGBChannels = commonGBChannelMapper.queryByDeviceId(gbDeviceId); + if (commonGBChannels.isEmpty()) { + return null; + }else { + return commonGBChannels.get(0); + } + } + + @Override + public int add(CommonGBChannel commonGBChannel) { + if (commonGBChannel.getDataType() == null || commonGBChannel.getDataDeviceId() == null) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "缺少通道数据类型或通道数据关联设备ID"); + } + CommonGBChannel commonGBChannelInDb = commonGBChannelMapper.queryByDataId(commonGBChannel.getDataType(), commonGBChannel.getDataDeviceId()); + Assert.isNull(commonGBChannelInDb, "此推流已经关联通道"); + + // 检验国标编号是否重复 + List channelList = commonGBChannelMapper.queryByDeviceId(commonGBChannel.getGbDeviceId()); + Assert.isTrue(channelList.isEmpty(), "国标编号已经存在"); + + commonGBChannel.setCreateTime(DateUtil.getNow()); + commonGBChannel.setUpdateTime(DateUtil.getNow()); + int result = commonGBChannelMapper.insert(commonGBChannel); + try { + // 发送通知 + eventPublisher.channelEventPublish(commonGBChannel, ChannelEvent.ChannelEventMessageType.ADD); + } catch (Exception e) { + log.warn("[通道移除通知] 发送失败,{}", commonGBChannel.getGbDeviceId(), e); + } + return result; + } + + @Override + @Transactional + public int delete(int gbId) { + // 移除国标级联关联的信息 + try { + platformChannelService.removeChannel(gbId); + }catch (Exception e) { + log.error("[移除通道国标级联共享失败]", e); + } + + CommonGBChannel channel = commonGBChannelMapper.queryById(gbId); + if (channel != null) { + commonGBChannelMapper.delete(gbId); + try { + // 发送通知 + eventPublisher.channelEventPublish(channel, ChannelEvent.ChannelEventMessageType.DEL); + } catch (Exception e) { + log.warn("[通道移除通知] 发送失败,{}", channel.getGbDeviceId(), e); + } + } + return 1; + } + + @Override + @Transactional + public void delete(Collection ids) { + // 移除国标级联关联的信息 + try { + platformChannelService.removeChannels(new ArrayList<>(ids)); + }catch (Exception e) { + log.error("[移除通道国标级联共享失败]", e); + } + List channelListInDb = commonGBChannelMapper.queryByIds(ids); + if (channelListInDb.isEmpty()) { + return; + } + commonGBChannelMapper.batchDelete(channelListInDb); + try { + // 发送通知 + eventPublisher.channelEventPublish(channelListInDb, ChannelEvent.ChannelEventMessageType.DEL); + } catch (Exception e) { + log.warn("[通道移除通知] 发送失败", e); + } + } + + @Override + public int update(CommonGBChannel commonGBChannel) { + log.info("[更新通道] 通道ID: {}, ", commonGBChannel.getGbId()); + if (commonGBChannel.getGbId() <= 0) { + log.warn("[更新通道] 未找到数据库ID,更新失败, {}({})", commonGBChannel.getGbName(), commonGBChannel.getGbDeviceId()); + return 0; + } + // 确定编号是否重复 + List channels = commonGBChannelMapper.queryByDeviceId(commonGBChannel.getGbDeviceId()); + if (channels.size() > 1) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "国标编号重复,请修改编号后保存"); + } + CommonGBChannel oldChannel = commonGBChannelMapper.queryById(commonGBChannel.getGbId()); + commonGBChannel.setUpdateTime(DateUtil.getNow()); + int result = commonGBChannelMapper.update(commonGBChannel); + + if (result > 0) { + try { + CommonGBChannel newChannel = commonGBChannelMapper.queryById(commonGBChannel.getGbId()); + // 发送通知 + eventPublisher.channelEventPublishForUpdate(newChannel, oldChannel); + + if (newChannel.getGbLongitude() != null && !Objects.equals(oldChannel.getGbLongitude(), newChannel.getGbLongitude()) + && newChannel.getGbLatitude() != null && !Objects.equals(oldChannel.getGbLatitude(), newChannel.getGbLatitude())) { + MobilePosition mobilePosition = new MobilePosition(); + mobilePosition.setDeviceId(newChannel.getGbDeviceId()); + mobilePosition.setChannelId(newChannel.getGbId()); + mobilePosition.setChannelDeviceId(newChannel.getGbDeviceId()); + mobilePosition.setDeviceName(newChannel.getGbName()); + mobilePosition.setCreateTime(DateUtil.getNow()); + mobilePosition.setTime(DateUtil.getNow()); + mobilePosition.setLongitude(newChannel.getGbLongitude()); + mobilePosition.setLatitude(newChannel.getGbLatitude()); + eventPublisher.mobilePositionEventPublish(mobilePosition); + } + + } catch (Exception e) { + log.warn("[更新通道通知] 发送失败,{}", JSONObject.toJSONString(commonGBChannel), e); + } + } + return result; + } + + @Override + public int offline(CommonGBChannel commonGBChannel) { + if (commonGBChannel.getGbId() <= 0) { + log.warn("[通道离线] 未找到数据库ID,更新失败, {}({})", commonGBChannel.getGbName(), commonGBChannel.getGbDeviceId()); + return 0; + } + int result = commonGBChannelMapper.updateStatusById(commonGBChannel.getGbId(), "OFF"); + if (result > 0) { + try { + // 发送通知 + eventPublisher.channelEventPublish(commonGBChannel, ChannelEvent.ChannelEventMessageType.OFF); + } catch (Exception e) { + log.warn("[通道离线通知] 发送失败,{}", commonGBChannel.getGbDeviceId(), e); + } + } + return result; + } + + @Override + @Transactional + public int offline(List commonGBChannelList) { + if (commonGBChannelList.isEmpty()) { + log.warn("[多个通道离线] 通道数量为0,更新失败"); + return 0; + } + log.info("[通道离线] 共 {} 个", commonGBChannelList.size()); + int limitCount = 1000; + int result = 0; + if (commonGBChannelList.size() > limitCount) { + for (int i = 0; i < commonGBChannelList.size(); i += limitCount) { + int toIndex = i + limitCount; + if (i + limitCount > commonGBChannelList.size()) { + toIndex = commonGBChannelList.size(); + } + result += commonGBChannelMapper.updateStatusForListById(commonGBChannelList.subList(i, toIndex), "OFF"); + } + } else { + result += commonGBChannelMapper.updateStatusForListById(commonGBChannelList, "OFF"); + } + if (result > 0) { + try { + // 发送catalog + eventPublisher.channelEventPublish(commonGBChannelList, ChannelEvent.ChannelEventMessageType.OFF); + } catch (Exception e) { + log.warn("[多个通道离线] 发送失败,数量:{}", commonGBChannelList.size(), e); + } + } + return result; + } + + @Override + public int online(CommonGBChannel commonGBChannel) { + if (commonGBChannel.getGbId() <= 0) { + log.warn("[通道上线] 未找到数据库ID,更新失败, {}({})", commonGBChannel.getGbName(), commonGBChannel.getGbDeviceId()); + return 0; + } + int result = commonGBChannelMapper.updateStatusById(commonGBChannel.getGbId(), "ON"); + if (result > 0) { + try { + // 发送通知 + eventPublisher.channelEventPublish(commonGBChannel, ChannelEvent.ChannelEventMessageType.ON); + } catch (Exception e) { + log.warn("[通道上线通知] 发送失败,{}", commonGBChannel.getGbDeviceId(), e); + } + } + return 0; + } + + @Override + @Transactional + public int online(List commonGBChannelList) { + if (commonGBChannelList.isEmpty()) { + log.warn("[多个通道上线] 通道数量为0,更新失败"); + return 0; + } + // 批量更新 + int limitCount = 1000; + int result = 0; + if (commonGBChannelList.size() > limitCount) { + for (int i = 0; i < commonGBChannelList.size(); i += limitCount) { + int toIndex = i + limitCount; + if (i + limitCount > commonGBChannelList.size()) { + toIndex = commonGBChannelList.size(); + } + result += commonGBChannelMapper.updateStatusForListById(commonGBChannelList.subList(i, toIndex), "ON"); + } + } else { + result += commonGBChannelMapper.updateStatusForListById(commonGBChannelList, "ON"); + } + try { + // 发送catalog + eventPublisher.channelEventPublish(commonGBChannelList, ChannelEvent.ChannelEventMessageType.ON); + } catch (Exception e) { + log.warn("[多个通道上线] 发送失败,数量:{}", commonGBChannelList.size(), e); + } + + return result; + } + + @Override + @Transactional + public void batchAdd(List commonGBChannels) { + if (commonGBChannels.isEmpty()) { + log.warn("[新增多个通道] 通道数量为0,更新失败"); + return; + } + // 批量保存 + int limitCount = 1000; + int result = 0; + if (commonGBChannels.size() > limitCount) { + for (int i = 0; i < commonGBChannels.size(); i += limitCount) { + int toIndex = i + limitCount; + if (i + limitCount > commonGBChannels.size()) { + toIndex = commonGBChannels.size(); + } + result += commonGBChannelMapper.batchAdd(commonGBChannels.subList(i, toIndex)); + } + } else { + result += commonGBChannelMapper.batchAdd(commonGBChannels); + } + try { + // 发送catalog + eventPublisher.channelEventPublish(commonGBChannels, ChannelEvent.ChannelEventMessageType.ADD); + } catch (Exception e) { + log.warn("[多个通道新增] 发送失败,数量:{}", commonGBChannels.size(), e); + } + log.warn("[新增多个通道] 通道数量为{},成功保存:{}", commonGBChannels.size(), result); + } + + @Override + public void batchUpdate(List commonGBChannels) { + if (commonGBChannels.isEmpty()) { + log.warn("[更新多个通道] 通道数量为0,更新失败"); + return; + } + List oldCommonGBChannelList = commonGBChannelMapper.queryOldChanelListByChannels(commonGBChannels); + // 批量保存 + int limitCount = 1000; + int result = 0; + if (commonGBChannels.size() > limitCount) { + for (int i = 0; i < commonGBChannels.size(); i += limitCount) { + int toIndex = i + limitCount; + if (i + limitCount > commonGBChannels.size()) { + toIndex = commonGBChannels.size(); + } + result += commonGBChannelMapper.batchUpdate(commonGBChannels.subList(i, toIndex)); + } + } else { + result += commonGBChannelMapper.batchUpdate(commonGBChannels); + } + log.info("[更新多个通道] 通道数量为{},成功保存:{}", commonGBChannels.size(), result); + // 发送通过更新通知 + try { + // 发送通知 + eventPublisher.channelEventPublishForUpdate(commonGBChannels, oldCommonGBChannelList); + } catch (Exception e) { + log.warn("[更新多个通道] 发送失败,{}个", commonGBChannels.size(), e); + } + } + + @Override + @Transactional + public void updateStatus(List commonGBChannels) { + if (commonGBChannels.isEmpty()) { + log.warn("[更新多个通道状态] 通道数量为0,更新失败"); + return; + } + List oldChanelListByChannels = commonGBChannelMapper.queryOldChanelListByChannels(commonGBChannels); + int limitCount = 1000; + int result = 0; + if (commonGBChannels.size() > limitCount) { + for (int i = 0; i < commonGBChannels.size(); i += limitCount) { + int toIndex = i + limitCount; + if (i + limitCount > commonGBChannels.size()) { + toIndex = commonGBChannels.size(); + } + result += commonGBChannelMapper.updateStatus(commonGBChannels.subList(i, toIndex)); + } + } else { + result += commonGBChannelMapper.updateStatus(commonGBChannels); + } + log.warn("[更新多个通道状态] 通道数量为{},成功保存:{}", commonGBChannels.size(), result); + // 发送通过更新通知 + try { + // 发送通知 + eventPublisher.channelEventPublishForUpdate(commonGBChannels, oldChanelListByChannels); + } catch (Exception e) { + log.warn("[更新多个通道] 发送失败,{}个", commonGBChannels.size(), e); + } + } + + + + @Override + public CommonGBChannel getOne(int id) { + return commonGBChannelMapper.queryById(id); + } + + @Override + public List getIndustryCodeList() { + IndustryCodeTypeEnum[] values = IndustryCodeTypeEnum.values(); + List result = new ArrayList<>(values.length); + for (IndustryCodeTypeEnum value : values) { + result.add(IndustryCodeType.getInstance(value)); + } + Collections.sort(result); + return result; + } + + @Override + public List getDeviceTypeList() { + DeviceTypeEnum[] values = DeviceTypeEnum.values(); + List result = new ArrayList<>(values.length); + for (DeviceTypeEnum value : values) { + result.add(DeviceType.getInstance(value)); + } + Collections.sort(result); + return result; + } + + @Override + public List getNetworkIdentificationTypeList() { + NetworkIdentificationTypeEnum[] values = NetworkIdentificationTypeEnum.values(); + List result = new ArrayList<>(values.length); + for (NetworkIdentificationTypeEnum value : values) { + result.add(NetworkIdentificationType.getInstance(value)); + } + Collections.sort(result); + return result; + } + + @Override + public void reset(int id, List chanelFields) { + log.info("[重置国标通道] id: {}", id); + Assert.notEmpty(chanelFields, "待重置字段为空"); + CommonGBChannel channel = getOne(id); + if (channel == null) { + log.warn("[重置国标通道] 未找到对应Id的通道: id: {}", id); + throw new ControllerException(ErrorCode.ERROR400); + } + if (channel.getDataType() != ChannelDataType.GB28181) { + log.warn("[重置国标通道] 非国标下级通道无法重置: id: {}", id); + throw new ControllerException(ErrorCode.ERROR100.getCode(), "非国标下级通道无法重置"); + } + List dbFields = new ArrayList<>(); + + for (String chanelField : chanelFields) { + BeanWrapperImpl wrapper = new BeanWrapperImpl(channel); + if (wrapper.isReadableProperty(chanelField)) { + dbFields.add(CaseFormat.LOWER_CAMEL.to(CaseFormat.LOWER_UNDERSCORE, chanelField)); + } + } + Assert.notEmpty(dbFields, "待重置字段为空"); + + // 这个多加一个参数,为了防止将非国标的通道通过此方法清空内容,导致意外发生 + commonGBChannelMapper.reset(id, dbFields, DateUtil.getNow()); + CommonGBChannel channelNew = getOne(id); + // 发送通过更新通知 + try { + // 发送通知 + eventPublisher.channelEventPublishForUpdate(channelNew, channel); + } catch (Exception e) { + log.warn("[通道移除通知] 发送失败,{}", channelNew.getGbDeviceId(), e); + } + } + + @Override + public PageInfo queryListByCivilCode(int page, int count, String query, Boolean online, Integer channelType, String civilCode) { + PageHelper.startPage(page, count); + if (query != null) { + query = query.replaceAll("/", "//") + .replaceAll("%", "/%") + .replaceAll("_", "/_"); + } + List all = commonGBChannelMapper.queryListByCivilCode(query, online, channelType, civilCode); + return new PageInfo<>(all); + } + + @Override + public PageInfo queryListByParentId(int page, int count, String query, Boolean online, Integer channelType, String groupDeviceId) { + PageHelper.startPage(page, count); + if (query != null) { + query = query.replaceAll("/", "//") + .replaceAll("%", "/%") + .replaceAll("_", "/_"); + } + List all = commonGBChannelMapper.queryListByParentId(query, online, channelType, groupDeviceId); + return new PageInfo<>(all); + } + + @Override + public void removeCivilCode(List allChildren) { + commonGBChannelMapper.removeCivilCode(allChildren); + // TODO 是否需要通知上级, 或者等添加新的行政区划时发送更新通知 + + } + + @Override + public void addChannelToRegion(String civilCode, List channelIds) { + List channelList = commonGBChannelMapper.queryByIds(channelIds); + if (channelList.isEmpty()) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "所有通道Id不存在"); + } + List channelListForOld = new ArrayList<>(channelList); + for (CommonGBChannel channel : channelList) { + channel.setGbCivilCode(civilCode); + } + int result = commonGBChannelMapper.updateRegion(civilCode, channelList); + // 发送通知 + if (result > 0) { + platformChannelService.checkRegionAdd(channelList); + try { + // 发送catalog + eventPublisher.channelEventPublishForUpdate(channelList, channelListForOld); + } catch (Exception e) { + log.warn("[多个通道添加行政区划] 发送失败,数量:{}", channelList.size(), e); + } + } + } + + @Override + @Transactional + public void deleteChannelToRegion(String civilCode, List channelIds) { + if (!ObjectUtils.isEmpty(civilCode)) { + deleteChannelToRegionByCivilCode(civilCode); + } + if (!ObjectUtils.isEmpty(channelIds)) { + deleteChannelToRegionByChannelIds(channelIds); + } + } + + @Override + public void deleteChannelToRegionByCivilCode(String civilCode) { + List channelList = commonGBChannelMapper.queryByCivilCode(civilCode); + if (channelList.isEmpty()) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "所有通道Id不存在"); + } + int result = commonGBChannelMapper.removeCivilCodeByChannels(channelList); + Region region = regionMapper.queryByDeviceId(civilCode); + if (region == null) { + platformChannelService.checkRegionRemove(channelList, null); + }else { + List regionList = new ArrayList<>(); + regionList.add(region); + platformChannelService.checkRegionRemove(channelList, regionList); + } + // TODO 发送通知 +// if (result > 0) { +// try { +// // 发送catalog +// eventPublisher.catalogEventPublish(null, channelList, CatalogEvent.UPDATE); +// }catch (Exception e) { +// log.warn("[多个通道添加行政区划] 发送失败,数量:{}", channelList.size(), e); +// } +// } + } + + @Override + public void deleteChannelToRegionByChannelIds(List channelIds) { + List channelList = commonGBChannelMapper.queryByIds(channelIds); + if (channelList.isEmpty()) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "所有通道Id不存在"); + } + int result = commonGBChannelMapper.removeCivilCodeByChannels(channelList); + + platformChannelService.checkRegionRemove(channelList, null); + // TODO 发送通知 +// if (result > 0) { +// try { +// // 发送catalog +// eventPublisher.catalogEventPublish(null, channelList, CatalogEvent.UPDATE); +// }catch (Exception e) { +// log.warn("[多个通道添加行政区划] 发送失败,数量:{}", channelList.size(), e); +// } +// } + } + + @Override + public void addChannelToRegionByGbDevice(String civilCode, List deviceIds) { + List channelList = commonGBChannelMapper.queryByDataTypeAndDeviceIds(ChannelDataType.GB28181, deviceIds); + if (channelList.isEmpty()) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "所有通道Id不存在"); + } + List channelListForOld = new ArrayList<>(channelList); + for (CommonGBChannel channel : channelList) { + channel.setGbCivilCode(civilCode); + } + int result = commonGBChannelMapper.updateRegion(civilCode, channelList); + // 发送通知 + if (result > 0) { + try { + // 发送catalog + eventPublisher.channelEventPublishForUpdate(channelList, channelListForOld); + } catch (Exception e) { + log.warn("[多个通道添加行政区划] 发送失败,数量:{}", channelList.size(), e); + } + } + } + + @Override + public void deleteChannelToRegionByGbDevice(List deviceIds) { + List channelList = commonGBChannelMapper.queryByDataTypeAndDeviceIds(ChannelDataType.GB28181, deviceIds); + if (channelList.isEmpty()) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "所有通道Id不存在"); + } + int result = commonGBChannelMapper.removeCivilCodeByChannels(channelList); + platformChannelService.checkRegionRemove(channelList, null); + } + + @Override + @Transactional + public void removeParentIdByBusinessGroup(String businessGroup) { + List channelList = commonGBChannelMapper.queryByBusinessGroup(businessGroup); + if (channelList.isEmpty()) { + return; + } + int result = commonGBChannelMapper.removeParentIdByChannels(channelList); + List groupList = groupMapper.queryByBusinessGroup(businessGroup); + platformChannelService.checkGroupRemove(channelList, groupList); + + } + + @Override + public void removeParentIdByGroupList(List groupList) { + List channelList = commonGBChannelMapper.queryByGroupList(groupList); + if (channelList.isEmpty()) { + return; + } + commonGBChannelMapper.removeParentIdByChannels(channelList); + platformChannelService.checkGroupRemove(channelList, groupList); + } + + @Override + public void updateBusinessGroup(String oldBusinessGroup, String newBusinessGroup) { + List channelList = commonGBChannelMapper.queryByBusinessGroup(oldBusinessGroup); + if (channelList.isEmpty()) { + log.info("[更新业务分组] 发现未关联任何通道: {}", oldBusinessGroup); + return; + } + List channelListForOld = new ArrayList<>(channelList); + int result = commonGBChannelMapper.updateBusinessGroupByChannelList(newBusinessGroup, channelList); + if (result > 0) { + for (CommonGBChannel channel : channelList) { + channel.setGbBusinessGroupId(newBusinessGroup); + } + // 发送catalog + try { + eventPublisher.channelEventPublishForUpdate(channelList, channelListForOld); + } catch (Exception e) { + log.warn("[多个通道业务分组] 发送失败,数量:{}", channelList.size(), e); + } + } + } + + @Override + public void updateParentIdGroup(String oldParentId, String newParentId) { + List channelList = commonGBChannelMapper.queryByParentId(oldParentId); + if (channelList.isEmpty()) { + return; + } + List channelListForOld = new ArrayList<>(channelList); + int result = commonGBChannelMapper.updateParentIdByChannelList(newParentId, channelList); + if (result > 0) { + for (CommonGBChannel channel : channelList) { + channel.setGbParentId(newParentId); + } + // 发送catalog + try { + eventPublisher.channelEventPublishForUpdate(channelList, channelListForOld); + } catch (Exception e) { + log.warn("[多个通道业务分组] 发送失败,数量:{}", channelList.size(), e); + } + } + } + + @Override + @Transactional + public void addChannelToGroup(String parentId, String businessGroup, List channelIds) { + List channelList = commonGBChannelMapper.queryByIds(channelIds); + if (channelList.isEmpty()) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "所有通道Id不存在"); + } + List channelListForOld = new ArrayList<>(channelList); + int result = commonGBChannelMapper.updateGroup(parentId, businessGroup, channelList); + for (CommonGBChannel commonGBChannel : channelList) { + commonGBChannel.setGbParentId(parentId); + commonGBChannel.setGbBusinessGroupId(businessGroup); + } + + // 发送通知 + if (result > 0) { + platformChannelService.checkGroupAdd(channelList); + try { + // 发送catalog + eventPublisher.channelEventPublishForUpdate(channelList, channelListForOld); + } catch (Exception e) { + log.warn("[多个通道添加行政区划] 发送失败,数量:{}", channelList.size(), e); + } + } + } + + @Override + public void deleteChannelToGroup(String parentId, String businessGroup, List channelIds) { + List channelList = commonGBChannelMapper.queryByIds(channelIds); + if (channelList.isEmpty()) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "所有通道Id不存在"); + } + commonGBChannelMapper.removeParentIdByChannels(channelList); + + Group group = groupMapper.queryOneByDeviceId(parentId, businessGroup); + if (group == null) { + platformChannelService.checkGroupRemove(channelList, null); + }else { + List groupList = new ArrayList<>(); + groupList.add(group); + platformChannelService.checkGroupRemove(channelList, groupList); + } + } + + @Override + @Transactional + public void addChannelToGroupByGbDevice(String parentId, String businessGroup, List deviceIds) { + List channelList = commonGBChannelMapper.queryByDataTypeAndDeviceIds(ChannelDataType.GB28181, deviceIds); + if (channelList.isEmpty()) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "所有通道Id不存在"); + } + List channelListForOld = new ArrayList<>(channelList); + + for (CommonGBChannel channel : channelList) { + channel.setGbParentId(parentId); + channel.setGbBusinessGroupId(businessGroup); + } + int result = commonGBChannelMapper.updateGroup(parentId, businessGroup, channelList); + + for (CommonGBChannel commonGBChannel : channelList) { + commonGBChannel.setGbParentId(parentId); + commonGBChannel.setGbBusinessGroupId(businessGroup); + } + // 发送通知 + if (result > 0) { + platformChannelService.checkGroupAdd(channelList); + try { + // 发送catalog + eventPublisher.channelEventPublishForUpdate(channelList, channelListForOld); + } catch (Exception e) { + log.warn("[多个通道添加行政区划] 发送失败,数量:{}", channelList.size(), e); + } + } + } + + @Override + public void deleteChannelToGroupByGbDevice(List deviceIds) { + List channelList = commonGBChannelMapper.queryByDataTypeAndDeviceIds(ChannelDataType.GB28181, deviceIds); + if (channelList.isEmpty()) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "所有通道Id不存在"); + } + commonGBChannelMapper.removeParentIdByChannels(channelList); + platformChannelService.checkGroupRemove(channelList, null); + } + + @Override + public CommonGBChannel queryOneWithPlatform(Integer platformId, String channelDeviceId) { + // 防止共享的通道编号重复 + List channelList = platformChannelMapper.queryOneWithPlatform(platformId, channelDeviceId); + if (!channelList.isEmpty()) { + return channelList.get(channelList.size() - 1); + }else { + return null; + } + } + + @Override + public void updateCivilCode(String oldCivilCode, String newCivilCode) { + List channelList = commonGBChannelMapper.queryByCivilCode(oldCivilCode); + if (channelList.isEmpty()) { + return; + } + List channelListForOld = new ArrayList<>(channelList); + int result = commonGBChannelMapper.updateCivilCodeByChannelList(newCivilCode, channelList); + if (result > 0) { + for (CommonGBChannel channel : channelList) { + channel.setGbCivilCode(newCivilCode); + } + // 发送catalog + try { + eventPublisher.channelEventPublishForUpdate(channelList, channelListForOld); + } catch (Exception e) { + log.warn("[多个通道业务分组] 发送失败,数量:{}", channelList.size(), e); + } + } + } + + @Override + public List queryListByStreamPushList(List streamPushList) { + return commonGBChannelMapper.queryListByStreamPushList(ChannelDataType.STREAM_PUSH, streamPushList); + } + + @Override + public PageInfo queryList(int page, int count, String query, Boolean online, Boolean hasRecordPlan, + Integer channelType, String civilCode, String parentDeviceId) { + PageHelper.startPage(page, count); + if (query != null) { + query = query.replaceAll("/", "//") + .replaceAll("%", "/%") + .replaceAll("_", "/_"); + } + List all = commonGBChannelMapper.queryList(query, online, hasRecordPlan, channelType, civilCode, parentDeviceId); + return new PageInfo<>(all); + } + + @Override + public PageInfo queryListByCivilCodeForUnusual(int page, int count, String query, Boolean online, Integer channelType) { + PageHelper.startPage(page, count); + if (query != null) { + query = query.replaceAll("/", "//") + .replaceAll("%", "/%") + .replaceAll("_", "/_"); + } + List all = commonGBChannelMapper.queryListByCivilCodeForUnusual(query, online, channelType); + return new PageInfo<>(all); + } + + @Override + public void clearChannelCivilCode(Boolean all, List channelIds) { + + List channelIdsForClear; + if (all != null && all) { + channelIdsForClear = commonGBChannelMapper.queryAllForUnusualCivilCode(); + }else { + channelIdsForClear = channelIds; + } + commonGBChannelMapper.removeCivilCodeByChannelIds(channelIdsForClear); + } + + @Override + public PageInfo queryListByParentForUnusual(int page, int count, String query, Boolean online, Integer channelType) { + PageHelper.startPage(page, count); + if (query != null) { + query = query.replaceAll("/", "//") + .replaceAll("%", "/%") + .replaceAll("_", "/_"); + } + List all = commonGBChannelMapper.queryListByParentForUnusual(query, online, channelType); + return new PageInfo<>(all); + } + + @Override + public void clearChannelParent(Boolean all, List channelIds) { + List channelIdsForClear; + if (all != null && all) { + channelIdsForClear = commonGBChannelMapper.queryAllForUnusualParent(); + }else { + channelIdsForClear = channelIds; + } + commonGBChannelMapper.removeParentIdByChannelIds(channelIdsForClear); + } + + @Override + public void updateGPSFromGPSMsgInfo(List gpsMsgInfoList) { + if (gpsMsgInfoList == null || gpsMsgInfoList.isEmpty()) { + return; + } + // 此处来源默认为WGS84, 所以直接入库 + commonGBChannelMapper.updateGpsByDeviceId(gpsMsgInfoList); +// +// Map gpsMsgInfoMap = new ConcurrentReferenceHashMap<>(); +// for (GPSMsgInfo gpsMsgInfo : gpsMsgInfoList) { +// gpsMsgInfoMap.put(gpsMsgInfo.getId(), gpsMsgInfo); +// } +// +// List channelList = commonGBChannelMapper.queryByGbDeviceIds(new ArrayList<>(gpsMsgInfoMap.keySet())); +// if (channelList.isEmpty()) { +// return; +// } +// channelList.forEach(commonGBChannel -> { +// MobilePosition mobilePosition = new MobilePosition(); +// mobilePosition.setDeviceId(commonGBChannel.getGbDeviceId()); +// mobilePosition.setChannelId(commonGBChannel.getGbId()); +// mobilePosition.setDeviceName(commonGBChannel.getGbName()); +// mobilePosition.setCreateTime(DateUtil.getNow()); +// mobilePosition.setTime(DateUtil.getNow()); +// mobilePosition.setLongitude(commonGBChannel.getGbLongitude()); +// mobilePosition.setLatitude(commonGBChannel.getGbLatitude()); +// eventPublisher.mobilePositionEventPublish(mobilePosition); +// }); + } + + @Transactional + @Override + public void updateGPS(List commonGBChannels) { + int limitCount = 1000; + if (commonGBChannels.size() > limitCount) { + for (int i = 0; i < commonGBChannels.size(); i += limitCount) { + int toIndex = i + limitCount; + if (i + limitCount > commonGBChannels.size()) { + toIndex = commonGBChannels.size(); + } + commonGBChannelMapper.updateGps(commonGBChannels.subList(i, toIndex)); + } + } else { + commonGBChannelMapper.updateGps(commonGBChannels); + } + } + + @Override + public List queryListForMap(String query, Boolean online, Boolean hasRecordPlan, Integer channelType) { + if (query != null) { + query = query.replaceAll("/", "//") + .replaceAll("%", "/%") + .replaceAll("_", "/_"); + } + return commonGBChannelMapper.queryList(query, online, hasRecordPlan, channelType, null, null); + } + + @Override + public CommonGBChannel queryCommonChannelByDeviceChannel(DeviceChannel channel) { + return commonGBChannelMapper.queryCommonChannelByDeviceChannel(channel.getDataType(), channel.getDataDeviceId(), channel.getDeviceId()); + } + + @Override + public void resetLevel() { + commonGBChannelMapper.resetLevel(); + } + + @Override + public byte[] getTile(int z, int x, int y, String geoCoordSys) { + double minLon = TileUtils.tile2lon(x, z); + double maxLon = TileUtils.tile2lon(x + 1, z); + double maxLat = TileUtils.tile2lat(y, z); + double minLat = TileUtils.tile2lat(y + 1, z); + + if (geoCoordSys != null) { + if (geoCoordSys.equalsIgnoreCase("GCJ02")) { + Double[] minPosition = Coordtransform.GCJ02ToWGS84(minLon, minLat); + minLon = minPosition[0]; + minLat = minPosition[1]; + + Double[] maxPosition = Coordtransform.GCJ02ToWGS84(maxLon, maxLat); + maxLon = maxPosition[0]; + maxLat = maxPosition[1]; + } + } + // 从数据库查询对应的数据 + List channelList = commonGBChannelMapper.queryCameraChannelInBox(minLon, maxLon, minLat, maxLat); + VectorTileEncoder encoder = new VectorTileEncoder(); + if (!channelList.isEmpty()) { + channelList.forEach(commonGBChannel -> { + double lon = commonGBChannel.getGbLongitude(); + double lat = commonGBChannel.getGbLatitude(); + // 转换为目标坐标系 + if (geoCoordSys != null) { + if (geoCoordSys.equalsIgnoreCase("GCJ02")) { + Double[] minPosition = Coordtransform.WGS84ToGCJ02(lon, lat); + lon = minPosition[0]; + lat = minPosition[1]; + } + } + + // 将 lon/lat 转为瓦片内像素坐标(0..256) + double[] px = TileUtils.lonLatToTilePixel(lon, lat, z, x, y); + Point pointGeom = geometryFactory.createPoint(new Coordinate(px[0], px[1])); + + BeanMap beanMap = BeanMapUtils.create(commonGBChannel); + encoder.addFeature("points", beanMap, pointGeom); + }); + } + return encoder.encode(); + } + + + @Override + public String drawThin(Map zoomParam, Extent extent, String geoCoordSys) { + long time = System.currentTimeMillis(); + + String id = UUID.randomUUID().toString(); + List channelListInExtent; + if (extent == null) { + log.info("[抽稀] ID: {}, 未设置范围,从数据库读取摄像头的范围", id); + extent = commonGBChannelMapper.queryExtent(); + channelListInExtent = commonGBChannelMapper.queryAllWithPosition(); + }else { + if (geoCoordSys != null && geoCoordSys.equalsIgnoreCase("GCJ02")) { + Double[] maxPosition = Coordtransform.GCJ02ToWGS84(extent.getMaxLng(), extent.getMaxLat()); + Double[] minPosition = Coordtransform.GCJ02ToWGS84(extent.getMinLng(), extent.getMinLat()); + + extent.setMaxLng(maxPosition[0]); + extent.setMaxLat(maxPosition[1]); + + extent.setMinLng(minPosition[0]); + extent.setMinLat(minPosition[1]); + } + // 获取数据源 + channelListInExtent = commonGBChannelMapper.queryListInExtent(extent.getMinLng(), extent.getMaxLng(), extent.getMinLat(), extent.getMaxLat()); + } + Assert.isTrue(!channelListInExtent.isEmpty(), "通道数据为空"); + + log.info("[开始抽稀] ID: {}, 范围,[{}, {}, {}, {}]", id, extent.getMinLng(), extent.getMinLat(), extent.getMaxLng(), extent.getMaxLat()); + + Extent finalExtent = extent; + // 记录进度 + saveProcess(id, 0, "开始抽稀"); + dynamicTask.startDelay(id, () -> { + try { + // 存储每层的抽稀结果, key为层级(zoom),value为摄像头数组 + Map> zoomCameraMap = new HashMap<>(); + + // 冗余一份已经处理过的摄像头的数据, 避免多次循环获取 + Map useCameraMap = new HashMap<>(); + AtomicReference process = new AtomicReference<>((double) 0); + for (Integer zoom : zoomParam.keySet()) { + + Map useCameraMapForZoom = new HashMap<>(); + Map cameraMapForZoom = new HashMap<>(); + + if (Objects.equals(zoom, Collections.max(zoomParam.keySet()))) { + // 最大层级不进行抽稀, 将未进行抽稀的数据直接存储到这个层级 + for (CommonGBChannel channel : channelListInExtent) { + if (!useCameraMap.containsKey(channel.getGbId())) { + channel.setMapLevel(zoom); + // 这个的key跟后面的不一致是因为无需抽稀, 直接存储原始数据 + cameraMapForZoom.put(channel.getGbId() + "", channel); + useCameraMap.put(channel.getGbId(), channel); + } + } + }else { + Double diff = zoomParam.get(zoom); + // 对这个层级展开抽稀 + log.info("[抽稀] ID:{},当前层级: {}, 坐标间隔: {}", id, zoom, diff); + + // 更新上级图层的数据到当前层级,确保当前层级展示时考虑到之前层级的数据 + for (CommonGBChannel channel : useCameraMap.values()) { + int lngGrid = (int)(channel.getGbLongitude() / diff); + int latGrid = (int)(channel.getGbLatitude() / diff); + String gridKey = latGrid + ":" + lngGrid; + useCameraMapForZoom.put(gridKey, channel); + } + + // 对数据开始执行抽稀 + for (CommonGBChannel channel : channelListInExtent) { + // 已经分配再其他层级的,本层级不再使用 + if (useCameraMap.containsKey(channel.getGbId())) { + continue; + } + int lngGrid = (int)(channel.getGbLongitude() / diff); + int latGrid = (int)(channel.getGbLatitude() / diff); + // 数据网格Id + String gridKey = latGrid + ":" + lngGrid; + if (useCameraMapForZoom.containsKey(gridKey)) { + continue; + } + if (cameraMapForZoom.containsKey(gridKey)) { + CommonGBChannel oldChannel = cameraMapForZoom.get(gridKey); + // 如果一个网格存在多个数据,则选择最接近中心点的, 目前只选择了经度方向作为参考 + if (channel.getGbLongitude() % diff < oldChannel.getGbLongitude() % diff) { + channel.setMapLevel(zoom); + cameraMapForZoom.put(gridKey, channel); + useCameraMap.put(channel.getGbId(), channel); + useCameraMap.remove(oldChannel.getGbId()); + oldChannel.setMapLevel(null); + } + }else { + channel.setMapLevel(zoom); + cameraMapForZoom.put(gridKey, channel); + useCameraMap.put(channel.getGbId(), channel); + } + } + } + // 存储 + zoomCameraMap.put(zoom, cameraMapForZoom.values()); + process.updateAndGet(v -> (v + 0.5 / zoomParam.size())); + saveProcess(id, process.get(), "抽稀图层: " + zoom); + } + + // 抽稀完成, 对数据发布mvt矢量瓦片 + List beforeData = new ArrayList<>(); + for (Integer zoom : zoomCameraMap.keySet()) { + beforeData.addAll(zoomCameraMap.get(zoom)); + log.info("[抽稀-发布mvt矢量瓦片] ID:{},当前层级: {}", id, zoom); + // 按照 z/x/y 数据组织数据, 矢量数据暂时保存在内存中 + // 按照范围生成 x y范围, + saveTile(id, zoom, "WGS84", beforeData); + saveTile(id, zoom, "GCJ02", beforeData); + process.updateAndGet(v -> (v + 0.5 / zoomParam.size())); + saveProcess(id, process.get(), "发布矢量瓦片: " + zoom); + } + // 记录原始数据,未保存做准备 + vectorTileCatch.addSource(id, new ArrayList<>(useCameraMap.values())); + + log.info("[抽稀完成] ID:{}, 耗时: {}ms", id, (System.currentTimeMillis() - time)); + saveProcess(id, 1, "抽稀完成"); + } catch (Exception e) { + log.info("[抽稀] 失败 ID:{}", id, e); + } + + }, 1); + + return id; + } + + private void saveTile(String id, int z, String geoCoordSys, Collection commonGBChannelList ) { + Map encoderMap = new HashMap<>(); + commonGBChannelList.forEach(commonGBChannel -> { + double lon = commonGBChannel.getGbLongitude(); + double lat = commonGBChannel.getGbLatitude(); + if (geoCoordSys != null && geoCoordSys.equalsIgnoreCase("GCJ02")) { + Double[] minPosition = Coordtransform.WGS84ToGCJ02(lon, lat); + lon = minPosition[0]; + lat = minPosition[1]; + } + double[] doubles = TileUtils.lonLatToTileXY(lon, lat, z); + int x = (int) doubles[0]; + int y = (int) doubles[1]; + String key = z + "_" + x + "_" + y + "_" + geoCoordSys; + VectorTileEncoder encoder =encoderMap.get(key); + if (encoder == null) { + encoder = new VectorTileEncoder(); + encoderMap.put(key, encoder); + } + // 将 lon/lat 转为瓦片内像素坐标(0..256) + double[] px = TileUtils.lonLatToTilePixel(lon, lat, z, x, y); + Point pointGeom = geometryFactory.createPoint(new Coordinate(px[0], px[1])); + BeanMap beanMap = BeanMapUtils.create(commonGBChannel); + encoder.addFeature("points", beanMap, pointGeom); + }); + encoderMap.forEach((key, encoder) -> { + vectorTileCatch.addVectorTile(id, key, encoder.encode()); + }); + } + + private void saveProcess(String id, double process, String msg) { + String key = VideoManagerConstants.DRAW_THIN_PROCESS_PREFIX + id; + Duration duration = Duration.ofMinutes(30); + redisTemplate.opsForValue().set(key, new DrawThinProcess(process, msg), duration); + } + + @Override + public DrawThinProcess thinProgress(String id) { + String key = VideoManagerConstants.DRAW_THIN_PROCESS_PREFIX + id; + return (DrawThinProcess) redisTemplate.opsForValue().get(key); + } + + @Override + @Transactional + public void saveThin(String id) { + commonGBChannelMapper.resetLevel(); + List channelList = vectorTileCatch.getChannelList(id); + if (channelList != null && !channelList.isEmpty()) { + int limitCount = 1000; + if (channelList.size() > limitCount) { + for (int i = 0; i < channelList.size(); i += limitCount) { + int toIndex = i + limitCount; + if (i + limitCount > channelList.size()) { + toIndex = channelList.size(); + } + commonGBChannelMapper.saveLevel(channelList.subList(i, toIndex)); + } + } else { + commonGBChannelMapper.saveLevel(channelList); + } + } + vectorTileCatch.save(id); + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/service/impl/GroupServiceImpl.java b/src/main/java/com/genersoft/iot/vmp/gb28181/service/impl/GroupServiceImpl.java new file mode 100644 index 0000000..fbbc1da --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/service/impl/GroupServiceImpl.java @@ -0,0 +1,334 @@ +package com.genersoft.iot.vmp.gb28181.service.impl; + +import com.genersoft.iot.vmp.common.VideoManagerConstants; +import com.genersoft.iot.vmp.conf.exception.ControllerException; +import com.genersoft.iot.vmp.gb28181.bean.*; +import com.genersoft.iot.vmp.gb28181.dao.CommonGBChannelMapper; +import com.genersoft.iot.vmp.gb28181.dao.GroupMapper; +import com.genersoft.iot.vmp.gb28181.event.EventPublisher; +import com.genersoft.iot.vmp.gb28181.event.subscribe.catalog.CatalogEvent; +import com.genersoft.iot.vmp.gb28181.service.IGbChannelService; +import com.genersoft.iot.vmp.gb28181.service.IGroupService; +import com.genersoft.iot.vmp.utils.DateUtil; +import com.genersoft.iot.vmp.vmanager.bean.ErrorCode; +import com.github.pagehelper.PageHelper; +import com.github.pagehelper.PageInfo; +import com.google.common.collect.Lists; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.CommandLineRunner; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; + +import java.util.*; + +/** + * 区域管理类 + */ +@Service +@Slf4j +public class GroupServiceImpl implements IGroupService, CommandLineRunner { + + @Autowired + private GroupMapper groupManager; + + @Autowired + private CommonGBChannelMapper commonGBChannelMapper; + + @Autowired + private IGbChannelService gbChannelService; + + @Autowired + private EventPublisher eventPublisher; + + @Autowired + private RedisTemplate redisTemplate; + + // 启动后请求组织结构同步 + @Override + public void run(String... args) throws Exception { + String key = VideoManagerConstants.VM_MSG_GROUP_LIST_REQUEST; + log.info("[redis发送通知] 发送 同步组织结构请求 {}", key); + redisTemplate.convertAndSend(key, ""); + } + + @Override + public void add(Group group) { + Assert.notNull(group, "参数不可为NULL"); + Assert.notNull(group.getDeviceId(), "分组编号不可为NULL"); + Assert.isTrue(group.getDeviceId().trim().length() == 20, "分组编号必须为20位"); + Assert.notNull(group.getName(), "分组名称不可为NULL"); + + GbCode gbCode = GbCode.decode(group.getDeviceId()); + Assert.notNull(gbCode, "分组编号不满足国标定义"); + + // 查询数据库中已经存在的. + List groupListInDb = groupManager.queryInGroupListByDeviceId(Lists.newArrayList(group)); + if (!ObjectUtils.isEmpty(groupListInDb)){ + throw new ControllerException(ErrorCode.ERROR100.getCode(), String.format("该节点编号 %s 已存在", group.getDeviceId())); + } + + if ("215".equals(gbCode.getTypeCode())){ + // 添加业务分组 + addBusinessGroup(group); + }else { + Assert.isTrue("216".equals(gbCode.getTypeCode()), "创建虚拟组织时设备编号11-13位应使用216"); + // 添加虚拟组织 + addGroup(group); + } + } + + private void addGroup(Group group) { + // 建立虚拟组织 + Assert.notNull(group.getBusinessGroup(), "所属的业务分组分组不存在"); + Group businessGroup = groupManager.queryBusinessGroup(group.getBusinessGroup()); + Assert.notNull(businessGroup, "所属的业务分组分组不存在"); + if (!ObjectUtils.isEmpty(group.getParentDeviceId())) { + Group parentGroup = groupManager.queryOneByDeviceId(group.getParentDeviceId(), group.getBusinessGroup()); + Assert.notNull(parentGroup, "所属的上级分组分组不存在"); + }else { + group.setParentDeviceId(null); + } + group.setCreateTime(DateUtil.getNow()); + group.setUpdateTime(DateUtil.getNow()); + groupManager.add(group); + } + + private void addBusinessGroup(Group group) { + group.setBusinessGroup(group.getDeviceId()); + group.setCreateTime(DateUtil.getNow()); + group.setUpdateTime(DateUtil.getNow()); + groupManager.addBusinessGroup(group); + } + + @Override + public List queryAllChildren(Integer id) { + List children = groupManager.getChildren(id); + if (ObjectUtils.isEmpty(children)) { + return children; + } + for (int i = 0; i < children.size(); i++) { + children.addAll(queryAllChildren(children.get(i).getId())); + } + return children; + } + + @Override + @Transactional + public void update(Group group) { + Assert.isTrue(group.getId()> 0, "更新必须携带分组ID"); + Assert.notNull(group.getDeviceId(), "编号不可为NULL"); + Assert.notNull(group.getBusinessGroup(), "业务分组不可为NULL"); + Group groupInDb = groupManager.queryOne(group.getId()); + Assert.notNull(groupInDb, "分组不存在"); + + // 查询数据库中已经存在的. + List groupListInDb = groupManager.queryInGroupListByDeviceId(Lists.newArrayList(group)); + if (!ObjectUtils.isEmpty(groupListInDb) && groupListInDb.get(0).getId() != group.getId()){ + throw new ControllerException(ErrorCode.ERROR100.getCode(), String.format("该该节点编号 %s 已存在", group.getDeviceId())); + } + + group.setName(group.getName()); + group.setUpdateTime(DateUtil.getNow()); + groupManager.update(group); + // 修改他的子节点 + if (!group.getDeviceId().equals(groupInDb.getDeviceId()) + || !group.getBusinessGroup().equals(groupInDb.getBusinessGroup())) { + List groupList = queryAllChildren(groupInDb.getId()); + if (!groupList.isEmpty()) { + int result = groupManager.updateChild(groupInDb.getId(), group); + if (result > 0) { + for (Group chjildGroup : groupList) { + chjildGroup.setParentDeviceId(group.getDeviceId()); + chjildGroup.setBusinessGroup(group.getBusinessGroup()); + // 将变化信息发送通知 + CommonGBChannel channel = CommonGBChannel.build(chjildGroup); + try { + // 发送catalog + eventPublisher.channelEventPublishForUpdate(channel, null); + }catch (Exception e) { + log.warn("[业务分组/虚拟组织变化] 发送失败,{}", group.getDeviceId(), e); + } + } + } + } + } + // 将变化信息发送通知 + CommonGBChannel channel = CommonGBChannel.build(group); + try { + // 发送catalog + eventPublisher.channelEventPublishForUpdate(channel, null); + }catch (Exception e) { + log.warn("[业务分组/虚拟组织变化] 发送失败,{}", group.getDeviceId(), e); + } + + // 由于编号变化,会需要处理太多内容以及可能发送大量消息,所以目前更新只只支持重命名 + GbCode decode = GbCode.decode(group.getDeviceId()); + if (!groupInDb.getDeviceId().equals(group.getDeviceId())) { + if (decode.getTypeCode().equals("215")) { + // 业务分组变化。需要将其下的所有业务分组修改 + gbChannelService.updateBusinessGroup(groupInDb.getDeviceId(), group.getDeviceId()); + }else { + // 虚拟组织修改,需要把其下的子节点修改父节点ID + gbChannelService.updateParentIdGroup(groupInDb.getDeviceId(), group.getDeviceId()); + } + } + } + + @Override + public Group queryGroupByDeviceId(String regionDeviceId) { + return groupManager.queryOneByOnlyDeviceId(regionDeviceId); + } + + @Override + public List queryForTree(String query, Integer parentId, Boolean hasChannel) { + + List groupTrees = groupManager.queryForTree(query, parentId); + if (parentId == null) { + return groupTrees; + } + // 查询含有的通道 + Group parentGroup = groupManager.queryOne(parentId); + if (parentGroup != null && hasChannel != null && hasChannel) { + List groupTreesForChannel = commonGBChannelMapper.queryForGroupTreeByParentId(query, parentGroup.getDeviceId()); + if (!ObjectUtils.isEmpty(groupTreesForChannel)) { + groupTrees.addAll(groupTreesForChannel); + } + } + return groupTrees; + } + + @Override + @Transactional + public boolean delete(int id) { + Group group = groupManager.queryOne(id); + Assert.notNull(group, "分组不存在"); + List groupListForDelete = new ArrayList<>(); + GbCode gbCode = GbCode.decode(group.getDeviceId()); + if (gbCode.getTypeCode().equals("215")) { + List groupList = groupManager.queryByBusinessGroup(group.getDeviceId()); + if (!groupList.isEmpty()) { + groupListForDelete.addAll(groupList); + } + // 业务分组 + gbChannelService.removeParentIdByBusinessGroup(group.getDeviceId()); + }else { + List groupList = queryAllChildren(group.getId()); + if (!groupList.isEmpty()) { + groupListForDelete.addAll(groupList); + } + groupListForDelete.add(group); + gbChannelService.removeParentIdByGroupList(groupListForDelete); + } + groupManager.batchDelete(groupListForDelete); + + for (Group groupForDelete : groupListForDelete) { + // 删除平台关联的分组信息。同时发送通知 + List platformList = groupManager.queryForPlatformByGroupId(groupForDelete.getId()); + if ( !platformList.isEmpty()) { + groupManager.deletePlatformGroup(groupForDelete.getId()); + // 将变化信息发送通知 + CommonGBChannel channel = CommonGBChannel.build(groupForDelete); + for (Platform platform : platformList) { + try { + // 发送catalog + eventPublisher.catalogEventPublish(platform, channel, CatalogEvent.DEL); + }catch (Exception e) { + log.warn("[业务分组/虚拟组织删除] 发送失败,{}", groupForDelete.getDeviceId(), e); + } + } + } + } + return true; + } + + @Override + @Transactional + public boolean batchAdd(List groupList) { + if (groupList== null || groupList.isEmpty()) { + return false; + } + Map groupMapForVerification = new HashMap<>(); + for (Group group : groupList) { + groupMapForVerification.put(group.getDeviceId(), group); + } + // 查询数据库中已经存在的. + List groupListInDb = groupManager.queryInGroupListByDeviceId(groupList); + if (!groupListInDb.isEmpty()) { + for (Group group : groupListInDb) { + groupMapForVerification.remove(group.getDeviceId()); + } + } + if (!groupMapForVerification.isEmpty()) { + List groupListForAdd = new ArrayList<>(groupMapForVerification.values()); + groupManager.batchAdd(groupListForAdd); + // 更新分组关系 + groupManager.updateParentId(groupListForAdd); + groupManager.updateParentIdWithBusinessGroup(groupListForAdd); + } + + return true; + } + + @Override + public List getPath(String deviceId, String businessGroup) { + Group businessGroupInDb = groupManager.queryBusinessGroup(businessGroup); + if (businessGroupInDb == null) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "业务分组不存在"); + } + List groupList = new LinkedList<>(); + groupList.add(businessGroupInDb); + Group group = groupManager.queryOneByDeviceId(deviceId, businessGroup); + if (group == null) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "虚拟组织不存在"); + } + List allParent = getAllParent(group); + groupList.addAll(allParent); + groupList.add(group); + return groupList; + } + + private List getAllParent(Group group) { + if (group.getParentId() == null || group.getBusinessGroup() == null) { + return new ArrayList<>(); + } + + List groupList = new ArrayList<>(); + Group parent = groupManager.queryOneByDeviceId(group.getParentDeviceId(), group.getBusinessGroup()); + if (parent == null) { + return groupList; + } + List allParent = getAllParent(parent); + allParent.add(parent); + return allParent; + } + + @Override + public PageInfo queryList(Integer page, Integer count, String query) { + PageHelper.startPage(page, count); + if (query != null) { + query = query.replaceAll("/", "//") + .replaceAll("%", "/%") + .replaceAll("_", "/_"); + } + List all = groupManager.query(query, null, null); + return new PageInfo<>(all); + } + + @Override + public Group queryGroupByAlias(String groupAlias) { + return groupManager.queryGroupByAlias(groupAlias); + } + + @Override + public void sync() { + try { + this.run(); + }catch (Exception e) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "同步失败: " + e.getMessage()); + } + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/service/impl/InviteStreamServiceImpl.java b/src/main/java/com/genersoft/iot/vmp/gb28181/service/impl/InviteStreamServiceImpl.java new file mode 100755 index 0000000..c3c7680 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/service/impl/InviteStreamServiceImpl.java @@ -0,0 +1,354 @@ +package com.genersoft.iot.vmp.gb28181.service.impl; + +import com.alibaba.fastjson2.JSON; +import com.genersoft.iot.vmp.common.*; +import com.genersoft.iot.vmp.conf.UserSetting; +import com.genersoft.iot.vmp.gb28181.bean.Device; +import com.genersoft.iot.vmp.gb28181.dao.DeviceChannelMapper; +import com.genersoft.iot.vmp.gb28181.dao.DeviceMapper; +import com.genersoft.iot.vmp.gb28181.service.IInviteStreamService; +import com.genersoft.iot.vmp.media.event.media.MediaDepartureEvent; +import com.genersoft.iot.vmp.service.bean.ErrorCallback; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.event.EventListener; +import org.springframework.data.redis.core.Cursor; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.core.ScanOptions; +import org.springframework.scheduling.annotation.Async; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CopyOnWriteArrayList; + +@Slf4j +@Service +public class InviteStreamServiceImpl implements IInviteStreamService { + + private final Map>> inviteErrorCallbackMap = new ConcurrentHashMap<>(); + + @Autowired + private RedisTemplate redisTemplate; + + @Autowired + private UserSetting userSetting; + + @Autowired + private DeviceMapper deviceMapper; + + @Autowired + private DeviceChannelMapper deviceChannelMapper; + + /** + * 流离开的处理 + */ + @Async("taskExecutor") + @EventListener + public void onApplicationEvent(MediaDepartureEvent event) { + if ("rtsp".equals(event.getSchema()) && "rtp".equals(event.getApp())) { + InviteInfo inviteInfo = getInviteInfoByStream(null, event.getStream()); + if (inviteInfo != null && (inviteInfo.getType() == InviteSessionType.PLAY || inviteInfo.getType() == InviteSessionType.PLAYBACK)) { + removeInviteInfo(inviteInfo); + Device device = deviceMapper.getDeviceByDeviceId(inviteInfo.getDeviceId()); + if (device != null) { + deviceChannelMapper.stopPlayById(inviteInfo.getChannelId()); + } + } + } + } + + @Override + public void updateInviteInfo(InviteInfo inviteInfo) { + if (InviteSessionStatus.ready == inviteInfo.getStatus()) { + updateInviteInfo(inviteInfo, Long.valueOf(userSetting.getPlayTimeout()) * 2); + } else { + updateInviteInfo(inviteInfo, null); + } + } + + @Override + public void updateInviteInfo(InviteInfo inviteInfo, Long time) { + if (inviteInfo == null || (inviteInfo.getDeviceId() == null || inviteInfo.getChannelId() == null)) { + log.warn("[更新Invite信息],参数不全: {}", JSON.toJSON(inviteInfo)); + return; + } + InviteInfo inviteInfoForUpdate; + + if (InviteSessionStatus.ready == inviteInfo.getStatus()) { + if (inviteInfo.getDeviceId() == null || inviteInfo.getChannelId() == null + || inviteInfo.getType() == null || inviteInfo.getStream() == null + ) { + return; + } + inviteInfoForUpdate = inviteInfo; + } else { + InviteInfo inviteInfoInRedis = getInviteInfo(inviteInfo.getType(), inviteInfo.getChannelId(), inviteInfo.getStream()); + if (inviteInfoInRedis == null) { + log.warn("[更新Invite信息],未从缓存中读取到Invite信息: deviceId: {}, channel: {}, stream: {}", + inviteInfo.getDeviceId(), inviteInfo.getChannelId(), inviteInfo.getStream()); + return; + } + if (inviteInfo.getStreamInfo() != null) { + inviteInfoInRedis.setStreamInfo(inviteInfo.getStreamInfo()); + } + if (inviteInfo.getSsrcInfo() != null) { + inviteInfoInRedis.setSsrcInfo(inviteInfo.getSsrcInfo()); + } + if (inviteInfo.getStreamMode() != null) { + inviteInfoInRedis.setStreamMode(inviteInfo.getStreamMode()); + } + if (inviteInfo.getReceiveIp() != null) { + inviteInfoInRedis.setReceiveIp(inviteInfo.getReceiveIp()); + } + if (inviteInfo.getReceivePort() != null) { + inviteInfoInRedis.setReceivePort(inviteInfo.getReceivePort()); + } + if (inviteInfo.getStatus() != null) { + inviteInfoInRedis.setStatus(inviteInfo.getStatus()); + } + + inviteInfoForUpdate = inviteInfoInRedis; + + } + if (inviteInfoForUpdate.getCreateTime() == null) { + inviteInfoForUpdate.setCreateTime(System.currentTimeMillis()); + } + String key = VideoManagerConstants.INVITE_PREFIX; + String objectKey = inviteInfoForUpdate.getType() + + ":" + inviteInfoForUpdate.getChannelId() + + ":" + inviteInfoForUpdate.getStream(); + if (time != null && time > 0) { + inviteInfoForUpdate.setExpirationTime(time); + } + redisTemplate.opsForHash().put(key, objectKey, inviteInfoForUpdate); + } + + @Override + public InviteInfo updateInviteInfoForStream(InviteInfo inviteInfo, String stream) { + + InviteInfo inviteInfoInDb = getInviteInfo(inviteInfo.getType(), inviteInfo.getChannelId(), inviteInfo.getStream()); + if (inviteInfoInDb == null) { + return null; + } + removeInviteInfo(inviteInfoInDb); + String key = VideoManagerConstants.INVITE_PREFIX; + String objectKey = inviteInfo.getType() + + ":" + inviteInfo.getChannelId() + + ":" + stream; + inviteInfoInDb.setStream(stream); + if (inviteInfoInDb.getSsrcInfo() != null) { + inviteInfoInDb.getSsrcInfo().setStream(stream); + } + if (InviteSessionStatus.ready == inviteInfo.getStatus()) { + inviteInfoInDb.setExpirationTime((long) (userSetting.getPlayTimeout() * 2)); + } + if (inviteInfoInDb.getCreateTime() == null) { + inviteInfoInDb.setCreateTime(System.currentTimeMillis()); + } + redisTemplate.opsForHash().put(key, objectKey, inviteInfoInDb); + return inviteInfoInDb; + } + + @Override + public InviteInfo getInviteInfo(InviteSessionType type, Integer channelId, String stream) { + String key = VideoManagerConstants.INVITE_PREFIX; + String keyPattern = (type != null ? type : "*") + + ":" + (channelId != null ? channelId : "*") + + ":" + (stream != null ? stream : "*"); + ScanOptions options = ScanOptions.scanOptions().match(keyPattern).count(20).build(); + try (Cursor> cursor = redisTemplate.opsForHash().scan(key, options)) { + if (cursor.hasNext()) { + InviteInfo inviteInfo = (InviteInfo) cursor.next().getValue(); + cursor.close(); + return inviteInfo; + + } + } catch (Exception e) { + log.error("[Redis-InviteInfo] 查询异常: ", e); + } + return null; + } + + @Override + public List getAllInviteInfo() { + List result = new ArrayList<>(); + String key = VideoManagerConstants.INVITE_PREFIX; + List values = redisTemplate.opsForHash().values(key); + if(values.isEmpty()) { + return result; + } + for (Object value : values) { + result.add((InviteInfo)value); + } + return result; + } + + @Override + public InviteInfo getInviteInfoByDeviceAndChannel(InviteSessionType type, Integer channelId) { + return getInviteInfo(type, channelId, null); + } + + @Override + public InviteInfo getInviteInfoByStream(InviteSessionType type, String stream) { + return getInviteInfo(type, null, stream); + } + + @Override + public void removeInviteInfo(InviteSessionType type, Integer channelId, String stream) { + String key = VideoManagerConstants.INVITE_PREFIX; + if (type == null && channelId == null && stream == null) { + redisTemplate.opsForHash().delete(key); + return; + } + InviteInfo inviteInfo = getInviteInfo(type, channelId, stream); + if (inviteInfo != null) { + String objectKey = inviteInfo.getType() + + ":" + inviteInfo.getChannelId() + + ":" + inviteInfo.getStream(); + redisTemplate.opsForHash().delete(key, objectKey); + } + } + + @Override + public void removeInviteInfoByDeviceAndChannel(InviteSessionType inviteSessionType, Integer channelId) { + removeInviteInfo(inviteSessionType, channelId, null); + } + + @Override + public void removeInviteInfo(InviteInfo inviteInfo) { + removeInviteInfo(inviteInfo.getType(), inviteInfo.getChannelId(), inviteInfo.getStream()); + } + + @Override + public void once(InviteSessionType type, Integer channelId, String stream, ErrorCallback callback) { + String key = buildKey(type, channelId, stream); + List> callbacks = inviteErrorCallbackMap.computeIfAbsent(key, k -> new CopyOnWriteArrayList<>()); + callbacks.add(callback); + + } + + private String buildKey(InviteSessionType type, Integer channelId, String stream) { + String key = type + ":" + channelId; + // 如果ssrc未null那么可以实现一个通道只能一次操作,ssrc不为null则可以支持一个通道多次invite + if (stream != null) { + key += (":" + stream); + } + return key; + } + + + @Override + public void clearInviteInfo(String deviceId) { + List inviteInfoList = getAllInviteInfo(); + for (InviteInfo inviteInfo : inviteInfoList) { + if (inviteInfo.getDeviceId().equals(deviceId)) { + removeInviteInfo(inviteInfo); + } + } + } + + @Override + public int getStreamInfoCount(String mediaServerId) { + int count = 0; + String key = VideoManagerConstants.INVITE_PREFIX; + List values = redisTemplate.opsForHash().values(key); + if (values.isEmpty()) { + return count; + } + for (Object value : values) { + InviteInfo inviteInfo = (InviteInfo)value; + if (inviteInfo != null + && inviteInfo.getStreamInfo() != null + && inviteInfo.getStreamInfo().getMediaServer() != null + && inviteInfo.getStreamInfo().getMediaServer().getId().equals(mediaServerId)) { + if (inviteInfo.getType().equals(InviteSessionType.DOWNLOAD) && inviteInfo.getStreamInfo().getProgress() == 1) { + continue; + } + count++; + } + } + return count; + } + + @Override + public void call(InviteSessionType type, Integer channelId, String stream, int code, String msg, StreamInfo data) { + String key = buildSubStreamKey(type, channelId, stream); + List> callbacks = inviteErrorCallbackMap.get(key); + if (callbacks == null || callbacks.isEmpty()) { + return; + } + for (ErrorCallback callback : callbacks) { + if (callback != null) { + callback.run(code, msg, data); + } + } + inviteErrorCallbackMap.remove(key); + } + + + private String buildSubStreamKey(InviteSessionType type, Integer channelId, String stream) { + String key = type + ":" + channelId; + if (stream != null) { + key += (":" + stream); + } + return key; + } + + @Override + public InviteInfo getInviteInfoBySSRC(String ssrc) { + List inviteInfoList = getAllInviteInfo(); + if (inviteInfoList.isEmpty()) { + return null; + } + for (InviteInfo inviteInfo : inviteInfoList) { + if (inviteInfo.getSsrcInfo() != null && ssrc.equals(inviteInfo.getSsrcInfo().getSsrc())) { + return inviteInfo; + } + } + return null; + } + + @Override + public InviteInfo updateInviteInfoForSSRC(InviteInfo inviteInfo, String ssrc) { + InviteInfo inviteInfoInDb = getInviteInfo(inviteInfo.getType(), inviteInfo.getChannelId(), inviteInfo.getStream()); + if (inviteInfoInDb == null) { + return null; + } + removeInviteInfo(inviteInfoInDb); + String key = VideoManagerConstants.INVITE_PREFIX; + String objectKey = inviteInfo.getType() + + ":" + inviteInfo.getChannelId() + + ":" + inviteInfo.getStream(); + if (inviteInfoInDb.getSsrcInfo() != null) { + inviteInfoInDb.getSsrcInfo().setSsrc(ssrc); + } + redisTemplate.opsForHash().put(key, objectKey, inviteInfoInDb); + return inviteInfoInDb; + } + + @Scheduled(fixedRate = 10000) //定时检测,清理错误的redis数据,防止因为错误数据导致的点播不可用 + public void execute(){ + String key = VideoManagerConstants.INVITE_PREFIX; + if(redisTemplate.opsForHash().size(key) == 0) { + return; + } + List values = redisTemplate.opsForHash().values(key); + for (Object value : values) { + InviteInfo inviteInfo = (InviteInfo)value; + if (inviteInfo.getStreamInfo() != null) { + continue; + } + if (inviteInfo.getCreateTime() == null || inviteInfo.getExpirationTime() == 0) { + removeInviteInfo(inviteInfo); + } + long time = inviteInfo.getCreateTime() + inviteInfo.getExpirationTime(); + if (System.currentTimeMillis() > time) { + removeInviteInfo(inviteInfo); + } + } + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/service/impl/PTZServiceImpl.java b/src/main/java/com/genersoft/iot/vmp/gb28181/service/impl/PTZServiceImpl.java new file mode 100644 index 0000000..011331f --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/service/impl/PTZServiceImpl.java @@ -0,0 +1,111 @@ +package com.genersoft.iot.vmp.gb28181.service.impl; + +import com.genersoft.iot.vmp.common.enums.ChannelDataType; +import com.genersoft.iot.vmp.conf.UserSetting; +import com.genersoft.iot.vmp.conf.exception.ControllerException; +import com.genersoft.iot.vmp.gb28181.bean.CommonGBChannel; +import com.genersoft.iot.vmp.gb28181.bean.Device; +import com.genersoft.iot.vmp.gb28181.bean.DeviceChannel; +import com.genersoft.iot.vmp.gb28181.bean.Preset; +import com.genersoft.iot.vmp.gb28181.service.IDeviceChannelService; +import com.genersoft.iot.vmp.gb28181.service.IDeviceService; +import com.genersoft.iot.vmp.gb28181.service.IPTZService; +import com.genersoft.iot.vmp.gb28181.transmit.cmd.impl.SIPCommander; +import com.genersoft.iot.vmp.service.bean.ErrorCallback; +import com.genersoft.iot.vmp.service.redisMsg.IRedisRpcPlayService; +import com.genersoft.iot.vmp.vmanager.bean.ErrorCode; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.util.Assert; + +import javax.sip.InvalidArgumentException; +import javax.sip.SipException; +import java.text.ParseException; +import java.util.List; + +@Slf4j +@Service +public class PTZServiceImpl implements IPTZService { + + + @Autowired + private SIPCommander cmder; + + @Autowired + private UserSetting userSetting; + + @Autowired + private IRedisRpcPlayService redisRpcPlayService; + + @Autowired + private IDeviceChannelService deviceChannelService; + + @Autowired + private IDeviceService deviceService; + + + @Override + public void ptz(Device device, String channelId, int cmdCode, int horizonSpeed, int verticalSpeed, int zoomSpeed) { + try { + cmder.frontEndCmd(device, channelId, cmdCode, horizonSpeed, verticalSpeed, zoomSpeed); + } catch (SipException | InvalidArgumentException | ParseException e) { + log.error("[命令发送失败] 云台控制: {}", e.getMessage()); + throw new ControllerException(ErrorCode.ERROR100.getCode(), "命令发送失败: " + e.getMessage()); + } + } + + @Override + public void frontEndCommand(Device device, String channelId, int cmdCode, int parameter1, int parameter2, int combindCode2) { + // 判断设备是否属于当前平台, 如果不属于则发起自动调用 + if (!userSetting.getServerId().equals(device.getServerId())) { + // 通道ID + DeviceChannel deviceChannel = deviceChannelService.getOneForSource(device.getDeviceId(), channelId); + Assert.notNull(deviceChannel, "通道不存在"); + String msg = redisRpcPlayService.frontEndCommand(device.getServerId(), deviceChannel.getId(), cmdCode, parameter1, parameter2, combindCode2); + if (msg != null) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), msg); + } + return; + } + try { + cmder.frontEndCmd(device, channelId, cmdCode, parameter1, parameter2, combindCode2); + } catch (SipException | InvalidArgumentException | ParseException e) { + log.error("[命令发送失败] 前端控制: {}", e.getMessage()); + throw new ControllerException(ErrorCode.ERROR100.getCode(), "命令发送失败: " + e.getMessage()); + } + } + + @Override + public void frontEndCommand(CommonGBChannel channel, Integer cmdCode, Integer parameter1, Integer parameter2, Integer combindCode2) { + if (channel.getDataType() != ChannelDataType.GB28181) { + // 只有国标通道的支持云台控制 + log.warn("[INFO 消息] 只有国标通道的支持云台控制, 通道ID: {}", channel.getGbId()); + throw new ControllerException(ErrorCode.ERROR100.getCode(), "不支持"); + } + Device device = deviceService.getDevice(channel.getDataDeviceId()); + if (device == null) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "未找到设备ID"); + } + DeviceChannel deviceChannel = deviceChannelService.getOneById(channel.getGbId()); + frontEndCommand(device, deviceChannel.getDeviceId(), cmdCode, parameter1, parameter2, combindCode2); + } + + @Override + public void queryPresetList(CommonGBChannel channel, ErrorCallback> callback) { + if (channel.getDataType() != ChannelDataType.GB28181) { + // 只有国标通道的支持云台控制 + log.warn("[INFO 消息] 只有国标通道的支持云台控制, 通道ID: {}", channel.getGbId()); + throw new ControllerException(ErrorCode.ERROR100.getCode(), "不支持"); + } + Device device = deviceService.getDevice(channel.getDataDeviceId()); + if (device == null) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "未找到设备"); + } + DeviceChannel deviceChannel = deviceChannelService.getOneForSourceById(channel.getGbId()); + if (deviceChannel == null) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "未找到通道"); + } + deviceService.queryPreset(device, deviceChannel.getDeviceId(), callback); + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/service/impl/PlatformChannelServiceImpl.java b/src/main/java/com/genersoft/iot/vmp/gb28181/service/impl/PlatformChannelServiceImpl.java new file mode 100755 index 0000000..44e310d --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/service/impl/PlatformChannelServiceImpl.java @@ -0,0 +1,763 @@ +package com.genersoft.iot.vmp.gb28181.service.impl; + +import com.genersoft.iot.vmp.common.enums.ChannelDataType; +import com.genersoft.iot.vmp.conf.UserSetting; +import com.genersoft.iot.vmp.gb28181.bean.*; +import com.genersoft.iot.vmp.gb28181.dao.*; +import com.genersoft.iot.vmp.gb28181.event.EventPublisher; +import com.genersoft.iot.vmp.gb28181.event.channel.ChannelEvent; +import com.genersoft.iot.vmp.gb28181.event.subscribe.catalog.CatalogEvent; +import com.genersoft.iot.vmp.gb28181.service.IPlatformChannelService; +import com.genersoft.iot.vmp.gb28181.transmit.cmd.ISIPCommanderForPlatform; +import com.github.pagehelper.PageHelper; +import com.github.pagehelper.PageInfo; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.event.EventListener; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.Assert; + +import javax.sip.InvalidArgumentException; +import javax.sip.SipException; +import java.text.ParseException; +import java.util.*; + +/** + * @author lin + */ +@Slf4j +@Service +public class PlatformChannelServiceImpl implements IPlatformChannelService { + + @Autowired + private PlatformChannelMapper platformChannelMapper; + + @Autowired + private EventPublisher eventPublisher; + + @Autowired + private GroupMapper groupMapper; + + + @Autowired + private RegionMapper regionMapper; + + @Autowired + private CommonGBChannelMapper commonGBChannelMapper; + + @Autowired + private PlatformMapper platformMapper; + + @Autowired + private ISIPCommanderForPlatform sipCommanderFroPlatform; + + @Autowired + private SubscribeHolder subscribeHolder; + + @Autowired + private UserSetting userSetting; + + // 监听通道信息变化 + @EventListener + public void onApplicationEvent(ChannelEvent event) { + if (event.getChannels().isEmpty()) { + return; + } + // 获取通道所关联的平台 + List allPlatform = platformMapper.queryByServerId(userSetting.getServerId()); + // 获取所用订阅 + List platforms = subscribeHolder.getAllCatalogSubscribePlatform(allPlatform); + + Map> platformMap = new HashMap<>(); + Map channelMap = new HashMap<>(); + if (platforms.isEmpty()) { + return; + } + for (CommonGBChannel deviceChannel : event.getChannels()) { + List parentPlatformsForGB = queryPlatFormListByChannelDeviceId( + deviceChannel.getGbId(), platforms); + platformMap.put(deviceChannel.getGbDeviceId(), parentPlatformsForGB); + channelMap.put(deviceChannel.getGbDeviceId(), deviceChannel); + } + if (platformMap.isEmpty()) { + return; + } + switch (event.getMessageType()) { + case ON: + case OFF: + case DEL: + for (String serverGbId : platformMap.keySet()) { + List platformList = platformMap.get(serverGbId); + if (platformList != null && !platformList.isEmpty()) { + for (Platform platform : platformList) { + SubscribeInfo subscribeInfo = subscribeHolder.getCatalogSubscribe(platform.getServerGBId()); + if (subscribeInfo == null) { + continue; + } + log.info("[Catalog事件: {}]平台:{},影响通道{}", event.getMessageType(), platform.getServerGBId(), serverGbId); + List deviceChannelList = new ArrayList<>(); + CommonGBChannel deviceChannel = new CommonGBChannel(); + deviceChannel.setGbDeviceId(serverGbId); + deviceChannelList.add(deviceChannel); + try { + sipCommanderFroPlatform.sendNotifyForCatalogOther(event.getMessageType().name(), platform, deviceChannelList, subscribeInfo, null); + } catch (InvalidArgumentException | ParseException | NoSuchFieldException | SipException | + IllegalAccessException e) { + log.error("[命令发送失败] 国标级联 Catalog通知: {}", e.getMessage()); + } + } + }else { + log.info("[Catalog事件: {}] 未找到上级平台: {}", event.getMessageType(), serverGbId); + } + } + break; + case VLOST: + break; + case DEFECT: + break; + case ADD: + case UPDATE: + for (String gbId : platformMap.keySet()) { + List parentPlatforms = platformMap.get(gbId); + if (parentPlatforms != null && !parentPlatforms.isEmpty()) { + for (Platform platform : parentPlatforms) { + SubscribeInfo subscribeInfo = subscribeHolder.getCatalogSubscribe(platform.getServerGBId()); + if (subscribeInfo == null) { + continue; + } + log.info("[Catalog事件: {}]平台:{},影响通道{}", event.getMessageType(), platform.getServerGBId(), gbId); + List channelList = new ArrayList<>(); + CommonGBChannel deviceChannel = channelMap.get(gbId); + channelList.add(deviceChannel); + try { + sipCommanderFroPlatform.sendNotifyForCatalogAddOrUpdate(event.getMessageType().name(), platform, channelList, subscribeInfo, null); + } catch (InvalidArgumentException | ParseException | NoSuchFieldException | + SipException | IllegalAccessException e) { + log.error("[命令发送失败] 国标级联 Catalog通知: {}", e.getMessage()); + } + } + } + } + break; + default: + break; + } + } + + @EventListener + public void onApplicationEvent(CatalogEvent event) { + log.info("[Catalog事件: {}]通道数量: {}", event.getType(), event.getChannels().size()); + Platform platform = event.getPlatform(); + if (platform == null || platform.getServerGBId() == null) { + return; + } + SubscribeInfo subscribe = subscribeHolder.getCatalogSubscribe(platform.getServerGBId()); + if (subscribe == null) { + return; + } + switch (event.getType()) { + case CatalogEvent.ON: + case CatalogEvent.OFF: + case CatalogEvent.DEL: + List channels = new ArrayList<>(); + if (event.getChannels() != null) { + channels.addAll(event.getChannels()); + } + if (!channels.isEmpty()) { + log.info("[Catalog事件: {}]平台:{},影响通道{}个", event.getType(), platform.getServerGBId(), channels.size()); + try { + sipCommanderFroPlatform.sendNotifyForCatalogOther(event.getType(), platform, channels, subscribe, null); + } catch (InvalidArgumentException | ParseException | NoSuchFieldException | SipException | + IllegalAccessException e) { + log.error("[命令发送失败] 国标级联 Catalog通知: {}", e.getMessage()); + } + } + break; + case CatalogEvent.VLOST: + break; + case CatalogEvent.DEFECT: + break; + case CatalogEvent.ADD: + case CatalogEvent.UPDATE: + List deviceChannelList = new ArrayList<>(); + if (event.getChannels() != null) { + deviceChannelList.addAll(event.getChannels()); + } + if (!deviceChannelList.isEmpty()) { + log.info("[Catalog事件: {}]平台:{},影响通道{}个", event.getType(), platform.getServerGBId(), deviceChannelList.size()); + try { + sipCommanderFroPlatform.sendNotifyForCatalogAddOrUpdate(event.getType(), platform, deviceChannelList, subscribe, null); + } catch (InvalidArgumentException | ParseException | NoSuchFieldException | SipException | + IllegalAccessException e) { + log.error("[命令发送失败] 国标级联 Catalog通知: {}", e.getMessage()); + } + } + break; + default: + break; + } + } + + + @Override + public PageInfo queryChannelList(int page, int count, String query, Integer channelType, Boolean online, Integer platformId, Boolean hasShare) { + PageHelper.startPage(page, count); + if (query != null) { + query = query.replaceAll("/", "//") + .replaceAll("%", "/%") + .replaceAll("_", "/_"); + } + List all = platformChannelMapper.queryForPlatformForWebList(platformId, query, channelType, online, hasShare); + return new PageInfo<>(all); + } + + /** + * 获取通道使用的分组中未分享的 + */ + @Transactional + public Set getGroupNotShareByChannelList(List channelList, Integer platformId) { + // 获取分组中未分享的节点 + Set groupList = groupMapper.queryNotShareGroupForPlatformByChannelList(channelList, platformId); + // 获取这些节点的所有父节点 + if (groupList.isEmpty()) { + return new HashSet<>(); + } + Set allGroup = getAllGroup(groupList); + allGroup.addAll(groupList); + // 获取全部节点中未分享的 + return groupMapper.queryNotShareGroupForPlatformByGroupList(allGroup, platformId); + } + + /** + * 获取通道使用的分组中未分享的 + */ + private Set getRegionNotShareByChannelList(List channelList, Integer platformId) { + // 获取分组中未分享的节点 + Set regionSet = regionMapper.queryNotShareRegionForPlatformByChannelList(channelList, platformId); + // 获取这些节点的所有父节点 + if (regionSet.isEmpty()) { + return new HashSet<>(); + } + Set allRegion = getAllRegion(regionSet); + allRegion.addAll(regionSet); + // 获取全部节点中未分享的 + return regionMapper.queryNotShareRegionForPlatformByRegionList(allRegion, platformId); + } + + /** + * 移除空的共享,并返回移除的分组 + */ + @Transactional + public Set deleteEmptyGroup(Set groupSet, Integer platformId) { + Iterator iterator = groupSet.iterator(); + while (iterator.hasNext()) { + Group group = iterator.next(); + // groupSet 为当前通道直接使用的分组,如果已经没有子分组与其他的通道,则可以移除 + // 获取分组子节点 + Set children = platformChannelMapper.queryShareChildrenGroup(group.getId(), platformId); + if (!children.isEmpty()) { + iterator.remove(); + continue; + } + // 获取分组关联的通道 + List channelList = commonGBChannelMapper.queryShareChannelByParentId(group.getDeviceId(), platformId); + if (!channelList.isEmpty()) { + iterator.remove(); + continue; + } + platformChannelMapper.removePlatformGroupById(group.getId(), platformId); + } + // 如果空了,说明没有通道需要处理了 + if (groupSet.isEmpty()) { + return new HashSet<>(); + } + Set parent = platformChannelMapper.queryShareParentGroupByGroupSet(groupSet, platformId); + if (parent.isEmpty()) { + return groupSet; + }else { + Set parentGroupSet = deleteEmptyGroup(parent, platformId); + groupSet.addAll(parentGroupSet); + return groupSet; + } + } + + /** + * 移除空的共享,并返回移除的行政区划 + */ + private Set deleteEmptyRegion(Set regionSet, Integer platformId) { + Iterator iterator = regionSet.iterator(); + while (iterator.hasNext()) { + Region region = iterator.next(); + // groupSet 为当前通道直接使用的分组,如果已经没有子分组与其他的通道,则可以移除 + // 获取分组子节点 + Set children = platformChannelMapper.queryShareChildrenRegion(region.getDeviceId(), platformId); + if (!children.isEmpty()) { + iterator.remove(); + continue; + } + // 获取分组关联的通道 + List channelList = commonGBChannelMapper.queryShareChannelByCivilCode(region.getDeviceId(), platformId); + if (!channelList.isEmpty()) { + iterator.remove(); + continue; + } + platformChannelMapper.removePlatformRegionById(region.getId(), platformId); + } + // 如果空了,说明没有通道需要处理了 + if (regionSet.isEmpty()) { + return new HashSet<>(); + } + Set parent = platformChannelMapper.queryShareParentRegionByRegionSet(regionSet, platformId); + if (parent.isEmpty()) { + return regionSet; + }else { + Set parentGroupSet = deleteEmptyRegion(parent, platformId); + regionSet.addAll(parentGroupSet); + return regionSet; + } + } + + private Set getAllGroup(Set groupList ) { + if (groupList.isEmpty()) { + return new HashSet<>(); + } + Set channelList = groupMapper.queryParentInChannelList(groupList); + if (channelList.isEmpty()) { + return channelList; + } + Set allParentRegion = getAllGroup(channelList); + channelList.addAll(allParentRegion); + return channelList; + } + + private Set getAllRegion(Set regionSet ) { + if (regionSet.isEmpty()) { + return new HashSet<>(); + } + + Set channelList = regionMapper.queryParentInChannelList(regionSet); + if (channelList.isEmpty()) { + return channelList; + } + Set allParentRegion = getAllRegion(channelList); + channelList.addAll(allParentRegion); + return channelList; + } + + @Override + @Transactional + public int addAllChannel(Integer platformId) { + List channelListNotShare = platformChannelMapper.queryNotShare(platformId, null); + Assert.notEmpty(channelListNotShare, "所有通道已共享"); + return addChannelList(platformId, channelListNotShare); + } + + @Override + @Transactional + public int addChannels(Integer platformId, List channelIds) { + List channelListNotShare = platformChannelMapper.queryNotShare(platformId, channelIds); + Assert.notEmpty(channelListNotShare, "通道已共享"); + return addChannelList(platformId, channelListNotShare); + } + + @Transactional + public int addChannelList(Integer platformId, List channelList) { + Platform platform = platformMapper.query(platformId); + if (platform == null) { + return 0; + } + int result = platformChannelMapper.addChannels(platformId, channelList); + if (result > 0) { + // 查询通道相关的行政区划信息是否共享,如果没共享就添加 + Set regionListNotShare = getRegionNotShareByChannelList(channelList, platformId); + if (!regionListNotShare.isEmpty()) { + int addGroupResult = platformChannelMapper.addPlatformRegion(new ArrayList<>(regionListNotShare), platformId); + if (addGroupResult > 0) { + for (Region region : regionListNotShare) { + // 分组信息排序时需要将顶层排在最后 + channelList.add(0, CommonGBChannel.build(region)); + } + } + } + + // 查询通道相关的分组信息是否共享,如果没共享就添加 + Set groupListNotShare = getGroupNotShareByChannelList(channelList, platformId); + if (!groupListNotShare.isEmpty()) { + int addGroupResult = platformChannelMapper.addPlatformGroup(new ArrayList<>(groupListNotShare), platformId); + if (addGroupResult > 0) { + for (Group group : groupListNotShare) { + // 分组信息排序时需要将顶层排在最后 + channelList.add(0, CommonGBChannel.build(group)); + } + } + } + // 发送消息 + try { + // 发送catalog + eventPublisher.catalogEventPublish(platform, channelList, CatalogEvent.ADD); + } catch (Exception e) { + log.warn("[关联通道] 发送失败,数量:{}", channelList.size(), e); + } + } + return result; + } + + @Override + public int removeAllChannel(Integer platformId) { + Platform platform = platformMapper.query(platformId); + if (platform == null) { + return 0; + } + + List channelListShare = platformChannelMapper.queryShare(platformId, null); + Assert.notEmpty(channelListShare, "未共享任何通道"); + int result = platformChannelMapper.removeChannelsWithPlatform(platformId, channelListShare); + if (result > 0) { + // 查询通道相关的分组信息 + Set regionSet = regionMapper.queryByChannelList(channelListShare); + Set deleteRegion = deleteEmptyRegion(regionSet, platformId); + if (!deleteRegion.isEmpty()) { + for (Region region : deleteRegion) { + channelListShare.add(0, CommonGBChannel.build(region)); + } + } + + // 查询通道相关的分组信息 + Set groupSet = groupMapper.queryByChannelList(channelListShare); + Set deleteGroup = deleteEmptyGroup(groupSet, platformId); + if (!deleteGroup.isEmpty()) { + for (Group group : deleteGroup) { + channelListShare.add(0, CommonGBChannel.build(group)); + } + } + // 发送消息 + try { + // 发送catalog + eventPublisher.catalogEventPublish(platform, channelListShare, CatalogEvent.DEL); + } catch (Exception e) { + log.warn("[移除全部关联通道] 发送失败,数量:{}", channelListShare.size(), e); + } + } + return result; + } + + @Override + @Transactional + public void addChannelByDevice(Integer platformId, List deviceIds) { + List channelList = commonGBChannelMapper.queryByGbDeviceIdsForIds(ChannelDataType.GB28181, deviceIds); + addChannels(platformId, channelList); + } + + @Override + @Transactional + public void removeChannelByDevice(Integer platformId, List deviceIds) { + List channelList = commonGBChannelMapper.queryByGbDeviceIdsForIds(ChannelDataType.GB28181, deviceIds); + removeChannels(platformId, channelList); + } + + @Transactional + public int removeChannelList(Integer platformId, List channelList) { + Platform platform = platformMapper.query(platformId); + if (platform == null) { + return 0; + } + int result = platformChannelMapper.removeChannelsWithPlatform(platformId, channelList); + if (result > 0) { + // 查询通道相关的分组信息 + Set regionSet = regionMapper.queryByChannelList(channelList); + Set deleteRegion = deleteEmptyRegion(regionSet, platformId); + if (!deleteRegion.isEmpty()) { + for (Region region : deleteRegion) { + channelList.add(0, CommonGBChannel.build(region)); + } + } + + // 查询通道相关的分组信息 + Set groupSet = groupMapper.queryByChannelList(channelList); + Set deleteGroup = deleteEmptyGroup(groupSet, platformId); + if (!deleteGroup.isEmpty()) { + for (Group group : deleteGroup) { + channelList.add(0, CommonGBChannel.build(group)); + } + } + // 发送消息 + try { + // 发送catalog + eventPublisher.catalogEventPublish(platform, channelList, CatalogEvent.DEL); + } catch (Exception e) { + log.warn("[移除关联通道] 发送失败,数量:{}", channelList.size(), e); + } + } + return result; + } + + @Override + @Transactional + public int removeChannels(Integer platformId, List channelIds) { + List channelList = platformChannelMapper.queryShare(platformId, channelIds); + if (channelList.isEmpty()) { + return 0; + } + return removeChannelList(platformId, channelList); + } + + @Override + @Transactional + public void removeChannels(List ids) { + List platformList = platformChannelMapper.queryPlatFormListByChannelList(ids); + if (platformList.isEmpty()) { + return; + } + + for (Platform platform : platformList) { + removeChannels(platform.getId(), ids); + } + } + + @Override + @Transactional + public void removeChannel(int channelId) { + List platformList = platformChannelMapper.queryPlatFormListByChannelId(channelId); + if (platformList.isEmpty()) { + return; + } + for (Platform platform : platformList) { + ArrayList ids = new ArrayList<>(); + ids.add(channelId); + removeChannels(platform.getId(), ids); + } + } + + @Override + public List queryByPlatform(Platform platform) { + if (platform == null) { + return null; + } + List commonGBChannelList = commonGBChannelMapper.queryWithPlatform(platform.getId()); + if (commonGBChannelList.isEmpty()) { + return new ArrayList<>(); + } + List channelList = new ArrayList<>(); + // 是否包含平台信息 + if (platform.getCatalogWithPlatform() > 0) { + CommonGBChannel channel = CommonGBChannel.build(platform); + channelList.add(channel); + } + // 关联的行政区划信息 + if (platform.getCatalogWithRegion() > 0) { + // 查询关联平台的行政区划信息 + List regionChannelList = regionMapper.queryByPlatform(platform.getId()); + if (!regionChannelList.isEmpty()) { + channelList.addAll(regionChannelList); + } + } + if (platform.getCatalogWithGroup() > 0) { + // 关联的分组信息 + List groupChannelList = groupMapper.queryForPlatform(platform.getId()); + if (!groupChannelList.isEmpty()) { + channelList.addAll(groupChannelList); + } + } + + channelList.addAll(commonGBChannelList); + return channelList; + } + + @Override + public void pushChannel(Integer platformId) { + Platform platform = platformMapper.query(platformId); + Assert.notNull(platform, "平台不存在"); + List channelList = queryByPlatform(platform); + if (channelList.isEmpty()){ + return; + } + SubscribeInfo subscribeInfo = SubscribeInfo.buildSimulated(platform.getServerGBId(), platform.getServerIp()); + + try { + sipCommanderFroPlatform.sendNotifyForCatalogAddOrUpdate(CatalogEvent.ADD, platform, channelList, subscribeInfo, null); + } catch (InvalidArgumentException | ParseException | NoSuchFieldException | + SipException | IllegalAccessException e) { + log.error("[命令发送失败] 国标级联 Catalog通知: {}", e.getMessage()); + } + } + + @Override + public void updateCustomChannel(PlatformChannel channel) { + platformChannelMapper.updateCustomChannel(channel); + Platform platform = platformMapper.query(channel.getPlatformId()); + CommonGBChannel commonGBChannel = platformChannelMapper.queryShareChannel(channel.getPlatformId(), channel.getGbId()); + // 发送消息 + try { + // 发送catalog + eventPublisher.catalogEventPublish(platform, commonGBChannel, CatalogEvent.UPDATE); + } catch (Exception e) { + log.warn("[自定义通道信息] 发送失败, 平台ID: {}, 通道: {}({})", channel.getPlatformId(), + channel.getGbName(), channel.getId(), e); + } + } + + @Override + @Transactional + public void checkGroupRemove(List channelList, List groupList) { + + List channelIds = new ArrayList<>(); + channelList.stream().forEach(commonGBChannel -> { + channelIds.add(commonGBChannel.getGbId()); + }); + // 获取关联这些通道的平台 + List platformList = platformChannelMapper.queryPlatFormListByChannelList(channelIds); + if (platformList.isEmpty()) { + return; + } + for (Platform platform : platformList) { + Set groupSet; + if (groupList == null || groupList.isEmpty()) { + groupSet = platformChannelMapper.queryShareGroup(platform.getId()); + }else { + groupSet = new HashSet<>(groupList); + } + // 清理空的分组并发送消息 + Set deleteGroup = deleteEmptyGroup(groupSet, platform.getId()); + + List channelListForEvent = new ArrayList<>(); + if (!deleteGroup.isEmpty()) { + for (Group group : deleteGroup) { + channelListForEvent.add(0, CommonGBChannel.build(group)); + } + } + // 发送消息 + try { + // 发送catalog + eventPublisher.catalogEventPublish(platform, channelListForEvent, CatalogEvent.DEL); + } catch (Exception e) { + log.warn("[移除关联通道] 发送失败,数量:{}", channelList.size(), e); + } + } + } + + @Override + @Transactional + public void checkRegionRemove(List channelList, List regionList) { + List channelIds = new ArrayList<>(); + channelList.stream().forEach(commonGBChannel -> { + channelIds.add(commonGBChannel.getGbId()); + }); + // 获取关联这些通道的平台 + List platformList = platformChannelMapper.queryPlatFormListByChannelList(channelIds); + if (platformList.isEmpty()) { + return; + } + for (Platform platform : platformList) { + Set regionSet; + if (regionList == null || regionList.isEmpty()) { + regionSet = platformChannelMapper.queryShareRegion(platform.getId()); + }else { + regionSet = new HashSet<>(regionList); + } + // 清理空的分组并发送消息 + Set deleteRegion = deleteEmptyRegion(regionSet, platform.getId()); + + List channelListForEvent = new ArrayList<>(); + if (!deleteRegion.isEmpty()) { + for (Region region : deleteRegion) { + channelListForEvent.add(0, CommonGBChannel.build(region)); + } + } + // 发送消息 + try { + // 发送catalog + eventPublisher.catalogEventPublish(platform, channelListForEvent, CatalogEvent.DEL); + } catch (Exception e) { + log.warn("[移除关联通道] 发送失败,数量:{}", channelList.size(), e); + } + } + } + + @Override + @Transactional + public void checkGroupAdd(List channelList) { + List channelIds = new ArrayList<>(); + channelList.stream().forEach(commonGBChannel -> { + channelIds.add(commonGBChannel.getGbId()); + }); + List platformList = platformChannelMapper.queryPlatFormListByChannelList(channelIds); + if (platformList.isEmpty()) { + return; + } + for (Platform platform : platformList) { + + Set addGroup = getGroupNotShareByChannelList(channelList, platform.getId()); + + List channelListForEvent = new ArrayList<>(); + if (!addGroup.isEmpty()) { + for (Group group : addGroup) { + channelListForEvent.add(0, CommonGBChannel.build(group)); + } + platformChannelMapper.addPlatformGroup(addGroup, platform.getId()); + // 发送消息 + try { + // 发送catalog + eventPublisher.catalogEventPublish(platform, channelListForEvent, CatalogEvent.ADD); + } catch (Exception e) { + log.warn("[移除关联通道] 发送失败,数量:{}", channelList.size(), e); + } + } + } + } + + @Override + public void checkRegionAdd(List channelList) { + List channelIds = new ArrayList<>(); + channelList.stream().forEach(commonGBChannel -> { + channelIds.add(commonGBChannel.getGbId()); + }); + List platformList = platformChannelMapper.queryPlatFormListByChannelList(channelIds); + if (platformList.isEmpty()) { + return; + } + for (Platform platform : platformList) { + + Set addRegion = getRegionNotShareByChannelList(channelList, platform.getId()); + List channelListForEvent = new ArrayList<>(); + if (!addRegion.isEmpty()) { + for (Region region : addRegion) { + channelListForEvent.add(0, CommonGBChannel.build(region)); + } + platformChannelMapper.addPlatformRegion(new ArrayList<>(addRegion), platform.getId()); + // 发送消息 + try { + // 发送catalog + eventPublisher.catalogEventPublish(platform, channelListForEvent, CatalogEvent.ADD); + } catch (Exception e) { + log.warn("[移除关联通道] 发送失败,数量:{}", channelList.size(), e); + } + } + } + } + + @Override + public List queryPlatFormListByChannelDeviceId(Integer channelId, List platforms) { + return platformChannelMapper.queryPlatFormListForGBWithGBId(channelId, platforms); + } + + @Override + public CommonGBChannel queryChannelByPlatformIdAndChannelId(Integer platformId, Integer channelId) { + return platformChannelMapper.queryShareChannel(platformId, channelId); + } + + @Override + public List queryChannelByPlatformIdAndChannelIds(Integer platformId, List channelIds) { + return platformChannelMapper.queryShare(platformId, channelIds); + } + + @Override + public List queryByPlatformBySharChannelId(String channelDeviceId) { + List commonGBChannels = commonGBChannelMapper.queryByDeviceId(channelDeviceId); + ArrayList ids = new ArrayList<>(); + for (CommonGBChannel commonGBChannel : commonGBChannels) { + ids.add(commonGBChannel.getGbId()); + } + return platformChannelMapper.queryPlatFormListByChannelList(ids); + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/service/impl/PlatformServiceImpl.java b/src/main/java/com/genersoft/iot/vmp/gb28181/service/impl/PlatformServiceImpl.java new file mode 100755 index 0000000..7d827c5 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/service/impl/PlatformServiceImpl.java @@ -0,0 +1,902 @@ +package com.genersoft.iot.vmp.gb28181.service.impl; + +import com.genersoft.iot.vmp.common.*; +import com.genersoft.iot.vmp.conf.DynamicTask; +import com.genersoft.iot.vmp.conf.SipConfig; +import com.genersoft.iot.vmp.conf.UserSetting; +import com.genersoft.iot.vmp.conf.exception.SsrcTransactionNotFoundException; +import com.genersoft.iot.vmp.gb28181.bean.*; +import com.genersoft.iot.vmp.gb28181.dao.PlatformChannelMapper; +import com.genersoft.iot.vmp.gb28181.dao.PlatformMapper; +import com.genersoft.iot.vmp.gb28181.event.SipSubscribe; +import com.genersoft.iot.vmp.gb28181.service.IGbChannelService; +import com.genersoft.iot.vmp.gb28181.service.IInviteStreamService; +import com.genersoft.iot.vmp.gb28181.service.IPlatformService; +import com.genersoft.iot.vmp.gb28181.session.SSRCFactory; +import com.genersoft.iot.vmp.gb28181.session.SipInviteSessionManager; +import com.genersoft.iot.vmp.gb28181.task.platformStatus.PlatformKeepaliveTask; +import com.genersoft.iot.vmp.gb28181.task.platformStatus.PlatformRegisterTask; +import com.genersoft.iot.vmp.gb28181.task.platformStatus.PlatformRegisterTaskInfo; +import com.genersoft.iot.vmp.gb28181.task.platformStatus.PlatformStatusTaskRunner; +import com.genersoft.iot.vmp.gb28181.transmit.cmd.ISIPCommanderForPlatform; +import com.genersoft.iot.vmp.gb28181.utils.SipUtils; +import com.genersoft.iot.vmp.media.bean.MediaInfo; +import com.genersoft.iot.vmp.media.bean.MediaServer; +import com.genersoft.iot.vmp.media.event.hook.HookData; +import com.genersoft.iot.vmp.media.event.hook.HookSubscribe; +import com.genersoft.iot.vmp.media.event.media.MediaDepartureEvent; +import com.genersoft.iot.vmp.media.event.mediaServer.MediaSendRtpStoppedEvent; +import com.genersoft.iot.vmp.media.service.IMediaServerService; +import com.genersoft.iot.vmp.service.ISendRtpServerService; +import com.genersoft.iot.vmp.service.bean.*; +import com.genersoft.iot.vmp.service.redisMsg.IRedisRpcService; +import com.genersoft.iot.vmp.storager.IRedisCatchStorage; +import com.github.pagehelper.PageHelper; +import com.github.pagehelper.PageInfo; +import gov.nist.javax.sip.message.SIPResponse; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.CommandLineRunner; +import org.springframework.context.event.EventListener; +import org.springframework.core.annotation.Order; +import org.springframework.scheduling.annotation.Async; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.Assert; + +import javax.sdp.*; +import javax.sip.InvalidArgumentException; +import javax.sip.ResponseEvent; +import javax.sip.SipException; +import java.text.ParseException; +import java.util.List; +import java.util.UUID; +import java.util.Vector; +import java.util.concurrent.TimeUnit; + +/** + * @author lin + */ +@Slf4j +@Service +@Order(value=15) +public class PlatformServiceImpl implements IPlatformService, CommandLineRunner { + + @Autowired + private PlatformMapper platformMapper; + + @Autowired + private IRedisCatchStorage redisCatchStorage; + + @Autowired + private SSRCFactory ssrcFactory; + + @Autowired + private IMediaServerService mediaServerService; + + @Autowired + private ISIPCommanderForPlatform commanderForPlatform; + + @Autowired + private DynamicTask dynamicTask; + + @Autowired + private SubscribeHolder subscribeHolder; + + @Autowired + private UserSetting userSetting; + + @Autowired + private IRedisRpcService redisRpcService; + + @Autowired + private SipConfig sipConfig; + + @Autowired + private SipInviteSessionManager sessionManager; + + @Autowired + private IInviteStreamService inviteStreamService; + + @Autowired + private PlatformChannelMapper platformChannelMapper; + + @Autowired + private IGbChannelService channelService; + + @Autowired + private ISendRtpServerService sendRtpServerService; + + @Autowired + private PlatformStatusTaskRunner statusTaskRunner; + + @Override + public void run(String... args) throws Exception { + + // 查找国标推流 + List sendRtpItems = redisCatchStorage.queryAllSendRTPServer(); + if (!sendRtpItems.isEmpty()) { + for (SendRtpInfo sendRtpItem : sendRtpItems) { + MediaServer mediaServerItem = mediaServerService.getOne(sendRtpItem.getMediaServerId()); + CommonGBChannel channel = channelService.getOne(sendRtpItem.getChannelId()); + if (channel == null){ + continue; + } + sendRtpServerService.delete(sendRtpItem); + if (mediaServerItem != null) { + ssrcFactory.releaseSsrc(sendRtpItem.getMediaServerId(), sendRtpItem.getSsrc()); + boolean stopResult = mediaServerService.initStopSendRtp(mediaServerItem, sendRtpItem.getApp(), sendRtpItem.getStream(), sendRtpItem.getSsrc()); + if (stopResult) { + Platform platform = queryPlatformByServerGBId(sendRtpItem.getTargetId()); + + if (platform != null) { + try { + commanderForPlatform.streamByeCmd(platform, sendRtpItem, channel); + } catch (InvalidArgumentException | ParseException | SipException e) { + log.error("[命令发送失败] 国标级联 发送BYE: {}", e.getMessage()); + } + } + } + } + } + } + + // 启动时 如果存在未过期的注册平台,则发送注销 + List registerTaskInfoList = statusTaskRunner.getAllRegisterTaskInfo(); + if (registerTaskInfoList.isEmpty()) { + return; + } + for (PlatformRegisterTaskInfo taskInfo : registerTaskInfoList) { + log.info("[国标级联] 启动服务是发现平台注册仍在有效期,注销: {}", taskInfo.getPlatformServerId()); + Platform platform = queryPlatformByServerGBId(taskInfo.getPlatformServerId()); + if (platform == null) { + statusTaskRunner.removeRegisterTask(taskInfo.getPlatformServerId()); + continue; + } + sendUnRegister(platform, taskInfo.getSipTransactionInfo()); + } + // 启动时所有平台默认离线 + platformMapper.offlineAll(userSetting.getServerId()); + } + @Scheduled(fixedDelay = 20, timeUnit = TimeUnit.SECONDS) //每3秒执行一次 + public void statusLostCheck(){ + // 每隔20秒检测,是否存在启用但是未注册的平台,存在则发起注册 + // 获取所有在线并且启用的平台 + List platformList = platformMapper.queryServerIdsWithEnableAndServer(userSetting.getServerId()); + if (platformList.isEmpty()) { + return; + } + for (Platform platform : platformList) { + if (statusTaskRunner.containsRegister(platform.getServerGBId()) && statusTaskRunner.containsKeepAlive(platform.getServerGBId())) { + continue; + } + if (statusTaskRunner.containsRegister(platform.getServerGBId())) { + SipTransactionInfo transactionInfo = statusTaskRunner.getRegisterTransactionInfo(platform.getServerGBId()); + // 注销后出发平台离线, 如果是启用的平台,那么下次丢失检测会检测到并重新注册上线 + sendUnRegister(platform, transactionInfo); + }else { + statusTaskRunner.removeKeepAliveTask(platform.getServerGBId()); + sendRegister(platform, null); + } + } + } + + private void sendRegister(Platform platform, SipTransactionInfo sipTransactionInfo) { + try { + commanderForPlatform.register(platform, sipTransactionInfo, eventResult -> { + log.info("[国标级联] {}({}),注册失败", platform.getName(), platform.getServerGBId()); + offline(platform); + }, null); + } catch (InvalidArgumentException | ParseException | SipException e) { + log.error("[命令发送失败] 国标级联: {}", e.getMessage()); + } + } + + private void sendUnRegister(Platform platform, SipTransactionInfo sipTransactionInfo) { + statusTaskRunner.removeRegisterTask(platform.getServerGBId()); + statusTaskRunner.removeKeepAliveTask(platform.getServerGBId()); + try { + commanderForPlatform.unregister(platform, sipTransactionInfo, null, eventResult -> { + log.info("[国标级联] 注销成功, 平台:{}", platform.getServerGBId()); + }); + } catch (InvalidArgumentException | ParseException | SipException e) { + log.error("[命令发送失败] 国标级联: {}", e.getMessage()); + } + } + + // 定时监听国标级联所进行的WVP服务是否正常, 如果异常则选择新的wvp执行 + @Scheduled(fixedDelay = 2, timeUnit = TimeUnit.SECONDS) //每3秒执行一次 + public void execute(){ + if (!userSetting.isAutoRegisterPlatform()) { + return; + } + // 查找非平台的国标级联执行服务Id + List serverIds = platformMapper.queryServerIdsWithEnableAndNotInServer(userSetting.getServerId()); + if (serverIds == null || serverIds.isEmpty()) { + return; + } + serverIds.forEach(serverId -> { + // 检查每个是否存活 + ServerInfo serverInfo = redisCatchStorage.queryServerInfo(serverId); + if (serverInfo != null) { + return; + } + log.info("[集群] 检测到 {} 已离线", serverId); + redisCatchStorage.removeOfflineWVPInfo(serverId); + String chooseServerId = redisCatchStorage.chooseOneServer(serverId); + if (!userSetting.getServerId().equals(chooseServerId)){ + return; + } + // 此平台需要选择新平台处理, 确定由当前平台即开始处理 + List platformList = platformMapper.queryByServerId(serverId); + platformList.forEach(platform -> { + log.info("[集群] 由本平台开启上级平台{}({})的注册", platform.getName(), platform.getServerGBId()); + // 设置平台使用当前平台的IP + platform.setAddress(getIpWithSameNetwork(platform.getAddress())); + platform.setServerId(userSetting.getServerId()); + platformMapper.update(platform); + // 检查就平台是否注册到期,没有则注销,由本平台重新注册 + List taskInfoList = statusTaskRunner.getRegisterTransactionInfoByServerId(serverId); + boolean needUnregister = false; + SipTransactionInfo sipTransactionInfo = null; + if (!taskInfoList.isEmpty()) { + for (PlatformRegisterTaskInfo taskInfo : taskInfoList) { + if (taskInfo.getPlatformServerId().equals(platform.getServerGBId()) + && taskInfo.getSipTransactionInfo() != null) { + needUnregister = true; + sipTransactionInfo = taskInfo.getSipTransactionInfo(); + break; + } + } + } + if (needUnregister) { + sendUnRegister(platform, sipTransactionInfo); + }else { + // 开始注册 + // 注册成功时由程序直接调用了online方法 + sendRegister(platform, null); + } + }); + }); + } + + /** + * 获取同网段的IP + */ + private String getIpWithSameNetwork(String ip){ + if (ip == null || sipConfig.getMonitorIps().size() == 1) { + return sipConfig.getMonitorIps().get(0); + } + String[] ipSplit = ip.split("\\."); + String ip1 = null, ip2 = null, ip3 = null; + for (String monitorIp : sipConfig.getMonitorIps()) { + String[] monitorIpSplit = monitorIp.split("\\."); + if (monitorIpSplit[0].equals(ipSplit[0]) && monitorIpSplit[1].equals(ipSplit[1]) && monitorIpSplit[2].equals(ipSplit[2])) { + ip3 = monitorIp; + }else if (monitorIpSplit[0].equals(ipSplit[0]) && monitorIpSplit[1].equals(ipSplit[1])) { + ip2 = monitorIp; + }else if (monitorIpSplit[0].equals(ipSplit[0])) { + ip1 = monitorIp; + } + } + if (ip3 != null) { + return ip3; + }else if (ip2 != null) { + return ip2; + }else if (ip1 != null) { + return ip1; + }else { + return sipConfig.getMonitorIps().get(0); + } + } + + /** + * 流离开的处理 + */ + @Async("taskExecutor") + @EventListener + public void onApplicationEvent(MediaDepartureEvent event) { + List sendRtpItems = sendRtpServerService.queryByStream(event.getStream()); + if (!sendRtpItems.isEmpty()) { + for (SendRtpInfo sendRtpItem : sendRtpItems) { + if (sendRtpItem != null && sendRtpItem.getApp().equals(event.getApp()) && sendRtpItem.isSendToPlatform()) { + String platformId = sendRtpItem.getTargetId(); + Platform platform = platformMapper.getParentPlatByServerGBId(platformId); + CommonGBChannel channel = channelService.getOne(sendRtpItem.getChannelId()); + try { + if (platform != null && channel != null) { + commanderForPlatform.streamByeCmd(platform, sendRtpItem, channel); + sendRtpServerService.delete(sendRtpItem); + } + } catch (SipException | InvalidArgumentException | ParseException e) { + log.error("[命令发送失败] 发送BYE: {}", e.getMessage()); + } + } + } + } + } + + + /** + * 发流停止 + */ + @Async("taskExecutor") + @EventListener + public void onApplicationEvent(MediaSendRtpStoppedEvent event) { + List sendRtpItems = sendRtpServerService.queryByStream(event.getStream()); + if (sendRtpItems != null && !sendRtpItems.isEmpty()) { + for (SendRtpInfo sendRtpItem : sendRtpItems) { + if (sendRtpItem != null && sendRtpItem.getApp().equals(event.getApp()) && sendRtpItem.isSendToPlatform()) { + Platform platform = platformMapper.getParentPlatByServerGBId(sendRtpItem.getTargetId()); + CommonGBChannel channel = channelService.getOne(sendRtpItem.getChannelId()); + ssrcFactory.releaseSsrc(sendRtpItem.getMediaServerId(), sendRtpItem.getSsrc()); + try { + commanderForPlatform.streamByeCmd(platform, sendRtpItem, channel); + } catch (SipException | InvalidArgumentException | ParseException e) { + log.error("[命令发送失败] 国标级联 发送BYE: {}", e.getMessage()); + } + sendRtpServerService.delete(sendRtpItem); + } + } + } + } + + @Override + public Platform queryPlatformByServerGBId(String platformGbId) { + return platformMapper.getParentPlatByServerGBId(platformGbId); + } + + @Override + public PageInfo queryPlatformList(int page, int count, String query) { + PageHelper.startPage(page, count); + if (query != null) { + query = query.replaceAll("/", "//") + .replaceAll("%", "/%") + .replaceAll("_", "/_"); + } + List all = platformMapper.queryList(query); + return new PageInfo<>(all); + } + + @Override + public boolean add(Platform platform) { + log.info("[国标级联]添加平台 {}", platform.getDeviceGBId()); + if (platform.getCatalogGroup() == 0) { + // 每次发送目录的数量默认为1 + platform.setCatalogGroup(1); + } + platform.setServerId(userSetting.getServerId()); + int result = platformMapper.add(platform); + + if (platform.isEnable()) { + // 保存时启用就发送注册 + // 注册成功时由程序直接调用了online方法 + sendRegister(platform, null); + } + return result > 0; + } + + + + @Override + public boolean update(Platform platform) { + Assert.isTrue(platform.getId() > 0, "ID必须存在"); + log.info("[国标级联] 更新平台 {}({})", platform.getName(), platform.getDeviceGBId()); + platform.setCharacterSet(platform.getCharacterSet().toUpperCase()); + Platform platformInDb = platformMapper.query(platform.getId()); + Assert.notNull(platformInDb, "平台不存在"); + if (!userSetting.getServerId().equals(platformInDb.getServerId())) { + return redisRpcService.updatePlatform(platformInDb.getServerId(), platform); + } + // 更新数据库 + if (platform.getCatalogGroup() == 0) { + platform.setCatalogGroup(1); + } + platformMapper.update(platform); + if (statusTaskRunner.containsRegister(platformInDb.getServerGBId())) { + SipTransactionInfo transactionInfo = statusTaskRunner.getRegisterTransactionInfo(platformInDb.getServerGBId()); + // 注销后出发平台离线, 如果是启用的平台,那么下次丢失检测会检测到并重新注册上线 + sendUnRegister(platformInDb, transactionInfo); + }else if (platform.isEnable()) { + sendRegister(platform, null); + } + + return false; + } + + @Override + public void online(Platform platform, SipTransactionInfo sipTransactionInfo) { + log.info("[国标级联]:{}, 平台上线", platform.getServerGBId()); + PlatformRegisterTask registerTask = new PlatformRegisterTask(platform.getServerGBId(), platform.getExpires() * 1000L - 500L, + sipTransactionInfo, (platformServerGbId) -> { + this.registerExpire(platformServerGbId, sipTransactionInfo); + }); + statusTaskRunner.addRegisterTask(registerTask); + + PlatformKeepaliveTask keepaliveTask = new PlatformKeepaliveTask(platform.getServerGBId(), platform.getKeepTimeout() * 1000L, + this::keepaliveExpire); + statusTaskRunner.addKeepAliveTask(keepaliveTask); + platformMapper.updateStatus(platform.getId(), true, userSetting.getServerId()); + + if (platform.getAutoPushChannel() != null && platform.getAutoPushChannel()) { + if (subscribeHolder.getCatalogSubscribe(platform.getServerGBId()) == null) { + log.info("[国标级联]:{}, 添加自动通道推送模拟订阅信息", platform.getServerGBId()); + addSimulatedSubscribeInfo(platform); + } + }else { + SubscribeInfo catalogSubscribe = subscribeHolder.getCatalogSubscribe(platform.getServerGBId()); + if (catalogSubscribe != null && catalogSubscribe.getExpires() == -1) { + subscribeHolder.removeCatalogSubscribe(platform.getServerGBId()); + } + } + } + + /** + * 注册到期处理 + */ + private void registerExpire(String platformServerId, SipTransactionInfo transactionInfo) { + log.info("[国标级联] 注册到期, 上级平台编号: {}", platformServerId); + Platform platform = queryPlatformByServerGBId(platformServerId); + if (platform == null || !platform.isEnable()) { + log.info("[国标级联] 注册到期, 上级平台编号: {}, 平台不存在或者未启用, 忽略", platformServerId); + return; + } + sendRegister(platform, transactionInfo); + } + + private void keepaliveExpire(String platformServerId, int failCount) { + Platform platform = queryPlatformByServerGBId(platformServerId); + if (platform == null || !platform.isEnable()) { + log.info("[国标级联] 心跳到期, 上级平台编号: {}, 平台不存在或者未启用, 忽略", platformServerId); + return; + } + try { + commanderForPlatform.keepalive(platform, eventResult -> { + // 心跳失败 + if (eventResult.type != SipSubscribe.EventResultType.timeout) { + log.warn("[国标级联] 发送心跳收到错误,code: {}, msg: {}", eventResult.statusCode, eventResult.msg); + } + + // 心跳超时失败 + if (failCount < 2) { + log.info("[国标级联] 心跳发送超时, 平台服务编号: {}", platformServerId); + PlatformKeepaliveTask keepaliveTask = new PlatformKeepaliveTask(platform.getServerGBId(), platform.getKeepTimeout() * 1000L, + this::keepaliveExpire); + keepaliveTask.setFailCount(failCount + 1); + statusTaskRunner.addKeepAliveTask(keepaliveTask); + }else { + // 心跳超时三次, 不再发送心跳, 平台离线 + log.info("[国标级联] 心跳发送超时三次,平台离线, 平台服务编号: {}", platformServerId); + offline(platform); + } + }, eventResult -> { + PlatformKeepaliveTask keepaliveTask = new PlatformKeepaliveTask(platform.getServerGBId(), platform.getKeepTimeout() * 1000L, + this::keepaliveExpire); + statusTaskRunner.addKeepAliveTask(keepaliveTask); + }); + } catch (SipException | InvalidArgumentException | ParseException e) { + log.error("[命令发送失败] 国标级联 发送心跳: {}", e.getMessage()); + if (failCount < 2) { + PlatformKeepaliveTask keepaliveTask = new PlatformKeepaliveTask(platform.getServerGBId(), platform.getKeepTimeout() * 1000L, + this::keepaliveExpire); + keepaliveTask.setFailCount(failCount + 1); + statusTaskRunner.addKeepAliveTask(keepaliveTask); + }else { + // 心跳超时三次, 不再发送心跳, 平台离线 + log.info("[国标级联] 心跳发送失败三次,平台离线, 平台服务编号: {}", platformServerId); + offline(platform); + } + } + } + + @Override + public void addSimulatedSubscribeInfo(Platform platform) { + // 自动添加一条模拟的订阅信息 + subscribeHolder.putCatalogSubscribe(platform.getServerGBId(), + SubscribeInfo.buildSimulated(platform.getServerGBId(), platform.getServerIp())); + } + + @Override + public void offline(Platform platform) { + log.info("[平台离线]:{}({})", platform.getName(), platform.getServerGBId()); + statusTaskRunner.removeRegisterTask(platform.getServerGBId()); + statusTaskRunner.removeKeepAliveTask(platform.getServerGBId()); + + subscribeHolder.removeCatalogSubscribe(platform.getServerGBId()); + subscribeHolder.removeMobilePositionSubscribe(platform.getServerGBId()); + + platformMapper.updateStatus(platform.getId(), false, userSetting.getServerId()); + + // 停止所有推流 + log.info("[平台离线] {}({}), 停止所有推流", platform.getName(), platform.getServerGBId()); + stopAllPush(platform.getServerGBId()); + } + + private void stopAllPush(String platformId) { + List sendRtpItems = sendRtpServerService.queryForPlatform(platformId); + if (sendRtpItems != null && !sendRtpItems.isEmpty()) { + for (SendRtpInfo sendRtpItem : sendRtpItems) { + ssrcFactory.releaseSsrc(sendRtpItem.getMediaServerId(), sendRtpItem.getSsrc()); + sendRtpServerService.delete(sendRtpItem); + MediaServer mediaInfo = mediaServerService.getOne(sendRtpItem.getMediaServerId()); + mediaServerService.stopSendRtp(mediaInfo, sendRtpItem.getApp(), sendRtpItem.getStream(), null); + } + } + } + + @Override + public void sendNotifyMobilePosition(String platformId) { + Platform platform = platformMapper.getParentPlatByServerGBId(platformId); + if (platform == null) { + return; + } + SubscribeInfo subscribe = subscribeHolder.getMobilePositionSubscribe(platform.getServerGBId()); + if (subscribe != null) { + + List channelList = platformChannelMapper.queryShare(platform.getId(), null); + if (channelList.isEmpty()) { + return; + } + for (CommonGBChannel channel : channelList) { + GPSMsgInfo gpsMsgInfo = redisCatchStorage.getGpsMsgInfo(channel.getGbDeviceId()); + // 无最新位置则发送当前位置 + if (gpsMsgInfo != null && (gpsMsgInfo.getLng() == 0 && gpsMsgInfo.getLat() == 0)) { + gpsMsgInfo = null; + } + + if (gpsMsgInfo == null && !userSetting.isSendPositionOnDemand()){ + gpsMsgInfo = new GPSMsgInfo(); + gpsMsgInfo.setId(channel.getGbDeviceId()); + gpsMsgInfo.setLng(channel.getGbLongitude()); + gpsMsgInfo.setLat(channel.getGbLatitude()); + gpsMsgInfo.setAltitude(channel.getGpsAltitude()); + gpsMsgInfo.setSpeed(channel.getGpsSpeed()); + gpsMsgInfo.setDirection(channel.getGpsDirection()); + gpsMsgInfo.setTime(channel.getGpsTime()); + } + + // 无最新位置不发送 + if (gpsMsgInfo != null) { + // 发送GPS消息 + try { + commanderForPlatform.sendNotifyMobilePosition(platform, gpsMsgInfo, channel, subscribe); + } catch (InvalidArgumentException | ParseException | NoSuchFieldException | SipException | + IllegalAccessException e) { + log.error("[命令发送失败] 国标级联 移动位置通知: {}", e.getMessage()); + } + } + } + } + } + + @Override + public void broadcastInvite(Platform platform, CommonGBChannel channel, String sourceId, MediaServer mediaServerItem, HookSubscribe.Event hookEvent, + SipSubscribe.Event errorEvent, InviteTimeOutCallback timeoutCallback) throws InvalidArgumentException, ParseException, SipException { + + if (mediaServerItem == null) { + log.info("[国标级联] 语音喊话未找到可用的zlm. platform: {}", platform.getServerGBId()); + return; + } + InviteInfo inviteInfoForOld = inviteStreamService.getInviteInfoByDeviceAndChannel(InviteSessionType.BROADCAST, channel.getGbId()); + + if (inviteInfoForOld != null && inviteInfoForOld.getStreamInfo() != null) { + // 如果zlm不存在这个流,则删除数据即可 + MediaServer mediaServerItemForStreamInfo = mediaServerService.getOne(inviteInfoForOld.getStreamInfo().getMediaServer().getId()); + if (mediaServerItemForStreamInfo != null) { + Boolean ready = mediaServerService.isStreamReady(mediaServerItemForStreamInfo, inviteInfoForOld.getStreamInfo().getApp(), inviteInfoForOld.getStreamInfo().getStream()); + if (!ready) { + // 错误存在于redis中的数据 + inviteStreamService.removeInviteInfo(inviteInfoForOld); + }else { + // 流确实尚在推流,直接回调结果 + HookData hookData = new HookData(); + hookData.setApp(inviteInfoForOld.getStreamInfo().getApp()); + hookData.setStream(inviteInfoForOld.getStreamInfo().getStream()); + hookData.setMediaServer(mediaServerItemForStreamInfo); + hookEvent.response(hookData); + return; + } + } + } + + String streamId = null; + if (mediaServerItem.isRtpEnable()) { + streamId = String.format("%s_%s", platform.getServerGBId(), channel.getGbDeviceId()); + } + // 默认不进行SSRC校验, TODO 后续可改为配置 + boolean ssrcCheck = false; + int tcpMode; + if (userSetting.getBroadcastForPlatform().equalsIgnoreCase("TCP-PASSIVE")) { + tcpMode = 1; + }else if (userSetting.getBroadcastForPlatform().equalsIgnoreCase("TCP-ACTIVE")) { + tcpMode = 2; + } else { + tcpMode = 0; + } + SSRCInfo ssrcInfo = mediaServerService.openRTPServer(mediaServerItem, streamId, null, ssrcCheck, false, null, true, false, false, tcpMode); + if (ssrcInfo == null || ssrcInfo.getPort() < 0) { + log.info("[国标级联] 发起语音喊话 开启端口监听失败, platform: {}, channel: {}", platform.getServerGBId(), channel.getGbDeviceId()); + SipSubscribe.EventResult eventResult = new SipSubscribe.EventResult<>(); + eventResult.statusCode = -1; + eventResult.msg = "端口监听失败"; + eventResult.type = SipSubscribe.EventResultType.failedToGetPort; + errorEvent.response(eventResult); + return; + } + log.info("[国标级联] 语音喊话,发起Invite消息 deviceId: {}, channelId: {},收流端口: {}, 收流模式:{}, SSRC: {}, SSRC校验:{}", + platform.getServerGBId(), channel.getGbDeviceId(), ssrcInfo.getPort(), userSetting.getBroadcastForPlatform(), ssrcInfo.getSsrc(), ssrcCheck); + + // 初始化redis中的invite消息状态 + InviteInfo inviteInfo = InviteInfo.getInviteInfo(platform.getServerGBId(), channel.getGbId(), ssrcInfo.getStream(), ssrcInfo, mediaServerItem.getId(), + mediaServerItem.getSdpIp(), ssrcInfo.getPort(), userSetting.getBroadcastForPlatform(), InviteSessionType.BROADCAST, + InviteSessionStatus.ready, userSetting.getRecordSip()); + inviteStreamService.updateInviteInfo(inviteInfo); + String timeOutTaskKey = UUID.randomUUID().toString(); + dynamicTask.startDelay(timeOutTaskKey, () -> { + // 执行超时任务时查询是否已经成功,成功了则不执行超时任务,防止超时任务取消失败的情况 + InviteInfo inviteInfoForBroadcast = inviteStreamService.getInviteInfo(InviteSessionType.BROADCAST, channel.getGbId(), null); + if (inviteInfoForBroadcast == null) { + log.info("[国标级联] 发起语音喊话 收流超时 deviceId: {}, channelId: {},端口:{}, SSRC: {}", platform.getServerGBId(), channel.getGbDeviceId(), ssrcInfo.getPort(), ssrcInfo.getSsrc()); + // 点播超时回复BYE 同时释放ssrc以及此次点播的资源 + try { + commanderForPlatform.streamByeCmd(platform, channel, ssrcInfo.getApp(), ssrcInfo.getStream(), null, null); + } catch (InvalidArgumentException | ParseException | SipException | SsrcTransactionNotFoundException e) { + log.error("[点播超时], 发送BYE失败 {}", e.getMessage()); + } finally { + timeoutCallback.run(1, "收流超时"); + mediaServerService.releaseSsrc(mediaServerItem.getId(), ssrcInfo.getSsrc()); + mediaServerService.closeRTPServer(mediaServerItem, ssrcInfo.getStream()); + sessionManager.removeByStream(ssrcInfo.getApp(), ssrcInfo.getStream()); + } + } + }, userSetting.getPlayTimeout()); + commanderForPlatform.broadcastInviteCmd(platform, channel,sourceId, mediaServerItem, ssrcInfo, (hookData)->{ + log.info("[国标级联] 发起语音喊话 收到上级推流 deviceId: {}, channelId: {}", platform.getServerGBId(), channel.getGbDeviceId()); + dynamicTask.stop(timeOutTaskKey); + // hook响应 + onPublishHandlerForBroadcast(hookData.getMediaServer(), hookData.getMediaInfo(), platform, channel); + // 收到流 + if (hookEvent != null) { + hookEvent.response(hookData); + } + }, event -> { + + inviteOKHandler(event, ssrcInfo, tcpMode, ssrcCheck, mediaServerItem, platform, channel, timeOutTaskKey, + null, inviteInfo, InviteSessionType.BROADCAST); + }, eventResult -> { + // 收到错误回复 + if (errorEvent != null) { + errorEvent.response(eventResult); + } + }); + } + + public void onPublishHandlerForBroadcast(MediaServer mediaServerItem, MediaInfo mediaInfo, Platform platform, CommonGBChannel channel) { + StreamInfo streamInfo = mediaServerService.getStreamInfoByAppAndStream(mediaServerItem, mediaInfo.getApp(), mediaInfo.getStream(), mediaInfo, null); + streamInfo.setChannelId(channel.getGbId()); + + InviteInfo inviteInfo = inviteStreamService.getInviteInfoByDeviceAndChannel(InviteSessionType.BROADCAST, channel.getGbId()); + if (inviteInfo != null) { + inviteInfo.setStatus(InviteSessionStatus.ok); + inviteInfo.setStreamInfo(streamInfo); + inviteStreamService.updateInviteInfo(inviteInfo); + } + } + + private void inviteOKHandler(SipSubscribe.EventResult eventResult, SSRCInfo ssrcInfo, int tcpMode, boolean ssrcCheck, MediaServer mediaServerItem, + Platform platform, CommonGBChannel channel, String timeOutTaskKey, ErrorCallback callback, + InviteInfo inviteInfo, InviteSessionType inviteSessionType){ + inviteInfo.setStatus(InviteSessionStatus.ok); + ResponseEvent responseEvent = (ResponseEvent) eventResult.event; + String contentString = new String(responseEvent.getResponse().getRawContent()); + String ssrcInResponse = SipUtils.getSsrcFromSdp(contentString); + // 兼容回复的消息中缺少ssrc(y字段)的情况 + if (ssrcInResponse == null) { + ssrcInResponse = ssrcInfo.getSsrc(); + } + if (ssrcInfo.getSsrc().equals(ssrcInResponse)) { + // ssrc 一致 + if (mediaServerItem.isRtpEnable()) { + // 多端口 + if (tcpMode == 2) { + tcpActiveHandler(platform, channel, contentString, mediaServerItem, tcpMode, ssrcCheck, + timeOutTaskKey, ssrcInfo, callback); + } + }else { + // 单端口 + if (tcpMode == 2) { + log.warn("[Invite 200OK] 单端口收流模式不支持tcp主动模式收流"); + } + } + }else { + log.info("[Invite 200OK] 收到invite 200, 发现下级自定义了ssrc: {}", ssrcInResponse); + // ssrc 不一致 + if (mediaServerItem.isRtpEnable()) { + // 多端口 + if (ssrcCheck) { + // ssrc检验 + // 更新ssrc + log.info("[Invite 200OK] SSRC修正 {}->{}", ssrcInfo.getSsrc(), ssrcInResponse); + // 释放ssrc + mediaServerService.releaseSsrc(mediaServerItem.getId(), ssrcInfo.getSsrc()); + Boolean result = mediaServerService.updateRtpServerSSRC(mediaServerItem, ssrcInfo.getStream(), ssrcInResponse); + if (!result) { + try { + log.warn("[Invite 200OK] 更新ssrc失败,停止喊话 {}/{}", platform.getServerGBId(), channel.getGbDeviceId()); + commanderForPlatform.streamByeCmd(platform, channel, ssrcInfo.getApp(), ssrcInfo.getStream(), null, null); + } catch (InvalidArgumentException | SipException | ParseException | SsrcTransactionNotFoundException e) { + log.error("[命令发送失败] 停止播放, 发送BYE: {}", e.getMessage()); + } + + dynamicTask.stop(timeOutTaskKey); + // 释放ssrc + mediaServerService.releaseSsrc(mediaServerItem.getId(), ssrcInfo.getSsrc()); + + sessionManager.removeByStream(ssrcInfo.getApp(), ssrcInfo.getStream()); + + callback.run(InviteErrorCode.ERROR_FOR_RESET_SSRC.getCode(), + "下级自定义了ssrc,重新设置收流信息失败", null); + inviteStreamService.call(inviteSessionType, channel.getGbId(), null, + InviteErrorCode.ERROR_FOR_RESET_SSRC.getCode(), + "下级自定义了ssrc,重新设置收流信息失败", null); + + }else { + ssrcInfo.setSsrc(ssrcInResponse); + inviteInfo.setSsrcInfo(ssrcInfo); + inviteInfo.setStream(ssrcInfo.getStream()); + if (tcpMode == 2) { + if (mediaServerItem.isRtpEnable()) { + tcpActiveHandler(platform, channel, contentString, mediaServerItem, tcpMode, ssrcCheck, + timeOutTaskKey, ssrcInfo, callback); + }else { + log.warn("[Invite 200OK] 单端口收流模式不支持tcp主动模式收流"); + } + } + inviteStreamService.updateInviteInfo(inviteInfo); + } + }else { + ssrcInfo.setSsrc(ssrcInResponse); + inviteInfo.setSsrcInfo(ssrcInfo); + inviteInfo.setStream(ssrcInfo.getStream()); + if (tcpMode == 2) { + if (mediaServerItem.isRtpEnable()) { + tcpActiveHandler(platform, channel, contentString, mediaServerItem, tcpMode, ssrcCheck, + timeOutTaskKey, ssrcInfo, callback); + }else { + log.warn("[Invite 200OK] 单端口收流模式不支持tcp主动模式收流"); + } + } + inviteStreamService.updateInviteInfo(inviteInfo); + } + }else { + if (ssrcInResponse != null) { + // 单端口 + // 重新订阅流上线 + SsrcTransaction ssrcTransaction = sessionManager.getSsrcTransactionByStream(ssrcInfo.getApp(), inviteInfo.getStream()); + sessionManager.removeByStream(ssrcInfo.getApp(), inviteInfo.getStream()); + inviteStreamService.updateInviteInfoForSSRC(inviteInfo, ssrcInResponse); + + ssrcTransaction.setPlatformId(platform.getServerGBId()); + ssrcTransaction.setChannelId(channel.getGbId()); + ssrcTransaction.setApp(ssrcInfo.getApp()); + ssrcTransaction.setStream(inviteInfo.getStream()); + ssrcTransaction.setSsrc(ssrcInResponse); + ssrcTransaction.setMediaServerId(mediaServerItem.getId()); + ssrcTransaction.setSipTransactionInfo(new SipTransactionInfo((SIPResponse) responseEvent.getResponse())); + ssrcTransaction.setType(inviteSessionType); + + sessionManager.put(ssrcTransaction); + } + } + } + } + + + private void tcpActiveHandler(Platform platform, CommonGBChannel channel, String contentString, + MediaServer mediaServerItem, int tcpMode, boolean ssrcCheck, + String timeOutTaskKey, SSRCInfo ssrcInfo, ErrorCallback callback){ + if (tcpMode != 2) { + return; + } + + String substring; + if (contentString.indexOf("y=") > 0) { + substring = contentString.substring(0, contentString.indexOf("y=")); + }else { + substring = contentString; + } + try { + SessionDescription sdp = SdpFactory.getInstance().createSessionDescription(substring); + int port = -1; + Vector mediaDescriptions = sdp.getMediaDescriptions(true); + for (Object description : mediaDescriptions) { + MediaDescription mediaDescription = (MediaDescription) description; + Media media = mediaDescription.getMedia(); + + Vector mediaFormats = media.getMediaFormats(false); + if (mediaFormats.contains("8") || mediaFormats.contains("0")) { + port = media.getMediaPort(); + break; + } + } + log.info("[TCP主动连接对方] serverGbId: {}, channelId: {}, 连接对方的地址:{}:{}, SSRC: {}, SSRC校验:{}", + platform.getServerGBId(), channel.getGbDeviceId(), sdp.getConnection().getAddress(), port, ssrcInfo.getSsrc(), ssrcCheck); + Boolean result = mediaServerService.connectRtpServer(mediaServerItem, sdp.getConnection().getAddress(), port, ssrcInfo.getStream()); + log.info("[TCP主动连接对方] 结果: {}", result); + } catch (SdpException e) { + log.error("[TCP主动连接对方] serverGbId: {}, channelId: {}, 解析200OK的SDP信息失败", platform.getServerGBId(), channel.getGbDeviceId(), e); + dynamicTask.stop(timeOutTaskKey); + mediaServerService.closeRTPServer(mediaServerItem, ssrcInfo.getStream()); + // 释放ssrc + mediaServerService.releaseSsrc(mediaServerItem.getId(), ssrcInfo.getSsrc()); + + sessionManager.removeByStream(ssrcInfo.getApp(), ssrcInfo.getStream()); + + callback.run(InviteErrorCode.ERROR_FOR_SDP_PARSING_EXCEPTIONS.getCode(), + InviteErrorCode.ERROR_FOR_SDP_PARSING_EXCEPTIONS.getMsg(), null); + inviteStreamService.call(InviteSessionType.PLAY, channel.getGbId(), null, + InviteErrorCode.ERROR_FOR_SDP_PARSING_EXCEPTIONS.getCode(), + InviteErrorCode.ERROR_FOR_SDP_PARSING_EXCEPTIONS.getMsg(), null); + } + } + + @Override + public void stopBroadcast(Platform platform, CommonGBChannel channel, String app, String stream, boolean sendBye, MediaServer mediaServerItem) { + + try { + if (sendBye) { + commanderForPlatform.streamByeCmd(platform, channel, app, stream, null, null); + } + } catch (InvalidArgumentException | SipException | ParseException | SsrcTransactionNotFoundException e) { + log.warn("[消息发送失败] 停止语音对讲, 平台:{},通道:{}", platform.getId(), channel.getGbDeviceId() ); + } finally { + mediaServerService.closeRTPServer(mediaServerItem, stream); + InviteInfo inviteInfo = inviteStreamService.getInviteInfo(null, channel.getGbId(), stream); + if (inviteInfo != null) { + // 释放ssrc + mediaServerService.releaseSsrc(mediaServerItem.getId(), inviteInfo.getSsrcInfo().getSsrc()); + inviteStreamService.removeInviteInfo(inviteInfo); + } + sessionManager.removeByStream(app, stream); + } + } + + @Override + public Platform queryOne(Integer platformId) { + return platformMapper.query(platformId); + } + + @Override + public List queryEnablePlatformList(String serverId) { + return platformMapper.queryEnableParentPlatformListByServerId(serverId,true); + } + + @Override + @Transactional + public void delete(Integer platformId, CommonCallback callback) { + Platform platform = platformMapper.query(platformId); + Assert.notNull(platform, "平台不存在"); + if (statusTaskRunner.containsRegister(platform.getServerGBId())) { + try { + SipTransactionInfo transactionInfo = statusTaskRunner.getRegisterTransactionInfo(platform.getServerGBId()); + sendUnRegister(platform, transactionInfo); + }catch (Exception ignored) {} + } + platformMapper.delete(platform.getId()); + + statusTaskRunner.removeRegisterTask(platform.getServerGBId()); + statusTaskRunner.removeKeepAliveTask(platform.getServerGBId()); + + subscribeHolder.removeCatalogSubscribe(platform.getServerGBId()); + subscribeHolder.removeMobilePositionSubscribe(platform.getServerGBId()); + if (callback != null) { + callback.run(true); + } + } + + @Override + public List queryAll(String serverId) { + return platformMapper.queryByServerId(serverId); + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/service/impl/PlayServiceImpl.java b/src/main/java/com/genersoft/iot/vmp/gb28181/service/impl/PlayServiceImpl.java new file mode 100755 index 0000000..8085ba4 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/service/impl/PlayServiceImpl.java @@ -0,0 +1,1809 @@ +package com.genersoft.iot.vmp.gb28181.service.impl; + +import com.genersoft.iot.vmp.common.*; +import com.genersoft.iot.vmp.conf.DynamicTask; +import com.genersoft.iot.vmp.conf.UserSetting; +import com.genersoft.iot.vmp.conf.exception.ControllerException; +import com.genersoft.iot.vmp.conf.exception.ServiceException; +import com.genersoft.iot.vmp.conf.exception.SsrcTransactionNotFoundException; +import com.genersoft.iot.vmp.gb28181.bean.*; +import com.genersoft.iot.vmp.gb28181.controller.bean.AudioBroadcastEvent; +import com.genersoft.iot.vmp.gb28181.event.SipSubscribe; +import com.genersoft.iot.vmp.gb28181.service.*; +import com.genersoft.iot.vmp.gb28181.session.AudioBroadcastManager; +import com.genersoft.iot.vmp.gb28181.session.SSRCFactory; +import com.genersoft.iot.vmp.gb28181.session.SipInviteSessionManager; +import com.genersoft.iot.vmp.gb28181.transmit.cmd.ISIPCommander; +import com.genersoft.iot.vmp.gb28181.transmit.cmd.ISIPCommanderForPlatform; +import com.genersoft.iot.vmp.gb28181.utils.SipUtils; +import com.genersoft.iot.vmp.media.bean.MediaInfo; +import com.genersoft.iot.vmp.media.bean.MediaServer; +import com.genersoft.iot.vmp.media.bean.RecordInfo; +import com.genersoft.iot.vmp.media.event.hook.Hook; +import com.genersoft.iot.vmp.media.event.hook.HookSubscribe; +import com.genersoft.iot.vmp.media.event.hook.HookType; +import com.genersoft.iot.vmp.media.event.media.MediaArrivalEvent; +import com.genersoft.iot.vmp.media.event.media.MediaDepartureEvent; +import com.genersoft.iot.vmp.media.event.media.MediaNotFoundEvent; +import com.genersoft.iot.vmp.media.service.IMediaServerService; +import com.genersoft.iot.vmp.media.zlm.dto.StreamAuthorityInfo; +import com.genersoft.iot.vmp.service.ICloudRecordService; +import com.genersoft.iot.vmp.service.IReceiveRtpServerService; +import com.genersoft.iot.vmp.service.ISendRtpServerService; +import com.genersoft.iot.vmp.service.bean.*; +import com.genersoft.iot.vmp.service.redisMsg.IRedisRpcPlayService; +import com.genersoft.iot.vmp.storager.IRedisCatchStorage; +import com.genersoft.iot.vmp.utils.DateUtil; +import com.genersoft.iot.vmp.vmanager.bean.AudioBroadcastResult; +import com.genersoft.iot.vmp.vmanager.bean.ErrorCode; +import com.genersoft.iot.vmp.vmanager.bean.StreamContent; +import gov.nist.javax.sip.message.SIPResponse; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.event.EventListener; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Service; +import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; + +import javax.sdp.*; +import javax.sip.InvalidArgumentException; +import javax.sip.ResponseEvent; +import javax.sip.SipException; +import javax.sip.header.CallIdHeader; +import javax.sip.message.Response; +import java.io.File; +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.text.ParseException; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; +import java.util.Vector; + +@SuppressWarnings(value = {"rawtypes", "unchecked"}) +@Slf4j +@Service("playService") +public class PlayServiceImpl implements IPlayService { + + @Autowired + private ISIPCommander cmder; + + @Autowired + private AudioBroadcastManager audioBroadcastManager; + + @Autowired + private IDeviceService deviceService; + + @Autowired + private ISIPCommanderForPlatform sipCommanderFroPlatform; + + @Autowired + private IRedisCatchStorage redisCatchStorage; + + @Autowired + private IInviteStreamService inviteStreamService; + + @Autowired + private HookSubscribe subscribe; + + @Autowired + private IMediaServerService mediaServerService; + + @Autowired + private SipInviteSessionManager sessionManager; + + @Autowired + private UserSetting userSetting; + + @Autowired + private IDeviceChannelService deviceChannelService; + + @Autowired + private DynamicTask dynamicTask; + + @Autowired + private ISIPCommanderForPlatform commanderForPlatform; + + @Autowired + private SSRCFactory ssrcFactory; + + @Autowired + private IPlatformService platformService; + + @Autowired + private IGbChannelService channelService; + + @Autowired + private ISendRtpServerService sendRtpServerService; + + @Autowired + private IReceiveRtpServerService receiveRtpServerService; + + @Autowired + private ICloudRecordService cloudRecordService; + + @Autowired + private IRedisRpcPlayService redisRpcPlayService; + + /** + * 流到来的处理 + */ + @Async("taskExecutor") + @EventListener + public void onApplicationEvent(MediaArrivalEvent event) { + if ("broadcast".equals(event.getApp()) || "talk".equals(event.getApp())) { + if (event.getStream().indexOf("_") > 0) { + String[] streamArray = event.getStream().split("_"); + if (streamArray.length == 2) { + String deviceId = streamArray[0]; + String channelId = streamArray[1]; + Device device = deviceService.getDeviceByDeviceId(deviceId); + DeviceChannel channel = deviceChannelService.getOneForSource(deviceId, channelId); + if (device == null) { + log.info("[语音对讲/喊话] 未找到设备:{}", deviceId); + return; + } + if (channel == null) { + log.info("[语音对讲/喊话] 未找到通道:{}", channelId); + return; + } + if ("broadcast".equals(event.getApp())) { + if (audioBroadcastManager.exit(channel.getId())) { + stopAudioBroadcast(device, channel); + } + // 开启语音对讲通道 + try { + audioBroadcastCmd(device, channel, event.getMediaServer(), + event.getApp(), event.getStream(), 60, false, (msg) -> { + log.info("[语音对讲] 通道建立成功, device: {}, channel: {}", deviceId, channelId); + }); + } catch (InvalidArgumentException | ParseException | SipException e) { + log.error("[命令发送失败] 语音对讲: {}", e.getMessage()); + } + }else if ("talk".equals(event.getApp())) { + // 开启语音对讲通道 + talkCmd(device, channel, event.getMediaServer(), event.getStream(), (msg) -> { + log.info("[语音对讲] 通道建立成功, device: {}, channel: {}", deviceId, channelId); + }); + } + } + } + } + + + } + + /** + * 流离开的处理 + */ + @Async("taskExecutor") + @EventListener + public void onApplicationEvent(MediaDepartureEvent event) { + List sendRtpInfos = sendRtpServerService.queryByStream(event.getStream()); + if (!sendRtpInfos.isEmpty()) { + for (SendRtpInfo sendRtpInfo : sendRtpInfos) { + if (sendRtpInfo != null && sendRtpInfo.isSendToPlatform() && sendRtpInfo.getApp().equals(event.getApp())) { + String platformId = sendRtpInfo.getTargetId(); + Device device = deviceService.getDeviceByDeviceId(platformId); + DeviceChannel channel = deviceChannelService.getOneById(sendRtpInfo.getChannelId()); + try { + if (device != null && channel != null) { + cmder.streamByeCmd(device, channel.getDeviceId(), event.getApp(), event.getStream(), sendRtpInfo.getCallId(), null); + if (sendRtpInfo.getPlayType().equals(InviteStreamType.BROADCAST) + || sendRtpInfo.getPlayType().equals(InviteStreamType.TALK)) { + AudioBroadcastCatch audioBroadcastCatch = audioBroadcastManager.get(channel.getId()); + if (audioBroadcastCatch != null) { + // 来自上级平台的停止对讲 + log.info("[停止对讲] 来自上级,平台:{}, 通道:{}", sendRtpInfo.getTargetId(), sendRtpInfo.getChannelId()); + audioBroadcastManager.del(sendRtpInfo.getChannelId()); + } + } + } + } catch (SipException | InvalidArgumentException | ParseException | + SsrcTransactionNotFoundException e) { + log.error("[命令发送失败] 发送BYE: {}", e.getMessage()); + } + } + } + } + + if ("broadcast".equals(event.getApp()) || "talk".equals(event.getApp())) { + if (event.getStream().indexOf("_") > 0) { + String[] streamArray = event.getStream().split("_"); + if (streamArray.length == 2) { + String deviceId = streamArray[0]; + String channelId = streamArray[1]; + Device device = deviceService.getDeviceByDeviceId(deviceId); + if (device == null) { + log.info("[语音对讲/喊话] 未找到设备:{}", deviceId); + return; + } + DeviceChannel channel = deviceChannelService.getOneForSource(deviceId, channelId); + if (channel == null) { + log.info("[语音对讲/喊话] 未找到通道:{}", channelId); + return; + } + if ("broadcast".equals(event.getApp())) { + stopAudioBroadcast(device, channel); + }else if ("talk".equals(event.getApp())) { + stopTalk(device, channel, false); + } + } + } + }else if ("rtp".equals(event.getApp())) { + // 释放ssrc + InviteInfo inviteInfo = inviteStreamService.getInviteInfoByStream(null, event.getStream()); + if (inviteInfo != null && inviteInfo.getStatus() == InviteSessionStatus.ok + && inviteInfo.getStreamInfo() != null && inviteInfo.getSsrcInfo() != null) { + // 发送bye + stop(inviteInfo); + } + + } + } + + /** + * 流未找到的处理 + */ + @Async("taskExecutor") + @EventListener + public void onApplicationEvent(MediaNotFoundEvent event) { + if (!"rtp".equals(event.getApp())) { + return; + } + String[] s = event.getStream().split("_"); + if ((s.length != 2 && s.length != 4)) { + return; + } + String deviceId = s[0]; + String channelId = s[1]; + Device device = redisCatchStorage.getDevice(deviceId); + if (device == null || !device.isOnLine()) { + return; + } + DeviceChannel deviceChannel = deviceChannelService.getOne(deviceId, channelId); + if (deviceChannel == null) { + return; + } + if (s.length == 2) { + log.info("[ZLM HOOK] 预览流未找到, 发起自动点播:{}->{}->{}/{}", event.getMediaServer().getId(), event.getSchema(), event.getApp(), event.getStream()); + play(event.getMediaServer(), deviceId, channelId, null, (code, msg, data) -> {}); + } else if (s.length == 4) { + // 此时为录像回放, 录像回放格式为> 设备ID_通道ID_开始时间_结束时间 + String startTimeStr = s[2]; + String endTimeStr = s[3]; + if (startTimeStr == null || endTimeStr == null || startTimeStr.length() != 14 || endTimeStr.length() != 14) { + return; + } + String startTime = DateUtil.urlToyyyy_MM_dd_HH_mm_ss(startTimeStr); + String endTime = DateUtil.urlToyyyy_MM_dd_HH_mm_ss(endTimeStr); + log.info("[ZLM HOOK] 回放流未找到, 发起自动点播:{}->{}->{}/{}-{}-{}", + event.getMediaServer().getId(), event.getSchema(), + event.getApp(), event.getStream(), + startTime, endTime + ); + + playBack(event.getMediaServer(), device, deviceChannel, startTime, endTime, (code, msg, data) -> {}); + } + } + + @Override + public void play(Device device, DeviceChannel channel, ErrorCallback callback) { + + // 判断设备是否属于当前平台, 如果不属于则发起自动调用 + if (!userSetting.getServerId().equals(device.getServerId())) { + redisRpcPlayService.play(device.getServerId(), channel.getId(), callback); + return; + } + MediaServer mediaServerItem = getNewMediaServerItem(device); + if (mediaServerItem == null) { + log.warn("[点播] 未找到可用的zlm deviceId: {},channelId:{}", device.getDeviceId(), channel.getDeviceId()); + throw new ControllerException(ErrorCode.ERROR100.getCode(), "未找到可用的zlm"); + } + play(mediaServerItem, device, channel, null, userSetting.getRecordSip(), callback); + } + + @Override + public SSRCInfo play(MediaServer mediaServerItem, String deviceId, String channelId, String ssrc, ErrorCallback callback) { + if (mediaServerItem == null) { + log.warn("[点播] 未找到可用的zlm deviceId: {},channelId:{}", deviceId, channelId); + throw new ControllerException(ErrorCode.ERROR100.getCode(), "未找到可用的zlm"); + } + Device device = redisCatchStorage.getDevice(deviceId); + if (device.getStreamMode().equalsIgnoreCase("TCP-ACTIVE") && !mediaServerItem.isRtpEnable()) { + log.warn("[点播] 单端口收流时不支持TCP主动方式收流 deviceId: {},channelId:{}", deviceId, channelId); + throw new ControllerException(ErrorCode.ERROR100.getCode(), "单端口收流时不支持TCP主动方式收流"); + } + DeviceChannel channel = deviceChannelService.getOneForSource(deviceId, channelId); + if (channel == null) { + log.warn("[点播] 未找到通道 deviceId: {},channelId:{}", deviceId, channelId); + throw new ControllerException(ErrorCode.ERROR100.getCode(), "未找到通道"); + } + + return play(mediaServerItem, device, channel, ssrc, userSetting.getRecordSip(), callback); + } + + private SSRCInfo play(MediaServer mediaServerItem, Device device, DeviceChannel channel, String ssrc, Boolean record, + ErrorCallback callback) { + if (mediaServerItem == null ) { + if (callback != null) { + callback.run(InviteErrorCode.ERROR_FOR_PARAMETER_ERROR.getCode(), + InviteErrorCode.ERROR_FOR_PARAMETER_ERROR.getMsg(), + null); + } + return null; + } + + InviteInfo inviteInfoInCatch = inviteStreamService.getInviteInfoByDeviceAndChannel(InviteSessionType.PLAY, channel.getId()); + if (inviteInfoInCatch != null ) { + if (inviteInfoInCatch.getStreamInfo() == null) { + // 释放生成的ssrc,使用上一次申请的 + + ssrcFactory.releaseSsrc(mediaServerItem.getId(), ssrc); + // 点播发起了但是尚未成功, 仅注册回调等待结果即可 + inviteStreamService.once(InviteSessionType.PLAY, channel.getId(), null, callback); + log.info("[点播开始] 已经请求中,等待结果, deviceId: {}, channelId({}): {}", device.getDeviceId(), channel.getDeviceId(), channel.getId()); + return inviteInfoInCatch.getSsrcInfo(); + }else { + StreamInfo streamInfo = inviteInfoInCatch.getStreamInfo(); + String streamId = streamInfo.getStream(); + if (streamId == null) { + callback.run(InviteErrorCode.ERROR_FOR_CATCH_DATA.getCode(), "点播失败, redis缓存streamId等于null", null); + inviteStreamService.call(InviteSessionType.PLAY, channel.getId(), null, + InviteErrorCode.ERROR_FOR_CATCH_DATA.getCode(), + "点播失败, redis缓存streamId等于null", + null); + return inviteInfoInCatch.getSsrcInfo(); + } + MediaServer mediaInfo = streamInfo.getMediaServer(); + Boolean ready = mediaServerService.isStreamReady(mediaInfo, "rtp", streamId); + if (ready != null && ready) { + if(callback != null) { + callback.run(InviteErrorCode.SUCCESS.getCode(), InviteErrorCode.SUCCESS.getMsg(), streamInfo); + } + inviteStreamService.call(InviteSessionType.PLAY, channel.getId(), null, + InviteErrorCode.SUCCESS.getCode(), + InviteErrorCode.SUCCESS.getMsg(), + streamInfo); + log.info("[点播已存在] 直接返回, 设备编号: {}, 通道编号: {}", device.getDeviceId(), channel.getDeviceId()); + return inviteInfoInCatch.getSsrcInfo(); + }else { + // 点播发起了但是尚未成功, 仅注册回调等待结果即可 + inviteStreamService.once(InviteSessionType.PLAY, channel.getId(), null, callback); + deviceChannelService.stopPlay(channel.getId()); + inviteStreamService.removeInviteInfoByDeviceAndChannel(InviteSessionType.PLAY, channel.getId()); + } + } + } + + String streamId = String.format("%s_%s", device.getDeviceId(), channel.getDeviceId()); + int tcpMode = device.getStreamMode().equals("TCP-ACTIVE")? 2: (device.getStreamMode().equals("TCP-PASSIVE")? 1:0); + RTPServerParam rtpServerParam = new RTPServerParam(); + rtpServerParam.setMediaServerItem(mediaServerItem); + rtpServerParam.setStreamId(streamId); + rtpServerParam.setPresetSsrc(ssrc); + rtpServerParam.setSsrcCheck(device.isSsrcCheck()); + rtpServerParam.setPlayback(false); + rtpServerParam.setPort(0); + rtpServerParam.setTcpMode(tcpMode); + rtpServerParam.setOnlyAuto(false); + rtpServerParam.setDisableAudio(!channel.isHasAudio()); + + SSRCInfo ssrcInfo = receiveRtpServerService.openRTPServer(rtpServerParam, (code, msg, result) -> { + + if (code == InviteErrorCode.SUCCESS.getCode() && result != null && result.getHookData() != null) { + // hook响应 + StreamInfo streamInfo = onPublishHandlerForPlay(result.getHookData().getMediaServer(), result.getHookData().getMediaInfo(), device, channel); + if (streamInfo == null){ + if (callback != null) { + callback.run(InviteErrorCode.ERROR_FOR_STREAM_PARSING_EXCEPTIONS.getCode(), + InviteErrorCode.ERROR_FOR_STREAM_PARSING_EXCEPTIONS.getMsg(), null); + } + inviteStreamService.call(InviteSessionType.PLAY, channel.getId(), null, + InviteErrorCode.ERROR_FOR_STREAM_PARSING_EXCEPTIONS.getCode(), + InviteErrorCode.ERROR_FOR_STREAM_PARSING_EXCEPTIONS.getMsg(), null); + return; + } + if (callback != null) { + callback.run(InviteErrorCode.SUCCESS.getCode(), InviteErrorCode.SUCCESS.getMsg(), streamInfo); + } + inviteStreamService.call(InviteSessionType.PLAY, channel.getId(), null, + InviteErrorCode.SUCCESS.getCode(), + InviteErrorCode.SUCCESS.getMsg(), + streamInfo); + + log.info("[点播成功] 设备编号: {}, 通道编号:{}, 码流类型:{}", device.getDeviceId(), channel.getDeviceId(), + channel.getStreamIdentification()); + snapOnPlay(result.getHookData().getMediaServer(), device.getDeviceId(), channel.getDeviceId(), streamId); + }else { + if (callback != null) { + callback.run(code, msg, null); + } + inviteStreamService.call(InviteSessionType.PLAY, channel.getId(), null, code, msg, null); + inviteStreamService.removeInviteInfoByDeviceAndChannel(InviteSessionType.PLAY, channel.getId()); + SsrcTransaction ssrcTransaction = sessionManager.getSsrcTransactionByStream("rtp", streamId); + if (ssrcTransaction != null) { + try { + cmder.streamByeCmd(device, channel.getDeviceId(),"rtp", streamId, null, null); + } catch (InvalidArgumentException | ParseException | SipException | SsrcTransactionNotFoundException e) { + log.error("[点播超时], 发送BYE失败 {}", e.getMessage()); + } finally { + sessionManager.removeByStream("rtp", streamId); + } + } + } + }); + if (ssrcInfo == null || ssrcInfo.getPort() <= 0) { + log.info("[点播端口/SSRC]获取失败,设备编号:{}, 通道编号:{},ssrcInfo;{}", device.getDeviceId(), channel.getDeviceId(), ssrcInfo); + callback.run(InviteErrorCode.ERROR_FOR_RESOURCE_EXHAUSTION.getCode(), "获取端口或者ssrc失败", null); + inviteStreamService.call(InviteSessionType.PLAY, channel.getId(), null, + InviteErrorCode.ERROR_FOR_RESOURCE_EXHAUSTION.getCode(), + InviteErrorCode.ERROR_FOR_RESOURCE_EXHAUSTION.getMsg(), + null); + return null; + } + log.info("[点播开始] 设备编号: {}, 通道编号: {}, 收流端口: {}, 流ID:{}, 收流模式:{}, SSRC: {}, SSRC校验:{}", + device.getDeviceId(), channel.getDeviceId(), channel.getStreamIdentification(), ssrcInfo.getPort(), ssrcInfo.getStream(), + ssrcInfo.getSsrc(), device.isSsrcCheck()); + + // 初始化redis中的invite消息状态 + InviteInfo inviteInfo = InviteInfo.getInviteInfo(device.getDeviceId(), channel.getId(), ssrcInfo.getStream(), ssrcInfo, mediaServerItem.getId(), + mediaServerItem.getSdpIp(), ssrcInfo.getPort(), device.getStreamMode(), InviteSessionType.PLAY, + InviteSessionStatus.ready, userSetting.getRecordSip()); + if (record != null) { + inviteInfo.setRecord(record); + }else { + inviteInfo.setRecord(userSetting.getRecordSip()); + } + + inviteStreamService.updateInviteInfo(inviteInfo); + + try { + cmder.playStreamCmd(mediaServerItem, ssrcInfo, device, channel, (eventResult) -> { + // 处理收到200ok后的TCP主动连接以及SSRC不一致的问题 + InviteOKHandler(eventResult, ssrcInfo, mediaServerItem, device, channel, callback, inviteInfo, InviteSessionType.PLAY); + }, (event) -> { + log.info("[点播失败]{}:{} deviceId: {}, channelId:{}",event.statusCode, event.msg, device.getDeviceId(), channel.getDeviceId()); + receiveRtpServerService.closeRTPServer(mediaServerItem, ssrcInfo); + + sessionManager.removeByStream(ssrcInfo.getApp(), ssrcInfo.getStream()); + if (callback != null) { + callback.run(event.statusCode, event.msg, null); + } + inviteStreamService.call(InviteSessionType.PLAY, channel.getId(), null, + event.statusCode, event.msg, null); + + inviteStreamService.removeInviteInfoByDeviceAndChannel(InviteSessionType.PLAY, channel.getId()); + }, userSetting.getPlayTimeout().longValue()); + } catch (InvalidArgumentException | SipException | ParseException e) { + log.error("[命令发送失败] 点播消息: {}", e.getMessage()); + receiveRtpServerService.closeRTPServer(mediaServerItem, ssrcInfo); + sessionManager.removeByStream(ssrcInfo.getApp(), ssrcInfo.getStream()); + if (callback != null) { + callback.run(InviteErrorCode.ERROR_FOR_SIP_SENDING_FAILED.getCode(), + InviteErrorCode.ERROR_FOR_SIP_SENDING_FAILED.getMsg(), null); + } + inviteStreamService.call(InviteSessionType.PLAY, channel.getId(), null, + InviteErrorCode.ERROR_FOR_SIP_SENDING_FAILED.getCode(), + InviteErrorCode.ERROR_FOR_SIP_SENDING_FAILED.getMsg(), null); + + inviteStreamService.removeInviteInfoByDeviceAndChannel(InviteSessionType.PLAY, channel.getId()); + } + return ssrcInfo; + } + + + private void talk(MediaServer mediaServerItem, Device device, DeviceChannel channel, String stream, + HookSubscribe.Event hookEvent, SipSubscribe.Event errorEvent, + Runnable timeoutCallback, AudioBroadcastEvent audioEvent) { + + String playSsrc = ssrcFactory.getPlaySsrc(mediaServerItem.getId()); + + if (playSsrc == null) { + audioEvent.call("ssrc已经用尽"); + return; + } + SendRtpInfo sendRtpInfo; + try { + sendRtpInfo = sendRtpServerService.createSendRtpInfo(mediaServerItem, null, null, playSsrc, device.getDeviceId(), "talk", stream, + channel.getId(), true, false); + sendRtpInfo.setPlayType(InviteStreamType.TALK); + }catch (PlayException e) { + log.info("[语音对讲]开始 获取发流端口失败 deviceId: {}, channelId: {},", device.getDeviceId(), channel.getDeviceId()); + return; + } + + sendRtpInfo.setOnlyAudio(true); + sendRtpInfo.setPt(8); + sendRtpInfo.setStatus(1); + sendRtpInfo.setTcpActive(false); + sendRtpInfo.setUsePs(false); + sendRtpInfo.setReceiveStream(stream + "_talk"); + + String callId = SipUtils.getNewCallId(); + log.info("[语音对讲]开始 deviceId: {}, channelId: {},收流端口: {}, 收流模式:{}, SSRC: {}, SSRC校验:{}", device.getDeviceId(), channel.getDeviceId(), sendRtpInfo.getLocalPort(), device.getStreamMode(), sendRtpInfo.getSsrc(), false); + // 超时处理 + String timeOutTaskKey = UUID.randomUUID().toString(); + dynamicTask.startDelay(timeOutTaskKey, () -> { + + log.info("[语音对讲] 收流超时 deviceId: {}, channelId: {},端口:{}, SSRC: {}", device.getDeviceId(), channel.getDeviceId(), sendRtpInfo.getPort(), sendRtpInfo.getSsrc()); + timeoutCallback.run(); + // 点播超时回复BYE 同时释放ssrc以及此次点播的资源 + try { + cmder.streamByeCmd(device, channel.getDeviceId(), null, null, callId, null); + } catch (InvalidArgumentException | ParseException | SipException | SsrcTransactionNotFoundException e) { + log.error("[语音对讲]超时, 发送BYE失败 {}", e.getMessage()); + } finally { + timeoutCallback.run(); + mediaServerService.releaseSsrc(mediaServerItem.getId(), sendRtpInfo.getSsrc()); + sessionManager.removeByStream(sendRtpInfo.getApp(), sendRtpInfo.getStream()); + } + }, userSetting.getPlayTimeout()); + + try { + Integer localPort = mediaServerService.startSendRtpPassive(mediaServerItem, sendRtpInfo, userSetting.getPlayTimeout() * 1000); + if (localPort == null || localPort <= 0) { + timeoutCallback.run(); + mediaServerService.releaseSsrc(mediaServerItem.getId(), sendRtpInfo.getSsrc()); + sessionManager.removeByStream(sendRtpInfo.getApp(), sendRtpInfo.getStream()); + return; + } + sendRtpInfo.setPort(localPort); + }catch (ControllerException e) { + mediaServerService.releaseSsrc(mediaServerItem.getId(), sendRtpInfo.getSsrc()); + log.info("[语音对讲]失败 deviceId: {}, channelId: {}", device.getDeviceId(), channel.getDeviceId()); + audioEvent.call("失败, " + e.getMessage()); + // 查看是否已经建立了通道,存在则发送bye + stopTalk(device, channel); + } + + + // 查看设备是否已经在推流 + try { + cmder.talkStreamCmd(mediaServerItem, sendRtpInfo, device, channel, callId, (hookData) -> { + log.info("[语音对讲] 流已生成, 开始推流: " + hookData); + dynamicTask.stop(timeOutTaskKey); + // TODO 暂不做处理 + }, (hookData) -> { + log.info("[语音对讲] 设备开始推流: " + hookData); + dynamicTask.stop(timeOutTaskKey); + + }, (event) -> { + dynamicTask.stop(timeOutTaskKey); + + if (event.event instanceof ResponseEvent) { + ResponseEvent responseEvent = (ResponseEvent) event.event; + if (responseEvent.getResponse() instanceof SIPResponse) { + SIPResponse response = (SIPResponse) responseEvent.getResponse(); + sendRtpInfo.setFromTag(response.getFromTag()); + sendRtpInfo.setToTag(response.getToTag()); + sendRtpInfo.setCallId(response.getCallIdHeader().getCallId()); + sendRtpServerService.update(sendRtpInfo); + + SsrcTransaction ssrcTransaction = SsrcTransaction.buildForDevice(device.getDeviceId(), sendRtpInfo.getChannelId(), response.getCallIdHeader().getCallId(), sendRtpInfo.getApp(), + sendRtpInfo.getStream(), sendRtpInfo.getSsrc(), sendRtpInfo.getMediaServerId(), + response, InviteSessionType.TALK); + + sessionManager.put(ssrcTransaction); + } else { + log.error("[语音对讲]收到的消息错误,response不是SIPResponse"); + } + } else { + log.error("[语音对讲]收到的消息错误,event不是ResponseEvent"); + } + + }, (event) -> { + dynamicTask.stop(timeOutTaskKey); + mediaServerService.closeRTPServer(mediaServerItem, sendRtpInfo.getStream()); + // 释放ssrc + mediaServerService.releaseSsrc(mediaServerItem.getId(), sendRtpInfo.getSsrc()); + sessionManager.removeByStream(sendRtpInfo.getApp(), sendRtpInfo.getStream()); + errorEvent.response(event); + }, userSetting.getPlayTimeout().longValue()); + } catch (InvalidArgumentException | SipException | ParseException e) { + + log.error("[命令发送失败] 对讲消息: {}", e.getMessage()); + dynamicTask.stop(timeOutTaskKey); + mediaServerService.closeRTPServer(mediaServerItem, sendRtpInfo.getStream()); + // 释放ssrc + mediaServerService.releaseSsrc(mediaServerItem.getId(), sendRtpInfo.getSsrc()); + + sessionManager.removeByStream(sendRtpInfo.getApp(), sendRtpInfo.getStream()); + SipSubscribe.EventResult eventResult = new SipSubscribe.EventResult(); + eventResult.type = SipSubscribe.EventResultType.cmdSendFailEvent; + eventResult.statusCode = -1; + eventResult.msg = "命令发送失败"; + errorEvent.response(eventResult); + } +// } + + } + + private void tcpActiveHandler(Device device, DeviceChannel channel, String contentString, + MediaServer mediaServerItem, SSRCInfo ssrcInfo, ErrorCallback callback){ + if (!device.getStreamMode().equalsIgnoreCase("TCP-ACTIVE")) { + return; + } + + String substring; + if (contentString.indexOf("y=") > 0) { + substring = contentString.substring(0, contentString.indexOf("y=")); + }else { + substring = contentString; + } + try { + SessionDescription sdp = SdpFactory.getInstance().createSessionDescription(substring); + int port = -1; + Vector mediaDescriptions = sdp.getMediaDescriptions(true); + for (Object description : mediaDescriptions) { + MediaDescription mediaDescription = (MediaDescription) description; + Media media = mediaDescription.getMedia(); + + Vector mediaFormats = media.getMediaFormats(false); + if (mediaFormats.contains("96")) { + port = media.getMediaPort(); + break; + } + } + log.info("[TCP主动连接对方] deviceId: {}, channelId: {}, 连接对方的地址:{}:{}, 收流模式:{}, SSRC: {}, SSRC校验:{}", device.getDeviceId(), channel.getDeviceId(), sdp.getConnection().getAddress(), port, device.getStreamMode(), ssrcInfo.getSsrc(), device.isSsrcCheck()); + Boolean result = mediaServerService.connectRtpServer(mediaServerItem, sdp.getConnection().getAddress(), port, ssrcInfo.getStream()); + log.info("[TCP主动连接对方] 结果: {}" , result); + if (!result) { + // 主动连接失败,结束流程, 清理数据 + receiveRtpServerService.closeRTPServer(mediaServerItem, ssrcInfo); + sessionManager.removeByStream(ssrcInfo.getApp(), ssrcInfo.getStream()); + callback.run(InviteErrorCode.ERROR_FOR_TCP_ACTIVE_CONNECTION_REFUSED_ERROR.getCode(), + InviteErrorCode.ERROR_FOR_TCP_ACTIVE_CONNECTION_REFUSED_ERROR.getMsg(), null); + inviteStreamService.call(InviteSessionType.BROADCAST, channel.getId(), null, + InviteErrorCode.ERROR_FOR_TCP_ACTIVE_CONNECTION_REFUSED_ERROR.getCode(), + InviteErrorCode.ERROR_FOR_TCP_ACTIVE_CONNECTION_REFUSED_ERROR.getMsg(), null); + } + } catch (SdpException e) { + log.error("[TCP主动连接对方] deviceId: {}, channelId: {}, 解析200OK的SDP信息失败", device.getDeviceId(), channel.getDeviceId(), e); + receiveRtpServerService.closeRTPServer(mediaServerItem, ssrcInfo); + + sessionManager.removeByStream(ssrcInfo.getApp(), ssrcInfo.getStream()); + + callback.run(InviteErrorCode.ERROR_FOR_SDP_PARSING_EXCEPTIONS.getCode(), + InviteErrorCode.ERROR_FOR_SDP_PARSING_EXCEPTIONS.getMsg(), null); + inviteStreamService.call(InviteSessionType.BROADCAST, channel.getId(), null, + InviteErrorCode.ERROR_FOR_SDP_PARSING_EXCEPTIONS.getCode(), + InviteErrorCode.ERROR_FOR_SDP_PARSING_EXCEPTIONS.getMsg(), null); + } + } + + /** + * 点播成功时调用截图. + * + * @param mediaServerItemInuse media + * @param deviceId 设备 ID + * @param channelId 通道 ID + * @param stream ssrc + */ + private void snapOnPlay(MediaServer mediaServerItemInuse, String deviceId, String channelId, String stream) { + String path = "snap"; + String fileName = deviceId + "_" + channelId + ".jpg"; + // 请求截图 + log.info("[请求截图]: " + fileName); + mediaServerService.getSnap(mediaServerItemInuse, "rtp", stream, 15, 1, path, fileName); + } + + public StreamInfo onPublishHandlerForPlay(MediaServer mediaServerItem, MediaInfo mediaInfo, Device device, DeviceChannel channel) { + StreamInfo streamInfo = null; + streamInfo = onPublishHandler(mediaServerItem, mediaInfo, device, channel); + if (streamInfo != null) { + deviceChannelService.startPlay(channel.getId(), streamInfo.getStream()); + InviteInfo inviteInfo = inviteStreamService.getInviteInfoByDeviceAndChannel(InviteSessionType.PLAY, channel.getId()); + if (inviteInfo != null) { + inviteInfo.setStatus(InviteSessionStatus.ok); + inviteInfo.setStreamInfo(streamInfo); + inviteStreamService.updateInviteInfo(inviteInfo); + } + } + return streamInfo; + + } + + private StreamInfo onPublishHandlerForPlayback(MediaServer mediaServerItem, MediaInfo mediaInfo, Device device, + DeviceChannel channel, String startTime, String endTime) { + StreamInfo streamInfo = onPublishHandler(mediaServerItem, mediaInfo, device, channel); + if (streamInfo != null) { + streamInfo.setStartTime(startTime); + streamInfo.setEndTime(endTime); + InviteInfo inviteInfo = inviteStreamService.getInviteInfoByStream(InviteSessionType.PLAYBACK, mediaInfo.getStream()); + if (inviteInfo != null) { + inviteInfo.setStatus(InviteSessionStatus.ok); + inviteInfo.setStreamInfo(streamInfo); + inviteStreamService.updateInviteInfo(inviteInfo); + } + } + return streamInfo; + } + + @Override + public MediaServer getNewMediaServerItem(Device device) { + if (device == null) { + return null; + } + MediaServer mediaServerItem; + if (ObjectUtils.isEmpty(device.getMediaServerId()) || "auto".equals(device.getMediaServerId())) { + mediaServerItem = mediaServerService.getMediaServerForMinimumLoad(null); + } else { + mediaServerItem = mediaServerService.getOne(device.getMediaServerId()); + } + if (mediaServerItem == null) { + log.warn("点播时未找到可使用的ZLM..."); + } + return mediaServerItem; + } + + @Override + public void playBack(Device device, DeviceChannel channel, String startTime, + String endTime, ErrorCallback callback) { + if (device == null) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "设备不存在"); + } + if (channel == null) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "通道不存在"); + } + if (!userSetting.getServerId().equals(device.getServerId())) { + redisRpcPlayService.playback(device.getServerId(), channel.getId(), startTime, endTime, callback); + return; + } + + MediaServer newMediaServerItem = getNewMediaServerItem(device); + if (newMediaServerItem == null) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "未找到可用的节点"); + } + if (device.getStreamMode().equalsIgnoreCase("TCP-ACTIVE") && ! newMediaServerItem.isRtpEnable()) { + log.warn("[录像回放] 单端口收流时不支持TCP主动方式收流 deviceId: {},channelId:{}", device.getDeviceId(), channel.getDeviceId()); + throw new ControllerException(ErrorCode.ERROR100.getCode(), "单端口收流时不支持TCP主动方式收流"); + } + + playBack(newMediaServerItem, device, channel, startTime, endTime, callback); + } + + private void playBack(MediaServer mediaServerItem, + Device device, DeviceChannel channel, String startTime, + String endTime, ErrorCallback callback) { + + String startTimeStr = startTime.replace("-", "") + .replace(":", "") + .replace(" ", ""); + String endTimeTimeStr = endTime.replace("-", "") + .replace(":", "") + .replace(" ", ""); + + String stream = device.getDeviceId() + "_" + channel.getDeviceId() + "_" + startTimeStr + "_" + endTimeTimeStr; + int tcpMode = device.getStreamMode().equals("TCP-ACTIVE")? 2: (device.getStreamMode().equals("TCP-PASSIVE")? 1:0); + + RTPServerParam rtpServerParam = new RTPServerParam(); + rtpServerParam.setMediaServerItem(mediaServerItem); + rtpServerParam.setStreamId(stream); + rtpServerParam.setSsrcCheck(device.isSsrcCheck()); + rtpServerParam.setPlayback(true); + rtpServerParam.setPort(0); + rtpServerParam.setTcpMode(tcpMode); + rtpServerParam.setOnlyAuto(false); + rtpServerParam.setDisableAudio(!channel.isHasAudio()); + SSRCInfo ssrcInfo = receiveRtpServerService.openRTPServer(rtpServerParam, (code, msg, result) -> { + if (code == InviteErrorCode.SUCCESS.getCode() && result != null && result.getHookData() != null) { + // hook响应 + StreamInfo streamInfo = onPublishHandlerForPlayback(result.getHookData().getMediaServer(), result.getHookData().getMediaInfo(), device, channel, startTime, endTime); + if (streamInfo == null) { + log.warn("设备回放API调用失败!"); + callback.run(InviteErrorCode.ERROR_FOR_STREAM_PARSING_EXCEPTIONS.getCode(), + InviteErrorCode.ERROR_FOR_STREAM_PARSING_EXCEPTIONS.getMsg(), null); + return; + } + callback.run(InviteErrorCode.SUCCESS.getCode(), InviteErrorCode.SUCCESS.getMsg(), streamInfo); + log.info("[录像回放] 成功 deviceId: {}, channelId: {}, 开始时间: {}, 结束时间: {}", device.getDeviceId(), channel.getGbDeviceId(), startTime, endTime); + }else { + if (callback != null) { + callback.run(code, msg, null); + } + inviteStreamService.call(InviteSessionType.PLAYBACK, channel.getId(), null, code, msg, null); + inviteStreamService.removeInviteInfoByDeviceAndChannel(InviteSessionType.PLAYBACK, channel.getId()); + SsrcTransaction ssrcTransaction = sessionManager.getSsrcTransactionByStream("rtp", stream); + if (ssrcTransaction != null) { + try { + cmder.streamByeCmd(device, channel.getDeviceId(),"rtp", stream, null, null); + } catch (InvalidArgumentException | ParseException | SipException | SsrcTransactionNotFoundException e) { + log.error("[录像回放] 发送BYE失败 {}", e.getMessage()); + } finally { + sessionManager.removeByStream("rtp", stream); + } + } + } + }); + if (ssrcInfo == null || ssrcInfo.getPort() <= 0) { + log.info("[回放端口/SSRC]获取失败,deviceId={},channelId={},ssrcInfo={}", device.getDeviceId(), channel.getDeviceId(), ssrcInfo); + if (callback != null) { + callback.run(InviteErrorCode.ERROR_FOR_RESOURCE_EXHAUSTION.getCode(), "获取端口或者ssrc失败", null); + } + inviteStreamService.call(InviteSessionType.PLAY, channel.getId(), null, + InviteErrorCode.ERROR_FOR_RESOURCE_EXHAUSTION.getCode(), + InviteErrorCode.ERROR_FOR_RESOURCE_EXHAUSTION.getMsg(), + null); + return; + } + + log.info("[录像回放] deviceId: {}, channelId: {}, 开始时间: {}, 结束时间: {}, 收流端口:{}, 收流模式:{}, SSRC: {}, SSRC校验:{}", + device.getDeviceId(), channel.getGbDeviceId(), startTime, endTime, ssrcInfo.getPort(), device.getStreamMode(), + ssrcInfo.getSsrc(), device.isSsrcCheck()); + // 初始化redis中的invite消息状态 + InviteInfo inviteInfo = InviteInfo.getInviteInfo(device.getDeviceId(), channel.getId(), ssrcInfo.getStream(), ssrcInfo, mediaServerItem.getId(), + mediaServerItem.getSdpIp(), ssrcInfo.getPort(), device.getStreamMode(), InviteSessionType.PLAYBACK, + InviteSessionStatus.ready, userSetting.getRecordSip()); + inviteStreamService.updateInviteInfo(inviteInfo); + + try { + cmder.playbackStreamCmd(mediaServerItem, ssrcInfo, device, channel, startTime, endTime, + eventResult -> { + // 处理收到200ok后的TCP主动连接以及SSRC不一致的问题 + InviteOKHandler(eventResult, ssrcInfo, mediaServerItem, device, channel, + callback, inviteInfo, InviteSessionType.PLAYBACK); + }, eventResult -> { + log.info("[录像回放] 失败,{} {}", eventResult.statusCode, eventResult.msg); + if (callback != null) { + callback.run(eventResult.statusCode, eventResult.msg, null); + } + + receiveRtpServerService.closeRTPServer(mediaServerItem, ssrcInfo); + sessionManager.removeByStream(ssrcInfo.getApp(), ssrcInfo.getStream()); + inviteStreamService.removeInviteInfo(inviteInfo); + }, userSetting.getPlayTimeout().longValue()); + } catch (InvalidArgumentException | SipException | ParseException e) { + log.error("[命令发送失败] 录像回放: {}", e.getMessage()); + if (callback != null) { + callback.run(InviteErrorCode.FAIL.getCode(), e.getMessage(), null); + } + receiveRtpServerService.closeRTPServer(mediaServerItem, ssrcInfo); + sessionManager.removeByStream(ssrcInfo.getApp(), ssrcInfo.getStream()); + inviteStreamService.removeInviteInfo(inviteInfo); + } + } + + + private void InviteOKHandler(SipSubscribe.EventResult eventResult, SSRCInfo ssrcInfo, MediaServer mediaServerItem, + Device device, DeviceChannel channel, ErrorCallback callback, + InviteInfo inviteInfo, InviteSessionType inviteSessionType){ + inviteInfo.setStatus(InviteSessionStatus.ok); + ResponseEvent responseEvent = (ResponseEvent) eventResult.event; + String contentString = new String(responseEvent.getResponse().getRawContent()); + String ssrcInResponse = SipUtils.getSsrcFromSdp(contentString); + // 兼容回复的消息中缺少ssrc(y字段)的情况 + if (ssrcInResponse == null) { + ssrcInResponse = ssrcInfo.getSsrc(); + } + if (ssrcInfo.getSsrc().equals(ssrcInResponse)) { + // ssrc 一致 + if (mediaServerItem.isRtpEnable()) { + // 多端口 + if (device.getStreamMode().equalsIgnoreCase("TCP-ACTIVE")) { + tcpActiveHandler(device, channel, contentString, mediaServerItem, ssrcInfo, callback); + } + }else { + // 单端口 + if (device.getStreamMode().equalsIgnoreCase("TCP-ACTIVE")) { + log.warn("[Invite 200OK] 单端口收流模式不支持tcp主动模式收流"); + } + + } + }else { + log.info("[Invite 200OK] 收到invite 200, 发现下级自定义了ssrc: {}", ssrcInResponse); + // ssrc 不一致 + if (mediaServerItem.isRtpEnable()) { + // 多端口 + if (device.isSsrcCheck()) { + // ssrc检验 + // 更新ssrc + log.info("[Invite 200OK] SSRC修正 {}->{}", ssrcInfo.getSsrc(), ssrcInResponse); + // 释放ssrc + mediaServerService.releaseSsrc(mediaServerItem.getId(), ssrcInfo.getSsrc()); + Boolean result = mediaServerService.updateRtpServerSSRC(mediaServerItem, ssrcInfo.getStream(), ssrcInResponse); + if (!result) { + try { + log.warn("[Invite 200OK] 更新ssrc失败,停止点播 {}/{}", device.getDeviceId(), channel.getDeviceId()); + cmder.streamByeCmd(device, channel.getDeviceId(), ssrcInfo.getApp(), ssrcInfo.getStream(), null, null); + } catch (InvalidArgumentException | SipException | ParseException | SsrcTransactionNotFoundException e) { + log.error("[命令发送失败] 停止播放, 发送BYE: {}", e.getMessage()); + } + + // 释放ssrc + mediaServerService.releaseSsrc(mediaServerItem.getId(), ssrcInfo.getSsrc()); + + sessionManager.removeByStream(ssrcInfo.getApp(), ssrcInfo.getStream()); + + callback.run(InviteErrorCode.ERROR_FOR_RESET_SSRC.getCode(), + "下级自定义了ssrc,重新设置收流信息失败", null); + inviteStreamService.call(inviteSessionType, channel.getId(), null, + InviteErrorCode.ERROR_FOR_RESET_SSRC.getCode(), + "下级自定义了ssrc,重新设置收流信息失败", null); + + }else { + ssrcInfo.setSsrc(ssrcInResponse); + inviteInfo.setSsrcInfo(ssrcInfo); + inviteInfo.setStream(ssrcInfo.getStream()); + if (device.getStreamMode().equalsIgnoreCase("TCP-ACTIVE")) { + if (mediaServerItem.isRtpEnable()) { + tcpActiveHandler(device, channel, contentString, mediaServerItem, ssrcInfo, callback); + }else { + log.warn("[Invite 200OK] 单端口收流模式不支持tcp主动模式收流"); + } + } + inviteStreamService.updateInviteInfo(inviteInfo); + } + } + }else { + if (ssrcInResponse != null) { + // 单端口 + // 重新订阅流上线 + SsrcTransaction ssrcTransaction = sessionManager.getSsrcTransactionByStream("rtp", inviteInfo.getStream()); + sessionManager.removeByStream("rtp", inviteInfo.getStream()); + inviteStreamService.updateInviteInfoForSSRC(inviteInfo, ssrcInResponse); + ssrcTransaction.setDeviceId(device.getDeviceId()); + ssrcTransaction.setChannelId(ssrcTransaction.getChannelId()); + ssrcTransaction.setCallId(ssrcTransaction.getCallId()); + ssrcTransaction.setSsrc(ssrcInResponse); + ssrcTransaction.setApp("rtp"); + ssrcTransaction.setStream(inviteInfo.getStream()); + ssrcTransaction.setMediaServerId(mediaServerItem.getId()); + ssrcTransaction.setSipTransactionInfo(new SipTransactionInfo((SIPResponse) responseEvent.getResponse())); + ssrcTransaction.setType(inviteSessionType); + + sessionManager.put(ssrcTransaction); + } + } + } + } + + @Override + public void download(Device device, DeviceChannel channel, String startTime, String endTime, int downloadSpeed, ErrorCallback callback) { + + if (!userSetting.getServerId().equals(device.getServerId())) { + redisRpcPlayService.download(device.getServerId(), channel.getId(), startTime, endTime, downloadSpeed, callback); + return; + } + + MediaServer newMediaServerItem = this.getNewMediaServerItem(device); + if (newMediaServerItem == null) { + callback.run(InviteErrorCode.ERROR_FOR_ASSIST_NOT_READY.getCode(), + InviteErrorCode.ERROR_FOR_ASSIST_NOT_READY.getMsg(), + null); + return; + } + + download(newMediaServerItem, device, channel, startTime, endTime, downloadSpeed, callback); + } + + + private void download(MediaServer mediaServer, Device device, DeviceChannel channel, String startTime, String endTime, int downloadSpeed, ErrorCallback callback) { + if (mediaServer == null ) { + callback.run(InviteErrorCode.ERROR_FOR_PARAMETER_ERROR.getCode(), + InviteErrorCode.ERROR_FOR_PARAMETER_ERROR.getMsg(), + null); + return; + } + + int tcpMode = device.getStreamMode().equals("TCP-ACTIVE")? 2: (device.getStreamMode().equals("TCP-PASSIVE")? 1:0); + // 录像下载不使用固定流地址,固定流地址会导致如果开始时间与结束时间一致时文件错误的叠加在一起 + RTPServerParam rtpServerParam = new RTPServerParam(); + rtpServerParam.setMediaServerItem(mediaServer); + rtpServerParam.setSsrcCheck(device.isSsrcCheck()); + rtpServerParam.setPlayback(true); + rtpServerParam.setPort(0); + rtpServerParam.setTcpMode(tcpMode); + rtpServerParam.setOnlyAuto(false); + rtpServerParam.setDisableAudio(!channel.isHasAudio()); + SSRCInfo ssrcInfo = receiveRtpServerService.openRTPServer(rtpServerParam, (code, msg, result) -> { + if (code == InviteErrorCode.SUCCESS.getCode() && result != null && result.getHookData() != null) { + // hook响应 + StreamInfo streamInfo = onPublishHandlerForDownload(mediaServer, result.getHookData().getMediaInfo(), device, channel, startTime, endTime); + if (streamInfo == null) { + log.warn("[录像下载] 获取流地址信息失败"); + callback.run(InviteErrorCode.ERROR_FOR_STREAM_PARSING_EXCEPTIONS.getCode(), + InviteErrorCode.ERROR_FOR_STREAM_PARSING_EXCEPTIONS.getMsg(), null); + return; + } + callback.run(InviteErrorCode.SUCCESS.getCode(), InviteErrorCode.SUCCESS.getMsg(), streamInfo); + log.info("[录像下载] 调用成功 deviceId: {}, channelId: {}, 开始时间: {}, 结束时间: {}", device.getDeviceId(), channel.getDeviceId(), startTime, endTime); + }else { + if (callback != null) { + callback.run(code, msg, null); + } + inviteStreamService.call(InviteSessionType.DOWNLOAD, channel.getId(), null, code, msg, null); + inviteStreamService.removeInviteInfoByDeviceAndChannel(InviteSessionType.DOWNLOAD, channel.getId()); + if (result != null && result.getSsrcInfo() != null) { + SsrcTransaction ssrcTransaction = sessionManager.getSsrcTransactionByStream(result.getSsrcInfo().getApp(), result.getSsrcInfo().getStream()); + if (ssrcTransaction != null) { + try { + cmder.streamByeCmd(device, channel.getDeviceId(), ssrcTransaction.getApp(), ssrcTransaction.getStream(), null, null); + } catch (InvalidArgumentException | ParseException | SipException | SsrcTransactionNotFoundException e) { + log.error("[录像下载] 发送BYE失败 {}", e.getMessage()); + } finally { + sessionManager.removeByStream(ssrcTransaction.getApp(), ssrcTransaction.getStream()); + } + } + } + } + }); + if (ssrcInfo == null || ssrcInfo.getPort() <= 0) { + log.info("[录像下载端口/SSRC]获取失败,deviceId={},channelId={},ssrcInfo={}", device.getDeviceId(), channel.getDeviceId(), ssrcInfo); + if (callback != null) { + callback.run(InviteErrorCode.ERROR_FOR_RESOURCE_EXHAUSTION.getCode(), "获取端口或者ssrc失败", null); + } + inviteStreamService.call(InviteSessionType.PLAY, channel.getId(), null, + InviteErrorCode.ERROR_FOR_RESOURCE_EXHAUSTION.getCode(), + InviteErrorCode.ERROR_FOR_RESOURCE_EXHAUSTION.getMsg(), + null); + return; + } + log.info("[录像下载] deviceId: {}, channelId: {}, 开始时间: {}, 结束时间: {}, 下载速度:{}, 收流端口:{}, 收流模式:{}, SSRC: {}({}), SSRC校验:{}", + device.getDeviceId(), channel.getDeviceId(), startTime, endTime, downloadSpeed, ssrcInfo.getPort(), device.getStreamMode(), + ssrcInfo.getSsrc(), String.format("%08x", Long.parseLong(ssrcInfo.getSsrc())).toUpperCase(), + device.isSsrcCheck()); + + // 初始化redis中的invite消息状态 + InviteInfo inviteInfo = InviteInfo.getInviteInfo(device.getDeviceId(), channel.getId(), ssrcInfo.getStream(), ssrcInfo, mediaServer.getId(), + mediaServer.getSdpIp(), ssrcInfo.getPort(), device.getStreamMode(), InviteSessionType.DOWNLOAD, + InviteSessionStatus.ready, true); + inviteInfo.setStartTime(startTime); + inviteInfo.setEndTime(endTime); + + inviteStreamService.updateInviteInfo(inviteInfo); + try { + cmder.downloadStreamCmd(mediaServer, ssrcInfo, device, channel, startTime, endTime, downloadSpeed, + eventResult -> { + // 对方返回错误 + callback.run(InviteErrorCode.FAIL.getCode(), String.format("录像下载失败, 错误码: %s, %s", eventResult.statusCode, eventResult.msg), null); + receiveRtpServerService.closeRTPServer(mediaServer, ssrcInfo); + sessionManager.removeByStream(ssrcInfo.getApp(), ssrcInfo.getStream()); + inviteStreamService.removeInviteInfo(inviteInfo); + }, eventResult ->{ + // 处理收到200ok后的TCP主动连接以及SSRC不一致的问题 + InviteOKHandler(eventResult, ssrcInfo, mediaServer, device, channel, + callback, inviteInfo, InviteSessionType.DOWNLOAD); + + // 注册录像回调事件,录像下载结束后写入下载地址 + HookSubscribe.Event hookEventForRecord = (hookData) -> { + log.info("[录像下载] 收到录像写入磁盘消息: , {}/{}-{}", + inviteInfo.getDeviceId(), inviteInfo.getChannelId(), ssrcInfo.getStream()); + log.info("[录像下载] 收到录像写入磁盘消息内容: " + hookData); + RecordInfo recordInfo = hookData.getRecordInfo(); + DownloadFileInfo downloadFileInfo = mediaServerService.getDownloadFilePath(mediaServer, recordInfo); + InviteInfo inviteInfoForNew = inviteStreamService.getInviteInfo(inviteInfo.getType() + , inviteInfo.getChannelId(), inviteInfo.getStream()); + if (inviteInfoForNew != null && inviteInfoForNew.getStreamInfo() != null) { + inviteInfoForNew.getStreamInfo().setDownLoadFilePath(downloadFileInfo); + // 不可以马上移除会导致后续接口拿不到下载地址 + inviteStreamService.updateInviteInfo(inviteInfoForNew, 60*15L); + } + }; + Hook hook = Hook.getInstance(HookType.on_record_mp4, "rtp", ssrcInfo.getStream(), mediaServer.getId()); + // 设置过期时间,下载失败时自动处理订阅数据 + hook.setExpireTime(System.currentTimeMillis() + 24 * 60 * 60 * 1000); + subscribe.addSubscribe(hook, hookEventForRecord); + }, userSetting.getPlayTimeout().longValue()); + } catch (InvalidArgumentException | SipException | ParseException e) { + log.error("[命令发送失败] 录像下载: {}", e.getMessage()); + callback.run(InviteErrorCode.FAIL.getCode(),e.getMessage(), null); + receiveRtpServerService.closeRTPServer(mediaServer, ssrcInfo); + sessionManager.removeByStream(ssrcInfo.getApp(), ssrcInfo.getStream()); + inviteStreamService.removeInviteInfo(inviteInfo); + } + } + + @Override + public StreamInfo getDownLoadInfo(Device device, DeviceChannel channel, String stream) { + + + InviteInfo inviteInfo = inviteStreamService.getInviteInfo(InviteSessionType.DOWNLOAD, channel.getId(), stream); + if (inviteInfo == null) { + String app = "rtp"; + StreamAuthorityInfo streamAuthorityInfo = redisCatchStorage.getStreamAuthorityInfo(app, stream); + if (streamAuthorityInfo != null) { + List allList = cloudRecordService.getAllList(null, app, stream, null, null, null, streamAuthorityInfo.getCallId(), null); + if (allList.isEmpty()) { + log.warn("[获取下载进度] 未查询到录像下载的信息 {}/{}-{}", device.getDeviceId(), channel.getDeviceId(), stream); + return null; + } + + String mediaServerId = allList.get(0).getMediaServerId(); + MediaServer mediaServer = mediaServerService.getOne(mediaServerId); + if (mediaServer == null) { + log.warn("[获取下载进度] 未查询到录像下载的节点信息 {}/{}-{}", device.getDeviceId(), channel.getDeviceId(), stream); + return null; + } + log.warn("[获取下载进度] 发现下载已经结束,直接从数据库获取到文件 {}/{}-{}", device.getDeviceId(), channel.getDeviceId(), stream); + DownloadFileInfo downloadFileInfo = mediaServerService.getDownloadFilePath(mediaServer, RecordInfo.getInstance(allList.get(0))); + StreamInfo streamInfo = new StreamInfo(); + streamInfo.setDownLoadFilePath(downloadFileInfo); + streamInfo.setApp(app); + streamInfo.setStream(stream); + streamInfo.setServerId(mediaServerId); + streamInfo.setProgress(1.0); + return streamInfo; + } + } + + if (inviteInfo == null || inviteInfo.getStreamInfo() == null) { + log.warn("[获取下载进度] 未查询到录像下载的信息 {}/{}-{}", device.getDeviceId(), channel.getDeviceId(), stream); + return null; + } + + if (inviteInfo.getStreamInfo().getProgress() == 1) { + return inviteInfo.getStreamInfo(); + } + + // 获取当前已下载时长 + MediaServer mediaServerItem = inviteInfo.getStreamInfo().getMediaServer(); + if (mediaServerItem == null) { + log.warn("[获取下载进度] 查询录像信息时发现节点不存在"); + return null; + } + String app = "rtp"; + Long duration = mediaServerService.updateDownloadProcess(mediaServerItem, app, stream); + if (duration == null || duration == 0) { + inviteInfo.getStreamInfo().setProgress(0); + } else { + String startTime = inviteInfo.getStreamInfo().getStartTime(); + String endTime = inviteInfo.getStreamInfo().getEndTime(); + // 此时start和end单位是秒 + long start = DateUtil.yyyy_MM_dd_HH_mm_ssToTimestamp(startTime); + long end = DateUtil.yyyy_MM_dd_HH_mm_ssToTimestamp(endTime); + + BigDecimal currentCount = new BigDecimal(duration); + BigDecimal totalCount = new BigDecimal((end - start) * 1000); + BigDecimal divide = currentCount.divide(totalCount, 2, RoundingMode.HALF_UP); + double process = divide.doubleValue(); + if (process > 0.999) { + process = 1.0; + } + inviteInfo.getStreamInfo().setProgress(process); + } + inviteStreamService.updateInviteInfo(inviteInfo); + return inviteInfo.getStreamInfo(); + } + + private StreamInfo onPublishHandlerForDownload(MediaServer mediaServerItemInuse, MediaInfo mediaInfo, Device device, DeviceChannel channel, String startTime, String endTime) { + StreamInfo streamInfo = onPublishHandler(mediaServerItemInuse, mediaInfo, device, channel); + if (streamInfo != null) { + streamInfo.setProgress(0); + streamInfo.setStartTime(startTime); + streamInfo.setEndTime(endTime); + InviteInfo inviteInfo = inviteStreamService.getInviteInfo(InviteSessionType.DOWNLOAD, channel.getId(), streamInfo.getStream()); + if (inviteInfo != null) { + log.info("[录像下载] 更新invite消息中的stream信息"); + inviteInfo.setStatus(InviteSessionStatus.ok); + inviteInfo.setStreamInfo(streamInfo); + inviteStreamService.updateInviteInfo(inviteInfo); + } + } + return streamInfo; + } + + + public StreamInfo onPublishHandler(MediaServer mediaServerItem, MediaInfo mediaInfo, Device device, DeviceChannel channel) { + StreamInfo streamInfo = mediaServerService.getStreamInfoByAppAndStream(mediaServerItem, "rtp", mediaInfo.getStream(), mediaInfo, null); + streamInfo.setDeviceId(device.getDeviceId()); + streamInfo.setChannelId(channel.getId()); + return streamInfo; + } + + + @Override + public void zlmServerOffline(MediaServer mediaServer) { + // 处理正在向上推流的上级平台 + List sendRtpInfos = sendRtpServerService.queryAll(); + if (!sendRtpInfos.isEmpty()) { + for (SendRtpInfo sendRtpInfo : sendRtpInfos) { + if (sendRtpInfo.getMediaServerId().equals(mediaServer.getId()) && sendRtpInfo.isSendToPlatform()) { + Platform platform = platformService.queryPlatformByServerGBId(sendRtpInfo.getTargetId()); + CommonGBChannel channel = channelService.getOne(sendRtpInfo.getChannelId()); + try { + sipCommanderFroPlatform.streamByeCmd(platform, sendRtpInfo, channel); + } catch (SipException | InvalidArgumentException | ParseException e) { + log.error("[命令发送失败] 国标级联 发送BYE: {}", e.getMessage()); + } + } + } + } + // 处理正在观看的国标设备 + List allSsrc = sessionManager.getAll(); + if (allSsrc.size() > 0) { + for (SsrcTransaction ssrcTransaction : allSsrc) { + if (ssrcTransaction.getMediaServerId().equals(mediaServer.getId())) { + Device device = deviceService.getDeviceByDeviceId(ssrcTransaction.getDeviceId()); + if (device == null) { + continue; + } + DeviceChannel deviceChannel = deviceChannelService.getOneById(ssrcTransaction.getChannelId()); + if (deviceChannel == null) { + continue; + } + try { + cmder.streamByeCmd(device, deviceChannel.getDeviceId(), ssrcTransaction.getApp(), + ssrcTransaction.getStream(), null, null); + } catch (InvalidArgumentException | ParseException | SipException | + SsrcTransactionNotFoundException e) { + log.error("[zlm离线]为正在使用此zlm的设备, 发送BYE失败 {}", e.getMessage()); + } + } + } + } + } + + @Override + public AudioBroadcastResult audioBroadcast(String deviceId, String channelDeviceId, Boolean broadcastMode) { + + Device device = deviceService.getDeviceByDeviceId(deviceId); + if (device == null) { + throw new ControllerException(ErrorCode.ERROR400.getCode(), "未找到设备: " + deviceId); + } + DeviceChannel deviceChannel = deviceChannelService.getOne(deviceId, channelDeviceId); + if (deviceChannel == null) { + throw new ControllerException(ErrorCode.ERROR400.getCode(), "未找到通道: " + channelDeviceId); + } + + if (!userSetting.getServerId().equals(device.getServerId())) { + return redisRpcPlayService.audioBroadcast(device.getServerId(), deviceId, channelDeviceId, broadcastMode); + } + log.info("[语音喊话] device: {}, channel: {}", device.getDeviceId(), deviceChannel.getDeviceId()); + MediaServer mediaServerItem = mediaServerService.getMediaServerForMinimumLoad(null); + if (broadcastMode == null) { + broadcastMode = true; + } + String app = broadcastMode?"broadcast":"talk"; + String stream = device.getDeviceId() + "_" + deviceChannel.getDeviceId(); + AudioBroadcastResult audioBroadcastResult = new AudioBroadcastResult(); + audioBroadcastResult.setApp(app); + audioBroadcastResult.setStream(stream); + audioBroadcastResult.setStreamInfo(new StreamContent(mediaServerService.getStreamInfoByAppAndStream(mediaServerItem, app, stream, null, null, null, false))); + audioBroadcastResult.setCodec("G.711"); + return audioBroadcastResult; + } + + @Override + public boolean audioBroadcastCmd(Device device, DeviceChannel deviceChannel, MediaServer mediaServerItem, String app, String stream, int timeout, boolean isFromPlatform, AudioBroadcastEvent event) throws InvalidArgumentException, ParseException, SipException { + Assert.notNull(device, "设备不存在"); + Assert.notNull(deviceChannel, "通道不存在"); + log.info("[语音喊话] device: {}, channel: {}", device.getDeviceId(), deviceChannel.getDeviceId()); + // 查询通道使用状态 + if (audioBroadcastManager.exit(deviceChannel.getId())) { + SendRtpInfo sendRtpInfo = sendRtpServerService.queryByChannelId(deviceChannel.getId(), device.getDeviceId()); + if (sendRtpInfo != null && sendRtpInfo.isOnlyAudio()) { + // 查询流是否存在,不存在则认为是异常状态 + Boolean streamReady = mediaServerService.isStreamReady(mediaServerItem, sendRtpInfo.getApp(), sendRtpInfo.getStream()); + if (streamReady) { + log.warn("语音广播已经开启: {}", deviceChannel.getDeviceId()); + event.call("语音广播已经开启"); + return false; + } else { + stopAudioBroadcast(device, deviceChannel); + } + } + } + + // 发送通知 + cmder.audioBroadcastCmd(device, deviceChannel.getDeviceId(), eventResultForOk -> { + // 发送成功 + AudioBroadcastCatch audioBroadcastCatch = new AudioBroadcastCatch(device.getDeviceId(), deviceChannel.getId(), mediaServerItem, app, stream, event, AudioBroadcastCatchStatus.Ready, isFromPlatform); + audioBroadcastManager.update(audioBroadcastCatch); + // 等待invite消息, 超时则结束 + String key = VideoManagerConstants.BROADCAST_WAITE_INVITE + device.getDeviceId(); + if (!SipUtils.isFrontEnd(device.getDeviceId())) { + key += audioBroadcastCatch.getChannelId(); + } + dynamicTask.startDelay(key, ()->{ + log.info("[语音广播]等待invite消息超时:{}/{}", device.getDeviceId(), deviceChannel.getDeviceId()); + stopAudioBroadcast(device, deviceChannel); + }, 10*1000); + }, eventResultForError -> { + // 发送失败 + log.error("语音广播发送失败: {}:{}", deviceChannel.getDeviceId(), eventResultForError.msg); + event.call("语音广播发送失败"); + stopAudioBroadcast(device, deviceChannel); + }); + return true; + } + + @Override + public boolean audioBroadcastInUse(Device device, DeviceChannel channel) { + if (audioBroadcastManager.exit(channel.getId())) { + SendRtpInfo sendRtpInfo = sendRtpServerService.queryByChannelId(channel.getId(), device.getDeviceId()); + if (sendRtpInfo != null && sendRtpInfo.isOnlyAudio()) { + // 查询流是否存在,不存在则认为是异常状态 + MediaServer mediaServerServiceOne = mediaServerService.getOne(sendRtpInfo.getMediaServerId()); + Boolean streamReady = mediaServerService.isStreamReady(mediaServerServiceOne, sendRtpInfo.getApp(), sendRtpInfo.getStream()); + if (streamReady) { + log.warn("语音广播通道使用中: {}", channel.getDeviceId()); + return true; + } + } + } + return false; + } + + + @Override + public void stopAudioBroadcast(Device device, DeviceChannel channel) { + log.info("[停止对讲] 设备:{}, 通道:{}", device.getDeviceId(), channel.getDeviceId()); + List audioBroadcastCatchList = new ArrayList<>(); + if (channel == null) { + audioBroadcastCatchList.addAll(audioBroadcastManager.getByDeviceId(device.getDeviceId())); + } else { + audioBroadcastCatchList.addAll(audioBroadcastManager.getByDeviceId(device.getDeviceId())); + } + if (!audioBroadcastCatchList.isEmpty()) { + for (AudioBroadcastCatch audioBroadcastCatch : audioBroadcastCatchList) { + if (audioBroadcastCatch == null) { + continue; + } + SendRtpInfo sendRtpInfo = sendRtpServerService.queryByChannelId(channel.getId(), device.getDeviceId()); + if (sendRtpInfo != null) { + sendRtpServerService.delete(sendRtpInfo); + MediaServer mediaServer = mediaServerService.getOne(sendRtpInfo.getMediaServerId()); + mediaServerService.stopSendRtp(mediaServer, sendRtpInfo.getApp(), sendRtpInfo.getStream(), null); + try { + cmder.streamByeCmdForDeviceInvite(device, channel.getDeviceId(), audioBroadcastCatch.getSipTransactionInfo(), null); + } catch (InvalidArgumentException | ParseException | SipException | + SsrcTransactionNotFoundException e) { + log.error("[消息发送失败] 发送语音喊话BYE失败"); + } + } + + audioBroadcastManager.del(channel.getId()); + } + } + } + + @Override + public void zlmServerOnline(MediaServer mediaServer) { + // 获取 + List inviteInfoList = inviteStreamService.getAllInviteInfo(); + if (inviteInfoList.isEmpty()) { + return; + } + + List rtpServerList = mediaServerService.listRtpServer(mediaServer); + if (rtpServerList.isEmpty()) { + return; + } + for (InviteInfo inviteInfo : inviteInfoList) { + if (!rtpServerList.contains(inviteInfo.getStream())){ + inviteStreamService.removeInviteInfo(inviteInfo); + } + } + } + + @Override + public void playbackPause(String streamId) throws ServiceException, InvalidArgumentException, ParseException, SipException { + + InviteInfo inviteInfo = inviteStreamService.getInviteInfoByStream(InviteSessionType.PLAYBACK, streamId); + if (null == inviteInfo || inviteInfo.getStreamInfo() == null) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "streamId不存在"); + } + Device device = deviceService.getDeviceByDeviceId(inviteInfo.getDeviceId()); + if (device == null) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "设备不存在"); + } + if (!userSetting.getServerId().equals(device.getServerId())) { + redisRpcPlayService.playbackPause(device.getServerId(), streamId); + return; + } + + inviteInfo.getStreamInfo().setPause(true); + inviteStreamService.updateInviteInfo(inviteInfo); + MediaServer mediaServerItem = inviteInfo.getStreamInfo().getMediaServer(); + if (null == mediaServerItem) { + log.warn("mediaServer 不存在!"); + throw new ServiceException("mediaServer不存在"); + } + // zlm 暂停RTP超时检查 + // 使用zlm中的流ID + String streamKey = inviteInfo.getStream(); + if (!mediaServerItem.isRtpEnable()) { + streamKey = Long.toHexString(Long.parseLong(inviteInfo.getSsrcInfo().getSsrc())).toUpperCase(); + } + Boolean result = mediaServerService.pauseRtpCheck(mediaServerItem, streamKey); + if (!result) { + throw new ServiceException("暂停RTP接收失败"); + } + + DeviceChannel channel = deviceChannelService.getOneById(inviteInfo.getChannelId()); + cmder.playPauseCmd(device, channel, inviteInfo.getStreamInfo()); + } + + @Override + public void playbackResume(String streamId) throws ServiceException, InvalidArgumentException, ParseException, SipException { + InviteInfo inviteInfo = inviteStreamService.getInviteInfoByStream(InviteSessionType.PLAYBACK, streamId); + if (null == inviteInfo || inviteInfo.getStreamInfo() == null) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "streamId不存在"); + } + Device device = deviceService.getDeviceByDeviceId(inviteInfo.getDeviceId()); + if (device == null) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "设备不存在"); + } + if (!userSetting.getServerId().equals(device.getServerId())) { + redisRpcPlayService.playbackResume(device.getServerId(), streamId); + return; + } + + inviteInfo.getStreamInfo().setPause(false); + inviteStreamService.updateInviteInfo(inviteInfo); + MediaServer mediaServerItem = inviteInfo.getStreamInfo().getMediaServer(); + if (null == mediaServerItem) { + log.warn("mediaServer 不存在!"); + throw new ServiceException("mediaServer不存在"); + } + // 使用zlm中的流ID + String streamKey = inviteInfo.getStream(); + if (!mediaServerItem.isRtpEnable()) { + streamKey = Long.toHexString(Long.parseLong(inviteInfo.getSsrcInfo().getSsrc())).toUpperCase(); + } + boolean result = mediaServerService.resumeRtpCheck(mediaServerItem, streamKey); + if (!result) { + throw new ServiceException("继续RTP接收失败"); + } + DeviceChannel channel = deviceChannelService.getOneById(inviteInfo.getChannelId()); + cmder.playResumeCmd(device, channel, inviteInfo.getStreamInfo()); + } + + @Override + public void playbackSeek(String streamId, long seekTime) throws InvalidArgumentException, ParseException, SipException { + InviteInfo inviteInfo = inviteStreamService.getInviteInfoByStream(InviteSessionType.PLAYBACK, streamId); + + if (null == inviteInfo || inviteInfo.getStreamInfo() == null) { + log.warn("streamId不存在!"); + throw new ControllerException(ErrorCode.ERROR400.getCode(), "streamId不存在"); + } + Device device = deviceService.getDeviceByDeviceId(inviteInfo.getDeviceId()); + DeviceChannel channel = deviceChannelService.getOneById(inviteInfo.getChannelId()); + cmder.playSeekCmd(device, channel, inviteInfo.getStreamInfo(), seekTime); + } + + @Override + public void playbackSpeed(String streamId, double speed) throws InvalidArgumentException, ParseException, SipException { + InviteInfo inviteInfo = inviteStreamService.getInviteInfoByStream(InviteSessionType.PLAYBACK, streamId); + + if (null == inviteInfo || inviteInfo.getStreamInfo() == null) { + log.warn("streamId不存在!"); + throw new ControllerException(ErrorCode.ERROR400.getCode(), "streamId不存在"); + } + Device device = deviceService.getDeviceByDeviceId(inviteInfo.getDeviceId()); + DeviceChannel channel = deviceChannelService.getOneById(inviteInfo.getChannelId()); + cmder.playSpeedCmd(device, channel, inviteInfo.getStreamInfo(), speed); + } + + @Override + public void startPushStream(SendRtpInfo sendRtpInfo, DeviceChannel channel, SIPResponse sipResponse, Platform platform, CallIdHeader callIdHeader) { + // 开始发流 + MediaServer mediaInfo = mediaServerService.getOne(sendRtpInfo.getMediaServerId()); + + if (mediaInfo != null) { + try { + if (sendRtpInfo.isTcpActive()) { + mediaServerService.startSendRtpPassive(mediaInfo, sendRtpInfo, null); + } else { + mediaServerService.startSendRtp(mediaInfo, sendRtpInfo); + } + redisCatchStorage.sendPlatformStartPlayMsg(sendRtpInfo, channel, platform); + }catch (ControllerException e) { + log.error("RTP推流失败: {}", e.getMessage()); + startSendRtpStreamFailHand(sendRtpInfo, platform, callIdHeader); + return; + } + + log.info("RTP推流成功[ {}/{} ],{}, ", sendRtpInfo.getApp(), sendRtpInfo.getStream(), + sendRtpInfo.isTcpActive()?"被动发流": sendRtpInfo.getIp() + ":" + sendRtpInfo.getPort()); + + } + } + + @Override + public void startSendRtpStreamFailHand(SendRtpInfo sendRtpInfo, Platform platform, CallIdHeader callIdHeader) { + if (sendRtpInfo.isOnlyAudio()) { + Device device = deviceService.getDeviceByDeviceId(sendRtpInfo.getTargetId()); + DeviceChannel deviceChannel = deviceChannelService.getOneById(sendRtpInfo.getChannelId()); + AudioBroadcastCatch audioBroadcastCatch = audioBroadcastManager.get(sendRtpInfo.getChannelId()); + if (audioBroadcastCatch != null) { + try { + cmder.streamByeCmd(device, deviceChannel.getDeviceId(), audioBroadcastCatch.getSipTransactionInfo(), null); + } catch (SipException | ParseException | InvalidArgumentException | + SsrcTransactionNotFoundException exception) { + log.error("[命令发送失败] 停止语音对讲: {}", exception.getMessage()); + } + } + } else { + if (platform != null) { + // 向上级平台 + CommonGBChannel channel = channelService.getOne(sendRtpInfo.getChannelId()); + try { + commanderForPlatform.streamByeCmd(platform, sendRtpInfo, channel); + } catch (SipException | InvalidArgumentException | ParseException e) { + log.error("[命令发送失败] 国标级联 发送BYE: {}", e.getMessage()); + } + } + + } + } + + @Override + public void talkCmd(Device device, DeviceChannel channel, MediaServer mediaServerItem, String stream, AudioBroadcastEvent event) { + if (device == null || channel == null) { + return; + } + // TODO 必须多端口模式才支持语音喊话鹤语音对讲 + log.info("[语音对讲] device: {}, channel: {}", device.getDeviceId(), channel.getDeviceId()); + // 查询通道使用状态 + if (audioBroadcastManager.exit(channel.getId())) { + SendRtpInfo sendRtpInfo = sendRtpServerService.queryByChannelId(channel.getId(), device.getDeviceId()); + if (sendRtpInfo != null && sendRtpInfo.isOnlyAudio()) { + // 查询流是否存在,不存在则认为是异常状态 + MediaServer mediaServer = mediaServerService.getOne(sendRtpInfo.getMediaServerId()); + Boolean streamReady = mediaServerService.isStreamReady(mediaServer, sendRtpInfo.getApp(), sendRtpInfo.getStream()); + if (streamReady) { + log.warn("[语音对讲] 正在语音广播,无法开启语音通话: {}", channel.getDeviceId()); + event.call("正在语音广播"); + return; + } else { + stopAudioBroadcast(device, channel); + } + } + } + + SendRtpInfo sendRtpInfo = sendRtpServerService.queryByChannelId(channel.getId(), device.getDeviceId()); + if (sendRtpInfo != null) { + MediaServer mediaServer = mediaServerService.getOne(sendRtpInfo.getMediaServerId()); + Boolean streamReady = mediaServerService.isStreamReady(mediaServer, "rtp", sendRtpInfo.getReceiveStream()); + if (streamReady) { + log.warn("[语音对讲] 进行中: {}", channel.getDeviceId()); + event.call("语音对讲进行中"); + return; + } else { + stopTalk(device, channel); + } + } + + talk(mediaServerItem, device, channel, stream, (hookData) -> { + log.info("[语音对讲] 收到设备发来的流"); + }, eventResult -> { + log.warn("[语音对讲] 失败,{}/{}, 错误码 {} {}", device.getDeviceId(), channel.getDeviceId(), eventResult.statusCode, eventResult.msg); + event.call("失败,错误码 " + eventResult.statusCode + ", " + eventResult.msg); + }, () -> { + log.warn("[语音对讲] 失败,{}/{} 超时", device.getDeviceId(), channel.getDeviceId()); + event.call("失败,超时 "); + stopTalk(device, channel); + }, errorMsg -> { + log.warn("[语音对讲] 失败,{}/{} {}", device.getDeviceId(), channel.getDeviceId(), errorMsg); + event.call(errorMsg); + stopTalk(device, channel); + }); + } + + private void stopTalk(Device device, DeviceChannel channel) { + stopTalk(device, channel, null); + } + + @Override + public void stopTalk(Device device, DeviceChannel channel, Boolean streamIsReady) { + log.info("[语音对讲] 停止, {}/{}", device.getDeviceId(), channel.getDeviceId()); + SendRtpInfo sendRtpInfo = sendRtpServerService.queryByChannelId(channel.getId(), device.getDeviceId()); + if (sendRtpInfo == null) { + log.info("[语音对讲] 停止失败, 未找到发送信息,可能已经停止"); + return; + } + // 停止向设备推流 + String mediaServerId = sendRtpInfo.getMediaServerId(); + if (mediaServerId == null) { + return; + } + + MediaServer mediaServer = mediaServerService.getOne(mediaServerId); + + if (streamIsReady == null || streamIsReady) { + mediaServerService.stopSendRtp(mediaServer, sendRtpInfo.getApp(), sendRtpInfo.getStream(), sendRtpInfo.getSsrc()); + } + + ssrcFactory.releaseSsrc(mediaServerId, sendRtpInfo.getSsrc()); + + SsrcTransaction ssrcTransaction = sessionManager.getSsrcTransactionByStream(sendRtpInfo.getApp(), sendRtpInfo.getStream()); + if (ssrcTransaction != null) { + try { + cmder.streamByeCmd(device, channel.getDeviceId(), sendRtpInfo.getApp(), sendRtpInfo.getStream(), null, null); + } catch (InvalidArgumentException | ParseException | SipException | SsrcTransactionNotFoundException e) { + log.info("[语音对讲] 停止消息发送失败,可能已经停止"); + } + } + sendRtpServerService.deleteByChannel(channel.getId(), device.getDeviceId()); + } + + @Override + public void getSnap(String deviceId, String channelId, String fileName, ErrorCallback errorCallback) { + Device device = deviceService.getDeviceByDeviceId(deviceId); + Assert.notNull(device, "设备不存在"); + DeviceChannel channel = deviceChannelService.getOne(deviceId, channelId); + Assert.notNull(channel, "通道不存在"); + InviteInfo inviteInfo = inviteStreamService.getInviteInfoByDeviceAndChannel(InviteSessionType.PLAY, channel.getId()); + if (inviteInfo != null) { + if (inviteInfo.getStreamInfo() != null) { + // 已存在线直接截图 + MediaServer mediaServer = inviteInfo.getStreamInfo().getMediaServer(); + String path = "snap"; + // 请求截图 + log.info("[请求截图]: " + fileName); + mediaServerService.getSnap(mediaServer, "rtp", inviteInfo.getStreamInfo().getStream(), 15, 1, path, fileName); + File snapFile = new File(path + File.separator + fileName); + if (snapFile.exists()) { + errorCallback.run(InviteErrorCode.SUCCESS.getCode(), InviteErrorCode.SUCCESS.getMsg(), snapFile.getAbsoluteFile()); + }else { + errorCallback.run(InviteErrorCode.FAIL.getCode(), InviteErrorCode.FAIL.getMsg(), null); + } + return; + } + } + + MediaServer newMediaServerItem = getNewMediaServerItem(device); + play(newMediaServerItem, deviceId, channelId, null, (code, msg, data)->{ + if (code == InviteErrorCode.SUCCESS.getCode()) { + InviteInfo inviteInfoForPlay = inviteStreamService.getInviteInfoByDeviceAndChannel(InviteSessionType.PLAY, channel.getId()); + if (inviteInfoForPlay != null && inviteInfoForPlay.getStreamInfo() != null) { + getSnap(deviceId, channelId, fileName, errorCallback); + }else { + errorCallback.run(InviteErrorCode.FAIL.getCode(), InviteErrorCode.FAIL.getMsg(), null); + } + }else { + errorCallback.run(InviteErrorCode.FAIL.getCode(), InviteErrorCode.FAIL.getMsg(), null); + } + }); + } + + @Override + public void stop(InviteSessionType type, Device device, DeviceChannel channel, String stream) { + if (!userSetting.getServerId().equals(device.getServerId())) { + redisRpcPlayService.stop(device.getServerId(), type, channel.getId(), stream); + }else { + InviteInfo inviteInfo = inviteStreamService.getInviteInfo(type, channel.getId(), stream); + if (inviteInfo == null) { + if (type == InviteSessionType.PLAY) { + deviceChannelService.stopPlay(channel.getId()); + } + return; + } + inviteStreamService.removeInviteInfo(inviteInfo); + if (InviteSessionStatus.ok == inviteInfo.getStatus()) { + try { + log.info("[停止点播/回放/下载] {}/{}", device.getDeviceId(), channel.getDeviceId()); + cmder.streamByeCmd(device, channel.getDeviceId(), "rtp", inviteInfo.getStream(), null, null); + } catch (InvalidArgumentException | SipException | ParseException | SsrcTransactionNotFoundException e) { + log.error("[命令发送失败] 停止点播/回放/下载, 发送BYE: {}", e.getMessage()); + throw new ControllerException(ErrorCode.ERROR100.getCode(), "命令发送失败: " + e.getMessage()); + } + } + + if (inviteInfo.getType() == InviteSessionType.PLAY) { + deviceChannelService.stopPlay(channel.getId()); + } + if (inviteInfo.getStreamInfo() != null) { + receiveRtpServerService.closeRTPServer(inviteInfo.getStreamInfo().getMediaServer(), inviteInfo.getSsrcInfo()); + } + } + } + + @Override + public void stop(InviteInfo inviteInfo) { + Assert.notNull(inviteInfo, "参数异常"); + DeviceChannel channel = deviceChannelService.getOneForSourceById(inviteInfo.getChannelId()); + if (channel == null) { + log.warn("[停止点播] 发现通道不存在"); + return; + } + Device device = deviceService.getDevice(channel.getDataDeviceId()); + if (device == null) { + log.warn("[停止点播] 发现设备不存在"); + return; + } + inviteStreamService.removeInviteInfo(inviteInfo); + if (InviteSessionStatus.ok == inviteInfo.getStatus()) { + try { + log.info("[停止点播/回放/下载] {}/{}", device.getDeviceId(), channel.getDeviceId()); + cmder.streamByeCmd(device, channel.getDeviceId(), "rtp", inviteInfo.getStream(), null, null); + } catch (InvalidArgumentException | SipException | ParseException | SsrcTransactionNotFoundException e) { + log.warn("[命令发送失败] 停止点播/回放/下载, 发送BYE: {}", e.getMessage()); + } + } + + if (inviteInfo.getType() == InviteSessionType.PLAY) { + deviceChannelService.stopPlay(channel.getId()); + } + if (inviteInfo.getStreamInfo() != null) { + receiveRtpServerService.closeRTPServer(inviteInfo.getStreamInfo().getMediaServer(), inviteInfo.getSsrcInfo()); + } + } + + @Override + public void play(CommonGBChannel channel, Boolean record, ErrorCallback callback) { + Device device = deviceService.getDevice(channel.getDataDeviceId()); + if (device == null) { + log.warn("[点播] 未找到通道{}的设备信息", channel); + throw new PlayException(Response.SERVER_INTERNAL_ERROR, "server internal error"); + } + DeviceChannel deviceChannel = deviceChannelService.getOneForSourceById(channel.getGbId()); + + MediaServer mediaServerItem = getNewMediaServerItem(device); + if (mediaServerItem == null) { + log.warn("[点播] 未找到可用的zlm deviceId: {},channelId:{}", device.getDeviceId(), deviceChannel.getDeviceId()); + throw new ControllerException(ErrorCode.ERROR100.getCode(), "未找到可用的zlm"); + } + play(mediaServerItem, device, deviceChannel, null, record, callback); + + } + + @Override + public void stopPlay(InviteSessionType inviteSessionType, CommonGBChannel channel) { + Device device = deviceService.getDevice(channel.getDataDeviceId()); + if (device == null) { + log.warn("[停止播放] 未找到通道{}的设备信息", channel); + throw new PlayException(Response.SERVER_INTERNAL_ERROR, "server internal error"); + } + DeviceChannel deviceChannel = deviceChannelService.getOneForSourceById(channel.getGbId()); + String stream = String.format("%s_%s", device.getDeviceId(), deviceChannel.getDeviceId()); + stop(inviteSessionType, device, deviceChannel, stream); + } + + @Override + public void stop(InviteSessionType inviteSessionType, CommonGBChannel channel, String stream) { + Device device = deviceService.getDevice(channel.getDataDeviceId()); + if (device == null) { + log.warn("[停止播放] 未找到通道{}的设备信息", channel); + throw new PlayException(Response.SERVER_INTERNAL_ERROR, "server internal error"); + } + DeviceChannel deviceChannel = deviceChannelService.getOneForSourceById(channel.getGbId()); + stop(inviteSessionType, device, deviceChannel, stream); + } + + @Override + public void playBack(CommonGBChannel channel, Long startTime, Long stopTime, ErrorCallback callback) { + if (startTime == null || stopTime == null) { + throw new PlayException(Response.BAD_REQUEST, "bad request"); + } + // 国标通道 + Device device = deviceService.getDevice(channel.getDataDeviceId()); + if (device == null) { + log.warn("[点播] 未找到通道{}的设备信息", channel); + throw new PlayException(Response.SERVER_INTERNAL_ERROR, "server internal error"); + } + DeviceChannel deviceChannel = deviceChannelService.getOneById(channel.getGbId()); + if (deviceChannel == null) { + log.warn("[点播] 未找到通道{}", channel.getGbDeviceId()); + throw new PlayException(Response.SERVER_INTERNAL_ERROR, "server internal error"); + } + String startTimeStr = DateUtil.timestampTo_yyyy_MM_dd_HH_mm_ss(startTime); + String stopTimeStr = DateUtil.timestampTo_yyyy_MM_dd_HH_mm_ss(stopTime); + playBack(device, deviceChannel, startTimeStr, stopTimeStr, callback); + } + + @Override + public void download(CommonGBChannel channel, Long startTime, Long stopTime, Integer downloadSpeed, ErrorCallback callback) { + if (startTime == null || stopTime == null || downloadSpeed == null) { + throw new PlayException(Response.BAD_REQUEST, "bad request"); + } + // 国标通道 + Device device = deviceService.getDevice(channel.getDataDeviceId()); + if (device == null) { + log.warn("[点播] 未找到通道{}的设备信息", channel); + throw new PlayException(Response.SERVER_INTERNAL_ERROR, "server internal error"); + } + DeviceChannel deviceChannel = deviceChannelService.getOneById(channel.getGbId()); + if (deviceChannel == null) { + log.warn("[点播] 未找到通道{}", channel.getGbDeviceId()); + throw new PlayException(Response.SERVER_INTERNAL_ERROR, "server internal error"); + } + String startTimeStr = DateUtil.timestampTo_yyyy_MM_dd_HH_mm_ss(startTime); + String stopTimeStr = DateUtil.timestampTo_yyyy_MM_dd_HH_mm_ss(stopTime); + download(device, deviceChannel, startTimeStr, stopTimeStr, downloadSpeed, callback); + + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/service/impl/RegionServiceImpl.java b/src/main/java/com/genersoft/iot/vmp/gb28181/service/impl/RegionServiceImpl.java new file mode 100644 index 0000000..f2091e9 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/service/impl/RegionServiceImpl.java @@ -0,0 +1,333 @@ +package com.genersoft.iot.vmp.gb28181.service.impl; + +import com.genersoft.iot.vmp.common.CivilCodePo; +import com.genersoft.iot.vmp.conf.exception.ControllerException; +import com.genersoft.iot.vmp.gb28181.bean.CommonGBChannel; +import com.genersoft.iot.vmp.gb28181.bean.Region; +import com.genersoft.iot.vmp.gb28181.bean.RegionTree; +import com.genersoft.iot.vmp.gb28181.dao.CommonGBChannelMapper; +import com.genersoft.iot.vmp.gb28181.dao.RegionMapper; +import com.genersoft.iot.vmp.gb28181.event.EventPublisher; +import com.genersoft.iot.vmp.gb28181.service.IGbChannelService; +import com.genersoft.iot.vmp.gb28181.service.IRegionService; +import com.genersoft.iot.vmp.utils.CivilCodeUtil; +import com.genersoft.iot.vmp.utils.DateUtil; +import com.genersoft.iot.vmp.vmanager.bean.ErrorCode; +import com.github.pagehelper.PageHelper; +import com.github.pagehelper.PageInfo; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.dao.DuplicateKeyException; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; + +import java.util.*; + +/** + * 区域管理类 + */ +@Service +public class RegionServiceImpl implements IRegionService { + + + private static final Logger log = LoggerFactory.getLogger(RegionServiceImpl.class); + @Autowired + private RegionMapper regionMapper; + + @Autowired + private CommonGBChannelMapper commonGBChannelMapper; + + @Autowired + private IGbChannelService gbChannelService; + + @Autowired + private EventPublisher eventPublisher; + + @Override + public void add(Region region) { + Assert.hasLength(region.getName(), "名称必须存在"); + Assert.hasLength(region.getDeviceId(), "国标编号必须存在"); + if (ObjectUtils.isEmpty(region.getParentDeviceId()) || ObjectUtils.isEmpty(region.getParentDeviceId().trim())) { + region.setParentDeviceId(null); + } + region.setCreateTime(DateUtil.getNow()); + region.setUpdateTime(DateUtil.getNow()); + try { + regionMapper.add(region); + }catch (DuplicateKeyException e){ + throw new ControllerException(ErrorCode.ERROR100.getCode(), "此行政区划已存在"); + } + + } + + @Override + @Transactional + public boolean deleteByDeviceId(Integer regionDeviceId) { + Region region = regionMapper.queryOne(regionDeviceId); + // 获取所有子节点 + List allChildren = getAllChildren(regionDeviceId); + allChildren.add(region); + // 设置使用这些节点的通道的civilCode为null, + gbChannelService.removeCivilCode(allChildren); + regionMapper.batchDelete(allChildren); + return true; + } + + private List getAllChildren(Integer deviceId) { + if (deviceId == null) { + return new ArrayList<>(); + } + List children = regionMapper.getChildren(deviceId); + if (ObjectUtils.isEmpty(children)) { + return children; + } + List regions = new ArrayList<>(children); + for (Region region : children) { + if (region.getDeviceId().length() < 8) { + regions.addAll(getAllChildren(region.getId())); + } + } + return regions; + } + + @Override + public PageInfo query(String query, int page, int count) { + PageHelper.startPage(page, count); + if (query != null) { + query = query.replaceAll("/", "//") + .replaceAll("%", "/%") + .replaceAll("_", "/_"); + } + List regionList = regionMapper.query(query, null); + return new PageInfo<>(regionList); + } + + @Override + @Transactional + public void update(Region region) { + Assert.notNull(region.getDeviceId(), "编号不可为NULL"); + Assert.notNull(region.getName(), "名称不可为NULL"); + Region regionInDb = regionMapper.queryOne(region.getId()); + Assert.notNull(regionInDb, "待更新行政区划在数据库中不存在"); + if (!regionInDb.getDeviceId().equals(region.getDeviceId())) { + Region regionNewInDb = regionMapper.queryByDeviceId(region.getDeviceId()); + Assert.isNull(regionNewInDb, "此行政区划已存在"); + // 编号发生变化,把分配了这个行政区划的通道全部更新,并发送数据 + gbChannelService.updateCivilCode(regionInDb.getDeviceId(), region.getDeviceId()); + // 子节点信息更新 + regionMapper.updateChild(region.getId(), region.getDeviceId()); + } + regionMapper.update(region); + // 发送变化通知 + try { + // 发送catalog + eventPublisher.channelEventPublishForUpdate(CommonGBChannel.build(region), null); + }catch (Exception e) { + log.warn("[行政区划变化] 发送失败,{}", region.getDeviceId(), e); + } + } + + @Override + public List getAllChild(String parent) { + List allChild = CivilCodeUtil.INSTANCE.getAllChild(parent); + Collections.sort(allChild); + return allChild; + } + + @Override + public Region queryRegionByDeviceId(String regionDeviceId) { + return null; + } + + @Override + public List queryForTree(Integer parent, Boolean hasChannel) { + List regionList = regionMapper.queryForTree(parent); + if (parent != null && hasChannel != null && hasChannel) { + Region parentRegion = regionMapper.queryOne(parent); + if (parentRegion != null) { + List channelList = commonGBChannelMapper.queryForRegionTreeByCivilCode(parentRegion.getDeviceId()); + regionList.addAll(channelList); + } + } + return regionList; + } + + @Override + public void syncFromChannel() { + // 获取未初始化的行政区划节点 + List civilCodeList = regionMapper.getUninitializedCivilCode(); + if (civilCodeList.isEmpty()) { + return; + } + List regionList = new ArrayList<>(); + // 收集节点的父节点,用于验证哪些节点的父节点不存在,方便一并存入 + Map regionMapForVerification = new HashMap<>(); + civilCodeList.forEach(civilCode->{ + CivilCodePo civilCodePo = CivilCodeUtil.INSTANCE.getCivilCodePo(civilCode); + if (civilCodePo != null) { + Region region = Region.getInstance(civilCodePo); + regionList.add(region); + // 获取全部的父节点 + List civilCodePoList = CivilCodeUtil.INSTANCE.getAllParentCode(civilCode); + if (!civilCodePoList.isEmpty()) { + for (CivilCodePo codePo : civilCodePoList) { + regionMapForVerification.put(codePo.getCode(), Region.getInstance(codePo)); + } + } + } + }); + if (regionList.isEmpty()){ + return; + } + if (!regionMapForVerification.isEmpty()) { + // 查询数据库中已经存在的. + List civilCodesInDb = regionMapper.queryInList(regionMapForVerification.keySet()); + if (!civilCodesInDb.isEmpty()) { + for (String code : civilCodesInDb) { + regionMapForVerification.remove(code); + } + } + } + for (Region region : regionList) { + regionMapForVerification.put(region.getDeviceId(), region); + } + + regionMapper.batchAdd(new ArrayList<>(regionMapForVerification.values())); + } + + @Override + public boolean delete(int id) { + return regionMapper.delete(id) > 0; + } + + @Override + @Transactional + public boolean batchAdd(List regionList) { + if (regionList== null || regionList.isEmpty()) { + return false; + } + Map regionMapForVerification = new HashMap<>(); + for (Region region : regionList) { + regionMapForVerification.put(region.getDeviceId(), region); + } + // 查询数据库中已经存在的. + List regionListInDb = regionMapper.queryInRegionListByDeviceId(regionList); + if (!regionListInDb.isEmpty()) { + for (Region region : regionListInDb) { + regionMapForVerification.remove(region.getDeviceId()); + } + } + if (!regionMapForVerification.isEmpty()) { + List regions = new ArrayList<>(regionMapForVerification.values()); + regionMapper.batchAdd(regions); + regionMapper.updateParentId(regions); + } + + return true; + } + + @Override + public List getPath(String deviceId) { + Region region = regionMapper.queryByDeviceId(deviceId); + if (region == null) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "行政区划不存在"); + } + List allParent = getAllParent(region); + allParent.add(region); + return allParent; + } + + + private List getAllParent(Region region) { + if (region.getParentId() == null) { + return new ArrayList<>(); + } + + List regionList = new LinkedList<>(); + Region parent = regionMapper.queryByDeviceId(region.getParentDeviceId()); + if (parent == null) { + return regionList; + } + regionList.add(parent); + List allParent = getAllParent(parent); + regionList.addAll(allParent); + return regionList; + } + + @Override + public String getDescription(String civilCode) { + + CivilCodePo civilCodePo = CivilCodeUtil.INSTANCE.getCivilCodePo(civilCode); + Assert.notNull(civilCodePo, String.format("节点%s未查询到", civilCode)); + StringBuilder sb = new StringBuilder(); + sb.append(civilCodePo.getName()); + List civilCodePoList = CivilCodeUtil.INSTANCE.getAllParentCode(civilCode); + if (civilCodePoList.isEmpty()) { + return sb.toString(); + } + for (int i = 0; i < civilCodePoList.size(); i++) { + CivilCodePo item = civilCodePoList.get(i); + sb.insert(0, item.getName()); + if (i != civilCodePoList.size() - 1) { + sb.insert(0, "/"); + } + } + return sb.toString(); + } + + @Override + @Transactional + public void addByCivilCode(String civilCode) { + CivilCodePo civilCodePo = CivilCodeUtil.INSTANCE.getCivilCodePo(civilCode); + // 查询是否已经存在此节点 + Assert.notNull(civilCodePo, String.format("节点%s未查询到", civilCode)); + List civilCodePoList = CivilCodeUtil.INSTANCE.getAllParentCode(civilCode); + civilCodePoList.add(civilCodePo); + + Set civilCodeSet = regionMapper.queryInCivilCodePoList(civilCodePoList); + if (!civilCodeSet.isEmpty()) { + civilCodePoList.removeIf(item -> civilCodeSet.contains(item.getCode())); + } + if (civilCodePoList.isEmpty()) { + return; + } + int parentId = -1; + for (int i = civilCodePoList.size() - 1; i > -1; i--) { + CivilCodePo codePo = civilCodePoList.get(i); + + Region region = new Region(); + region.setDeviceId(codePo.getCode()); + region.setParentDeviceId(codePo.getParentCode()); + region.setName(civilCodePo.getName()); + region.setCreateTime(DateUtil.getNow()); + region.setUpdateTime(DateUtil.getNow()); + if (parentId == -1 && codePo.getParentCode() != null) { + Region parentRegion = regionMapper.queryByDeviceId(codePo.getParentCode()); + if (parentRegion == null){ + log.error(String.format("行政区划%sy已存在,但查询错误", codePo.getParentCode())); + throw new ControllerException(ErrorCode.ERROR100.getCode(), String.format("行政区划%sy已存在,但查询错误", codePo.getParentCode())); + } + region.setParentId(parentRegion.getId()); + }else { + region.setParentId(parentId); + } + regionMapper.add(region); + parentId = region.getId(); + } + } + + @Override + public PageInfo queryList(int page, int count, String query) { + PageHelper.startPage(page, count); + if (query != null) { + query = query.replaceAll("/", "//") + .replaceAll("%", "/%") + .replaceAll("_", "/_"); + } + List all = regionMapper.query(query, null); + return new PageInfo<>(all); + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/service/impl/SourceDownloadServiceForGbImpl.java b/src/main/java/com/genersoft/iot/vmp/gb28181/service/impl/SourceDownloadServiceForGbImpl.java new file mode 100644 index 0000000..283083e --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/service/impl/SourceDownloadServiceForGbImpl.java @@ -0,0 +1,35 @@ +package com.genersoft.iot.vmp.gb28181.service.impl; + +import com.genersoft.iot.vmp.common.InviteSessionType; +import com.genersoft.iot.vmp.common.StreamInfo; +import com.genersoft.iot.vmp.common.enums.ChannelDataType; +import com.genersoft.iot.vmp.gb28181.bean.CommonGBChannel; +import com.genersoft.iot.vmp.gb28181.service.IPlayService; +import com.genersoft.iot.vmp.gb28181.service.ISourceDownloadService; +import com.genersoft.iot.vmp.service.bean.ErrorCallback; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +@Slf4j +@Service(ChannelDataType.DOWNLOAD_SERVICE + ChannelDataType.GB28181) +public class SourceDownloadServiceForGbImpl implements ISourceDownloadService { + + @Autowired + private IPlayService deviceChannelPlayService; + + @Override + public void download(CommonGBChannel channel, Long startTime, Long stopTime, Integer downloadSpeed, ErrorCallback callback) { + + } + + @Override + public void stopDownload(CommonGBChannel channel, String stream) { + // 国标通道 + try { + deviceChannelPlayService.stop(InviteSessionType.DOWNLOAD, channel, stream); + } catch (Exception e) { + log.error("[停止下载失败] {}({})", channel.getGbName(), channel.getGbDeviceId(), e); + } + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/service/impl/SourcePTZServiceForGbImpl.java b/src/main/java/com/genersoft/iot/vmp/gb28181/service/impl/SourcePTZServiceForGbImpl.java new file mode 100644 index 0000000..5bc7b8c --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/service/impl/SourcePTZServiceForGbImpl.java @@ -0,0 +1,351 @@ +package com.genersoft.iot.vmp.gb28181.service.impl; + +import com.genersoft.iot.vmp.common.enums.ChannelDataType; +import com.genersoft.iot.vmp.gb28181.bean.*; +import com.genersoft.iot.vmp.gb28181.service.IPTZService; +import com.genersoft.iot.vmp.gb28181.service.ISourcePTZService; +import com.genersoft.iot.vmp.service.bean.ErrorCallback; +import com.genersoft.iot.vmp.vmanager.bean.ErrorCode; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Slf4j +@Service(ChannelDataType.PTZ_SERVICE + ChannelDataType.GB28181) +public class SourcePTZServiceForGbImpl implements ISourcePTZService { + + @Autowired + private IPTZService ptzService; + + @Override + public void ptz(CommonGBChannel channel, FrontEndControlCodeForPTZ frontEndControlCode, ErrorCallback callback) { + try { + int cmdCode = 0; + int panSpeed = 0; + int titleSpeed = 0; + int zoomSpeed = 0; + if (frontEndControlCode != null) { + if (frontEndControlCode.getPan() != null) { + if (frontEndControlCode.getPan() == 0) { + cmdCode = cmdCode | 1 << 1; + } else if (frontEndControlCode.getPan() == 1) { + cmdCode = cmdCode | 1; + } + } + if (frontEndControlCode.getTilt() != null) { + if (frontEndControlCode.getTilt() == 0) { + cmdCode = cmdCode | 1 << 3; + } else if (frontEndControlCode.getTilt() == 1) { + cmdCode = cmdCode | 1 << 2; + } + } + + if (frontEndControlCode.getZoom() != null) { + if (frontEndControlCode.getZoom() == 0) { + cmdCode = cmdCode | 1 << 5; + } else if (frontEndControlCode.getZoom() == 1) { + cmdCode = cmdCode | 1 << 4; + } + } + if (frontEndControlCode.getPanSpeed() == null) { + callback.run(ErrorCode.ERROR100.getCode(), "参数异常", null); + return; + } + if (frontEndControlCode.getTiltSpeed() == null) { + callback.run(ErrorCode.ERROR100.getCode(), "参数异常", null); + return; + } + if (frontEndControlCode.getZoomSpeed() == null) { + callback.run(ErrorCode.ERROR100.getCode(), "参数异常", null); + return; + } + panSpeed = (int)(frontEndControlCode.getPanSpeed()/100D* 255); + titleSpeed = (int)(frontEndControlCode.getTiltSpeed()/100D* 255);; + zoomSpeed = (int)(frontEndControlCode.getZoomSpeed()/100D* 16); + } + ptzService.frontEndCommand(channel, cmdCode, panSpeed, titleSpeed, zoomSpeed); + callback.run(ErrorCode.SUCCESS.getCode(), ErrorCode.SUCCESS.getMsg(), null); + }catch (Exception e) { + log.error("[云台控制失败] ", e); + callback.run(ErrorCode.ERROR100.getCode(), e.getMessage(), null); + } + } + + @Override + public void preset(CommonGBChannel channel, FrontEndControlCodeForPreset frontEndControlCode, ErrorCallback callback) { + try { + int cmdCode = 0; + int parameter1 = 0; + int parameter2 = 0; + int parameter3 = 0; + if (frontEndControlCode != null) { + if (frontEndControlCode.getCode() != null) { + if (frontEndControlCode.getCode() == 1) { + cmdCode = 0x81; + } else if (frontEndControlCode.getCode() == 2) { + cmdCode = 0x82; + }else if (frontEndControlCode.getCode() == 3) { + cmdCode = 0x83; + } + } + if (frontEndControlCode.getPresetId() == null) { + callback.run(ErrorCode.ERROR100.getCode(), "参数异常", null); + return; + } + parameter2 = frontEndControlCode.getPresetId(); + } + ptzService.frontEndCommand(channel, cmdCode, parameter1, parameter2, parameter3); + callback.run(ErrorCode.SUCCESS.getCode(), ErrorCode.SUCCESS.getMsg(), null); + }catch (Exception e) { + log.error("[预置位控制失败] ", e); + callback.run(ErrorCode.ERROR100.getCode(), e.getMessage(), null); + } + } + + @Override + public void fi(CommonGBChannel channel, FrontEndControlCodeForFI frontEndControlCode, ErrorCallback callback) { + try { + int cmdCode = 1 << 6; + int focusSpeed = 0; + int irisSpeed = 0; + int parameter3 = 0; + if (frontEndControlCode != null) { + if (frontEndControlCode.getFocus() != null) { + if (frontEndControlCode.getFocus() == 0) { + cmdCode = cmdCode | 1 << 1; + } else if (frontEndControlCode.getFocus() == 1) { + cmdCode = cmdCode | 1; + }else { + log.error("[FI失败] 未知的聚焦指令 {}", frontEndControlCode.getFocus()); + callback.run(ErrorCode.ERROR100.getCode(), "未知的指令", null); + } + } + if (frontEndControlCode.getIris() != null) { + if (frontEndControlCode.getIris() == 0) { + cmdCode = cmdCode | 1 << 3; + } else if (frontEndControlCode.getIris() == 1) { + cmdCode = cmdCode | 1 << 2; + }else { + log.error("[FI失败] 未知的光圈指令 {}", frontEndControlCode.getIris()); + callback.run(ErrorCode.ERROR100.getCode(), "未知的指令", null); + } + } + if (frontEndControlCode.getFocusSpeed() == null) { + callback.run(ErrorCode.ERROR100.getCode(), "参数异常", null); + return; + } + if (frontEndControlCode.getIrisSpeed() == null) { + callback.run(ErrorCode.ERROR100.getCode(), "参数异常", null); + return; + } + focusSpeed = frontEndControlCode.getFocusSpeed(); + irisSpeed = frontEndControlCode.getIrisSpeed(); + } + ptzService.frontEndCommand(channel, cmdCode, focusSpeed, irisSpeed, parameter3); + callback.run(ErrorCode.SUCCESS.getCode(), ErrorCode.SUCCESS.getMsg(), null); + }catch (Exception e) { + log.error("[云台控制失败] ", e); + callback.run(ErrorCode.ERROR100.getCode(), e.getMessage(), null); + } + } + + @Override + public void tour(CommonGBChannel channel, FrontEndControlCodeForTour frontEndControlCode, ErrorCallback callback) { + try { + int cmdCode = 0; + int parameter1 = 0; + int parameter2 = 0; + int parameter3 = 0; + if (frontEndControlCode != null) { + if (frontEndControlCode.getCode() != null) { + if (frontEndControlCode.getCode() == 1) { + cmdCode = 0x84; + if (frontEndControlCode.getPresetId() == null) { + callback.run(ErrorCode.ERROR100.getCode(), "参数异常", null); + return; + } + parameter2 = frontEndControlCode.getPresetId(); + } else if (frontEndControlCode.getCode() == 2) { + cmdCode = 0x85; + if (frontEndControlCode.getPresetId() == null) { + callback.run(ErrorCode.ERROR100.getCode(), "参数异常", null); + return; + } + parameter2 = frontEndControlCode.getPresetId(); + }else if (frontEndControlCode.getCode() == 3) { + cmdCode = 0x86; + if (frontEndControlCode.getPresetId() == null) { + callback.run(ErrorCode.ERROR100.getCode(), "参数异常", null); + return; + } + parameter2 = frontEndControlCode.getPresetId(); + if (frontEndControlCode.getTourSpeed() == null) { + callback.run(ErrorCode.ERROR100.getCode(), "参数异常", null); + return; + } + parameter3 = frontEndControlCode.getTourSpeed(); + }else if (frontEndControlCode.getCode() == 4) { + cmdCode = 0x87; + if (frontEndControlCode.getPresetId() == null) { + callback.run(ErrorCode.ERROR100.getCode(), "参数异常", null); + return; + } + parameter2 = frontEndControlCode.getPresetId(); + if (frontEndControlCode.getTourTime() == null) { + callback.run(ErrorCode.ERROR100.getCode(), "参数异常", null); + return; + } + parameter3 = frontEndControlCode.getTourTime(); + }else if (frontEndControlCode.getCode() == 5) { + cmdCode = 0x88; + }else if (frontEndControlCode.getCode() == 6) { + }else { + log.error("[巡航控制失败] 未知的指令 {}", frontEndControlCode.getCode()); + callback.run(ErrorCode.ERROR100.getCode(), "未知的指令", null); + } + if (frontEndControlCode.getTourId() == null) { + callback.run(ErrorCode.ERROR100.getCode(), "参数异常", null); + return; + } + parameter1 = frontEndControlCode.getTourId(); + } + + } + ptzService.frontEndCommand(channel, cmdCode, parameter1, parameter2, parameter3); + callback.run(ErrorCode.SUCCESS.getCode(), ErrorCode.SUCCESS.getMsg(), null); + }catch (Exception e) { + log.error("[巡航控制失败] ", e); + callback.run(ErrorCode.ERROR100.getCode(), e.getMessage(), null); + } + } + + @Override + public void scan(CommonGBChannel channel, FrontEndControlCodeForScan frontEndControlCode, ErrorCallback callback) { + try { + int cmdCode = 0; + int parameter1 = 0; + int parameter2 = 0; + int parameter3 = 0; + if (frontEndControlCode != null) { + if (frontEndControlCode.getCode() != null) { + if (frontEndControlCode.getCode() == 1) { + cmdCode = 0x89; + if (frontEndControlCode.getScanId() == null) { + callback.run(ErrorCode.ERROR100.getCode(), "参数异常", null); + return; + } + parameter1 = frontEndControlCode.getScanId(); + } else if (frontEndControlCode.getCode() == 2) { + cmdCode = 0x89; + if (frontEndControlCode.getScanId() == null) { + callback.run(ErrorCode.ERROR100.getCode(), "参数异常", null); + return; + } + parameter1 = frontEndControlCode.getScanId(); + parameter2 = 1; + }else if (frontEndControlCode.getCode() == 3) { + cmdCode = 0x89; + if (frontEndControlCode.getScanId() == null) { + callback.run(ErrorCode.ERROR100.getCode(), "参数异常", null); + return; + } + parameter1 = frontEndControlCode.getScanId(); + parameter2 = 2; + }else if (frontEndControlCode.getCode() == 4) { + cmdCode = 0x8A; + if (frontEndControlCode.getScanId() == null) { + callback.run(ErrorCode.ERROR100.getCode(), "参数异常", null); + return; + } + if (frontEndControlCode.getScanSpeed() == null) { + callback.run(ErrorCode.ERROR100.getCode(), "参数异常", null); + return; + } + parameter1 = frontEndControlCode.getScanId(); + parameter2 = frontEndControlCode.getScanSpeed(); + }else if (frontEndControlCode.getCode() == 5) { + }else { + log.error("[巡航控制失败] 未知的指令 {}", frontEndControlCode.getCode()); + callback.run(ErrorCode.ERROR100.getCode(), "未知的指令", null); + } + } + } + ptzService.frontEndCommand(channel, cmdCode, parameter1, parameter2, parameter3); + callback.run(ErrorCode.SUCCESS.getCode(), ErrorCode.SUCCESS.getMsg(), null); + }catch (Exception e) { + log.error("[巡航控制失败] ", e); + callback.run(ErrorCode.ERROR100.getCode(), e.getMessage(), null); + } + } + + @Override + public void auxiliary(CommonGBChannel channel, FrontEndControlCodeForAuxiliary frontEndControlCode, ErrorCallback callback) { + try { + int cmdCode = 0; + int parameter1 = 0; + int parameter2 = 0; + int parameter3 = 0; + if (frontEndControlCode != null) { + if (frontEndControlCode.getCode() != null) { + if (frontEndControlCode.getCode() == 1) { + cmdCode = 0x8C; + if (frontEndControlCode.getAuxiliaryId() == null) { + callback.run(ErrorCode.ERROR100.getCode(), "参数异常", null); + return; + } + parameter1 = frontEndControlCode.getAuxiliaryId(); + } else if (frontEndControlCode.getCode() == 2) { + cmdCode = 0x8D; + if (frontEndControlCode.getAuxiliaryId() == null) { + callback.run(ErrorCode.ERROR100.getCode(), "参数异常", null); + return; + } + parameter1 = frontEndControlCode.getAuxiliaryId(); + }else { + log.error("[辅助开关失败] 未知的指令 {}", frontEndControlCode.getCode()); + callback.run(ErrorCode.ERROR100.getCode(), "未知的指令", null); + } + } + } + ptzService.frontEndCommand(channel, cmdCode, parameter1, parameter2, parameter3); + callback.run(ErrorCode.SUCCESS.getCode(), ErrorCode.SUCCESS.getMsg(), null); + }catch (Exception e) { + log.error("[辅助开关失败] ", e); + callback.run(ErrorCode.ERROR100.getCode(), e.getMessage(), null); + } + } + + @Override + public void wiper(CommonGBChannel channel, FrontEndControlCodeForWiper frontEndControlCode, ErrorCallback callback) { + try { + int cmdCode = 0; + int parameter1 = 1; + int parameter2 = 0; + int parameter3 = 0; + if (frontEndControlCode != null) { + if (frontEndControlCode.getCode() != null) { + if (frontEndControlCode.getCode() == 1) { + cmdCode = 0x8C; + } else if (frontEndControlCode.getCode() == 2) { + cmdCode = 0x8D; + }else { + log.error("[雨刷开关失败] 未知的指令 {}", frontEndControlCode.getCode()); + callback.run(ErrorCode.ERROR100.getCode(), "未知的指令", null); + } + } + } + ptzService.frontEndCommand(channel, cmdCode, parameter1, parameter2, parameter3); + callback.run(ErrorCode.SUCCESS.getCode(), ErrorCode.SUCCESS.getMsg(), null); + }catch (Exception e) { + log.error("[雨刷开关失败] ", e); + callback.run(ErrorCode.ERROR100.getCode(), e.getMessage(), null); + } + } + + @Override + public void queryPreset(CommonGBChannel channel, ErrorCallback> callback) { + ptzService.queryPresetList(channel, callback); + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/service/impl/SourcePlayServiceForGbImpl.java b/src/main/java/com/genersoft/iot/vmp/gb28181/service/impl/SourcePlayServiceForGbImpl.java new file mode 100644 index 0000000..8dde900 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/service/impl/SourcePlayServiceForGbImpl.java @@ -0,0 +1,51 @@ +package com.genersoft.iot.vmp.gb28181.service.impl; + +import com.genersoft.iot.vmp.common.InviteSessionType; +import com.genersoft.iot.vmp.common.StreamInfo; +import com.genersoft.iot.vmp.common.enums.ChannelDataType; +import com.genersoft.iot.vmp.conf.exception.ControllerException; +import com.genersoft.iot.vmp.gb28181.bean.CommonGBChannel; +import com.genersoft.iot.vmp.gb28181.bean.Platform; +import com.genersoft.iot.vmp.gb28181.bean.PlayException; +import com.genersoft.iot.vmp.gb28181.service.IPlayService; +import com.genersoft.iot.vmp.gb28181.service.ISourcePlayService; +import com.genersoft.iot.vmp.service.bean.ErrorCallback; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import javax.sip.message.Response; + +@Slf4j +@Service(ChannelDataType.PLAY_SERVICE + ChannelDataType.GB28181) +public class SourcePlayServiceForGbImpl implements ISourcePlayService { + + @Autowired + private IPlayService deviceChannelPlayService; + + @Override + public void play(CommonGBChannel channel, Platform platform, Boolean record, ErrorCallback callback) { + // 国标通道 + try { + deviceChannelPlayService.play(channel, record, callback); + } catch (PlayException e) { + callback.run(e.getCode(), e.getMsg(), null); + } catch (ControllerException e) { + log.error("[点播失败] {}({}), {}", channel.getGbName(), channel.getGbDeviceId(), e.getMsg()); + callback.run(Response.BUSY_HERE, "busy here", null); + } catch (Exception e) { + log.error("[点播失败] {}({})", channel.getGbName(), channel.getGbDeviceId(), e); + callback.run(Response.BUSY_HERE, "busy here", null); + } + } + + @Override + public void stopPlay(CommonGBChannel channel) { + // 国标通道 + try { + deviceChannelPlayService.stopPlay(InviteSessionType.PLAY, channel); + } catch (Exception e) { + log.error("[停止点播失败] {}({})", channel.getGbName(), channel.getGbDeviceId(), e); + } + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/service/impl/SourcePlaybackServiceForGbImpl.java b/src/main/java/com/genersoft/iot/vmp/gb28181/service/impl/SourcePlaybackServiceForGbImpl.java new file mode 100644 index 0000000..859cda2 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/service/impl/SourcePlaybackServiceForGbImpl.java @@ -0,0 +1,113 @@ +package com.genersoft.iot.vmp.gb28181.service.impl; + +import com.genersoft.iot.vmp.common.InviteSessionType; +import com.genersoft.iot.vmp.common.StreamInfo; +import com.genersoft.iot.vmp.common.enums.ChannelDataType; +import com.genersoft.iot.vmp.gb28181.bean.CommonGBChannel; +import com.genersoft.iot.vmp.gb28181.bean.CommonRecordInfo; +import com.genersoft.iot.vmp.gb28181.bean.PlayException; +import com.genersoft.iot.vmp.gb28181.bean.RecordItem; +import com.genersoft.iot.vmp.gb28181.service.IDeviceChannelService; +import com.genersoft.iot.vmp.gb28181.service.IPlayService; +import com.genersoft.iot.vmp.gb28181.service.ISourcePlaybackService; +import com.genersoft.iot.vmp.service.bean.ErrorCallback; +import com.genersoft.iot.vmp.vmanager.bean.ErrorCode; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import javax.sip.message.Response; +import java.util.ArrayList; +import java.util.List; + +@Slf4j +@Service(ChannelDataType.PLAYBACK_SERVICE + ChannelDataType.GB28181) +public class SourcePlaybackServiceForGbImpl implements ISourcePlaybackService { + + @Autowired + private IPlayService playService; + + @Autowired + private IDeviceChannelService channelService; + + @Override + public void playback(CommonGBChannel channel, Long startTime, Long stopTime, ErrorCallback callback) { + try { + playService.playBack(channel, startTime, stopTime, callback); + } catch (PlayException e) { + callback.run(e.getCode(), e.getMsg(), null); + } catch (Exception e) { + callback.run(Response.BUSY_HERE, "busy here", null); + } + } + + @Override + public void stopPlayback(CommonGBChannel channel, String stream) { + // 国标通道 + try { + playService.stop(InviteSessionType.PLAYBACK, channel, stream); + } catch (Exception e) { + log.error("[停止点播失败] {}({})", channel.getGbName(), channel.getGbDeviceId(), e); + } + } + + @Override + public void playbackPause(CommonGBChannel channel, String stream) { + // 国标通道 + try { + playService.playbackPause(stream); + } catch (Exception e) { + log.error("[停止点播失败] {}({})", channel.getGbName(), channel.getGbDeviceId(), e); + } + } + + @Override + public void playbackResume(CommonGBChannel channel, String stream) { + // 国标通道 + try { + playService.playbackPause(stream); + } catch (Exception e) { + log.error("[停止点播失败] {}({})", channel.getGbName(), channel.getGbDeviceId(), e); + } + } + + @Override + public void playbackSeek(CommonGBChannel channel, String stream, long seekTime) { + // 国标通道 + try { + playService.playbackPause(stream); + } catch (Exception e) { + log.error("[停止点播失败] {}({})", channel.getGbName(), channel.getGbDeviceId(), e); + } + } + + @Override + public void playbackSpeed(CommonGBChannel channel, String stream, Double speed) { + // 国标通道 + try { + playService.playbackSpeed(stream, speed); + } catch (Exception e) { + log.error("[停止点播失败] {}({})", channel.getGbName(), channel.getGbDeviceId(), e); + } + } + + @Override + public void queryRecord(CommonGBChannel channel, String startTime, String endTime, ErrorCallback> callback) { + channelService.queryRecordInfo(channel, startTime, endTime, (code, msg, data) -> { + if (code == ErrorCode.SUCCESS.getCode()) { + List recordList = data.getRecordList(); + List recordInfoList = new ArrayList<>(); + for (RecordItem recordItem : recordList) { + CommonRecordInfo recordInfo = new CommonRecordInfo(); + recordInfo.setStartTime(recordItem.getStartTime()); + recordInfo.setEndTime(recordItem.getEndTime()); + recordInfo.setFileSize(recordItem.getFileSize()); + recordInfoList.add(recordInfo); + } + callback.run(ErrorCode.SUCCESS.getCode(), ErrorCode.SUCCESS.getMsg(), recordInfoList); + }else { + callback.run(code, msg, null); + } + }); + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/session/AudioBroadcastManager.java b/src/main/java/com/genersoft/iot/vmp/gb28181/session/AudioBroadcastManager.java new file mode 100644 index 0000000..57067f1 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/session/AudioBroadcastManager.java @@ -0,0 +1,62 @@ +package com.genersoft.iot.vmp.gb28181.session; + +import com.genersoft.iot.vmp.conf.SipConfig; +import com.genersoft.iot.vmp.gb28181.bean.AudioBroadcastCatch; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * 语音广播消息管理类 + * @author lin + */ +@Slf4j +@Component +public class AudioBroadcastManager { + + @Autowired + private SipConfig config; + + public static Map data = new ConcurrentHashMap<>(); + + + public void update(AudioBroadcastCatch audioBroadcastCatch) { + data.put(audioBroadcastCatch.getChannelId(), audioBroadcastCatch); + } + + public void del(Integer channelId) { + data.remove(channelId); + + } + + public List getAll(){ + Collection values = data.values(); + return new ArrayList<>(values); + } + + + public boolean exit(Integer channelId) { + return data.containsKey(channelId); + } + + public AudioBroadcastCatch get(Integer channelId) { + return data.get(channelId); + } + + public List getByDeviceId(String deviceId) { + List audioBroadcastCatchList= new ArrayList<>(); + for (AudioBroadcastCatch broadcastCatch : data.values()) { + if (broadcastCatch.getDeviceId().equals(deviceId)) { + audioBroadcastCatchList.add(broadcastCatch); + } + } + + return audioBroadcastCatchList; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/session/CatalogDataManager.java b/src/main/java/com/genersoft/iot/vmp/gb28181/session/CatalogDataManager.java new file mode 100755 index 0000000..c524d72 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/session/CatalogDataManager.java @@ -0,0 +1,310 @@ +package com.genersoft.iot.vmp.gb28181.session; + +import com.genersoft.iot.vmp.gb28181.bean.*; +import com.genersoft.iot.vmp.gb28181.service.IDeviceChannelService; +import com.genersoft.iot.vmp.gb28181.service.IGroupService; +import com.genersoft.iot.vmp.gb28181.service.IRegionService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.CommandLineRunner; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +import java.time.Instant; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.TimeUnit; + +@Slf4j +@Component +public class CatalogDataManager implements CommandLineRunner { + + @Autowired + private IDeviceChannelService deviceChannelService; + + @Autowired + private IRegionService regionService; + + @Autowired + private IGroupService groupService; + + @Autowired + private RedisTemplate redisTemplate; + + private final Map dataMap = new ConcurrentHashMap<>(); + + private final String key = "VMP_CATALOG_DATA"; + + public String buildMapKey(String deviceId, int sn ) { + return deviceId + "_" + sn; + } + + public void addReady(Device device, int sn ) { + CatalogData catalogData = dataMap.get(buildMapKey(device.getDeviceId(),sn)); + if (catalogData != null) { + Set redisKeysForChannel = catalogData.getRedisKeysForChannel(); + if (redisKeysForChannel != null && !redisKeysForChannel.isEmpty()) { + for (String deleteKey : redisKeysForChannel) { + redisTemplate.opsForHash().delete(key, deleteKey); + } + } + Set redisKeysForRegion = catalogData.getRedisKeysForRegion(); + if (redisKeysForRegion != null && !redisKeysForRegion.isEmpty()) { + for (String deleteKey : redisKeysForRegion) { + redisTemplate.opsForHash().delete(key, deleteKey); + } + } + Set redisKeysForGroup = catalogData.getRedisKeysForGroup(); + if (redisKeysForGroup != null && !redisKeysForGroup.isEmpty()) { + for (String deleteKey : redisKeysForGroup) { + redisTemplate.opsForHash().delete(key, deleteKey); + } + } + dataMap.remove(buildMapKey(device.getDeviceId(),sn)); + } + catalogData = new CatalogData(); + catalogData.setDevice(device); + catalogData.setSn(sn); + catalogData.setStatus(CatalogData.CatalogDataStatus.ready); + catalogData.setTime(Instant.now()); + dataMap.put(buildMapKey(device.getDeviceId(),sn), catalogData); + } + + public void put(String deviceId, int sn, int total, Device device, List deviceChannelList, + List regionList, List groupList) { + CatalogData catalogData = dataMap.get(buildMapKey(device.getDeviceId(),sn)); + if (catalogData == null ) { + log.warn("[缓存-Catalog] 未找到缓存对象,可能已经结束"); + return; + } + catalogData.setStatus(CatalogData.CatalogDataStatus.runIng); + catalogData.setTotal(total); + catalogData.setTime(Instant.now()); + + if (deviceChannelList != null && !deviceChannelList.isEmpty()) { + for (DeviceChannel deviceChannel : deviceChannelList) { + String keyForChannel = "CHANNEL:" + deviceId + ":" + deviceChannel.getDeviceId() + ":" + sn; + redisTemplate.opsForHash().put(key, keyForChannel, deviceChannel); + catalogData.getRedisKeysForChannel().add(keyForChannel); + } + } + + if (regionList != null && !regionList.isEmpty()) { + for (Region region : regionList) { + String keyForRegion = "REGION:" + deviceId + ":" + region.getDeviceId() + ":" + sn; + redisTemplate.opsForHash().put(key, keyForRegion, region); + catalogData.getRedisKeysForRegion().add(keyForRegion); + } + } + + if (groupList != null && !groupList.isEmpty()) { + for (Group group : groupList) { + String keyForGroup = "GROUP:" + deviceId + ":" + group.getDeviceId() + ":" + sn; + redisTemplate.opsForHash().put(key, keyForGroup, group); + catalogData.getRedisKeysForGroup().add(keyForGroup); + } + } + } + + public List getDeviceChannelList(String deviceId, int sn) { + List result = new ArrayList<>(); + CatalogData catalogData = dataMap.get(buildMapKey(deviceId,sn)); + if (catalogData == null ) { + log.warn("[Redis-Catalog] 未找到缓存对象,可能已经结束"); + return result; + } + for (String objectKey : catalogData.getRedisKeysForChannel()) { + DeviceChannel deviceChannel = (DeviceChannel) redisTemplate.opsForHash().get(key, objectKey); + if (deviceChannel != null) { + result.add(deviceChannel); + } + } + return result; + } + + public List getRegionList(String deviceId, int sn) { + List result = new ArrayList<>(); + CatalogData catalogData = dataMap.get(buildMapKey(deviceId,sn)); + if (catalogData == null ) { + log.warn("[Redis-Catalog] 未找到缓存对象,可能已经结束"); + return result; + } + for (String objectKey : catalogData.getRedisKeysForRegion()) { + Region region = (Region) redisTemplate.opsForHash().get(key, objectKey); + if (region != null) { + result.add(region); + } + } + return result; + } + + public List getGroupList(String deviceId, int sn) { + List result = new ArrayList<>(); + CatalogData catalogData = dataMap.get(buildMapKey(deviceId,sn)); + if (catalogData == null ) { + log.warn("[Redis-Catalog] 未找到缓存对象,可能已经结束"); + return result; + } + for (String objectKey : catalogData.getRedisKeysForGroup()) { + Group group = (Group) redisTemplate.opsForHash().get(key, objectKey); + if (group != null) { + result.add(group); + } + } + return result; + } + + public SyncStatus getSyncStatus(String deviceId) { + if (dataMap.isEmpty()) { + return null; + } + Set keySet = dataMap.keySet(); + for (String key : keySet) { + CatalogData catalogData = dataMap.get(key); + if (catalogData != null && deviceId.equals(catalogData.getDevice().getDeviceId())) { + SyncStatus syncStatus = new SyncStatus(); + syncStatus.setCurrent(catalogData.getRedisKeysForChannel().size()); + syncStatus.setTotal(catalogData.getTotal()); + syncStatus.setErrorMsg(catalogData.getErrorMsg()); + syncStatus.setTime(catalogData.getTime()); + if (catalogData.getStatus().equals(CatalogData.CatalogDataStatus.ready) || catalogData.getStatus().equals(CatalogData.CatalogDataStatus.end)) { + syncStatus.setSyncIng(false); + }else { + syncStatus.setSyncIng(true); + } + if (catalogData.getErrorMsg() != null) { + // 失败的同步信息,返回一次后直接移除 + dataMap.remove(key); + } + return syncStatus; + } + } + return null; + } + + public boolean isSyncRunning(String deviceId) { + if (dataMap.isEmpty()) { + return false; + } + Set keySet = dataMap.keySet(); + for (String key : keySet) { + CatalogData catalogData = dataMap.get(key); + if (catalogData != null && deviceId.equals(catalogData.getDevice().getDeviceId())) { + // 此时检查是否过期 + Instant instantBefore30S = Instant.now().minusMillis(TimeUnit.SECONDS.toMillis(30)); + if ((catalogData.getStatus().equals(CatalogData.CatalogDataStatus.end) + || catalogData.getStatus().equals(CatalogData.CatalogDataStatus.ready)) + && catalogData.getTime().isBefore(instantBefore30S)) { + dataMap.remove(key); + return false; + } + + return !catalogData.getStatus().equals(CatalogData.CatalogDataStatus.end); + } + } + return false; + } + + @Override + public void run(String... args) throws Exception { + // 启动时清理旧的数据 + redisTemplate.delete(key); + } + + @Scheduled(fixedDelay = 5 * 1000) //每5秒执行一次, 发现数据5秒未更新则移除数据并认为数据接收超时 + private void timerTask(){ + if (dataMap.isEmpty()) { + return; + } + Set keys = dataMap.keySet(); + + Instant instantBefore5S = Instant.now().minusMillis(TimeUnit.SECONDS.toMillis(5)); + Instant instantBefore30S = Instant.now().minusMillis(TimeUnit.SECONDS.toMillis(30)); + for (String dataKey : keys) { + CatalogData catalogData = dataMap.get(dataKey); + if ( catalogData.getTime().isBefore(instantBefore5S)) { + if (catalogData.getStatus().equals(CatalogData.CatalogDataStatus.runIng)) { + String deviceId = catalogData.getDevice().getDeviceId(); + int sn = catalogData.getSn(); + List deviceChannelList = getDeviceChannelList(deviceId, sn); + try { + if (catalogData.getTotal() == deviceChannelList.size()) { + deviceChannelService.resetChannels(catalogData.getDevice().getId(), deviceChannelList); + }else { + deviceChannelService.updateChannels(catalogData.getDevice(), deviceChannelList); + } + List regionList = getRegionList(deviceId, sn); + if ( regionList!= null && !regionList.isEmpty()) { + regionService.batchAdd(regionList); + } + List groupList = getGroupList(deviceId, sn); + if (groupList != null && !groupList.isEmpty()) { + groupService.batchAdd(groupList); + } + }catch (Exception e) { + log.error("[国标通道同步] 入库失败: ", e); + } + String errorMsg = "更新成功,共" + catalogData.getTotal() + "条,已更新" + deviceChannelList.size() + "条"; + catalogData.setErrorMsg(errorMsg); + catalogData.setStatus(CatalogData.CatalogDataStatus.end); + }else if (catalogData.getStatus().equals(CatalogData.CatalogDataStatus.ready)) { + String errorMsg = "同步失败,等待回复超时"; + catalogData.setErrorMsg(errorMsg); + } + } + if ((catalogData.getStatus().equals(CatalogData.CatalogDataStatus.end) || catalogData.getStatus().equals(CatalogData.CatalogDataStatus.ready)) + && catalogData.getTime().isBefore(instantBefore30S)) { // 超过三十秒,如果标记为end则删除 + dataMap.remove(dataKey); + Set redisKeysForChannel = catalogData.getRedisKeysForChannel(); + if (redisKeysForChannel != null && !redisKeysForChannel.isEmpty()) { + for (String deleteKey : redisKeysForChannel) { + redisTemplate.opsForHash().delete(key, deleteKey); + } + } + Set redisKeysForRegion = catalogData.getRedisKeysForRegion(); + if (redisKeysForRegion != null && !redisKeysForRegion.isEmpty()) { + for (String deleteKey : redisKeysForRegion) { + redisTemplate.opsForHash().delete(key, deleteKey); + } + } + Set redisKeysForGroup = catalogData.getRedisKeysForGroup(); + if (redisKeysForGroup != null && !redisKeysForGroup.isEmpty()) { + for (String deleteKey : redisKeysForGroup) { + redisTemplate.opsForHash().delete(key, deleteKey); + } + } + } + } + } + + + public void setChannelSyncEnd(String deviceId, int sn, String errorMsg) { + CatalogData catalogData = dataMap.get(buildMapKey(deviceId,sn)); + if (catalogData == null) { + return; + } + catalogData.setStatus(CatalogData.CatalogDataStatus.end); + catalogData.setErrorMsg(errorMsg); + catalogData.setTime(Instant.now()); + } + + public int size(String deviceId, int sn) { + CatalogData catalogData = dataMap.get(buildMapKey(deviceId,sn)); + if (catalogData == null) { + return 0; + } + return catalogData.getRedisKeysForChannel().size() + catalogData.getErrorChannel().size(); + } + + public int sumNum(String deviceId, int sn) { + CatalogData catalogData = dataMap.get(buildMapKey(deviceId,sn)); + if (catalogData == null) { + return 0; + } + return catalogData.getTotal(); + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/session/CommonSessionManager.java b/src/main/java/com/genersoft/iot/vmp/gb28181/session/CommonSessionManager.java new file mode 100755 index 0000000..2d8c7e1 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/session/CommonSessionManager.java @@ -0,0 +1,86 @@ +package com.genersoft.iot.vmp.gb28181.session; + +import com.genersoft.iot.vmp.common.CommonCallback; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +import java.util.Calendar; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * 通用回调管理 + */ +@Component +public class CommonSessionManager { + + public static Map callbackMap = new ConcurrentHashMap<>(); + + /** + * 存储回调相关的信息 + */ + class CommonSession{ + public String session; + public long createTime; + public int timeout; + + public CommonCallback callback; + public CommonCallback timeoutCallback; + } + + /** + * 添加回调 + * @param sessionId 唯一标识 + * @param callback 回调 + * @param timeout 超时时间, 单位分钟 + */ + public void add(String sessionId, CommonCallback callback, CommonCallback timeoutCallback, + Integer timeout) { + CommonSession commonSession = new CommonSession(); + commonSession.session = sessionId; + commonSession.callback = callback; + commonSession.createTime = System.currentTimeMillis(); + if (timeoutCallback != null) { + commonSession.timeoutCallback = timeoutCallback; + } + if (timeout != null) { + commonSession.timeout = timeout; + } + callbackMap.put(sessionId, commonSession); + } + + public void add(String sessionId, CommonCallback callback) { + add(sessionId, callback, null, 1); + } + + public CommonCallback get(String sessionId, boolean destroy) { + CommonSession commonSession = callbackMap.get(sessionId); + if (destroy) { + callbackMap.remove(sessionId); + } + return commonSession.callback; + } + + public CommonCallback get(String sessionId) { + return get(sessionId, false); + } + + public void delete(String sessionID) { + callbackMap.remove(sessionID); + } + + @Scheduled(fixedRate= 60) //每分钟执行一次 + public void execute(){ + Calendar cal = Calendar.getInstance(); + cal.add(Calendar.MINUTE, -1); + for (String session : callbackMap.keySet()) { + if (callbackMap.get(session).createTime < cal.getTimeInMillis()) { + // 超时 + if (callbackMap.get(session).timeoutCallback != null) { + callbackMap.get(session).timeoutCallback.run("timeout"); + } + callbackMap.remove(session); + } + } + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/session/SSRCFactory.java b/src/main/java/com/genersoft/iot/vmp/gb28181/session/SSRCFactory.java new file mode 100755 index 0000000..67f9c29 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/session/SSRCFactory.java @@ -0,0 +1,125 @@ +package com.genersoft.iot.vmp.gb28181.session; + +import com.genersoft.iot.vmp.conf.SipConfig; +import com.genersoft.iot.vmp.conf.UserSetting; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.stereotype.Component; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; + +/** + * ssrc使用 + */ +@Slf4j +@Component +public class SSRCFactory { + + /** + * 播流最大并发个数 + */ + private static final Integer MAX_STREAM_COUNT = 10000; + + /** + * 播流最大并发个数 + */ + private static final String SSRC_INFO_KEY = "VMP_SSRC_INFO_"; + + @Autowired + private StringRedisTemplate redisTemplate; + + @Autowired + private SipConfig sipConfig; + + @Autowired + private UserSetting userSetting; + + + public void initMediaServerSSRC(String mediaServerId, Set usedSet) { + String sipDomain = sipConfig.getDomain(); + String ssrcPrefix = sipDomain.length() >= 8 ? sipDomain.substring(3, 8) : sipDomain; + String redisKey = SSRC_INFO_KEY + userSetting.getServerId() + "_" + mediaServerId; + List ssrcList = new ArrayList<>(); + for (int i = 1; i < MAX_STREAM_COUNT; i++) { + String ssrc = String.format("%s%04d", ssrcPrefix, i); + + if (null == usedSet || !usedSet.contains(ssrc)) { + ssrcList.add(ssrc); + + } + } + if (redisTemplate.opsForSet().size(redisKey) != null) { + redisTemplate.delete(redisKey); + } + redisTemplate.opsForSet().add(redisKey, ssrcList.toArray(new String[0])); + } + + + /** + * 获取视频预览的SSRC值,第一位固定为0 + * + * @return ssrc + */ + public String getPlaySsrc(String mediaServerId) { + return "0" + getSN(mediaServerId); + } + + /** + * 获取录像回放的SSRC值,第一位固定为1 + */ + public String getPlayBackSsrc(String mediaServerId) { + return "1" + getSN(mediaServerId); + } + + /** + * 释放ssrc,主要用完的ssrc一定要释放,否则会耗尽 + * + * @param ssrc 需要重置的ssrc + */ + public void releaseSsrc(String mediaServerId, String ssrc) { + if (ssrc == null) { + return; + } + String sn = ssrc.substring(1); + String redisKey = SSRC_INFO_KEY + userSetting.getServerId() + "_" + mediaServerId; + redisTemplate.opsForSet().add(redisKey, sn); + } + + /** + * 获取后四位数SN,随机数 + */ + private String getSN(String mediaServerId) { + String redisKey = SSRC_INFO_KEY + userSetting.getServerId() + "_" + mediaServerId; + Long size = redisTemplate.opsForSet().size(redisKey); + if (size == null || size == 0) { + log.info("[获取 SSRC 失败] redisKey: {}", redisKey); + throw new RuntimeException("ssrc已经用完"); + } else { + // 在集合中移除并返回一个随机成员。 + return redisTemplate.opsForSet().pop(redisKey); + } + } + + /** + * 重置一个流媒体服务的所有ssrc + * + * @param mediaServerId 流媒体服务ID + */ + public void reset(String mediaServerId) { + this.initMediaServerSSRC(mediaServerId, null); + } + + /** + * 是否已经存在了某个MediaServer的SSRC信息 + * + * @param mediaServerId 流媒体服务ID + */ + public boolean hasMediaServerSSRC(String mediaServerId) { + String redisKey = SSRC_INFO_KEY + userSetting.getServerId() + "_" + mediaServerId; + return Boolean.TRUE.equals(redisTemplate.hasKey(redisKey)); + } + +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/session/SipInviteSessionManager.java b/src/main/java/com/genersoft/iot/vmp/gb28181/session/SipInviteSessionManager.java new file mode 100755 index 0000000..a4468d9 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/session/SipInviteSessionManager.java @@ -0,0 +1,90 @@ +package com.genersoft.iot.vmp.gb28181.session; + +import com.genersoft.iot.vmp.common.VideoManagerConstants; +import com.genersoft.iot.vmp.conf.UserSetting; +import com.genersoft.iot.vmp.gb28181.bean.SsrcTransaction; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Component; + +import java.util.ArrayList; +import java.util.List; + +/** + * 视频流session管理器,管理视频预览、预览回放的通信句柄 + */ +@Component +public class SipInviteSessionManager { + + @Autowired + private UserSetting userSetting; + + @Autowired + private RedisTemplate redisTemplate; + + /** + * 添加一个点播/回放的事务信息 + */ + public void put(SsrcTransaction ssrcTransaction){ + redisTemplate.opsForHash().put(VideoManagerConstants.SIP_INVITE_SESSION_STREAM + userSetting.getServerId() + , ssrcTransaction.getApp() + ssrcTransaction.getStream(), ssrcTransaction); + + redisTemplate.opsForHash().put(VideoManagerConstants.SIP_INVITE_SESSION_CALL_ID + userSetting.getServerId() + , ssrcTransaction.getCallId(), ssrcTransaction); + } + + public SsrcTransaction getSsrcTransactionByStream(String app, String stream){ + String key = VideoManagerConstants.SIP_INVITE_SESSION_STREAM + userSetting.getServerId(); + return (SsrcTransaction)redisTemplate.opsForHash().get(key, app + stream); + } + + public SsrcTransaction getSsrcTransactionByCallId(String callId){ + String key = VideoManagerConstants.SIP_INVITE_SESSION_CALL_ID + userSetting.getServerId(); + return (SsrcTransaction)redisTemplate.opsForHash().get(key, callId); + } + + public List getSsrcTransactionByDeviceId(String deviceId){ + String key = VideoManagerConstants.SIP_INVITE_SESSION_CALL_ID + userSetting.getServerId(); + List values = redisTemplate.opsForHash().values(key); + List result = new ArrayList<>(); + for (Object value : values) { + SsrcTransaction ssrcTransaction = (SsrcTransaction) value; + if (ssrcTransaction != null && deviceId.equals(ssrcTransaction.getDeviceId())) { + result.add(ssrcTransaction); + } + } + return result; + } + + public void removeByStream(String app, String stream) { + SsrcTransaction ssrcTransaction = getSsrcTransactionByStream(app, stream); + if (ssrcTransaction == null ) { + return; + } + redisTemplate.opsForHash().delete(VideoManagerConstants.SIP_INVITE_SESSION_STREAM + userSetting.getServerId(), app + stream); + if (ssrcTransaction.getCallId() != null) { + redisTemplate.opsForHash().delete(VideoManagerConstants.SIP_INVITE_SESSION_CALL_ID + userSetting.getServerId(), ssrcTransaction.getCallId()); + } + } + + public void removeByCallId(String callId) { + SsrcTransaction ssrcTransaction = getSsrcTransactionByCallId(callId); + if (ssrcTransaction == null ) { + return; + } + redisTemplate.opsForHash().delete(VideoManagerConstants.SIP_INVITE_SESSION_CALL_ID + userSetting.getServerId(), callId); + if (ssrcTransaction.getStream() != null) { + redisTemplate.opsForHash().delete(VideoManagerConstants.SIP_INVITE_SESSION_STREAM + userSetting.getServerId(), ssrcTransaction.getApp() + ssrcTransaction.getStream()); + } + } + + public List getAll() { + String key = VideoManagerConstants.SIP_INVITE_SESSION_CALL_ID + userSetting.getServerId(); + List values = redisTemplate.opsForHash().values(key); + List result = new ArrayList<>(); + for (Object value : values) { + result.add((SsrcTransaction) value); + } + return result; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/session/SseSessionManager.java b/src/main/java/com/genersoft/iot/vmp/gb28181/session/SseSessionManager.java new file mode 100644 index 0000000..6c88f7d --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/session/SseSessionManager.java @@ -0,0 +1,72 @@ +package com.genersoft.iot.vmp.gb28181.session; + +import com.genersoft.iot.vmp.conf.DynamicTask; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; +import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +@Component +@Slf4j +public class SseSessionManager { + + private static final Map sseSessionMap = new ConcurrentHashMap<>(); + + @Autowired + private DynamicTask dynamicTask; + + public SseEmitter conect(String browserId){ + SseEmitter sseEmitter = new SseEmitter(0L); + sseEmitter.onError((err)-> { + log.error("[SSE推送] 连接错误, 浏览器 ID: {}, {}", browserId, err.getMessage()); + sseSessionMap.remove(browserId); + sseEmitter.completeWithError(err); + }); + +// sseEmitter.onTimeout(() -> { +// log.info("[SSE推送] 连接超时, 浏览器 ID: {}", browserId); +// sseSessionMap.remove(browserId); +// sseEmitter.complete(); +// dynamicTask.stop(key); +// }); + + sseEmitter.onCompletion(() -> { + log.info("[SSE推送] 连接结束, 浏览器 ID: {}", browserId); + sseSessionMap.remove(browserId); + }); + + sseSessionMap.put(browserId, sseEmitter); + + log.info("[SSE推送] 连接已建立, 浏览器 ID: {}, 当前在线数: {}", browserId, sseSessionMap.size()); + return sseEmitter; + } + + @Scheduled(fixedRate = 1000) //每1秒执行一次 + public void execute(){ + if (sseSessionMap.isEmpty()){ + return; + } + sendForAll("keepalive", "alive"); + } + + + public void sendForAll(String event, Object data) { + for (String browserId : sseSessionMap.keySet()) { + SseEmitter sseEmitter = sseSessionMap.get(browserId); + if (sseEmitter == null) { + continue; + }; + try { + sseEmitter.send(SseEmitter.event().name(event).data(data)); + } catch (Exception e) { + log.error("[SSE推送] 发送失败: {}", e.getMessage()); + sseSessionMap.remove(browserId); + sseEmitter.completeWithError(e); + } + } + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/task/deviceStatus/DeviceStatusTask.java b/src/main/java/com/genersoft/iot/vmp/gb28181/task/deviceStatus/DeviceStatusTask.java new file mode 100644 index 0000000..0e75435 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/task/deviceStatus/DeviceStatusTask.java @@ -0,0 +1,60 @@ +package com.genersoft.iot.vmp.gb28181.task.deviceStatus; + +import com.genersoft.iot.vmp.common.DeviceStatusCallback; +import com.genersoft.iot.vmp.gb28181.bean.SipTransactionInfo; +import lombok.Data; +import lombok.extern.slf4j.Slf4j; +import org.jetbrains.annotations.NotNull; + +import java.util.concurrent.Delayed; +import java.util.concurrent.TimeUnit; + +@Slf4j +@Data +public class DeviceStatusTask implements Delayed { + + private String deviceId; + + private SipTransactionInfo transactionInfo; + + /** + * 超时时间(单位: 毫秒) + */ + private long delayTime; + + private DeviceStatusCallback callback; + + public static DeviceStatusTask getInstance(String deviceId, SipTransactionInfo transactionInfo, long delayTime, DeviceStatusCallback callback) { + DeviceStatusTask deviceStatusTask = new DeviceStatusTask(); + deviceStatusTask.setDeviceId(deviceId); + deviceStatusTask.setTransactionInfo(transactionInfo); + deviceStatusTask.setDelayTime(delayTime); + deviceStatusTask.setCallback(callback); + return deviceStatusTask; + } + + public void expired() { + if (callback == null) { + log.info("[设备离线] 未找到过期处理回调, {}", deviceId); + return; + } + callback.run(deviceId, transactionInfo); + } + + @Override + public long getDelay(@NotNull TimeUnit unit) { + return unit.convert(delayTime - System.currentTimeMillis(), TimeUnit.MILLISECONDS); + } + + @Override + public int compareTo(@NotNull Delayed o) { + return (int) (this.getDelay(TimeUnit.MILLISECONDS) - o.getDelay(TimeUnit.MILLISECONDS)); + } + + public DeviceStatusTaskInfo getInfo(){ + DeviceStatusTaskInfo taskInfo = new DeviceStatusTaskInfo(); + taskInfo.setTransactionInfo(transactionInfo); + taskInfo.setDeviceId(deviceId); + return taskInfo; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/task/deviceStatus/DeviceStatusTaskInfo.java b/src/main/java/com/genersoft/iot/vmp/gb28181/task/deviceStatus/DeviceStatusTaskInfo.java new file mode 100644 index 0000000..af2aa75 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/task/deviceStatus/DeviceStatusTaskInfo.java @@ -0,0 +1,17 @@ +package com.genersoft.iot.vmp.gb28181.task.deviceStatus; + +import com.genersoft.iot.vmp.gb28181.bean.SipTransactionInfo; +import lombok.Data; + +@Data +public class DeviceStatusTaskInfo{ + + private String deviceId; + + private SipTransactionInfo transactionInfo; + + /** + * 过期时间,单位毫秒 + */ + private long expireTime; +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/task/deviceStatus/DeviceStatusTaskRunner.java b/src/main/java/com/genersoft/iot/vmp/gb28181/task/deviceStatus/DeviceStatusTaskRunner.java new file mode 100644 index 0000000..a8e9c79 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/task/deviceStatus/DeviceStatusTaskRunner.java @@ -0,0 +1,131 @@ +package com.genersoft.iot.vmp.gb28181.task.deviceStatus; + +import com.genersoft.iot.vmp.conf.UserSetting; +import com.genersoft.iot.vmp.gb28181.bean.SipTransactionInfo; +import com.genersoft.iot.vmp.utils.redis.RedisUtil; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.DelayQueue; +import java.util.concurrent.TimeUnit; + +@Slf4j +@Component +public class DeviceStatusTaskRunner { + + private final Map subscribes = new ConcurrentHashMap<>(); + + private final DelayQueue delayQueue = new DelayQueue<>(); + + @Autowired + private RedisTemplate redisTemplate; + + @Autowired + private UserSetting userSetting; + + private final String prefix = "VMP_DEVICE_STATUS"; + + // 状态过期检查 + @Scheduled(fixedDelay = 500, timeUnit = TimeUnit.MILLISECONDS) + public void expirationCheck(){ + while (!delayQueue.isEmpty()) { + DeviceStatusTask take = null; + try { + take = delayQueue.take(); + try { + removeTask(take.getDeviceId()); + take.expired(); + }catch (Exception e) { + log.error("[设备状态到期] 到期处理时出现异常, 设备编号: {} ", take.getDeviceId()); + } + } catch (InterruptedException e) { + log.error("[设备状态任务] ", e); + } + } + } + + public void addTask(DeviceStatusTask task) { + Duration duration = Duration.ofSeconds((task.getDelayTime() - System.currentTimeMillis())/1000); + if (duration.getSeconds() < 0) { + return; + } + subscribes.put(task.getDeviceId(), task); + String key = String.format("%s_%s_%s", prefix, userSetting.getServerId(), task.getDeviceId()); + redisTemplate.opsForValue().set(key, task.getInfo(), duration); + delayQueue.offer(task); + } + + public boolean removeTask(String key) { + DeviceStatusTask task = subscribes.get(key); + if (task == null) { + return false; + } + String redisKey = String.format("%s_%s_%s", prefix, userSetting.getServerId(), task.getDeviceId()); + redisTemplate.delete(redisKey); + subscribes.remove(key); + if (delayQueue.contains(task)) { + boolean remove = delayQueue.remove(task); + if (!remove) { + log.info("[移除状态任务] 从延时队列内移除失败: {}", key); + } + } + return true; + } + + public SipTransactionInfo getTransactionInfo(String key) { + DeviceStatusTask task = subscribes.get(key); + if (task == null) { + return null; + } + return task.getTransactionInfo(); + } + + public boolean updateDelay(String key, long expirationTime) { + DeviceStatusTask task = subscribes.get(key); + if (task == null) { + return false; + } + log.debug("[更新状态任务时间] 编号: {}", key); + // 如果值更改时间,如果队列中有多个元素时 超时无法出发。目前采用移除再加入的方法 + delayQueue.remove(task); + task.setDelayTime(expirationTime); + delayQueue.offer(task); + String redisKey = String.format("%s_%s_%s", prefix, userSetting.getServerId(), task.getDeviceId()); + Duration duration = Duration.ofSeconds((expirationTime - System.currentTimeMillis())/1000); + redisTemplate.expire(redisKey, duration); + return true; + } + + public boolean containsKey(String key) { + return subscribes.containsKey(key); + } + + public List getAllTaskInfo(){ + String scanKey = String.format("%s_%s_*", prefix, userSetting.getServerId()); + List values = RedisUtil.scan(redisTemplate, scanKey); + if (values.isEmpty()) { + return new ArrayList<>(); + } + List result = new ArrayList<>(); + for (Object value : values) { + String redisKey = (String)value; + DeviceStatusTaskInfo taskInfo = (DeviceStatusTaskInfo)redisTemplate.opsForValue().get(redisKey); + if (taskInfo == null) { + continue; + } + Long expire = redisTemplate.getExpire(redisKey, TimeUnit.MILLISECONDS); + taskInfo.setExpireTime(expire); + result.add(taskInfo); + } + return result; + + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/task/deviceSubscribe/SubscribeTask.java b/src/main/java/com/genersoft/iot/vmp/gb28181/task/deviceSubscribe/SubscribeTask.java new file mode 100644 index 0000000..e33ac38 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/task/deviceSubscribe/SubscribeTask.java @@ -0,0 +1,49 @@ +package com.genersoft.iot.vmp.gb28181.task.deviceSubscribe; + +import com.genersoft.iot.vmp.common.SubscribeCallback; +import com.genersoft.iot.vmp.gb28181.bean.SipTransactionInfo; +import lombok.Data; +import org.jetbrains.annotations.NotNull; + +import java.util.concurrent.Delayed; +import java.util.concurrent.TimeUnit; + +@Data +public abstract class SubscribeTask implements Delayed { + + private String deviceId; + + private SubscribeCallback callback; + + private SipTransactionInfo transactionInfo; + + /** + * 超时时间(单位: 毫秒) + */ + private long delayTime; + + public abstract void expired(); + + public abstract String getKey(); + + public abstract String getName(); + + @Override + public long getDelay(@NotNull TimeUnit unit) { + return unit.convert(delayTime - System.currentTimeMillis(), TimeUnit.MILLISECONDS); + } + + @Override + public int compareTo(@NotNull Delayed o) { + return (int) (this.getDelay(TimeUnit.MILLISECONDS) - o.getDelay(TimeUnit.MILLISECONDS)); + } + + public SubscribeTaskInfo getInfo(){ + SubscribeTaskInfo subscribeTaskInfo = new SubscribeTaskInfo(); + subscribeTaskInfo.setName(getName()); + subscribeTaskInfo.setTransactionInfo(transactionInfo); + subscribeTaskInfo.setDeviceId(deviceId); + subscribeTaskInfo.setKey(getKey()); + return subscribeTaskInfo; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/task/deviceSubscribe/SubscribeTaskInfo.java b/src/main/java/com/genersoft/iot/vmp/gb28181/task/deviceSubscribe/SubscribeTaskInfo.java new file mode 100644 index 0000000..aa9dd8c --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/task/deviceSubscribe/SubscribeTaskInfo.java @@ -0,0 +1,22 @@ +package com.genersoft.iot.vmp.gb28181.task.deviceSubscribe; + +import com.genersoft.iot.vmp.gb28181.bean.SipTransactionInfo; +import lombok.Data; + +@Data +public class SubscribeTaskInfo { + + private String deviceId; + + private SipTransactionInfo transactionInfo; + + private String name; + + private String key; + + /** + * 过期时间,单位: 秒 + */ + private long expireTime; + +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/task/deviceSubscribe/SubscribeTaskRunner.java b/src/main/java/com/genersoft/iot/vmp/gb28181/task/deviceSubscribe/SubscribeTaskRunner.java new file mode 100644 index 0000000..7e70935 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/task/deviceSubscribe/SubscribeTaskRunner.java @@ -0,0 +1,130 @@ +package com.genersoft.iot.vmp.gb28181.task.deviceSubscribe; + +import com.genersoft.iot.vmp.conf.UserSetting; +import com.genersoft.iot.vmp.gb28181.bean.SipTransactionInfo; +import com.genersoft.iot.vmp.utils.redis.RedisUtil; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.DelayQueue; +import java.util.concurrent.TimeUnit; + +@Slf4j +@Component +public class SubscribeTaskRunner{ + + private final Map subscribes = new ConcurrentHashMap<>(); + + private final DelayQueue delayQueue = new DelayQueue<>(); + + @Autowired + private RedisTemplate redisTemplate; + + @Autowired + private UserSetting userSetting; + + private final String prefix = "VMP_DEVICE_SUBSCRIBE"; + + // 订阅过期检查 + @Scheduled(fixedDelay = 500, timeUnit = TimeUnit.MILLISECONDS) + public void expirationCheck(){ + while (!delayQueue.isEmpty()) { + SubscribeTask take = null; + try { + take = delayQueue.take(); + try { + removeSubscribe(take.getKey()); + take.expired(); + }catch (Exception e) { + log.error("[设备订阅到期] {} 到期处理时出现异常, 设备编号: {} ", take.getName(), take.getDeviceId()); + } + } catch (InterruptedException e) { + log.error("[设备订阅任务] ", e); + } + } + } + + public void addSubscribe(SubscribeTask task) { + Duration duration = Duration.ofSeconds((task.getDelayTime() - System.currentTimeMillis())/1000); + if (duration.getSeconds() < 0) { + return; + } + subscribes.put(task.getKey(), task); + String key = String.format("%s_%s_%s", prefix, userSetting.getServerId(), task.getKey()); + redisTemplate.opsForValue().set(key, task.getInfo(), duration); + delayQueue.offer(task); + } + + public boolean removeSubscribe(String key) { + SubscribeTask task = subscribes.get(key); + if (task == null) { + return false; + } + String redisKey = String.format("%s_%s_%s", prefix, userSetting.getServerId(), task.getKey()); + redisTemplate.delete(redisKey); + subscribes.remove(key); + if (delayQueue.contains(task)) { + boolean remove = delayQueue.remove(task); + if (!remove) { + log.info("[移除订阅任务] 从延时队列内移除失败: {}", key); + } + } + return true; + } + + public SipTransactionInfo getTransactionInfo(String key) { + SubscribeTask task = subscribes.get(key); + if (task == null) { + return null; + } + return task.getTransactionInfo(); + } + + public boolean updateDelay(String key, long expirationTime) { + SubscribeTask task = subscribes.get(key); + if (task == null) { + return false; + } + log.info("[更新订阅任务时间] {}, 编号: {}", task.getName(), key); + delayQueue.remove(task); + task.setDelayTime(expirationTime); + delayQueue.offer(task); + String redisKey = String.format("%s_%s_%s", prefix, userSetting.getServerId(), task.getKey()); + Duration duration = Duration.ofSeconds((expirationTime - System.currentTimeMillis())/1000); + redisTemplate.expire(redisKey, duration); + return true; + } + + public boolean containsKey(String key) { + return subscribes.containsKey(key); + } + + public List getAllTaskInfo(){ + String scanKey = String.format("%s_%s_*", prefix, userSetting.getServerId()); + List values = RedisUtil.scan(redisTemplate, scanKey); + if (values.isEmpty()) { + return new ArrayList<>(); + } + List result = new ArrayList<>(); + for (Object value : values) { + String redisKey = (String)value; + SubscribeTaskInfo taskInfo = (SubscribeTaskInfo)redisTemplate.opsForValue().get(redisKey); + if (taskInfo == null) { + continue; + } + Long expire = redisTemplate.getExpire(redisKey, TimeUnit.SECONDS); + taskInfo.setExpireTime(expire); + result.add(taskInfo); + } + return result; + + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/task/deviceSubscribe/impl/SubscribeTaskForCatalog.java b/src/main/java/com/genersoft/iot/vmp/gb28181/task/deviceSubscribe/impl/SubscribeTaskForCatalog.java new file mode 100644 index 0000000..8ca7b39 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/task/deviceSubscribe/impl/SubscribeTaskForCatalog.java @@ -0,0 +1,48 @@ +package com.genersoft.iot.vmp.gb28181.task.deviceSubscribe.impl; + +import com.genersoft.iot.vmp.common.SubscribeCallback; +import com.genersoft.iot.vmp.gb28181.bean.Device; +import com.genersoft.iot.vmp.gb28181.bean.SipTransactionInfo; +import com.genersoft.iot.vmp.gb28181.task.deviceSubscribe.SubscribeTask; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class SubscribeTaskForCatalog extends SubscribeTask { + + public static final String name = "catalog"; + + public static SubscribeTask getInstance(Device device, SubscribeCallback callback, SipTransactionInfo transactionInfo) { + if (device.getSubscribeCycleForCatalog() <= 0) { + return null; + } + SubscribeTaskForCatalog subscribeTaskForCatalog = new SubscribeTaskForCatalog(); + subscribeTaskForCatalog.setDelayTime((device.getSubscribeCycleForCatalog() * 1000L - 500L) + System.currentTimeMillis()); + subscribeTaskForCatalog.setDeviceId(device.getDeviceId()); + subscribeTaskForCatalog.setCallback(callback); + subscribeTaskForCatalog.setTransactionInfo(transactionInfo); + return subscribeTaskForCatalog; + } + + @Override + public void expired() { + if (super.getCallback() == null) { + log.info("[设备订阅到期] 目录订阅 未找到到期处理回调, 编号: {}", getDeviceId()); + return; + } + getCallback().run(getDeviceId(), getTransactionInfo()); + } + + @Override + public String getKey() { + return String.format("%s_%s", name, getDeviceId()); + } + + @Override + public String getName() { + return name; + } + + public static String getKey(Device device) { + return String.format("%s_%s", SubscribeTaskForCatalog.name, device.getDeviceId()); + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/task/deviceSubscribe/impl/SubscribeTaskForMobilPosition.java b/src/main/java/com/genersoft/iot/vmp/gb28181/task/deviceSubscribe/impl/SubscribeTaskForMobilPosition.java new file mode 100644 index 0000000..48bf9c5 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/task/deviceSubscribe/impl/SubscribeTaskForMobilPosition.java @@ -0,0 +1,48 @@ +package com.genersoft.iot.vmp.gb28181.task.deviceSubscribe.impl; + +import com.genersoft.iot.vmp.common.SubscribeCallback; +import com.genersoft.iot.vmp.gb28181.bean.Device; +import com.genersoft.iot.vmp.gb28181.bean.SipTransactionInfo; +import com.genersoft.iot.vmp.gb28181.task.deviceSubscribe.SubscribeTask; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class SubscribeTaskForMobilPosition extends SubscribeTask { + + public static final String name = "mobilPosition"; + + public static SubscribeTask getInstance(Device device, SubscribeCallback callback, SipTransactionInfo transactionInfo) { + if (device.getSubscribeCycleForMobilePosition() <= 0) { + return null; + } + SubscribeTaskForMobilPosition subscribeTaskForMobilPosition = new SubscribeTaskForMobilPosition(); + subscribeTaskForMobilPosition.setDelayTime((device.getSubscribeCycleForMobilePosition() * 1000L - 500L) + System.currentTimeMillis()); + subscribeTaskForMobilPosition.setDeviceId(device.getDeviceId()); + subscribeTaskForMobilPosition.setCallback(callback); + subscribeTaskForMobilPosition.setTransactionInfo(transactionInfo); + return subscribeTaskForMobilPosition; + } + + @Override + public void expired() { + if (super.getCallback() == null) { + log.info("[设备订阅到期] 移动位置订阅 未找到到期处理回调, 编号: {}", getDeviceId()); + return; + } + getCallback().run(getDeviceId(), getTransactionInfo()); + } + + @Override + public String getKey() { + return String.format("%s_%s", name, getDeviceId()); + } + + @Override + public String getName() { + return name; + } + + public static String getKey(Device device) { + return String.format("%s_%s", SubscribeTaskForMobilPosition.name, device.getDeviceId()); + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/task/platformStatus/PlatformKeepaliveTask.java b/src/main/java/com/genersoft/iot/vmp/gb28181/task/platformStatus/PlatformKeepaliveTask.java new file mode 100644 index 0000000..a1b2f40 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/task/platformStatus/PlatformKeepaliveTask.java @@ -0,0 +1,64 @@ +package com.genersoft.iot.vmp.gb28181.task.platformStatus; + +import com.genersoft.iot.vmp.gb28181.bean.PlatformKeepaliveCallback; +import lombok.Getter; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; +import org.jetbrains.annotations.NotNull; + +import java.util.concurrent.Delayed; +import java.util.concurrent.TimeUnit; + +/** + * 平台心跳任务 + */ +@Slf4j +public class PlatformKeepaliveTask implements Delayed { + + @Getter + private String platformServerId; + + /** + * 超时时间(单位: 毫秒) + */ + @Getter + @Setter + private long delayTime; + + /** + * 到期回调 + */ + @Getter + private PlatformKeepaliveCallback callback; + + /** + * 心跳发送失败次数 + */ + @Getter + @Setter + private int failCount; + + public PlatformKeepaliveTask(String platformServerId, long delayTime, PlatformKeepaliveCallback callback) { + this.platformServerId = platformServerId; + this.delayTime = System.currentTimeMillis() + delayTime; + this.callback = callback; + } + + public void expired() { + if (callback == null) { + log.info("[平台心跳到期] 未找到到期处理回调, 平台上级编号: {}", platformServerId); + return; + } + getCallback().run(platformServerId, failCount); + } + + @Override + public long getDelay(@NotNull TimeUnit unit) { + return unit.convert(delayTime - System.currentTimeMillis(), TimeUnit.MILLISECONDS); + } + + @Override + public int compareTo(@NotNull Delayed o) { + return (int) (this.getDelay(TimeUnit.MILLISECONDS) - o.getDelay(TimeUnit.MILLISECONDS)); + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/task/platformStatus/PlatformRegisterTask.java b/src/main/java/com/genersoft/iot/vmp/gb28181/task/platformStatus/PlatformRegisterTask.java new file mode 100644 index 0000000..781dc96 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/task/platformStatus/PlatformRegisterTask.java @@ -0,0 +1,70 @@ +package com.genersoft.iot.vmp.gb28181.task.platformStatus; + +import com.genersoft.iot.vmp.common.CommonCallback; +import com.genersoft.iot.vmp.gb28181.bean.SipTransactionInfo; +import lombok.Getter; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; +import org.jetbrains.annotations.NotNull; + +import java.util.concurrent.Delayed; +import java.util.concurrent.TimeUnit; + +/** + * 平台注册任务 + */ +@Slf4j +public class PlatformRegisterTask implements Delayed { + + @Getter + private String platformServerId; + + /** + * 超时时间(单位: 毫秒) + */ + @Getter + @Setter + private long delayTime; + + @Getter + private SipTransactionInfo sipTransactionInfo; + + /** + * 到期回调 + */ + @Getter + private CommonCallback callback; + + + public PlatformRegisterTask(String platformServerId, long delayTime, SipTransactionInfo sipTransactionInfo, CommonCallback callback) { + this.platformServerId = platformServerId; + this.delayTime = System.currentTimeMillis() + delayTime; + this.callback = callback; + this.sipTransactionInfo = sipTransactionInfo; + } + + public void expired() { + if (callback == null) { + log.info("[平台注册到期] 未找到到期处理回调, 平台上级编号: {}", platformServerId); + return; + } + getCallback().run(platformServerId); + } + + @Override + public long getDelay(@NotNull TimeUnit unit) { + return unit.convert(delayTime - System.currentTimeMillis(), TimeUnit.MILLISECONDS); + } + + @Override + public int compareTo(@NotNull Delayed o) { + return (int) (this.getDelay(TimeUnit.MILLISECONDS) - o.getDelay(TimeUnit.MILLISECONDS)); + } + + public PlatformRegisterTaskInfo getInfo() { + PlatformRegisterTaskInfo taskInfo = new PlatformRegisterTaskInfo(); + taskInfo.setPlatformServerId(platformServerId); + taskInfo.setSipTransactionInfo(sipTransactionInfo); + return taskInfo; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/task/platformStatus/PlatformRegisterTaskInfo.java b/src/main/java/com/genersoft/iot/vmp/gb28181/task/platformStatus/PlatformRegisterTaskInfo.java new file mode 100644 index 0000000..3b3ba6c --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/task/platformStatus/PlatformRegisterTaskInfo.java @@ -0,0 +1,22 @@ +package com.genersoft.iot.vmp.gb28181.task.platformStatus; + +import com.genersoft.iot.vmp.gb28181.bean.SipTransactionInfo; +import lombok.Data; +import lombok.extern.slf4j.Slf4j; + +/** + * 平台注册任务可序列化的信息 + */ +@Slf4j +@Data +public class PlatformRegisterTaskInfo{ + + private String platformServerId; + + private SipTransactionInfo sipTransactionInfo; + + /** + * 过期时间,单位: 毫秒 + */ + private long expireTime; +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/task/platformStatus/PlatformStatusTaskRunner.java b/src/main/java/com/genersoft/iot/vmp/gb28181/task/platformStatus/PlatformStatusTaskRunner.java new file mode 100644 index 0000000..a043d85 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/task/platformStatus/PlatformStatusTaskRunner.java @@ -0,0 +1,165 @@ +package com.genersoft.iot.vmp.gb28181.task.platformStatus; + +import com.genersoft.iot.vmp.conf.UserSetting; +import com.genersoft.iot.vmp.gb28181.bean.SipTransactionInfo; +import com.genersoft.iot.vmp.utils.redis.RedisUtil; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.DelayQueue; +import java.util.concurrent.TimeUnit; + +@Slf4j +@Component +public class PlatformStatusTaskRunner { + + private final Map registerSubscribes = new ConcurrentHashMap<>(); + + private final DelayQueue registerDelayQueue = new DelayQueue<>(); + + private final Map keepaliveSubscribes = new ConcurrentHashMap<>(); + + private final DelayQueue keepaliveTaskDelayQueue = new DelayQueue<>(); + + @Autowired + private RedisTemplate redisTemplate; + + @Autowired + private UserSetting userSetting; + + private final String prefix = "VMP_PLATFORM_STATUS"; + + // 订阅过期检查 + @Scheduled(fixedDelay = 500, timeUnit = TimeUnit.MILLISECONDS) + public void expirationCheckForRegister(){ + while (!registerDelayQueue.isEmpty()) { + PlatformRegisterTask take = null; + try { + take = registerDelayQueue.take(); + try { + removeRegisterTask(take.getPlatformServerId()); + take.expired(); + }catch (Exception e) { + log.error("[平台注册到期] 到期处理时出现异常, 平台上级编号: {} ", take.getPlatformServerId()); + } + } catch (InterruptedException e) { + log.error("[平台注册到期] ", e); + } + } + } + @Scheduled(fixedDelay = 500, timeUnit = TimeUnit.MILLISECONDS) + public void expirationCheckForKeepalive(){ + while (!keepaliveTaskDelayQueue.isEmpty()) { + PlatformKeepaliveTask take = null; + try { + take = keepaliveTaskDelayQueue.take(); + try { + removeKeepAliveTask(take.getPlatformServerId()); + take.expired(); + }catch (Exception e) { + log.error("[平台心跳到期] 到期处理时出现异常, 平台上级编号: {} ", take.getPlatformServerId()); + } + } catch (InterruptedException e) { + log.error("[平台心跳到期] ", e); + } + } + } + + public void addRegisterTask(PlatformRegisterTask task) { + Duration duration = Duration.ofSeconds((task.getDelayTime() - System.currentTimeMillis())/1000); + if (duration.getSeconds() < 0) { + return; + } + registerSubscribes.put(task.getPlatformServerId(), task); + String key = String.format("%s_%s_%s", prefix, userSetting.getServerId(), task.getPlatformServerId()); + redisTemplate.opsForValue().set(key, task.getInfo(), duration); + registerDelayQueue.offer(task); + } + + public boolean removeRegisterTask(String platformServerId) { + PlatformRegisterTask task = registerSubscribes.get(platformServerId); + if (task != null) { + registerSubscribes.remove(platformServerId); + } + String redisKey = String.format("%s_%s_%s", prefix, userSetting.getServerId(), platformServerId); + redisTemplate.delete(redisKey); + if (registerDelayQueue.contains(task)) { + boolean remove = registerDelayQueue.remove(task); + if (!remove) { + log.info("[移除平台注册任务] 从延时队列内移除失败: {}", platformServerId); + } + } + return true; + } + + public SipTransactionInfo getRegisterTransactionInfo(String platformServerId) { + PlatformRegisterTask task = registerSubscribes.get(platformServerId); + if (task == null) { + return null; + } + return task.getSipTransactionInfo(); + } + + public boolean containsRegister(String platformServerId) { + return registerSubscribes.containsKey(platformServerId); + } + + public List getAllRegisterTaskInfo(){ + return getRegisterTransactionInfoByServerId(userSetting.getServerId()); + } + + public void addKeepAliveTask(PlatformKeepaliveTask task) { + Duration duration = Duration.ofSeconds((task.getDelayTime() - System.currentTimeMillis())/1000); + if (duration.getSeconds() < 0) { + return; + } + keepaliveSubscribes.put(task.getPlatformServerId(), task); + keepaliveTaskDelayQueue.offer(task); + } + + public boolean removeKeepAliveTask(String platformServerId) { + PlatformKeepaliveTask task = keepaliveSubscribes.get(platformServerId); + if (task != null) { + keepaliveSubscribes.remove(platformServerId); + } + if (keepaliveTaskDelayQueue.contains(task)) { + boolean remove = keepaliveTaskDelayQueue.remove(task); + if (!remove) { + log.info("[移除平台心跳任务] 从延时队列内移除失败: {}", platformServerId); + } + } + return true; + } + + public boolean containsKeepAlive(String platformServerId) { + return keepaliveSubscribes.containsKey(platformServerId); + } + + public List getRegisterTransactionInfoByServerId(String serverId) { + String scanKey = String.format("%s_%s_*", prefix, serverId); + List values = RedisUtil.scan(redisTemplate, scanKey); + if (values.isEmpty()) { + return new ArrayList<>(); + } + List result = new ArrayList<>(); + for (Object value : values) { + String redisKey = (String)value; + PlatformRegisterTaskInfo taskInfo = (PlatformRegisterTaskInfo)redisTemplate.opsForValue().get(redisKey); + if (taskInfo == null) { + continue; + } + Long expire = redisTemplate.getExpire(redisKey, TimeUnit.MILLISECONDS); + taskInfo.setExpireTime(expire); + result.add(taskInfo); + } + return result; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/ISIPProcessorObserver.java b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/ISIPProcessorObserver.java new file mode 100644 index 0000000..2480f37 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/ISIPProcessorObserver.java @@ -0,0 +1,6 @@ +package com.genersoft.iot.vmp.gb28181.transmit; + +import javax.sip.SipListener; + +public interface ISIPProcessorObserver extends SipListener { +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/SIPProcessorObserver.java b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/SIPProcessorObserver.java new file mode 100644 index 0000000..3ac4be1 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/SIPProcessorObserver.java @@ -0,0 +1,200 @@ +package com.genersoft.iot.vmp.gb28181.transmit; + +import com.genersoft.iot.vmp.gb28181.event.EventPublisher; +import com.genersoft.iot.vmp.gb28181.event.SipSubscribe; +import com.genersoft.iot.vmp.gb28181.event.sip.SipEvent; +import com.genersoft.iot.vmp.gb28181.transmit.event.request.ISIPRequestProcessor; +import com.genersoft.iot.vmp.gb28181.transmit.event.response.ISIPResponseProcessor; +import gov.nist.javax.sip.message.SIPResponse; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Component; + +import javax.sip.*; +import javax.sip.header.CSeqHeader; +import javax.sip.header.CallIdHeader; +import javax.sip.message.Response; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * @description: SIP信令处理类观察者 + * @author: panlinlin + * @date: 2021年11月5日 下午15:32 + */ +@Slf4j +@Component +public class SIPProcessorObserver implements ISIPProcessorObserver { + + private static final Map requestProcessorMap = new ConcurrentHashMap<>(); + private static final Map responseProcessorMap = new ConcurrentHashMap<>(); + + @Autowired + private SipSubscribe sipSubscribe; + + @Autowired + private EventPublisher eventPublisher; + + /** + * 添加 request订阅 + * @param method 方法名 + * @param processor 处理程序 + */ + public void addRequestProcessor(String method, ISIPRequestProcessor processor) { + requestProcessorMap.put(method, processor); + } + + /** + * 添加 response订阅 + * @param method 方法名 + * @param processor 处理程序 + */ + public void addResponseProcessor(String method, ISIPResponseProcessor processor) { + responseProcessorMap.put(method, processor); + } + + /** + * 分发RequestEvent事件 + * @param requestEvent RequestEvent事件 + */ + @Override + @Async("taskExecutor") + public void processRequest(RequestEvent requestEvent) { + String method = requestEvent.getRequest().getMethod(); + ISIPRequestProcessor sipRequestProcessor = requestProcessorMap.get(method); + if (sipRequestProcessor == null) { + log.warn("不支持方法{}的request", method); + // TODO 回复错误玛 + return; + } + requestProcessorMap.get(method).process(requestEvent); + + } + + /** + * 分发ResponseEvent事件 + * @param responseEvent responseEvent事件 + */ + @Override + @Async("taskExecutor") + public void processResponse(ResponseEvent responseEvent) { + SIPResponse response = (SIPResponse)responseEvent.getResponse(); + int status = response.getStatusCode(); + + // Success + if (((status >= Response.OK) && (status < Response.MULTIPLE_CHOICES)) || status == Response.UNAUTHORIZED) { + ISIPResponseProcessor sipRequestProcessor = responseProcessorMap.get(response.getCSeqHeader().getMethod()); + if (sipRequestProcessor != null) { + sipRequestProcessor.process(responseEvent); + } + + CallIdHeader callIdHeader = response.getCallIdHeader(); + CSeqHeader cSeqHeader = response.getCSeqHeader(); + if (callIdHeader != null) { + SipEvent sipEvent = sipSubscribe.getSubscribe(callIdHeader.getCallId() + cSeqHeader.getSeqNumber()); + if (sipEvent != null) { + if (sipEvent.getOkEvent() != null) { + SipSubscribe.EventResult eventResult = new SipSubscribe.EventResult<>(responseEvent); + sipEvent.getOkEvent().response(eventResult); + } + sipSubscribe.removeSubscribe(callIdHeader.getCallId() + cSeqHeader.getSeqNumber()); + } + } + } else if ((status >= Response.TRYING) && (status < Response.OK)) { + // 增加其它无需回复的响应,如101、180等 + // 更新sip订阅的时间 +// sipSubscribe.updateTimeout(response.getCallIdHeader().getCallId()); + } else { + log.warn("接收到失败的response响应!status:" + status + ",message:" + response.getReasonPhrase()); + if (responseEvent.getResponse() != null && !sipSubscribe.isEmpty() ) { + CallIdHeader callIdHeader = response.getCallIdHeader(); + CSeqHeader cSeqHeader = response.getCSeqHeader(); + if (callIdHeader != null) { + SipEvent sipEvent = sipSubscribe.getSubscribe(callIdHeader.getCallId() + cSeqHeader.getSeqNumber()); + if (sipEvent != null ) { + if (sipEvent.getErrorEvent() != null) { + SipSubscribe.EventResult eventResult = new SipSubscribe.EventResult<>(responseEvent); + sipEvent.getErrorEvent().response(eventResult); + } + sipSubscribe.removeSubscribe(callIdHeader.getCallId() + cSeqHeader.getSeqNumber()); + } + } + } + if (responseEvent.getDialog() != null) { + responseEvent.getDialog().delete(); + } + } + + + } + + /** + * 向超时订阅发送消息 + * @param timeoutEvent timeoutEvent事件 + */ + @Override + public void processTimeout(TimeoutEvent timeoutEvent) { + log.info("[消息发送超时]"); +// ClientTransaction clientTransaction = timeoutEvent.getClientTransaction(); +// +// if (clientTransaction != null) { +// log.info("[发送错误订阅] clientTransaction != null"); +// Request request = clientTransaction.getRequest(); +// if (request != null) { +// log.info("[发送错误订阅] request != null"); +// CallIdHeader callIdHeader = (CallIdHeader) request.getHeader(CallIdHeader.NAME); +// if (callIdHeader != null) { +// log.info("[发送错误订阅]"); +// SipSubscribe.Event subscribe = sipSubscribe.getErrorSubscribe(callIdHeader.getCallId()); +// SipSubscribe.EventResult eventResult = new SipSubscribe.EventResult(timeoutEvent); +// if (subscribe != null){ +// subscribe.response(eventResult); +// } +// sipSubscribe.removeOkSubscribe(callIdHeader.getCallId()); +// sipSubscribe.removeErrorSubscribe(callIdHeader.getCallId()); +// } +// } +// } +// eventPublisher.requestTimeOut(timeoutEvent); + } + + @Override + public void processIOException(IOExceptionEvent exceptionEvent) { + System.out.println("processIOException"); + } + + @Override + public void processTransactionTerminated(TransactionTerminatedEvent transactionTerminatedEvent) { +// if (transactionTerminatedEvent.isServerTransaction()) { +// ServerTransaction serverTransaction = transactionTerminatedEvent.getServerTransaction(); +// serverTransaction.get +// } + + +// Transaction transaction = null; +// System.out.println("processTransactionTerminated"); +// if (transactionTerminatedEvent.isServerTransaction()) { +// transaction = transactionTerminatedEvent.getServerTransaction(); +// }else { +// transaction = transactionTerminatedEvent.getClientTransaction(); +// } +// +// System.out.println(transaction.getBranchId()); +// System.out.println(transaction.getState()); +// System.out.println(transaction.getRequest().getMethod()); +// CallIdHeader header = (CallIdHeader)transaction.getRequest().getHeader(CallIdHeader.NAME); +// SipSubscribe.EventResult terminatedEventEventResult = new SipSubscribe.EventResult<>(transactionTerminatedEvent); + +// sipSubscribe.getErrorSubscribe(header.getCallId()).response(terminatedEventEventResult); + } + + @Override + public void processDialogTerminated(DialogTerminatedEvent dialogTerminatedEvent) { + CallIdHeader callId = dialogTerminatedEvent.getDialog().getCallId(); + } + + + + +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/SIPSender.java b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/SIPSender.java new file mode 100644 index 0000000..c2d9080 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/SIPSender.java @@ -0,0 +1,170 @@ +package com.genersoft.iot.vmp.gb28181.transmit; + +import com.genersoft.iot.vmp.conf.SipConfig; +import com.genersoft.iot.vmp.gb28181.SipLayer; +import com.genersoft.iot.vmp.gb28181.bean.SipTransactionInfo; +import com.genersoft.iot.vmp.gb28181.event.SipSubscribe; +import com.genersoft.iot.vmp.gb28181.event.sip.SipEvent; +import com.genersoft.iot.vmp.gb28181.utils.SipUtils; +import com.genersoft.iot.vmp.utils.GitUtil; +import gov.nist.javax.sip.SipProviderImpl; +import gov.nist.javax.sip.address.SipUri; +import gov.nist.javax.sip.message.SIPRequest; +import gov.nist.javax.sip.message.SIPResponse; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import org.springframework.util.ObjectUtils; + +import javax.sip.SipException; +import javax.sip.header.*; +import javax.sip.message.Message; +import javax.sip.message.Request; +import javax.sip.message.Response; +import java.text.ParseException; + +/** + * 发送SIP消息 + * + * @author lin + */ +@Slf4j +@Component +public class SIPSender { + + @Autowired + private SipLayer sipLayer; + + @Autowired + private GitUtil gitUtil; + + @Autowired + private SipSubscribe sipSubscribe; + + @Autowired + private SipConfig sipConfig; + + public void transmitRequest(String ip, Message message) throws SipException, ParseException { + transmitRequest(ip, message, null, null, null); + } + + public void transmitRequest(String ip, Message message, SipSubscribe.Event errorEvent) throws SipException, ParseException { + transmitRequest(ip, message, errorEvent, null, null); + } + + public void transmitRequest(String ip, Message message, SipSubscribe.Event errorEvent, SipSubscribe.Event okEvent) throws SipException { + transmitRequest(ip, message, errorEvent, okEvent, null); + } + + public void transmitRequest(String ip, Message message, SipSubscribe.Event errorEvent, SipSubscribe.Event okEvent, Long timeout) throws SipException { + ViaHeader viaHeader = (ViaHeader) message.getHeader(ViaHeader.NAME); + String transport = "UDP"; + if (viaHeader == null) { + log.warn("[消息头缺失]: ViaHeader, 使用默认的UDP方式处理数据"); + } else { + transport = viaHeader.getTransport(); + } + if (message.getHeader(UserAgentHeader.NAME) == null) { + try { + message.addHeader(SipUtils.createUserAgentHeader(gitUtil)); + } catch (ParseException e) { + log.error("添加UserAgentHeader失败", e); + } + } + CallIdHeader callIdHeader = (CallIdHeader) message.getHeader(CallIdHeader.NAME); + CSeqHeader cSeqHeader = (CSeqHeader) message.getHeader(CSeqHeader.NAME); + String key = callIdHeader.getCallId() + cSeqHeader.getSeqNumber(); + if (okEvent != null || errorEvent != null) { + + FromHeader fromHeader = (FromHeader) message.getHeader(FromHeader.NAME); + SipEvent sipEvent = SipEvent.getInstance(key, eventResult -> { + sipSubscribe.removeSubscribe(key); + if(okEvent != null) { + okEvent.response(eventResult); + } + }, (eventResult -> { + sipSubscribe.removeSubscribe(key); + if (errorEvent != null) { + errorEvent.response(eventResult); + } + }), timeout == null ? sipConfig.getTimeout() : timeout); + SipTransactionInfo sipTransactionInfo = new SipTransactionInfo(); + sipTransactionInfo.setFromTag(fromHeader.getTag()); + sipTransactionInfo.setCallId(callIdHeader.getCallId()); + + if (message instanceof SIPResponse) { + SIPResponse response = (SIPResponse) message; + sipTransactionInfo.setToTag(response.getToHeader().getTag()); + sipTransactionInfo.setViaBranch(response.getTopmostViaHeader().getBranch()); + }else if (message instanceof SIPRequest) { + SIPRequest request = (SIPRequest) message; + sipTransactionInfo.setViaBranch(request.getTopmostViaHeader().getBranch()); + SipUri sipUri = (SipUri)request.getRequestLine().getUri(); + sipTransactionInfo.setUser(sipUri.getUser()); + } + + ExpiresHeader expiresHeader = (ExpiresHeader) message.getHeader(ExpiresHeader.NAME); + if (expiresHeader != null) { + sipTransactionInfo.setExpires(expiresHeader.getExpires()); + } + sipEvent.setSipTransactionInfo(sipTransactionInfo); + sipSubscribe.addSubscribe(key, sipEvent); + } + try { + if ("TCP".equals(transport)) { + SipProviderImpl tcpSipProvider = sipLayer.getTcpSipProvider(ip); + if (tcpSipProvider == null) { + log.error("[发送信息失败] 未找到tcp://{}的监听信息", ip); + return; + } + if (message instanceof Request) { + tcpSipProvider.sendRequest((Request) message); + } else if (message instanceof Response) { + tcpSipProvider.sendResponse((Response) message); + } + + } else if ("UDP".equals(transport)) { + SipProviderImpl sipProvider = sipLayer.getUdpSipProvider(ip); + if (sipProvider == null) { + log.error("[发送信息失败] 未找到udp://{}的监听信息", ip); + return; + } + if (message instanceof Request) { + sipProvider.sendRequest((Request) message); + } else if (message instanceof Response) { + sipProvider.sendResponse((Response) message); + } + } + }catch (SipException e) { + sipSubscribe.removeSubscribe(key); + throw e; + } + } + + public CallIdHeader getNewCallIdHeader(String ip, String transport) { + if (ObjectUtils.isEmpty(transport)) { + return sipLayer.getUdpSipProvider().getNewCallId(); + } + SipProviderImpl sipProvider; + if (ObjectUtils.isEmpty(ip)) { + sipProvider = transport.equalsIgnoreCase("TCP") ? sipLayer.getTcpSipProvider() + : sipLayer.getUdpSipProvider(); + } else { + sipProvider = transport.equalsIgnoreCase("TCP") ? sipLayer.getTcpSipProvider(ip) + : sipLayer.getUdpSipProvider(ip); + } + + if (sipProvider == null) { + sipProvider = sipLayer.getUdpSipProvider(); + } + + if (sipProvider != null) { + return sipProvider.getNewCallId(); + } else { + log.warn("[新建CallIdHeader失败], ip={}, transport={}", ip, transport); + return null; + } + } + + +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/callback/DeferredResultHolder.java b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/callback/DeferredResultHolder.java new file mode 100755 index 0000000..d39ce28 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/callback/DeferredResultHolder.java @@ -0,0 +1,133 @@ +package com.genersoft.iot.vmp.gb28181.transmit.callback; + +import com.genersoft.iot.vmp.vmanager.bean.DeferredResultEx; +import org.springframework.stereotype.Component; +import org.springframework.util.ObjectUtils; +import org.springframework.web.context.request.async.DeferredResult; + +import java.util.Collection; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +/** + * @description: 异步请求处理 + * @author: swwheihei + * @date: 2020年5月8日 下午7:59:05 + */ +@SuppressWarnings(value = {"rawtypes", "unchecked"}) +@Component +public class DeferredResultHolder { + + public static final String CALLBACK_CMD_PLAY = "CALLBACK_PLAY"; + + public static final String CALLBACK_CMD_PLAYBACK = "CALLBACK_PLAYBACK"; + + public static final String CALLBACK_CMD_DOWNLOAD = "CALLBACK_DOWNLOAD"; + + + public static final String UPLOAD_FILE_CHANNEL = "UPLOAD_FILE_CHANNEL"; + + public static final String CALLBACK_CMD_MOBILE_POSITION = "CALLBACK_CMD_MOBILE_POSITION"; + + public static final String CALLBACK_CMD_SNAP= "CALLBACK_SNAP"; + + private Map> map = new ConcurrentHashMap<>(); + + + public void put(String key, String id, DeferredResultEx result) { + Map deferredResultMap = map.get(key); + if (deferredResultMap == null) { + deferredResultMap = new ConcurrentHashMap<>(); + map.put(key, deferredResultMap); + } + deferredResultMap.put(id, result); + } + + public void put(String key, String id, DeferredResult result) { + Map deferredResultMap = map.computeIfAbsent(key, k -> new ConcurrentHashMap<>()); + deferredResultMap.put(id, new DeferredResultEx(result)); + } + + public DeferredResultEx get(String key, String id) { + Map deferredResultMap = map.get(key); + if (deferredResultMap == null || ObjectUtils.isEmpty(id)) { + return null; + } + return deferredResultMap.get(id); + } + + public Collection getAllByKey(String key) { + Map deferredResultMap = map.get(key); + if (deferredResultMap == null) { + return null; + } + return deferredResultMap.values(); + } + + public boolean exist(String key, String id){ + if (key == null) { + return false; + } + Map deferredResultMap = map.get(key); + if (id == null) { + return deferredResultMap != null; + }else { + return deferredResultMap != null && deferredResultMap.get(id) != null; + } + } + + /** + * 释放单个请求 + * @param msg + */ + public void invokeResult(RequestMessage msg) { + Map deferredResultMap = map.get(msg.getKey()); + if (deferredResultMap == null) { + return; + } + DeferredResultEx result = deferredResultMap.get(msg.getId()); + if (result == null) { + return; + } + result.getDeferredResult().setResult(msg.getData()); + deferredResultMap.remove(msg.getId()); + if (deferredResultMap.size() == 0) { + map.remove(msg.getKey()); + } + } + + /** + * 释放所有的请求 + * @param msg + */ + public void invokeAllResult(RequestMessage msg) { + Map deferredResultMap = map.get(msg.getKey()); + if (deferredResultMap == null) { + return; + } + synchronized (this) { + deferredResultMap = map.get(msg.getKey()); + if (deferredResultMap == null) { + return; + } + Set ids = deferredResultMap.keySet(); + for (String id : ids) { + DeferredResultEx result = deferredResultMap.get(id); + if (result == null) { + return; + } + if (result.getFilter() != null) { + Object handler = result.getFilter().handler(msg.getData()); + result.getDeferredResult().setResult(handler); + }else { + result.getDeferredResult().setResult(msg.getData()); + } + + } + map.remove(msg.getKey()); + } + } + + +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/callback/RequestMessage.java b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/callback/RequestMessage.java new file mode 100755 index 0000000..5a22f6d --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/callback/RequestMessage.java @@ -0,0 +1,18 @@ +package com.genersoft.iot.vmp.gb28181.transmit.callback; + +import lombok.Data; + +/** + * @description: 请求信息定义 + * @author: swwheihei + * @date: 2020年5月8日 下午1:09:18 + */ +@Data +public class RequestMessage { + + private String id; + + private String key; + + private Object data; +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/cmd/ISIPCommander.java b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/cmd/ISIPCommander.java new file mode 100755 index 0000000..0fffa2a --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/cmd/ISIPCommander.java @@ -0,0 +1,317 @@ +package com.genersoft.iot.vmp.gb28181.transmit.cmd; + +import com.genersoft.iot.vmp.common.StreamInfo; +import com.genersoft.iot.vmp.conf.exception.SsrcTransactionNotFoundException; +import com.genersoft.iot.vmp.gb28181.bean.*; +import com.genersoft.iot.vmp.gb28181.bean.Device; +import com.genersoft.iot.vmp.gb28181.bean.DeviceAlarm; +import com.genersoft.iot.vmp.gb28181.bean.DeviceChannel; +import com.genersoft.iot.vmp.gb28181.event.SipSubscribe; +import com.genersoft.iot.vmp.media.event.hook.HookSubscribe; +import com.genersoft.iot.vmp.media.bean.MediaServer; +import com.genersoft.iot.vmp.service.bean.ErrorCallback; +import com.genersoft.iot.vmp.service.bean.SSRCInfo; +import gov.nist.javax.sip.message.SIPRequest; + +import javax.sip.InvalidArgumentException; +import javax.sip.SipException; +import java.text.ParseException; +import java.util.List; + +/** + * @description:设备能力接口,用于定义设备的控制、查询能力 + * @author: swwheihei + * @date: 2020年5月3日 下午9:16:34 + */ +public interface ISIPCommander { + + /** + * 云台控制,支持方向与缩放控制 + * + * @param device 控制设备 + * @param channelId 预览通道 + * @param leftRight 镜头左移右移 0:停止 1:左移 2:右移 + * @param upDown 镜头上移下移 0:停止 1:上移 2:下移 + * @param inOut 镜头放大缩小 0:停止 1:缩小 2:放大 + * @param moveSpeed 镜头移动速度 + * @param zoomSpeed 镜头缩放速度 + */ + void ptzCmd(Device device,String channelId,int leftRight, int upDown, int inOut, int moveSpeed, int zoomSpeed) throws InvalidArgumentException, SipException, ParseException; + + /** + * 前端控制,包括PTZ指令、FI指令、预置位指令、巡航指令、扫描指令和辅助开关指令 + * + * @param device 控制设备 + * @param channelId 预览通道 + * @param cmdCode 指令码 + * @param parameter1 数据1 + * @param parameter2 数据2 + * @param combineCode2 组合码2 + */ + void frontEndCmd(Device device, String channelId, int cmdCode, int parameter1, int parameter2, int combineCode2) throws SipException, InvalidArgumentException, ParseException; + + /** + * 前端控制指令(用于转发上级指令) + * @param device 控制设备 + * @param channelId 预览通道 + * @param cmdString 前端控制指令串 + */ + void fronEndCmd(Device device, String channelId, String cmdString, SipSubscribe.Event errorEvent, SipSubscribe.Event okEvent) throws InvalidArgumentException, SipException, ParseException; + + /** + * 请求预览视频流 + * @param device 视频设备 + * @param channel 预览通道 + */ + void playStreamCmd(MediaServer mediaServerItem, SSRCInfo ssrcInfo, Device device, DeviceChannel channel, SipSubscribe.Event okEvent, SipSubscribe.Event errorEvent, Long timeout) throws InvalidArgumentException, SipException, ParseException; + + /** + * 请求回放视频流 + * + * @param device 视频设备 + * @param channel 预览通道 + * @param startTime 开始时间,格式要求:yyyy-MM-dd HH:mm:ss + * @param endTime 结束时间,格式要求:yyyy-MM-dd HH:mm:ss + */ + void playbackStreamCmd(MediaServer mediaServerItem, SSRCInfo ssrcInf, Device device, DeviceChannel channel, String startTime, String endTime, SipSubscribe.Event okEvent, SipSubscribe.Event errorEvent, Long timeout) throws InvalidArgumentException, SipException, ParseException; + + /** + * 请求历史媒体下载 + * + * @param device 视频设备 + * @param channel 预览通道 + * @param startTime 开始时间,格式要求:yyyy-MM-dd HH:mm:ss + * @param endTime 结束时间,格式要求:yyyy-MM-dd HH:mm:ss + * @param downloadSpeed 下载倍速参数 + */ + void downloadStreamCmd(MediaServer mediaServerItem, SSRCInfo ssrcInfo, Device device, DeviceChannel channel, + String startTime, String endTime, int downloadSpeed, + SipSubscribe.Event errorEvent, SipSubscribe.Event okEvent, Long timeout) throws InvalidArgumentException, SipException, ParseException; + + + /** + * 视频流停止 + */ + void streamByeCmd(Device device, String channelId, String app, String stream, String callId, SipSubscribe.Event okEvent) throws InvalidArgumentException, SipException, ParseException, SsrcTransactionNotFoundException; + + void talkStreamCmd(MediaServer mediaServerItem, SendRtpInfo sendRtpItem, Device device, DeviceChannel channelId, String callId, HookSubscribe.Event event, HookSubscribe.Event eventForPush, SipSubscribe.Event okEvent, SipSubscribe.Event errorEvent, Long timeout) throws InvalidArgumentException, SipException, ParseException; + + void streamByeCmd(Device device, String channelId, SipTransactionInfo sipTransactionInfo, SipSubscribe.Event okEvent) throws InvalidArgumentException, SipException, ParseException, SsrcTransactionNotFoundException; + + /** + * 回放暂停 + */ + void playPauseCmd(Device device, DeviceChannel channel, StreamInfo streamInfo) throws InvalidArgumentException, ParseException, SipException; + + /** + * 回放恢复 + */ + void playResumeCmd(Device device, DeviceChannel channel, StreamInfo streamInfo) throws InvalidArgumentException, ParseException, SipException; + + /** + * 回放拖动播放 + */ + void playSeekCmd(Device device, DeviceChannel channel, StreamInfo streamInfo, long seekTime) throws InvalidArgumentException, ParseException, SipException; + + /** + * 回放倍速播放 + */ + void playSpeedCmd(Device device, DeviceChannel channel, StreamInfo streamInfo, Double speed) throws InvalidArgumentException, ParseException, SipException; + + /** + * 回放控制 + * @param device + * @param streamInfo + * @param content + */ + void playbackControlCmd(Device device, DeviceChannel channel, StreamInfo streamInfo, String content,SipSubscribe.Event errorEvent, SipSubscribe.Event okEvent) throws SipException, InvalidArgumentException, ParseException; + + + void streamByeCmdForDeviceInvite(Device device, String channelId, SipTransactionInfo sipTransactionInfo, SipSubscribe.Event okEvent) throws InvalidArgumentException, SipException, ParseException, SsrcTransactionNotFoundException; + + /** + * /** + * 语音广播 + * + * @param device 视频设备 + */ + void audioBroadcastCmd(Device device, String channelId, SipSubscribe.Event okEvent, SipSubscribe.Event errorEvent) throws InvalidArgumentException, SipException, ParseException; + + /** + * 音视频录像控制 + * + * @param device 视频设备 + * @param channelId 预览通道 + * @param recordCmdStr 录像命令:Record / StopRecord + */ + void recordCmd(Device device, String channelId, String recordCmdStr, ErrorCallback callback) throws InvalidArgumentException, SipException, ParseException; + + /** + * 远程启动控制命令 + * + * @param device 视频设备 + */ + void teleBootCmd(Device device) throws InvalidArgumentException, SipException, ParseException; + + /** + * 报警布防/撤防命令 + * + * @param device 视频设备 + */ + void guardCmd(Device device, String guardCmdStr, ErrorCallback callback) throws InvalidArgumentException, SipException, ParseException; + + /** + * 报警复位命令 + * + * @param device 视频设备 + * @param alarmMethod 报警方式(可选) + * @param alarmType 报警类型(可选) + */ + void alarmResetCmd(Device device, String alarmMethod, String alarmType, ErrorCallback callback) throws InvalidArgumentException, SipException, ParseException; + + /** + * 强制关键帧命令,设备收到此命令应立刻发送一个IDR帧 + * + * @param device 视频设备 + * @param channelId 预览通道 + */ + void iFrameCmd(Device device, String channelId) throws InvalidArgumentException, SipException, ParseException; + + /** + * 看守位控制命令 + * + */ + void homePositionCmd(Device device, String channelId, Boolean enabled, Integer resetTime, Integer presetIndex, ErrorCallback callback) throws InvalidArgumentException, SipException, ParseException; + + /** + * 设备配置命令 + * + * @param device 视频设备 + */ + void deviceConfigCmd(Device device); + + /** + * 设备配置命令:basicParam + */ + void deviceBasicConfigCmd(Device device, BasicParam basicParam, ErrorCallback callback) throws InvalidArgumentException, SipException, ParseException; + + /** + * 查询设备状态 + * + * @param device 视频设备 + */ + void deviceStatusQuery(Device device, ErrorCallback callback) throws InvalidArgumentException, SipException, ParseException; + + /** + * 查询设备信息 + * + * @param device 视频设备 + * @param callback + * @return + */ + void deviceInfoQuery(Device device, ErrorCallback callback) throws InvalidArgumentException, SipException, ParseException; + + /** + * 查询目录列表 + * + * @param device 视频设备 + */ + void catalogQuery(Device device, int sn, SipSubscribe.Event errorEvent) throws SipException, InvalidArgumentException, ParseException; + + /** + * 查询录像信息 + * + * @param device 视频设备 + * @param startTime 开始时间,格式要求:yyyy-MM-dd HH:mm:ss + * @param endTime 结束时间,格式要求:yyyy-MM-dd HH:mm:ss + * @param sn + */ + void recordInfoQuery(Device device, String channelId, String startTime, String endTime, int sn, Integer Secrecy, String type, SipSubscribe.Event okEvent, SipSubscribe.Event errorEvent) throws InvalidArgumentException, SipException, ParseException; + + /** + * 查询报警信息 + * + * @param device 视频设备 + * @param startPriority 报警起始级别(可选) + * @param endPriority 报警终止级别(可选) + * @param alarmMethod 报警方式条件(可选) + * @param alarmType 报警类型 + * @param startTime 报警发生起始时间(可选) + * @param endTime 报警发生终止时间(可选) + * @return true = 命令发送成功 + */ + void alarmInfoQuery(Device device, String startPriority, String endPriority, String alarmMethod, + String alarmType, String startTime, String endTime, ErrorCallback callback) throws InvalidArgumentException, SipException, ParseException; + + /** + * 查询设备配置 + * + * @param device 视频设备 + * @param channelId 通道编码(可选) + * @param configType 配置类型: + */ + void deviceConfigQuery(Device device, String channelId, String configType, ErrorCallback callback) throws InvalidArgumentException, SipException, ParseException; + + /** + * 查询设备预置位置 + * + * @param device 视频设备 + */ + void presetQuery(Device device, String channelId, ErrorCallback> callback) throws InvalidArgumentException, SipException, ParseException; + + /** + * 查询移动设备位置数据 + * + * @param device 视频设备 + */ + void mobilePostitionQuery(Device device, SipSubscribe.Event errorEvent) throws InvalidArgumentException, SipException, ParseException; + + /** + * 订阅、取消订阅移动位置 + * + * @param device 视频设备 + * @return true = 命令发送成功 + */ + SIPRequest mobilePositionSubscribe(Device device, SipTransactionInfo transactionInfo, SipSubscribe.Event okEvent , SipSubscribe.Event errorEvent) throws InvalidArgumentException, SipException, ParseException; + + /** + * 订阅、取消订阅报警信息 + * @param device 视频设备 + * @param expires 订阅过期时间(0 = 取消订阅) + * @param startPriority 报警起始级别(可选) + * @param endPriority 报警终止级别(可选) + * @param startTime 报警发生起始时间(可选) + * @param endTime 报警发生终止时间(可选) + * @return true = 命令发送成功 + */ + void alarmSubscribe(Device device, int expires, String startPriority, String endPriority, String alarmMethod, String startTime, String endTime) throws InvalidArgumentException, SipException, ParseException; + + /** + * 订阅、取消订阅目录信息 + * @param device 视频设备 + * @return true = 命令发送成功 + */ + SIPRequest catalogSubscribe(Device device, SipTransactionInfo transactionInfo, SipSubscribe.Event okEvent ,SipSubscribe.Event errorEvent) throws InvalidArgumentException, SipException, ParseException; + + /** + * 拉框控制命令 + * + * @param device 控制设备 + * @param channelId 通道id + * @param cmdString 前端控制指令串 + */ + void dragZoomCmd(Device device, String channelId, String cmdString, ErrorCallback callback) throws InvalidArgumentException, SipException, ParseException; + + + void playbackControlCmd(Device device, DeviceChannel channel, String stream, String content, SipSubscribe.Event errorEvent, SipSubscribe.Event okEvent) throws SipException, InvalidArgumentException, ParseException; + + /** + * 向设备发送报警NOTIFY消息, 用于互联结构下,此时将设备当成一个平级平台看待 + * @param device 设备 + * @param deviceAlarm 报警信息信息 + * @return + */ + void sendAlarmMessage(Device device, DeviceAlarm deviceAlarm) throws InvalidArgumentException, SipException, ParseException; + + +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/cmd/ISIPCommanderForPlatform.java b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/cmd/ISIPCommanderForPlatform.java new file mode 100755 index 0000000..c94a072 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/cmd/ISIPCommanderForPlatform.java @@ -0,0 +1,154 @@ +package com.genersoft.iot.vmp.gb28181.transmit.cmd; + +import com.genersoft.iot.vmp.conf.exception.SsrcTransactionNotFoundException; +import com.genersoft.iot.vmp.gb28181.bean.*; +import com.genersoft.iot.vmp.gb28181.event.SipSubscribe; +import com.genersoft.iot.vmp.media.event.hook.HookSubscribe; +import com.genersoft.iot.vmp.media.bean.MediaServer; +import com.genersoft.iot.vmp.service.bean.GPSMsgInfo; +import com.genersoft.iot.vmp.service.bean.SSRCInfo; + +import javax.sip.InvalidArgumentException; +import javax.sip.SipException; +import javax.sip.header.WWWAuthenticateHeader; +import java.text.ParseException; +import java.util.List; + +public interface ISIPCommanderForPlatform { + + /** + * 向上级平台注册 + * + * @param parentPlatform + * @return + */ + void register(Platform parentPlatform, SipSubscribe.Event errorEvent , SipSubscribe.Event okEvent) throws InvalidArgumentException, ParseException, SipException; + + void register(Platform parentPlatform, SipTransactionInfo sipTransactionInfo, SipSubscribe.Event errorEvent , SipSubscribe.Event okEvent) throws InvalidArgumentException, ParseException, SipException; + + + void register(Platform parentPlatform, SipTransactionInfo sipTransactionInfo, WWWAuthenticateHeader www, SipSubscribe.Event errorEvent , SipSubscribe.Event okEvent, boolean isRegister) throws SipException, InvalidArgumentException, ParseException; + + /** + * 向上级平台注销 + * + * @param parentPlatform + * @return + */ + void unregister(Platform parentPlatform, SipTransactionInfo sipTransactionInfo, SipSubscribe.Event errorEvent , SipSubscribe.Event okEvent) throws InvalidArgumentException, ParseException, SipException; + + + /** + * 向上级平发送心跳信息 + * + * @param parentPlatform + * @return callId(作为接受回复的判定) + */ + String keepalive(Platform parentPlatform, SipSubscribe.Event errorEvent, SipSubscribe.Event okEvent) + throws SipException, InvalidArgumentException, ParseException; + + + /** + * 向上级回复通道信息 + * + * @param channel 通道信息 + * @param parentPlatform 平台信息 + * @param sn + * @param fromTag + * @param size + * @return + */ + void catalogQuery(CommonGBChannel channel, Platform parentPlatform, String sn, String fromTag, int size) + throws SipException, InvalidArgumentException, ParseException; + + void catalogQuery(List channels, Platform parentPlatform, String sn, String fromTag) + throws InvalidArgumentException, ParseException, SipException; + + /** + * 向上级回复DeviceInfo查询信息 + * + * @param parentPlatform 平台信息 + * @param sn SN + * @param fromTag FROM头的tag信息 + * @return + */ + void deviceInfoResponse(Platform parentPlatform, Device device, String sn, String fromTag) throws SipException, InvalidArgumentException, ParseException; + + /** + * 向上级回复DeviceStatus查询信息 + * + * @param parentPlatform 平台信息 + * @param sn + * @param fromTag + * @return + */ + void deviceStatusResponse(Platform parentPlatform, String channelId, String sn, String fromTag, boolean status) throws SipException, InvalidArgumentException, ParseException; + + /** + * 向上级回复移动位置订阅消息 + * + * @param parentPlatform 平台信息 + * @param gpsMsgInfo GPS信息 + * @param subscribeInfo 订阅相关的信息 + * @return + */ + void sendNotifyMobilePosition(Platform parentPlatform, GPSMsgInfo gpsMsgInfo, CommonGBChannel channel, SubscribeInfo subscribeInfo) + throws InvalidArgumentException, ParseException, NoSuchFieldException, SipException, IllegalAccessException; + + /** + * 向上级回复报警消息 + * + * @param parentPlatform 平台信息 + * @param deviceAlarm 报警信息信息 + * @return + */ + void sendAlarmMessage(Platform parentPlatform, DeviceAlarm deviceAlarm) throws SipException, InvalidArgumentException, ParseException; + + /** + * 回复catalog事件-增加/更新 + * + * @param parentPlatform + * @param deviceChannels + */ + void sendNotifyForCatalogAddOrUpdate(String type, Platform parentPlatform, List deviceChannels, SubscribeInfo subscribeInfo, Integer index) throws InvalidArgumentException, ParseException, NoSuchFieldException, SipException, IllegalAccessException; + + /** + * 回复catalog事件-删除 + * + * @param parentPlatform + * @param deviceChannels + */ + void sendNotifyForCatalogOther(String type, Platform parentPlatform, List deviceChannels, + SubscribeInfo subscribeInfo, Integer index) throws InvalidArgumentException, + ParseException, NoSuchFieldException, SipException, IllegalAccessException; + + /** + * 回复recordInfo + * + * @param deviceChannel 通道信息 + * @param parentPlatform 平台信息 + * @param fromTag fromTag + * @param recordInfo 录像信息 + */ + void recordInfo(CommonGBChannel deviceChannel, Platform parentPlatform, String fromTag, RecordInfo recordInfo) + throws SipException, InvalidArgumentException, ParseException; + + /** + * 录像播放推送完成时发送MediaStatus消息 + * + * @param platform + * @param sendRtpItem + * @return + */ + void sendMediaStatusNotify(Platform platform, SendRtpInfo sendRtpItem, CommonGBChannel channel) throws SipException, InvalidArgumentException, ParseException; + + void streamByeCmd(Platform platform, SendRtpInfo sendRtpItem, CommonGBChannel channel) throws SipException, InvalidArgumentException, ParseException; + + void streamByeCmd(Platform platform, CommonGBChannel channel, String app, String stream, String callId, SipSubscribe.Event okEvent) throws InvalidArgumentException, SipException, ParseException, SsrcTransactionNotFoundException; + + void broadcastInviteCmd(Platform platform, CommonGBChannel channel, String sourceId, MediaServer mediaServerItem, + SSRCInfo ssrcInfo, HookSubscribe.Event event, SipSubscribe.Event okEvent, + SipSubscribe.Event errorEvent) throws ParseException, SipException, InvalidArgumentException; + + void broadcastResultCmd(Platform platform, CommonGBChannel deviceChannel, String sn, boolean result, SipSubscribe.Event errorEvent, SipSubscribe.Event okEvent) throws InvalidArgumentException, SipException, ParseException; +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/cmd/SIPRequestHeaderPlarformProvider.java b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/cmd/SIPRequestHeaderPlarformProvider.java new file mode 100755 index 0000000..f82ec20 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/cmd/SIPRequestHeaderPlarformProvider.java @@ -0,0 +1,391 @@ +package com.genersoft.iot.vmp.gb28181.transmit.cmd; + +import com.genersoft.iot.vmp.conf.SipConfig; +import com.genersoft.iot.vmp.gb28181.SipLayer; +import com.genersoft.iot.vmp.gb28181.bean.*; +import com.genersoft.iot.vmp.gb28181.utils.SipUtils; +import com.genersoft.iot.vmp.storager.IRedisCatchStorage; +import com.genersoft.iot.vmp.utils.GitUtil; +import com.genersoft.iot.vmp.utils.IpPortUtil; +import gov.nist.javax.sip.message.MessageFactoryImpl; +import gov.nist.javax.sip.message.SIPRequest; +import jakarta.validation.constraints.NotNull; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import org.springframework.util.DigestUtils; + +import javax.sip.InvalidArgumentException; +import javax.sip.PeerUnavailableException; +import javax.sip.SipFactory; +import javax.sip.address.Address; +import javax.sip.address.SipURI; +import javax.sip.header.*; +import javax.sip.message.Request; +import java.text.ParseException; +import java.util.ArrayList; +import java.util.UUID; + +/** + * @description: 平台命令request创造器 TODO 冗余代码太多待优化 + * @author: panll + * @date: 2020年5月6日 上午9:29:02 + */ +@Component +public class SIPRequestHeaderPlarformProvider { + + @Autowired + private SipConfig sipConfig; + + @Autowired + private SipLayer sipLayer; + + @Autowired + private GitUtil gitUtil; + + @Autowired + private IRedisCatchStorage redisCatchStorage; + + public Request createRegisterRequest(@NotNull Platform parentPlatform, long CSeq, String fromTag, String toTag, CallIdHeader callIdHeader, int expires) throws ParseException, InvalidArgumentException, PeerUnavailableException { + Request request = null; + String sipAddress = parentPlatform.getDeviceIp() + ":" + parentPlatform.getDevicePort(); + //请求行 + SipURI requestLine = SipFactory.getInstance().createAddressFactory().createSipURI(parentPlatform.getServerGBId(), + parentPlatform.getServerIp() + ":" + parentPlatform.getServerPort()); + //via + ArrayList viaHeaders = new ArrayList(); + ViaHeader viaHeader = SipFactory.getInstance().createHeaderFactory().createViaHeader(parentPlatform.getDeviceIp(), + parentPlatform.getDevicePort(), parentPlatform.getTransport(), SipUtils.getNewViaTag()); + viaHeader.setRPort(); + viaHeaders.add(viaHeader); + //from + SipURI fromSipURI = SipFactory.getInstance().createAddressFactory().createSipURI(parentPlatform.getDeviceGBId(), sipConfig.getDomain()); + Address fromAddress = SipFactory.getInstance().createAddressFactory().createAddress(fromSipURI); + FromHeader fromHeader = SipFactory.getInstance().createHeaderFactory().createFromHeader(fromAddress, fromTag); + //to + SipURI toSipURI = SipFactory.getInstance().createAddressFactory().createSipURI(parentPlatform.getDeviceGBId(), sipConfig.getDomain()); + Address toAddress = SipFactory.getInstance().createAddressFactory().createAddress(toSipURI); + ToHeader toHeader = SipFactory.getInstance().createHeaderFactory().createToHeader(toAddress,toTag); + + //Forwards + MaxForwardsHeader maxForwards = SipFactory.getInstance().createHeaderFactory().createMaxForwardsHeader(70); + + //ceq + CSeqHeader cSeqHeader = SipFactory.getInstance().createHeaderFactory().createCSeqHeader(CSeq, Request.REGISTER); + request = SipFactory.getInstance().createMessageFactory().createRequest(requestLine, Request.REGISTER, callIdHeader, + cSeqHeader,fromHeader, toHeader, viaHeaders, maxForwards); + + Address concatAddress = SipFactory.getInstance().createAddressFactory().createAddress(SipFactory.getInstance().createAddressFactory() + .createSipURI(parentPlatform.getDeviceGBId(), sipAddress)); + request.addHeader(SipFactory.getInstance().createHeaderFactory().createContactHeader(concatAddress)); + + ExpiresHeader expiresHeader = SipFactory.getInstance().createHeaderFactory().createExpiresHeader(expires); + request.addHeader(expiresHeader); + + request.addHeader(SipUtils.createUserAgentHeader(gitUtil)); + + return request; + } + + public Request createRegisterRequest(@NotNull Platform parentPlatform, String fromTag, String toTag, + WWWAuthenticateHeader www , CallIdHeader callIdHeader, int expires) throws ParseException, PeerUnavailableException, InvalidArgumentException { + + + Request registerRequest = createRegisterRequest(parentPlatform, redisCatchStorage.getCSEQ(), fromTag, toTag, callIdHeader, expires); + SipURI requestURI = SipFactory.getInstance().createAddressFactory().createSipURI(parentPlatform.getServerGBId(), IpPortUtil.concatenateIpAndPort(parentPlatform.getServerIp(), String.valueOf(parentPlatform.getServerPort()))); + if (www == null) { + AuthorizationHeader authorizationHeader = SipFactory.getInstance().createHeaderFactory().createAuthorizationHeader("Digest"); + String username = parentPlatform.getUsername(); + if ( username == null || username.isEmpty()) + { + authorizationHeader.setUsername(parentPlatform.getDeviceGBId()); + } else { + authorizationHeader.setUsername(username); + } + authorizationHeader.setURI(requestURI); + authorizationHeader.setAlgorithm("MD5"); + registerRequest.addHeader(authorizationHeader); + return registerRequest; + } + String realm = www.getRealm(); + String nonce = www.getNonce(); + String scheme = www.getScheme(); + + // 参考 https://blog.csdn.net/y673533511/article/details/88388138 + // qop 保护质量 包含auth(默认的)和auth-int(增加了报文完整性检测)两种策略 + String qop = www.getQop(); + + String cNonce = null; + String nc = "00000001"; + if (qop != null) { + if ("auth".equalsIgnoreCase(qop)) { + // 客户端随机数,这是一个不透明的字符串值,由客户端提供,并且客户端和服务器都会使用,以避免用明文文本。 + // 这使得双方都可以查验对方的身份,并对消息的完整性提供一些保护 + cNonce = UUID.randomUUID().toString(); + + }else if ("auth-int".equalsIgnoreCase(qop)){ + // TODO + } + } + String HA1 = DigestUtils.md5DigestAsHex((parentPlatform.getDeviceGBId() + ":" + realm + ":" + parentPlatform.getPassword()).getBytes()); + String HA2=DigestUtils.md5DigestAsHex((Request.REGISTER + ":" + requestURI.toString()).getBytes()); + + StringBuffer reStr = new StringBuffer(200); + reStr.append(HA1); + reStr.append(":"); + reStr.append(nonce); + reStr.append(":"); + if (qop != null) { + reStr.append(nc); + reStr.append(":"); + reStr.append(cNonce); + reStr.append(":"); + reStr.append(qop); + reStr.append(":"); + } + reStr.append(HA2); + + String RESPONSE = DigestUtils.md5DigestAsHex(reStr.toString().getBytes()); + + AuthorizationHeader authorizationHeader = SipFactory.getInstance().createHeaderFactory().createAuthorizationHeader(scheme); + authorizationHeader.setUsername(parentPlatform.getDeviceGBId()); + authorizationHeader.setRealm(realm); + authorizationHeader.setNonce(nonce); + authorizationHeader.setURI(requestURI); + authorizationHeader.setResponse(RESPONSE); + authorizationHeader.setAlgorithm("MD5"); + if (qop != null) { + authorizationHeader.setQop(qop); + authorizationHeader.setCNonce(cNonce); + authorizationHeader.setNonceCount(1); + } + registerRequest.addHeader(authorizationHeader); + + return registerRequest; + } + + public Request createMessageRequest(Platform parentPlatform, String content, SendRtpInfo sendRtpItem) throws PeerUnavailableException, ParseException, InvalidArgumentException { + CallIdHeader callIdHeader = SipFactory.getInstance().createHeaderFactory().createCallIdHeader(sendRtpItem.getCallId()); + callIdHeader.setCallId(sendRtpItem.getCallId()); + return createMessageRequest(parentPlatform, content, sendRtpItem.getToTag(), SipUtils.getNewViaTag(), sendRtpItem.getFromTag(), callIdHeader); + } + + public Request createMessageRequest(Platform parentPlatform, String content, String fromTag, String viaTag, CallIdHeader callIdHeader) throws PeerUnavailableException, ParseException, InvalidArgumentException { + return createMessageRequest(parentPlatform, content, fromTag, viaTag, null, callIdHeader); + } + + + public Request createMessageRequest(Platform parentPlatform, String content, String fromTag, String viaTag, String toTag, CallIdHeader callIdHeader) throws PeerUnavailableException, ParseException, InvalidArgumentException { + Request request = null; + String serverAddress = parentPlatform.getServerIp()+ ":" + parentPlatform.getServerPort(); + // sipuri + SipURI requestURI = SipFactory.getInstance().createAddressFactory().createSipURI(parentPlatform.getServerGBId(), serverAddress); + // via + ArrayList viaHeaders = new ArrayList(); + ViaHeader viaHeader = SipFactory.getInstance().createHeaderFactory().createViaHeader(parentPlatform.getDeviceIp(), parentPlatform.getDevicePort(), + parentPlatform.getTransport(), viaTag); + viaHeader.setRPort(); + viaHeaders.add(viaHeader); + // from + // SipURI fromSipURI = SipFactory.getInstance().createAddressFactory().createSipURI(parentPlatform.getDeviceGBId(), parentPlatform.getDeviceIp() + ":" + parentPlatform.getDeviceIp()); + SipURI fromSipURI = SipFactory.getInstance().createAddressFactory().createSipURI(parentPlatform.getDeviceGBId(), sipConfig.getDomain()); + Address fromAddress = SipFactory.getInstance().createAddressFactory().createAddress(fromSipURI); + FromHeader fromHeader = SipFactory.getInstance().createHeaderFactory().createFromHeader(fromAddress, fromTag); + // to + SipURI toSipURI = SipFactory.getInstance().createAddressFactory().createSipURI(parentPlatform.getServerGBId(), serverAddress); + Address toAddress = SipFactory.getInstance().createAddressFactory().createAddress(toSipURI); + ToHeader toHeader = SipFactory.getInstance().createHeaderFactory().createToHeader(toAddress, toTag); + + // Forwards + MaxForwardsHeader maxForwards = SipFactory.getInstance().createHeaderFactory().createMaxForwardsHeader(70); + // ceq + CSeqHeader cSeqHeader = SipFactory.getInstance().createHeaderFactory().createCSeqHeader(redisCatchStorage.getCSEQ(), Request.MESSAGE); + MessageFactoryImpl messageFactory = (MessageFactoryImpl) SipFactory.getInstance().createMessageFactory(); + // 设置编码, 防止中文乱码 + messageFactory.setDefaultContentEncodingCharset(parentPlatform.getCharacterSet()); + request = messageFactory.createRequest(requestURI, Request.MESSAGE, callIdHeader, cSeqHeader, fromHeader, + toHeader, viaHeaders, maxForwards); + + request.addHeader(SipUtils.createUserAgentHeader(gitUtil)); + + ContentTypeHeader contentTypeHeader = SipFactory.getInstance().createHeaderFactory().createContentTypeHeader("Application", "MANSCDP+xml"); + request.setContent(content, contentTypeHeader); + return request; + } + + public SIPRequest createNotifyRequest(Platform parentPlatform, String content, SubscribeInfo subscribeInfo) throws PeerUnavailableException, ParseException, InvalidArgumentException { + SIPRequest request = null; + // sipuri + SipURI requestURI = SipFactory.getInstance().createAddressFactory().createSipURI(parentPlatform.getServerGBId(), IpPortUtil.concatenateIpAndPort(parentPlatform.getServerIp(), String.valueOf(parentPlatform.getServerPort()))); + // via + ArrayList viaHeaders = new ArrayList<>(); + ViaHeader viaHeader = SipFactory.getInstance().createHeaderFactory().createViaHeader(parentPlatform.getDeviceIp(), parentPlatform.getDevicePort(), + parentPlatform.getTransport(), SipUtils.getNewViaTag()); + viaHeader.setRPort(); + viaHeaders.add(viaHeader); + // from + SipURI fromSipURI = SipFactory.getInstance().createAddressFactory().createSipURI(parentPlatform.getDeviceGBId(), + parentPlatform.getDeviceIp() + ":" + parentPlatform.getDevicePort()); + Address fromAddress = SipFactory.getInstance().createAddressFactory().createAddress(fromSipURI); + FromHeader fromHeader = SipFactory.getInstance().createHeaderFactory().createFromHeader(fromAddress, subscribeInfo.getTransactionInfo() != null ? subscribeInfo.getTransactionInfo() .getToTag(): subscribeInfo.getSimulatedToTag()); + // to + SipURI toSipURI = SipFactory.getInstance().createAddressFactory().createSipURI(parentPlatform.getServerGBId(), parentPlatform.getServerGBDomain()); + Address toAddress = SipFactory.getInstance().createAddressFactory().createAddress(toSipURI); + ToHeader toHeader = SipFactory.getInstance().createHeaderFactory().createToHeader(toAddress, subscribeInfo.getTransactionInfo() != null ?subscribeInfo.getTransactionInfo().getFromTag(): subscribeInfo.getSimulatedFromTag()); + + // Forwards + MaxForwardsHeader maxForwards = SipFactory.getInstance().createHeaderFactory().createMaxForwardsHeader(70); + // ceq + CSeqHeader cSeqHeader = SipFactory.getInstance().createHeaderFactory().createCSeqHeader(redisCatchStorage.getCSEQ(), Request.NOTIFY); + MessageFactoryImpl messageFactory = (MessageFactoryImpl) SipFactory.getInstance().createMessageFactory(); + // 设置编码, 防止中文乱码 + messageFactory.setDefaultContentEncodingCharset("gb2312"); + + CallIdHeader callIdHeader = SipFactory.getInstance().createHeaderFactory().createCallIdHeader(subscribeInfo.getTransactionInfo() != null ? subscribeInfo.getTransactionInfo().getCallId(): subscribeInfo.getSimulatedCallId()); + + request = (SIPRequest) messageFactory.createRequest(requestURI, Request.NOTIFY, callIdHeader, cSeqHeader, fromHeader, + toHeader, viaHeaders, maxForwards); + + request.addHeader(SipUtils.createUserAgentHeader(gitUtil)); + + EventHeader event = SipFactory.getInstance().createHeaderFactory().createEventHeader(subscribeInfo.getEventType()); + if (subscribeInfo.getEventId() != null) { + event.setEventId(subscribeInfo.getEventId()); + } + + request.addHeader(event); + + SubscriptionStateHeader active = SipFactory.getInstance().createHeaderFactory().createSubscriptionStateHeader("active"); + request.setHeader(active); + + String sipAddress = parentPlatform.getDeviceIp() + ":" + parentPlatform.getDevicePort(); + Address concatAddress = SipFactory.getInstance().createAddressFactory().createAddress(SipFactory.getInstance().createAddressFactory() + .createSipURI(parentPlatform.getDeviceGBId(), sipAddress)); + request.addHeader(SipFactory.getInstance().createHeaderFactory().createContactHeader(concatAddress)); + + ContentTypeHeader contentTypeHeader = SipFactory.getInstance().createHeaderFactory().createContentTypeHeader("Application", "MANSCDP+xml"); + request.setContent(content, contentTypeHeader); + return request; + } + + public SIPRequest createByeRequest(Platform platform, SendRtpInfo sendRtpItem, CommonGBChannel channel) throws PeerUnavailableException, ParseException, InvalidArgumentException { + + if (sendRtpItem == null ) { + return null; + } + + SIPRequest request = null; + // sipuri + SipURI requestURI = SipFactory.getInstance().createAddressFactory().createSipURI(platform.getServerGBId(), IpPortUtil.concatenateIpAndPort(platform.getServerIp(), String.valueOf(platform.getServerPort()))); + // via + ArrayList viaHeaders = new ArrayList<>(); + ViaHeader viaHeader = SipFactory.getInstance().createHeaderFactory().createViaHeader(platform.getDeviceIp(), platform.getDevicePort(), + platform.getTransport(), SipUtils.getNewViaTag()); + viaHeader.setRPort(); + viaHeaders.add(viaHeader); + // from + SipURI fromSipURI = SipFactory.getInstance().createAddressFactory().createSipURI(channel.getGbDeviceId(), + platform.getDeviceIp() + ":" + platform.getDevicePort()); + Address fromAddress = SipFactory.getInstance().createAddressFactory().createAddress(fromSipURI); + FromHeader fromHeader = SipFactory.getInstance().createHeaderFactory().createFromHeader(fromAddress, sendRtpItem.getToTag()); + // to + SipURI toSipURI = SipFactory.getInstance().createAddressFactory().createSipURI(platform.getServerGBId(), platform.getServerGBDomain()); + Address toAddress = SipFactory.getInstance().createAddressFactory().createAddress(toSipURI); + ToHeader toHeader = SipFactory.getInstance().createHeaderFactory().createToHeader(toAddress, sendRtpItem.getFromTag()); + + // Forwards + MaxForwardsHeader maxForwards = SipFactory.getInstance().createHeaderFactory().createMaxForwardsHeader(70); + // ceq + CSeqHeader cSeqHeader = SipFactory.getInstance().createHeaderFactory().createCSeqHeader(redisCatchStorage.getCSEQ(), Request.BYE); + + CallIdHeader callIdHeader = SipFactory.getInstance().createHeaderFactory().createCallIdHeader(sendRtpItem.getCallId()); + + request = (SIPRequest) SipFactory.getInstance().createMessageFactory().createRequest(requestURI, Request.BYE, callIdHeader, cSeqHeader, fromHeader, + toHeader, viaHeaders, maxForwards); + + request.addHeader(SipUtils.createUserAgentHeader(gitUtil)); + + String sipAddress = platform.getDeviceIp() + ":" + platform.getDevicePort(); + Address concatAddress = SipFactory.getInstance().createAddressFactory().createAddress(SipFactory.getInstance().createAddressFactory() + .createSipURI(platform.getDeviceGBId(), sipAddress)); + + request.addHeader(SipFactory.getInstance().createHeaderFactory().createContactHeader(concatAddress)); + + return request; + } + + public Request createInviteRequest(Platform platform,String sourceId, String channelId, String content, String viaTag, String fromTag, String ssrc, CallIdHeader callIdHeader) throws PeerUnavailableException, ParseException, InvalidArgumentException { + Request request = null; + //请求行 + String platformHostAddress = platform.getServerIp() + ":" + platform.getServerPort(); + String localHostAddress = sipLayer.getLocalIp(platform.getDeviceIp())+":"+ platform.getDevicePort(); + SipURI requestLine = SipFactory.getInstance().createAddressFactory().createSipURI(sourceId, platformHostAddress); + //via + ArrayList viaHeaders = new ArrayList(); + ViaHeader viaHeader = SipFactory.getInstance().createHeaderFactory().createViaHeader(sipLayer.getLocalIp(platform.getDeviceIp()), platform.getDevicePort(), platform.getTransport(), viaTag); + viaHeader.setRPort(); + viaHeaders.add(viaHeader); + + //from + SipURI fromSipURI = SipFactory.getInstance().createAddressFactory().createSipURI(platform.getDeviceGBId(), sipConfig.getDomain()); + Address fromAddress = SipFactory.getInstance().createAddressFactory().createAddress(fromSipURI); + FromHeader fromHeader = SipFactory.getInstance().createHeaderFactory().createFromHeader(fromAddress, fromTag); //必须要有标记,否则无法创建会话,无法回应ack + //to + SipURI toSipURI = SipFactory.getInstance().createAddressFactory().createSipURI(sourceId, platformHostAddress); + Address toAddress = SipFactory.getInstance().createAddressFactory().createAddress(toSipURI); + ToHeader toHeader = SipFactory.getInstance().createHeaderFactory().createToHeader(toAddress,null); + + //Forwards + MaxForwardsHeader maxForwards = SipFactory.getInstance().createHeaderFactory().createMaxForwardsHeader(70); + + //ceq + CSeqHeader cSeqHeader = SipFactory.getInstance().createHeaderFactory().createCSeqHeader(redisCatchStorage.getCSEQ(), Request.INVITE); + request = SipFactory.getInstance().createMessageFactory().createRequest(requestLine, Request.INVITE, callIdHeader, cSeqHeader,fromHeader, toHeader, viaHeaders, maxForwards); + + request.addHeader(SipUtils.createUserAgentHeader(gitUtil)); + + Address concatAddress = SipFactory.getInstance().createAddressFactory().createAddress(SipFactory.getInstance().createAddressFactory().createSipURI(sipConfig.getId(),localHostAddress)); + request.addHeader(SipFactory.getInstance().createHeaderFactory().createContactHeader(concatAddress)); + // Subject + SubjectHeader subjectHeader = SipFactory.getInstance().createHeaderFactory().createSubjectHeader(String.format("%s:%s,%s:%s", sourceId, ssrc, channelId, 0)); + request.addHeader(subjectHeader); + ContentTypeHeader contentTypeHeader = SipFactory.getInstance().createHeaderFactory().createContentTypeHeader("APPLICATION", "SDP"); + request.setContent(content, contentTypeHeader); + return request; + } + + public Request createByteRequest(Platform platform, String channelId, SipTransactionInfo transactionInfo) throws PeerUnavailableException, ParseException, InvalidArgumentException { + String deviceHostAddress = platform.getDeviceIp() + ":" + platform.getDevicePort(); + Request request = null; + SipURI requestLine = SipFactory.getInstance().createAddressFactory().createSipURI(channelId, deviceHostAddress); + + // via + ArrayList viaHeaders = new ArrayList(); + ViaHeader viaHeader = SipFactory.getInstance().createHeaderFactory().createViaHeader(sipLayer.getLocalIp(platform.getDeviceIp()), platform.getDevicePort(), platform.getTransport(), SipUtils.getNewViaTag()); + viaHeaders.add(viaHeader); + //from + SipURI fromSipURI = SipFactory.getInstance().createAddressFactory().createSipURI(sipConfig.getId(),sipConfig.getDomain()); + Address fromAddress = SipFactory.getInstance().createAddressFactory().createAddress(fromSipURI); + FromHeader fromHeader = SipFactory.getInstance().createHeaderFactory().createFromHeader(fromAddress, transactionInfo.isAsSender()?transactionInfo.getFromTag():transactionInfo.getToTag()); + //to + SipURI toSipURI = SipFactory.getInstance().createAddressFactory().createSipURI(channelId, deviceHostAddress); + Address toAddress = SipFactory.getInstance().createAddressFactory().createAddress(toSipURI); + ToHeader toHeader = SipFactory.getInstance().createHeaderFactory().createToHeader(toAddress,transactionInfo.isAsSender()?transactionInfo.getToTag():transactionInfo.getFromTag()); + + //Forwards + MaxForwardsHeader maxForwards = SipFactory.getInstance().createHeaderFactory().createMaxForwardsHeader(70); + + //ceq + CSeqHeader cSeqHeader = SipFactory.getInstance().createHeaderFactory().createCSeqHeader(redisCatchStorage.getCSEQ(), Request.BYE); + CallIdHeader callIdHeader = SipFactory.getInstance().createHeaderFactory().createCallIdHeader(transactionInfo.getCallId()); + request = SipFactory.getInstance().createMessageFactory().createRequest(requestLine, Request.BYE, callIdHeader, cSeqHeader,fromHeader, toHeader, viaHeaders, maxForwards); + + request.addHeader(SipUtils.createUserAgentHeader(gitUtil)); + + Address concatAddress = SipFactory.getInstance().createAddressFactory().createAddress(SipFactory.getInstance().createAddressFactory().createSipURI(sipConfig.getId(), IpPortUtil.concatenateIpAndPort(sipLayer.getLocalIp(platform.getDeviceIp()), String.valueOf(platform.getDevicePort())))); + request.addHeader(SipFactory.getInstance().createHeaderFactory().createContactHeader(concatAddress)); + + request.addHeader(SipUtils.createUserAgentHeader(gitUtil)); + + return request; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/cmd/SIPRequestHeaderProvider.java b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/cmd/SIPRequestHeaderProvider.java new file mode 100755 index 0000000..cd50c0c --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/cmd/SIPRequestHeaderProvider.java @@ -0,0 +1,359 @@ +package com.genersoft.iot.vmp.gb28181.transmit.cmd; + +import com.genersoft.iot.vmp.conf.SipConfig; +import com.genersoft.iot.vmp.gb28181.SipLayer; +import com.genersoft.iot.vmp.gb28181.bean.Device; +import com.genersoft.iot.vmp.gb28181.bean.SipTransactionInfo; +import com.genersoft.iot.vmp.gb28181.utils.SipUtils; +import com.genersoft.iot.vmp.storager.IRedisCatchStorage; +import com.genersoft.iot.vmp.utils.GitUtil; +import com.genersoft.iot.vmp.utils.IpPortUtil; +import gov.nist.javax.sip.message.SIPRequest; +import gov.nist.javax.sip.message.SIPResponse; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import javax.sip.InvalidArgumentException; +import javax.sip.PeerUnavailableException; +import javax.sip.SipException; +import javax.sip.SipFactory; +import javax.sip.address.Address; +import javax.sip.address.SipURI; +import javax.sip.header.*; +import javax.sip.message.Request; +import java.text.ParseException; +import java.util.ArrayList; + +/** + * @description:摄像头命令request创造器 TODO 冗余代码太多待优化 + * @author: swwheihei + * @date: 2020年5月6日 上午9:29:02 + */ +@Component +public class SIPRequestHeaderProvider { + + @Autowired + private SipConfig sipConfig; + + @Autowired + private SipLayer sipLayer; + + @Autowired + private GitUtil gitUtil; + + @Autowired + private IRedisCatchStorage redisCatchStorage; + + + public Request createMessageRequest(Device device, String content, String viaTag, String fromTag, String toTag, CallIdHeader callIdHeader) throws ParseException, InvalidArgumentException, PeerUnavailableException { + Request request = null; + // sipuri + SipURI requestURI = SipFactory.getInstance().createAddressFactory().createSipURI(device.getDeviceId(), device.getHostAddress()); + // via + ArrayList viaHeaders = new ArrayList<>(); + ViaHeader viaHeader = SipFactory.getInstance().createHeaderFactory().createViaHeader(sipLayer.getLocalIp(device.getLocalIp()), sipConfig.getPort(), device.getTransport(), viaTag); + viaHeader.setRPort(); + viaHeaders.add(viaHeader); + // from + SipURI fromSipURI = SipFactory.getInstance().createAddressFactory().createSipURI(sipConfig.getId(), sipConfig.getDomain()); + Address fromAddress = SipFactory.getInstance().createAddressFactory().createAddress(fromSipURI); + FromHeader fromHeader = SipFactory.getInstance().createHeaderFactory().createFromHeader(fromAddress, fromTag); + // to + SipURI toSipURI = SipFactory.getInstance().createAddressFactory().createSipURI(device.getDeviceId(), device.getHostAddress()); + Address toAddress = SipFactory.getInstance().createAddressFactory().createAddress(toSipURI); + ToHeader toHeader = SipFactory.getInstance().createHeaderFactory().createToHeader(toAddress, toTag); + + // Forwards + MaxForwardsHeader maxForwards = SipFactory.getInstance().createHeaderFactory().createMaxForwardsHeader(70); + // ceq + CSeqHeader cSeqHeader = SipFactory.getInstance().createHeaderFactory().createCSeqHeader(redisCatchStorage.getCSEQ(), Request.MESSAGE); + + request = SipFactory.getInstance().createMessageFactory().createRequest(requestURI, Request.MESSAGE, callIdHeader, cSeqHeader, fromHeader, + toHeader, viaHeaders, maxForwards); + + request.addHeader(SipUtils.createUserAgentHeader(gitUtil)); + + ContentTypeHeader contentTypeHeader = SipFactory.getInstance().createHeaderFactory().createContentTypeHeader("Application", "MANSCDP+xml"); + request.setContent(content, contentTypeHeader); + return request; + } + + public Request createInviteRequest(Device device, String channelId, String content, String viaTag, String fromTag, String toTag, String ssrc, CallIdHeader callIdHeader) throws ParseException, InvalidArgumentException, PeerUnavailableException { + Request request = null; + //请求行 + SipURI requestLine = SipFactory.getInstance().createAddressFactory().createSipURI(channelId, device.getHostAddress()); + //via + ArrayList viaHeaders = new ArrayList(); + HeaderFactory headerFactory = SipFactory.getInstance().createHeaderFactory(); + ViaHeader viaHeader = SipFactory.getInstance().createHeaderFactory().createViaHeader(sipLayer.getLocalIp(device.getLocalIp()), sipConfig.getPort(), device.getTransport(), viaTag); + viaHeader.setRPort(); + viaHeaders.add(viaHeader); + + //from + SipURI fromSipURI = SipFactory.getInstance().createAddressFactory().createSipURI(sipConfig.getId(), sipConfig.getDomain()); + Address fromAddress = SipFactory.getInstance().createAddressFactory().createAddress(fromSipURI); + FromHeader fromHeader = SipFactory.getInstance().createHeaderFactory().createFromHeader(fromAddress, fromTag); //必须要有标记,否则无法创建会话,无法回应ack + //to + SipURI toSipURI = SipFactory.getInstance().createAddressFactory().createSipURI(channelId, device.getHostAddress()); + Address toAddress = SipFactory.getInstance().createAddressFactory().createAddress(toSipURI); + ToHeader toHeader = SipFactory.getInstance().createHeaderFactory().createToHeader(toAddress,null); + + //Forwards + MaxForwardsHeader maxForwards = SipFactory.getInstance().createHeaderFactory().createMaxForwardsHeader(70); + + //ceq + CSeqHeader cSeqHeader = SipFactory.getInstance().createHeaderFactory().createCSeqHeader(redisCatchStorage.getCSEQ(), Request.INVITE); + request = SipFactory.getInstance().createMessageFactory().createRequest(requestLine, Request.INVITE, callIdHeader, cSeqHeader,fromHeader, toHeader, viaHeaders, maxForwards); + + request.addHeader(SipUtils.createUserAgentHeader(gitUtil)); + + Address concatAddress = SipFactory.getInstance().createAddressFactory().createAddress(SipFactory.getInstance().createAddressFactory().createSipURI(sipConfig.getId(), IpPortUtil.concatenateIpAndPort(sipLayer.getLocalIp(device.getLocalIp()), String.valueOf(sipConfig.getPort())))); + // Address concatAddress = SipFactory.getInstance().createAddressFactory().createAddress(SipFactory.getInstance().createAddressFactory().createSipURI(sipConfig.getId(), device.getHost().getIp()+":"+device.getHost().getPort())); + request.addHeader(SipFactory.getInstance().createHeaderFactory().createContactHeader(concatAddress)); + // Subject + SubjectHeader subjectHeader = SipFactory.getInstance().createHeaderFactory().createSubjectHeader(String.format("%s:%s,%s:%s", channelId, ssrc, sipConfig.getId(), 0)); + request.addHeader(subjectHeader); + ContentTypeHeader contentTypeHeader = SipFactory.getInstance().createHeaderFactory().createContentTypeHeader("APPLICATION", "SDP"); + request.setContent(content, contentTypeHeader); + return request; + } + + public Request createPlaybackInviteRequest(Device device, String channelId, String content, String viaTag, String fromTag, String toTag, CallIdHeader callIdHeader, String ssrc) throws ParseException, InvalidArgumentException, PeerUnavailableException { + Request request = null; + //请求行 + SipURI requestLine = SipFactory.getInstance().createAddressFactory().createSipURI(channelId, device.getHostAddress()); + // via + ArrayList viaHeaders = new ArrayList(); + ViaHeader viaHeader = SipFactory.getInstance().createHeaderFactory().createViaHeader(sipLayer.getLocalIp(device.getLocalIp()), sipConfig.getPort(), device.getTransport(), viaTag); + viaHeader.setRPort(); + viaHeaders.add(viaHeader); + //from + SipURI fromSipURI = SipFactory.getInstance().createAddressFactory().createSipURI(sipConfig.getId(), sipConfig.getDomain()); + Address fromAddress = SipFactory.getInstance().createAddressFactory().createAddress(fromSipURI); + FromHeader fromHeader = SipFactory.getInstance().createHeaderFactory().createFromHeader(fromAddress, fromTag); //必须要有标记,否则无法创建会话,无法回应ack + //to + SipURI toSipURI = SipFactory.getInstance().createAddressFactory().createSipURI(channelId, device.getHostAddress()); + Address toAddress = SipFactory.getInstance().createAddressFactory().createAddress(toSipURI); + ToHeader toHeader = SipFactory.getInstance().createHeaderFactory().createToHeader(toAddress,null); + + //Forwards + MaxForwardsHeader maxForwards = SipFactory.getInstance().createHeaderFactory().createMaxForwardsHeader(70); + + //ceq + CSeqHeader cSeqHeader = SipFactory.getInstance().createHeaderFactory().createCSeqHeader(redisCatchStorage.getCSEQ(), Request.INVITE); + request = SipFactory.getInstance().createMessageFactory().createRequest(requestLine, Request.INVITE, callIdHeader, cSeqHeader,fromHeader, toHeader, viaHeaders, maxForwards); + + Address concatAddress = SipFactory.getInstance().createAddressFactory().createAddress(SipFactory.getInstance().createAddressFactory().createSipURI(sipConfig.getId(), IpPortUtil.concatenateIpAndPort(sipLayer.getLocalIp(device.getLocalIp()), String.valueOf(sipConfig.getPort())))); + // Address concatAddress = SipFactory.getInstance().createAddressFactory().createAddress(SipFactory.getInstance().createAddressFactory().createSipURI(sipConfig.getId(), device.getHost().getIp()+":"+device.getHost().getPort())); + request.addHeader(SipFactory.getInstance().createHeaderFactory().createContactHeader(concatAddress)); + + request.addHeader(SipUtils.createUserAgentHeader(gitUtil)); + + // Subject + SubjectHeader subjectHeader = SipFactory.getInstance().createHeaderFactory().createSubjectHeader(String.format("%s:%s,%s:%s", channelId, ssrc, sipConfig.getId(), 0)); + request.addHeader(subjectHeader); + + ContentTypeHeader contentTypeHeader = SipFactory.getInstance().createHeaderFactory().createContentTypeHeader("APPLICATION", "SDP"); + request.setContent(content, contentTypeHeader); + return request; + } + + public Request createByteRequest(Device device, String channelId, SipTransactionInfo transactionInfo) throws ParseException, InvalidArgumentException, PeerUnavailableException { + Request request = null; + //请求行 + SipURI requestLine = SipFactory.getInstance().createAddressFactory().createSipURI(channelId, device.getHostAddress()); +// SipURI requestLine = SipFactory.getInstance().createAddressFactory().createSipURI(device.getDeviceId(), device.getHostAddress()); + // via + ArrayList viaHeaders = new ArrayList(); +// ViaHeader viaHeader = SipFactory.getInstance().createHeaderFactory().createViaHeader(sipLayer.getLocalIp(device.getLocalIp()), sipConfig.getPort(), device.getTransport(), transactionInfo.getViaBranch()); + ViaHeader viaHeader = SipFactory.getInstance().createHeaderFactory().createViaHeader(sipLayer.getLocalIp(device.getLocalIp()), sipConfig.getPort(), device.getTransport(), SipUtils.getNewViaTag()); +// viaHeader.setRPort(); + viaHeaders.add(viaHeader); + //from +// SipURI fromSipURI = SipFactory.getInstance().createAddressFactory().createSipURI(sipConfig.getId(),sipConfig.getDomain()); + SipURI fromSipURI = SipFactory.getInstance().createAddressFactory().createSipURI(sipConfig.getId(), IpPortUtil.concatenateIpAndPort(sipLayer.getLocalIp(device.getLocalIp()), String.valueOf(sipConfig.getPort()))); + Address fromAddress = SipFactory.getInstance().createAddressFactory().createAddress(fromSipURI); + FromHeader fromHeader = SipFactory.getInstance().createHeaderFactory().createFromHeader(fromAddress, transactionInfo.getFromTag()); + //to + SipURI toSipURI = SipFactory.getInstance().createAddressFactory().createSipURI(channelId,device.getHostAddress()); +// SipURI toSipURI = SipFactory.getInstance().createAddressFactory().createSipURI(device.getDeviceId(),device.getHostAddress()); + Address toAddress = SipFactory.getInstance().createAddressFactory().createAddress(toSipURI); + ToHeader toHeader = SipFactory.getInstance().createHeaderFactory().createToHeader(toAddress, transactionInfo.getToTag()); + + //Forwards + MaxForwardsHeader maxForwards = SipFactory.getInstance().createHeaderFactory().createMaxForwardsHeader(70); + + //ceq + CSeqHeader cSeqHeader = SipFactory.getInstance().createHeaderFactory().createCSeqHeader(redisCatchStorage.getCSEQ(), Request.BYE); + CallIdHeader callIdHeader = SipFactory.getInstance().createHeaderFactory().createCallIdHeader(transactionInfo.getCallId()); + request = SipFactory.getInstance().createMessageFactory().createRequest(requestLine, Request.BYE, callIdHeader, cSeqHeader,fromHeader, toHeader, viaHeaders, maxForwards); + + request.addHeader(SipUtils.createUserAgentHeader(gitUtil)); + + Address concatAddress = SipFactory.getInstance().createAddressFactory().createAddress(SipFactory.getInstance().createAddressFactory().createSipURI(sipConfig.getId(), IpPortUtil.concatenateIpAndPort(sipLayer.getLocalIp(device.getLocalIp()), String.valueOf(sipConfig.getPort())))); + request.addHeader(SipFactory.getInstance().createHeaderFactory().createContactHeader(concatAddress)); + + request.addHeader(SipUtils.createUserAgentHeader(gitUtil)); + + return request; + } + + public Request createByteRequestForDeviceInvite(Device device, String channelId, SipTransactionInfo transactionInfo) throws ParseException, InvalidArgumentException, PeerUnavailableException { + Request request = null; + //请求行 + SipURI requestLine = SipFactory.getInstance().createAddressFactory().createSipURI(channelId, device.getHostAddress()); + // via + ArrayList viaHeaders = new ArrayList(); + ViaHeader viaHeader = SipFactory.getInstance().createHeaderFactory().createViaHeader(sipLayer.getLocalIp(device.getLocalIp()), sipConfig.getPort(), device.getTransport(), SipUtils.getNewViaTag()); + viaHeaders.add(viaHeader); + //from + SipURI fromSipURI = SipFactory.getInstance().createAddressFactory().createSipURI(sipConfig.getId(),sipConfig.getDomain()); + Address fromAddress = SipFactory.getInstance().createAddressFactory().createAddress(fromSipURI); + FromHeader fromHeader = SipFactory.getInstance().createHeaderFactory().createFromHeader(fromAddress, transactionInfo.getToTag()); + //to + SipURI toSipURI = SipFactory.getInstance().createAddressFactory().createSipURI(channelId,device.getHostAddress()); + Address toAddress = SipFactory.getInstance().createAddressFactory().createAddress(toSipURI); + ToHeader toHeader = SipFactory.getInstance().createHeaderFactory().createToHeader(toAddress, transactionInfo.getFromTag()); + + //Forwards + MaxForwardsHeader maxForwards = SipFactory.getInstance().createHeaderFactory().createMaxForwardsHeader(70); + + //ceq + CSeqHeader cSeqHeader = SipFactory.getInstance().createHeaderFactory().createCSeqHeader(redisCatchStorage.getCSEQ(), Request.BYE); + CallIdHeader callIdHeader = SipFactory.getInstance().createHeaderFactory().createCallIdHeader(transactionInfo.getCallId()); + request = SipFactory.getInstance().createMessageFactory().createRequest(requestLine, Request.BYE, callIdHeader, cSeqHeader,fromHeader, toHeader, viaHeaders, maxForwards); + + request.addHeader(SipUtils.createUserAgentHeader(gitUtil)); + + Address concatAddress = SipFactory.getInstance().createAddressFactory().createAddress(SipFactory.getInstance().createAddressFactory().createSipURI(sipConfig.getId(), IpPortUtil.concatenateIpAndPort(sipLayer.getLocalIp(device.getLocalIp()), String.valueOf(sipConfig.getPort())))); + request.addHeader(SipFactory.getInstance().createHeaderFactory().createContactHeader(concatAddress)); + + request.addHeader(SipUtils.createUserAgentHeader(gitUtil)); + + return request; + } + + public Request createSubscribeRequest(Device device, String content, SipTransactionInfo sipTransactionInfo, Integer expires, String event, CallIdHeader callIdHeader) throws ParseException, InvalidArgumentException, PeerUnavailableException { + Request request = null; + // sipuri + SipURI requestURI = SipFactory.getInstance().createAddressFactory().createSipURI(device.getDeviceId(), device.getHostAddress()); + // via + ArrayList viaHeaders = new ArrayList(); + ViaHeader viaHeader = SipFactory.getInstance().createHeaderFactory().createViaHeader(sipLayer.getLocalIp(device.getLocalIp()), sipConfig.getPort(), + device.getTransport(), SipUtils.getNewViaTag()); + viaHeader.setRPort(); + viaHeaders.add(viaHeader); + // from + SipURI fromSipURI = SipFactory.getInstance().createAddressFactory().createSipURI(sipConfig.getId(), sipConfig.getDomain()); + Address fromAddress = SipFactory.getInstance().createAddressFactory().createAddress(fromSipURI); + FromHeader fromHeader = SipFactory.getInstance().createHeaderFactory().createFromHeader(fromAddress, sipTransactionInfo == null ? SipUtils.getNewFromTag() :sipTransactionInfo.getFromTag()); + // to + SipURI toSipURI = SipFactory.getInstance().createAddressFactory().createSipURI(device.getDeviceId(), device.getHostAddress()); + Address toAddress = SipFactory.getInstance().createAddressFactory().createAddress(toSipURI); + ToHeader toHeader = SipFactory.getInstance().createHeaderFactory().createToHeader(toAddress, sipTransactionInfo == null ? null :sipTransactionInfo.getToTag()); + + // Forwards + MaxForwardsHeader maxForwards = SipFactory.getInstance().createHeaderFactory().createMaxForwardsHeader(70); + + // ceq + CSeqHeader cSeqHeader = SipFactory.getInstance().createHeaderFactory().createCSeqHeader(redisCatchStorage.getCSEQ(), Request.SUBSCRIBE); + + request = SipFactory.getInstance().createMessageFactory().createRequest(requestURI, Request.SUBSCRIBE, callIdHeader, cSeqHeader, fromHeader, + toHeader, viaHeaders, maxForwards); + + + Address concatAddress = SipFactory.getInstance().createAddressFactory().createAddress(SipFactory.getInstance().createAddressFactory().createSipURI(sipConfig.getId(), IpPortUtil.concatenateIpAndPort(sipLayer.getLocalIp(device.getLocalIp()), String.valueOf(sipConfig.getPort())))); + request.addHeader(SipFactory.getInstance().createHeaderFactory().createContactHeader(concatAddress)); + + // Expires + ExpiresHeader expireHeader = SipFactory.getInstance().createHeaderFactory().createExpiresHeader(expires); + request.addHeader(expireHeader); + + // Event + EventHeader eventHeader = SipFactory.getInstance().createHeaderFactory().createEventHeader(event); + if (sipTransactionInfo != null && sipTransactionInfo.getEventId() != null) { + eventHeader.setEventId(sipTransactionInfo.getEventId()); + }else { + int random = (int) Math.floor(Math.random() * 10000); + eventHeader.setEventId(random + ""); + } + request.addHeader(eventHeader); + + ContentTypeHeader contentTypeHeader = SipFactory.getInstance().createHeaderFactory().createContentTypeHeader("Application", "MANSCDP+xml"); + request.setContent(content, contentTypeHeader); + + request.addHeader(SipUtils.createUserAgentHeader(gitUtil)); + + return request; + } + + public SIPRequest createInfoRequest(Device device, String channelId, String content, SipTransactionInfo transactionInfo) + throws SipException, ParseException, InvalidArgumentException { + if (device == null || transactionInfo == null) { + return null; + } + SIPRequest request = null; + //请求行 + SipURI requestLine = SipFactory.getInstance().createAddressFactory().createSipURI(channelId, device.getHostAddress()); + // via + ArrayList viaHeaders = new ArrayList(); + ViaHeader viaHeader = SipFactory.getInstance().createHeaderFactory().createViaHeader(sipLayer.getLocalIp(device.getLocalIp()), sipConfig.getPort(), device.getTransport(), SipUtils.getNewViaTag()); + viaHeaders.add(viaHeader); + //from + SipURI fromSipURI = SipFactory.getInstance().createAddressFactory().createSipURI(sipConfig.getId(),sipConfig.getDomain()); + Address fromAddress = SipFactory.getInstance().createAddressFactory().createAddress(fromSipURI); + FromHeader fromHeader = SipFactory.getInstance().createHeaderFactory().createFromHeader(fromAddress, transactionInfo.getFromTag()); + //to + SipURI toSipURI = SipFactory.getInstance().createAddressFactory().createSipURI(channelId,device.getHostAddress()); + Address toAddress = SipFactory.getInstance().createAddressFactory().createAddress(toSipURI); + ToHeader toHeader = SipFactory.getInstance().createHeaderFactory().createToHeader(toAddress, transactionInfo.getToTag()); + + //Forwards + MaxForwardsHeader maxForwards = SipFactory.getInstance().createHeaderFactory().createMaxForwardsHeader(70); + + //ceq + CSeqHeader cSeqHeader = SipFactory.getInstance().createHeaderFactory().createCSeqHeader(redisCatchStorage.getCSEQ(), Request.INFO); + CallIdHeader callIdHeader = SipFactory.getInstance().createHeaderFactory().createCallIdHeader(transactionInfo.getCallId()); + request = (SIPRequest)SipFactory.getInstance().createMessageFactory().createRequest(requestLine, Request.INFO, callIdHeader, cSeqHeader,fromHeader, toHeader, viaHeaders, maxForwards); + + request.addHeader(SipUtils.createUserAgentHeader(gitUtil)); + + Address concatAddress = SipFactory.getInstance().createAddressFactory().createAddress(SipFactory.getInstance().createAddressFactory().createSipURI(sipConfig.getId(), IpPortUtil.concatenateIpAndPort(sipLayer.getLocalIp(device.getLocalIp()), String.valueOf(sipConfig.getPort())))); + request.addHeader(SipFactory.getInstance().createHeaderFactory().createContactHeader(concatAddress)); + + request.addHeader(SipUtils.createUserAgentHeader(gitUtil)); + + if (content != null) { + ContentTypeHeader contentTypeHeader = SipFactory.getInstance().createHeaderFactory().createContentTypeHeader("Application", + "MANSRTSP"); + request.setContent(content, contentTypeHeader); + } + return request; + } + + public Request createAckRequest(String localIp, SipURI sipURI, SIPResponse sipResponse) throws ParseException, InvalidArgumentException, PeerUnavailableException { + + + // via + ArrayList viaHeaders = new ArrayList(); + ViaHeader viaHeader = SipFactory.getInstance().createHeaderFactory().createViaHeader(localIp, sipConfig.getPort(), sipResponse.getTopmostViaHeader().getTransport(), SipUtils.getNewViaTag()); + viaHeaders.add(viaHeader); + + //Forwards + MaxForwardsHeader maxForwards = SipFactory.getInstance().createHeaderFactory().createMaxForwardsHeader(70); + + //ceq + CSeqHeader cSeqHeader = SipFactory.getInstance().createHeaderFactory().createCSeqHeader(sipResponse.getCSeqHeader().getSeqNumber(), Request.ACK); + + Request request = SipFactory.getInstance().createMessageFactory().createRequest(sipURI, Request.ACK, sipResponse.getCallIdHeader(), cSeqHeader, sipResponse.getFromHeader(), sipResponse.getToHeader(), viaHeaders, maxForwards); + + request.addHeader(SipUtils.createUserAgentHeader(gitUtil)); + + Address concatAddress = SipFactory.getInstance().createAddressFactory().createAddress(SipFactory.getInstance().createAddressFactory().createSipURI(sipConfig.getId(), IpPortUtil.concatenateIpAndPort(localIp, String.valueOf(sipConfig.getPort())))); + request.addHeader(SipFactory.getInstance().createHeaderFactory().createContactHeader(concatAddress)); + + request.addHeader(SipUtils.createUserAgentHeader(gitUtil)); + + return request; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/cmd/impl/SIPCommander.java b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/cmd/impl/SIPCommander.java new file mode 100755 index 0000000..eacfcdb --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/cmd/impl/SIPCommander.java @@ -0,0 +1,1438 @@ +package com.genersoft.iot.vmp.gb28181.transmit.cmd.impl; + +import com.genersoft.iot.vmp.common.InviteSessionType; +import com.genersoft.iot.vmp.common.StreamInfo; +import com.genersoft.iot.vmp.conf.SipConfig; +import com.genersoft.iot.vmp.conf.UserSetting; +import com.genersoft.iot.vmp.conf.exception.SsrcTransactionNotFoundException; +import com.genersoft.iot.vmp.gb28181.SipLayer; +import com.genersoft.iot.vmp.gb28181.bean.*; +import com.genersoft.iot.vmp.gb28181.event.MessageSubscribe; +import com.genersoft.iot.vmp.gb28181.event.SipSubscribe; +import com.genersoft.iot.vmp.gb28181.event.sip.MessageEvent; +import com.genersoft.iot.vmp.gb28181.session.SipInviteSessionManager; +import com.genersoft.iot.vmp.gb28181.transmit.SIPSender; +import com.genersoft.iot.vmp.gb28181.transmit.cmd.ISIPCommander; +import com.genersoft.iot.vmp.gb28181.transmit.cmd.SIPRequestHeaderProvider; +import com.genersoft.iot.vmp.gb28181.utils.NumericUtil; +import com.genersoft.iot.vmp.gb28181.utils.SipUtils; +import com.genersoft.iot.vmp.media.bean.MediaServer; +import com.genersoft.iot.vmp.media.event.hook.Hook; +import com.genersoft.iot.vmp.media.event.hook.HookSubscribe; +import com.genersoft.iot.vmp.media.event.hook.HookType; +import com.genersoft.iot.vmp.media.service.IMediaServerService; +import com.genersoft.iot.vmp.service.bean.ErrorCallback; +import com.genersoft.iot.vmp.service.bean.SSRCInfo; +import com.genersoft.iot.vmp.utils.DateUtil; +import com.genersoft.iot.vmp.vmanager.bean.ErrorCode; +import gov.nist.javax.sip.message.SIPRequest; +import gov.nist.javax.sip.message.SIPResponse; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.DependsOn; +import org.springframework.stereotype.Component; +import org.springframework.util.ObjectUtils; + +import javax.sip.InvalidArgumentException; +import javax.sip.ResponseEvent; +import javax.sip.SipException; +import javax.sip.SipFactory; +import javax.sip.header.CallIdHeader; +import javax.sip.message.Request; +import java.text.ParseException; +import java.util.List; + +/** + * @description:设备能力接口,用于定义设备的控制、查询能力 + * @author: swwheihei + * @date: 2020年5月3日 下午9:22:48 + */ +@Component +@DependsOn("sipLayer") +@Slf4j +public class SIPCommander implements ISIPCommander { + + @Autowired + private SipConfig sipConfig; + + @Autowired + private SipLayer sipLayer; + + @Autowired + private SIPSender sipSender; + + @Autowired + private SIPRequestHeaderProvider headerProvider; + + @Autowired + private SipInviteSessionManager sessionManager; + + @Autowired + private UserSetting userSetting; + + @Autowired + private HookSubscribe subscribe; + + @Autowired + private IMediaServerService mediaServerService; + + @Autowired + private MessageSubscribe messageSubscribe; + + /** + * 云台指令码计算 + * + * @param cmdCode 指令码 + * @param parameter1 数据1 + * @param parameter2 数据2 + * @param combineCode2 组合码2 + */ + public static String frontEndCmdString(int cmdCode, int parameter1, int parameter2, int combineCode2) { + StringBuilder builder = new StringBuilder("A50F01"); + String strTmp; + strTmp = String.format("%02X", cmdCode); + builder.append(strTmp, 0, 2); + strTmp = String.format("%02X", parameter1); + builder.append(strTmp, 0, 2); + strTmp = String.format("%02X", parameter2); + builder.append(strTmp, 0, 2); + strTmp = String.format("%02X", combineCode2 << 4); + builder.append(strTmp, 0, 2); + //计算校验码 + int checkCode = (0XA5 + 0X0F + 0X01 + cmdCode + parameter1 + parameter2 + (combineCode2 << 4)) % 0X100; + strTmp = String.format("%02X", checkCode); + builder.append(strTmp, 0, 2); + return builder.toString(); + } + + /** + * 云台控制,支持方向与缩放控制 + * + * @param device 控制设备 + * @param channelId 预览通道 + * @param leftRight 镜头左移右移 0:停止 1:左移 2:右移 + * @param upDown 镜头上移下移 0:停止 1:上移 2:下移 + * @param inOut 镜头放大缩小 0:停止 1:缩小 2:放大 + * @param moveSpeed 镜头移动速度 + * @param zoomSpeed 镜头缩放速度 + */ + @Override + public void ptzCmd(Device device, String channelId, int leftRight, int upDown, int inOut, int moveSpeed, + int zoomSpeed) throws InvalidArgumentException, SipException, ParseException { + String cmdStr = SipUtils.cmdString(leftRight, upDown, inOut, moveSpeed, zoomSpeed); + StringBuilder ptzXml = new StringBuilder(200); + String charset = device.getCharset(); + ptzXml.append("\r\n"); + ptzXml.append("\r\n"); + ptzXml.append("DeviceControl\r\n"); + ptzXml.append("" + (int) ((Math.random() * 9 + 1) * 100000) + "\r\n"); + ptzXml.append("" + channelId + "\r\n"); + ptzXml.append("" + cmdStr + "\r\n"); + ptzXml.append("\r\n"); + ptzXml.append("5\r\n"); + ptzXml.append("\r\n"); + ptzXml.append("\r\n"); + + Request request = headerProvider.createMessageRequest(device, ptzXml.toString(), SipUtils.getNewViaTag(), SipUtils.getNewFromTag(), null, sipSender.getNewCallIdHeader(sipLayer.getLocalIp(device.getLocalIp()),device.getTransport())); + + sipSender.transmitRequest(sipLayer.getLocalIp(device.getLocalIp()),request); + } + + /** + * 前端控制,包括PTZ指令、FI指令、预置位指令、巡航指令、扫描指令和辅助开关指令 + * + * @param device 控制设备 + * @param channelId 预览通道 + * @param cmdCode 指令码 + * @param parameter1 数据1 + * @param parameter2 数据2 + * @param combineCode2 组合码2 + */ + @Override + public void frontEndCmd(Device device, String channelId, int cmdCode, int parameter1, int parameter2, int combineCode2) throws SipException, InvalidArgumentException, ParseException { + + String cmdStr = frontEndCmdString(cmdCode, parameter1, parameter2, combineCode2); + StringBuffer ptzXml = new StringBuffer(200); + String charset = device.getCharset(); + ptzXml.append("\r\n"); + ptzXml.append("\r\n"); + ptzXml.append("DeviceControl\r\n"); + ptzXml.append("" + (int) ((Math.random() * 9 + 1) * 100000) + "\r\n"); + ptzXml.append("" + channelId + "\r\n"); + ptzXml.append("" + cmdStr + "\r\n"); + ptzXml.append("\r\n"); + ptzXml.append("5\r\n"); + ptzXml.append("\r\n"); + ptzXml.append("\r\n"); + + SIPRequest request = (SIPRequest) headerProvider.createMessageRequest(device, ptzXml.toString(), SipUtils.getNewViaTag(), SipUtils.getNewFromTag(), null,sipSender.getNewCallIdHeader(sipLayer.getLocalIp(device.getLocalIp()),device.getTransport())); + sipSender.transmitRequest(sipLayer.getLocalIp(device.getLocalIp()),request); + + } + + /** + * 前端控制指令(用于转发上级指令) + * + * @param device 控制设备 + * @param channelId 预览通道 + * @param cmdString 前端控制指令串 + */ + @Override + public void fronEndCmd(Device device, String channelId, String cmdString, SipSubscribe.Event errorEvent, SipSubscribe.Event okEvent) throws InvalidArgumentException, SipException, ParseException { + + StringBuffer ptzXml = new StringBuffer(200); + String charset = device.getCharset(); + ptzXml.append("\r\n"); + ptzXml.append("\r\n"); + ptzXml.append("DeviceControl\r\n"); + ptzXml.append("" + (int) ((Math.random() * 9 + 1) * 100000) + "\r\n"); + ptzXml.append("" + channelId + "\r\n"); + ptzXml.append("" + cmdString + "\r\n"); + ptzXml.append("\r\n"); + ptzXml.append("5\r\n"); + ptzXml.append("\r\n"); + ptzXml.append("\r\n"); + + + Request request = headerProvider.createMessageRequest(device, ptzXml.toString(), SipUtils.getNewViaTag(), SipUtils.getNewFromTag(), null,sipSender.getNewCallIdHeader(sipLayer.getLocalIp(device.getLocalIp()),device.getTransport())); + sipSender.transmitRequest(sipLayer.getLocalIp(device.getLocalIp()),request, errorEvent, okEvent); + + } + + /** + * 请求预览视频流 + * + * @param device 视频设备 + * @param channel 预览通道 + * @param errorEvent sip错误订阅 + */ + @Override + public void playStreamCmd(MediaServer mediaServerItem, SSRCInfo ssrcInfo, Device device, DeviceChannel channel, + SipSubscribe.Event okEvent, SipSubscribe.Event errorEvent, Long timeout) throws InvalidArgumentException, SipException, ParseException { + String stream = ssrcInfo.getStream(); + + if (device == null) { + return; + } + String sdpIp; + if (!ObjectUtils.isEmpty(device.getSdpIp())) { + sdpIp = device.getSdpIp(); + }else { + sdpIp = mediaServerItem.getSdpIp(); + } + StringBuffer content = new StringBuffer(200); + content.append("v=0\r\n"); + content.append("o=" + device.getDeviceId() + " 0 0 IN IP4 " + sdpIp + "\r\n"); + content.append("s=Play\r\n"); + content.append("c=IN IP4 " + sdpIp + "\r\n"); + content.append("t=0 0\r\n"); + + if (userSetting.getSeniorSdp()) { + if ("TCP-PASSIVE".equalsIgnoreCase(device.getStreamMode())) { + content.append("m=video " + ssrcInfo.getPort() + " TCP/RTP/AVP 96 126 125 99 34 98 97\r\n"); + } else if ("TCP-ACTIVE".equalsIgnoreCase(device.getStreamMode())) { + content.append("m=video " + ssrcInfo.getPort() + " TCP/RTP/AVP 96 126 125 99 34 98 97\r\n"); + } else if ("UDP".equalsIgnoreCase(device.getStreamMode())) { + content.append("m=video " + ssrcInfo.getPort() + " RTP/AVP 96 126 125 99 34 98 97\r\n"); + } + content.append("a=recvonly\r\n"); + content.append("a=rtpmap:96 PS/90000\r\n"); + content.append("a=fmtp:126 profile-level-id=42e01e\r\n"); + content.append("a=rtpmap:126 H264/90000\r\n"); + content.append("a=rtpmap:125 H264S/90000\r\n"); + content.append("a=fmtp:125 profile-level-id=42e01e\r\n"); + content.append("a=rtpmap:99 H265/90000\r\n"); + content.append("a=rtpmap:98 H264/90000\r\n"); + content.append("a=rtpmap:97 MPEG4/90000\r\n"); + if ("TCP-PASSIVE".equalsIgnoreCase(device.getStreamMode())) { // tcp被动模式 + content.append("a=setup:passive\r\n"); + content.append("a=connection:new\r\n"); + } else if ("TCP-ACTIVE".equalsIgnoreCase(device.getStreamMode())) { // tcp主动模式 + content.append("a=setup:active\r\n"); + content.append("a=connection:new\r\n"); + } + } else { + if ("TCP-PASSIVE".equalsIgnoreCase(device.getStreamMode())) { + content.append("m=video " + ssrcInfo.getPort() + " TCP/RTP/AVP 96 97 98 99\r\n"); + } else if ("TCP-ACTIVE".equalsIgnoreCase(device.getStreamMode())) { + content.append("m=video " + ssrcInfo.getPort() + " TCP/RTP/AVP 96 97 98 99\r\n"); + } else if ("UDP".equalsIgnoreCase(device.getStreamMode())) { + content.append("m=video " + ssrcInfo.getPort() + " RTP/AVP 96 97 98 99\r\n"); + } + content.append("a=recvonly\r\n"); + content.append("a=rtpmap:96 PS/90000\r\n"); + content.append("a=rtpmap:98 H264/90000\r\n"); + content.append("a=rtpmap:97 MPEG4/90000\r\n"); + content.append("a=rtpmap:99 H265/90000\r\n"); + if ("TCP-PASSIVE".equalsIgnoreCase(device.getStreamMode())) { // tcp被动模式 + content.append("a=setup:passive\r\n"); + content.append("a=connection:new\r\n"); + } else if ("TCP-ACTIVE".equalsIgnoreCase(device.getStreamMode())) { // tcp主动模式 + content.append("a=setup:active\r\n"); + content.append("a=connection:new\r\n"); + } + } + + if (!ObjectUtils.isEmpty(channel.getStreamIdentification())) { + content.append("a=" + channel.getStreamIdentification() + "\r\n"); + } + + content.append("y=" + ssrcInfo.getSsrc() + "\r\n");//ssrc + // f字段:f= v/编码格式/分辨率/帧率/码率类型/码率大小a/编码格式/码率大小/采样率 +// content.append("f=v/2/5/25/1/4000a/1/8/1" + "\r\n"); // 未发现支持此特性的设备 + + Request request = headerProvider.createInviteRequest(device, channel.getDeviceId(), content.toString(), SipUtils.getNewViaTag(), SipUtils.getNewFromTag(), null, ssrcInfo.getSsrc(),sipSender.getNewCallIdHeader(sipLayer.getLocalIp(device.getLocalIp()),device.getTransport())); + sipSender.transmitRequest(sipLayer.getLocalIp(device.getLocalIp()), request, (e -> { + sessionManager.removeByStream(ssrcInfo.getApp(), ssrcInfo.getStream()); + mediaServerService.releaseSsrc(mediaServerItem.getId(), ssrcInfo.getSsrc()); + errorEvent.response(e); + }), e -> { + ResponseEvent responseEvent = (ResponseEvent) e.event; + SIPResponse response = (SIPResponse) responseEvent.getResponse(); + String callId = response.getCallIdHeader().getCallId(); + SsrcTransaction ssrcTransaction = SsrcTransaction.buildForDevice(device.getDeviceId(), channel.getId(), + callId,ssrcInfo.getApp(), ssrcInfo.getStream(), ssrcInfo.getSsrc(), mediaServerItem.getId(), response, + InviteSessionType.PLAY); + sessionManager.put(ssrcTransaction); + okEvent.response(e); + }, timeout); + } + + /** + * 请求回放视频流 + * + * @param device 视频设备 + * @param channel 预览通道 + * @param startTime 开始时间,格式要求:yyyy-MM-dd HH:mm:ss + * @param endTime 结束时间,格式要求:yyyy-MM-dd HH:mm:ss + */ + @Override + public void playbackStreamCmd(MediaServer mediaServerItem, SSRCInfo ssrcInfo, Device device, DeviceChannel channel, + String startTime, String endTime, + SipSubscribe.Event okEvent, SipSubscribe.Event errorEvent, Long timeout) throws InvalidArgumentException, SipException, ParseException { + + + log.info("{} 分配的ZLM为: {} [{}:{}]", ssrcInfo.getStream(), mediaServerItem.getId(), mediaServerItem.getSdpIp(), ssrcInfo.getPort()); + String sdpIp; + if (!ObjectUtils.isEmpty(device.getSdpIp())) { + sdpIp = device.getSdpIp(); + }else { + sdpIp = mediaServerItem.getSdpIp(); + } + StringBuffer content = new StringBuffer(200); + content.append("v=0\r\n"); + content.append("o=" + device.getDeviceId() + " 0 0 IN IP4 " + sdpIp + "\r\n"); + content.append("s=Playback\r\n"); + content.append("u=" + channel.getDeviceId() + ":0\r\n"); + content.append("c=IN IP4 " + sdpIp + "\r\n"); + content.append("t=" + DateUtil.yyyy_MM_dd_HH_mm_ssToTimestamp(startTime) + " " + + DateUtil.yyyy_MM_dd_HH_mm_ssToTimestamp(endTime) + "\r\n"); + + String streamMode = device.getStreamMode(); + + if (userSetting.getSeniorSdp()) { + if ("TCP-PASSIVE".equalsIgnoreCase(streamMode)) { + content.append("m=video " + ssrcInfo.getPort() + " TCP/RTP/AVP 96 126 125 99 34 98 97\r\n"); + } else if ("TCP-ACTIVE".equalsIgnoreCase(streamMode)) { + content.append("m=video " + ssrcInfo.getPort() + " TCP/RTP/AVP 96 126 125 99 34 98 97\r\n"); + } else if ("UDP".equalsIgnoreCase(streamMode)) { + content.append("m=video " + ssrcInfo.getPort() + " RTP/AVP 96 126 125 99 34 98 97\r\n"); + } + content.append("a=recvonly\r\n"); + content.append("a=rtpmap:96 PS/90000\r\n"); + content.append("a=fmtp:126 profile-level-id=42e01e\r\n"); + content.append("a=rtpmap:126 H264/90000\r\n"); + content.append("a=rtpmap:125 H264S/90000\r\n"); + content.append("a=fmtp:125 profile-level-id=42e01e\r\n"); + content.append("a=rtpmap:99 H265/90000\r\n"); + content.append("a=rtpmap:98 H264/90000\r\n"); + content.append("a=rtpmap:97 MPEG4/90000\r\n"); + if ("TCP-PASSIVE".equalsIgnoreCase(streamMode)) { // tcp被动模式 + content.append("a=setup:passive\r\n"); + content.append("a=connection:new\r\n"); + } else if ("TCP-ACTIVE".equalsIgnoreCase(streamMode)) { // tcp主动模式 + content.append("a=setup:active\r\n"); + content.append("a=connection:new\r\n"); + } + } else { + if ("TCP-PASSIVE".equalsIgnoreCase(streamMode)) { + content.append("m=video " + ssrcInfo.getPort() + " TCP/RTP/AVP 96 97 98 99\r\n"); + } else if ("TCP-ACTIVE".equalsIgnoreCase(streamMode)) { + content.append("m=video " + ssrcInfo.getPort() + " TCP/RTP/AVP 96 97 98 99\r\n"); + } else if ("UDP".equalsIgnoreCase(streamMode)) { + content.append("m=video " + ssrcInfo.getPort() + " RTP/AVP 96 97 98 99\r\n"); + } + content.append("a=recvonly\r\n"); + content.append("a=rtpmap:96 PS/90000\r\n"); + content.append("a=rtpmap:97 MPEG4/90000\r\n"); + content.append("a=rtpmap:98 H264/90000\r\n"); + content.append("a=rtpmap:99 H265/90000\r\n"); + if ("TCP-PASSIVE".equalsIgnoreCase(streamMode)) { + // tcp被动模式 + content.append("a=setup:passive\r\n"); + content.append("a=connection:new\r\n"); + } else if ("TCP-ACTIVE".equalsIgnoreCase(streamMode)) { + // tcp主动模式 + content.append("a=setup:active\r\n"); + content.append("a=connection:new\r\n"); + } + } + + //ssrc + content.append("y=" + ssrcInfo.getSsrc() + "\r\n"); + + Request request = headerProvider.createPlaybackInviteRequest(device, channel.getDeviceId(), content.toString(), SipUtils.getNewViaTag(), SipUtils.getNewFromTag(), null,sipSender.getNewCallIdHeader(sipLayer.getLocalIp(device.getLocalIp()),device.getTransport()), ssrcInfo.getSsrc()); + + sipSender.transmitRequest(sipLayer.getLocalIp(device.getLocalIp()), request, errorEvent, event -> { + ResponseEvent responseEvent = (ResponseEvent) event.event; + SIPResponse response = (SIPResponse) responseEvent.getResponse(); + SsrcTransaction ssrcTransaction = SsrcTransaction.buildForDevice(device.getDeviceId(), + channel.getId(), sipSender.getNewCallIdHeader(sipLayer.getLocalIp(device.getLocalIp()), + device.getTransport()).getCallId(), ssrcInfo.getApp(), ssrcInfo.getStream(), ssrcInfo.getSsrc(), + mediaServerItem.getId(), response, InviteSessionType.PLAYBACK); + sessionManager.put(ssrcTransaction); + okEvent.response(event); + }, timeout); + } + + /** + * 请求历史媒体下载 + */ + @Override + public void downloadStreamCmd(MediaServer mediaServerItem, SSRCInfo ssrcInfo, Device device, DeviceChannel channel, + String startTime, String endTime, int downloadSpeed, + SipSubscribe.Event errorEvent, SipSubscribe.Event okEvent, Long timeout) throws InvalidArgumentException, SipException, ParseException { + + log.info("[发送-请求历史媒体下载-命令] 流ID: {},节点为: {} [{}:{}]", ssrcInfo.getStream(), mediaServerItem.getId(), mediaServerItem.getSdpIp(), ssrcInfo.getPort()); + String sdpIp; + if (!ObjectUtils.isEmpty(device.getSdpIp())) { + sdpIp = device.getSdpIp(); + }else { + sdpIp = mediaServerItem.getSdpIp(); + } + StringBuffer content = new StringBuffer(200); + content.append("v=0\r\n"); + content.append("o=" + device.getDeviceId() + " 0 0 IN IP4 " + sdpIp + "\r\n"); + content.append("s=Download\r\n"); + content.append("u=" + channel.getDeviceId() + ":0\r\n"); + content.append("c=IN IP4 " + sdpIp + "\r\n"); + content.append("t=" + DateUtil.yyyy_MM_dd_HH_mm_ssToTimestamp(startTime) + " " + + DateUtil.yyyy_MM_dd_HH_mm_ssToTimestamp(endTime) + "\r\n"); + + String streamMode = device.getStreamMode().toUpperCase(); + + if (userSetting.getSeniorSdp()) { + if ("TCP-PASSIVE".equals(streamMode)) { + content.append("m=video " + ssrcInfo.getPort() + " TCP/RTP/AVP 96 126 125 99 34 98 97\r\n"); + } else if ("TCP-ACTIVE".equals(streamMode)) { + content.append("m=video " + ssrcInfo.getPort() + " TCP/RTP/AVP 96 126 125 99 34 98 97\r\n"); + } else if ("UDP".equals(streamMode)) { + content.append("m=video " + ssrcInfo.getPort() + " RTP/AVP 96 126 125 99 34 98 97\r\n"); + } + content.append("a=recvonly\r\n"); + content.append("a=rtpmap:96 PS/90000\r\n"); + content.append("a=fmtp:126 profile-level-id=42e01e\r\n"); + content.append("a=rtpmap:126 H264/90000\r\n"); + content.append("a=rtpmap:125 H264S/90000\r\n"); + content.append("a=fmtp:125 profile-level-id=42e01e\r\n"); + content.append("a=rtpmap:99 MP4V-ES/90000\r\n"); + content.append("a=fmtp:99 profile-level-id=3\r\n"); + content.append("a=rtpmap:98 H264/90000\r\n"); + content.append("a=rtpmap:97 MPEG4/90000\r\n"); + if ("TCP-PASSIVE".equals(streamMode)) { // tcp被动模式 + content.append("a=setup:passive\r\n"); + content.append("a=connection:new\r\n"); + } else if ("TCP-ACTIVE".equals(streamMode)) { // tcp主动模式 + content.append("a=setup:active\r\n"); + content.append("a=connection:new\r\n"); + } + } else { + if ("TCP-PASSIVE".equals(streamMode)) { + content.append("m=video " + ssrcInfo.getPort() + " TCP/RTP/AVP 96 97 98 99\r\n"); + } else if ("TCP-ACTIVE".equals(streamMode)) { + content.append("m=video " + ssrcInfo.getPort() + " TCP/RTP/AVP 96 97 98 99\r\n"); + } else if ("UDP".equals(streamMode)) { + content.append("m=video " + ssrcInfo.getPort() + " RTP/AVP 96 97 98 99\r\n"); + } + content.append("a=recvonly\r\n"); + content.append("a=rtpmap:96 PS/90000\r\n"); + content.append("a=rtpmap:97 MPEG4/90000\r\n"); + content.append("a=rtpmap:98 H264/90000\r\n"); + content.append("a=rtpmap:99 H265/90000\r\n"); + if ("TCP-PASSIVE".equals(streamMode)) { // tcp被动模式 + content.append("a=setup:passive\r\n"); + content.append("a=connection:new\r\n"); + } else if ("TCP-ACTIVE".equals(streamMode)) { // tcp主动模式 + content.append("a=setup:active\r\n"); + content.append("a=connection:new\r\n"); + } + } + content.append("a=downloadspeed:" + downloadSpeed + "\r\n"); + + content.append("y=" + ssrcInfo.getSsrc() + "\r\n");//ssrc + log.debug("此时请求下载信令的ssrc===>{}",ssrcInfo.getSsrc()); + // 添加订阅 + CallIdHeader newCallIdHeader = sipSender.getNewCallIdHeader(sipLayer.getLocalIp(device.getLocalIp()), device.getTransport()); + Request request = headerProvider.createPlaybackInviteRequest(device, channel.getDeviceId(), content.toString(), SipUtils.getNewViaTag(), SipUtils.getNewFromTag(), null,newCallIdHeader, ssrcInfo.getSsrc()); + + sipSender.transmitRequest(sipLayer.getLocalIp(device.getLocalIp()), request, errorEvent, event -> { + ResponseEvent responseEvent = (ResponseEvent) event.event; + SIPResponse response = (SIPResponse) responseEvent.getResponse(); + String contentString =new String(response.getRawContent()); + String ssrc = SipUtils.getSsrcFromSdp(contentString); + SsrcTransaction ssrcTransaction = SsrcTransaction.buildForDevice(device.getDeviceId(), channel.getId(), + response.getCallIdHeader().getCallId(), ssrcInfo.getApp(), ssrcInfo.getStream(), ssrc, + mediaServerItem.getId(), response, InviteSessionType.DOWNLOAD); + sessionManager.put(ssrcTransaction); + okEvent.response(event); + }, timeout); + } + + @Override + public void talkStreamCmd(MediaServer mediaServerItem, SendRtpInfo sendRtpItem, Device device, DeviceChannel channel, + String callId, HookSubscribe.Event event, HookSubscribe.Event eventForPush, SipSubscribe.Event okEvent, + SipSubscribe.Event errorEvent, Long timeout) throws InvalidArgumentException, SipException, ParseException { + + String stream = sendRtpItem.getStream(); + + if (device == null) { + return; + } + if (!mediaServerItem.isRtpEnable()) { + // 单端口暂不支持语音喊话 + log.info("[语音喊话] 单端口暂不支持此操作"); + return; + } + + log.info("[语音喊话] {} 分配的ZLM为: {} [{}:{}]", stream, mediaServerItem.getId(), mediaServerItem.getIp(), sendRtpItem.getPort()); + Hook hook = Hook.getInstance(HookType.on_media_arrival, "rtp", stream, mediaServerItem.getId()); + subscribe.addSubscribe(hook, (hookData) -> { + if (event != null) { + event.response(hookData); + subscribe.removeSubscribe(hook); + } + }); + + CallIdHeader callIdHeader = sipSender.getNewCallIdHeader(sipLayer.getLocalIp(device.getLocalIp()), device.getTransport()); + callIdHeader.setCallId(callId); + Hook publishHook = Hook.getInstance(HookType.on_publish, "rtp", stream, mediaServerItem.getId()); + subscribe.addSubscribe(publishHook, (hookData) -> { + if (eventForPush != null) { + eventForPush.response(hookData); + } + }); + // + StringBuffer content = new StringBuffer(200); + content.append("v=0\r\n"); + content.append("o=" + device.getDeviceId() + " 0 0 IN IP4 " + mediaServerItem.getSdpIp() + "\r\n"); + content.append("s=Talk\r\n"); + content.append("c=IN IP4 " + mediaServerItem.getSdpIp() + "\r\n"); + content.append("t=0 0\r\n"); + + content.append("m=audio " + sendRtpItem.getPort() + " TCP/RTP/AVP 8\r\n"); + content.append("a=setup:passive\r\n"); + content.append("a=connection:new\r\n"); + content.append("a=sendrecv\r\n"); + content.append("a=rtpmap:8 PCMA/8000\r\n"); + + content.append("y=" + sendRtpItem.getSsrc() + "\r\n");//ssrc + // f字段:f= v/编码格式/分辨率/帧率/码率类型/码率大小a/编码格式/码率大小/采样率 + content.append("f=v/////a/1/8/1" + "\r\n"); + + Request request = headerProvider.createInviteRequest(device, channel.getDeviceId(), content.toString(), + SipUtils.getNewViaTag(), SipUtils.getNewFromTag(), null, sendRtpItem.getSsrc(), callIdHeader); + sipSender.transmitRequest(sipLayer.getLocalIp(device.getLocalIp()), request, (e -> { + sessionManager.removeByStream(sendRtpItem.getApp(), sendRtpItem.getStream()); + mediaServerService.releaseSsrc(mediaServerItem.getId(), sendRtpItem.getSsrc()); + errorEvent.response(e); + }), e -> { + // 这里为例避免一个通道的点播只有一个callID这个参数使用一个固定值 + ResponseEvent responseEvent = (ResponseEvent) e.event; + SIPResponse response = (SIPResponse) responseEvent.getResponse(); + SsrcTransaction ssrcTransaction = SsrcTransaction.buildForDevice(device.getDeviceId(), channel.getId(), "talk",sendRtpItem.getApp(), stream, sendRtpItem.getSsrc(), mediaServerItem.getId(), response, InviteSessionType.TALK); + sessionManager.put(ssrcTransaction); + okEvent.response(e); + }, timeout); + } + + /** + * 视频流停止 + */ + @Override + public void streamByeCmd(Device device, String channelId, String app, String stream, String callId, SipSubscribe.Event okEvent) throws InvalidArgumentException, SipException, ParseException, SsrcTransactionNotFoundException { + if (device == null) { + log.warn("[发送BYE] device为null"); + return; + } + SsrcTransaction ssrcTransaction = null; + if (callId != null) { + ssrcTransaction = sessionManager.getSsrcTransactionByCallId(callId); + }else if (stream != null) { + ssrcTransaction = sessionManager.getSsrcTransactionByStream(app, stream); + } + + if (ssrcTransaction == null) { + log.info("[发送BYE] 未找到事务信息,设备: device: {}, channel: {}", device.getDeviceId(), channelId); + throw new SsrcTransactionNotFoundException(device.getDeviceId(), channelId, callId, stream); + } + + log.info("[发送BYE] 设备: device: {}, channel: {}, callId: {}", device.getDeviceId(), channelId, ssrcTransaction.getCallId()); + sessionManager.removeByCallId(ssrcTransaction.getCallId()); + Request byteRequest = headerProvider.createByteRequest(device, channelId, ssrcTransaction.getSipTransactionInfo()); + sipSender.transmitRequest(sipLayer.getLocalIp(device.getLocalIp()), byteRequest, null, okEvent); + } + + @Override + public void streamByeCmd(Device device, String channelId, SipTransactionInfo sipTransactionInfo, SipSubscribe.Event okEvent) throws InvalidArgumentException, SipException, ParseException, SsrcTransactionNotFoundException { + Request byteRequest = headerProvider.createByteRequest(device, channelId, sipTransactionInfo); + sipSender.transmitRequest(sipLayer.getLocalIp(device.getLocalIp()), byteRequest, null, okEvent); + } + + @Override + public void streamByeCmdForDeviceInvite(Device device, String channelId, SipTransactionInfo sipTransactionInfo, SipSubscribe.Event okEvent) throws InvalidArgumentException, SipException, ParseException, SsrcTransactionNotFoundException { + Request byteRequest = headerProvider.createByteRequestForDeviceInvite(device, channelId, sipTransactionInfo); + sipSender.transmitRequest(sipLayer.getLocalIp(device.getLocalIp()), byteRequest, null, okEvent); + } + + /** + * 语音广播 + * + * @param device 视频设备 + */ + @Override + public void audioBroadcastCmd(Device device, String channelId, SipSubscribe.Event okEvent, SipSubscribe.Event errorEvent) throws InvalidArgumentException, SipException, ParseException { + StringBuffer broadcastXml = new StringBuffer(200); + String charset = device.getCharset(); + broadcastXml.append("\r\n"); + broadcastXml.append("\r\n"); + broadcastXml.append("Broadcast\r\n"); + broadcastXml.append("" + (int)((Math.random()*9+1)*100000) + "\r\n"); + broadcastXml.append("" + sipConfig.getId() + "\r\n"); + broadcastXml.append("" + channelId + "\r\n"); + broadcastXml.append("\r\n"); + + Request request = headerProvider.createMessageRequest(device, broadcastXml.toString(), SipUtils.getNewViaTag(), SipUtils.getNewFromTag(), null,sipSender.getNewCallIdHeader(sipLayer.getLocalIp(device.getLocalIp()),device.getTransport())); + sipSender.transmitRequest(sipLayer.getLocalIp(device.getLocalIp()), request, errorEvent, okEvent); + + } + + + /** + * 音视频录像控制 + * + * @param device 视频设备 + * @param channelId 预览通道 + * @param recordCmdStr 录像命令:Record / StopRecord + */ + @Override + public void recordCmd(Device device, String channelId, String recordCmdStr, ErrorCallback callback) throws InvalidArgumentException, SipException, ParseException { + final String cmdType = "DeviceControl"; + final int sn = (int) ((Math.random() * 9 + 1) * 100000); + + StringBuffer cmdXml = new StringBuffer(200); + String charset = device.getCharset(); + cmdXml.append("\r\n"); + cmdXml.append("\r\n"); + cmdXml.append("" + cmdType + "\r\n"); + cmdXml.append("" + sn + "\r\n"); + if (ObjectUtils.isEmpty(channelId)) { + cmdXml.append("" + device.getDeviceId() + "\r\n"); + } else { + cmdXml.append("" + channelId + "\r\n"); + } + cmdXml.append("" + recordCmdStr + "\r\n"); + cmdXml.append("\r\n"); + + MessageEvent messageEvent = MessageEvent.getInstance(cmdType, sn + "", channelId, 1000L, callback); + messageSubscribe.addSubscribe(messageEvent); + + Request request = headerProvider.createMessageRequest(device, cmdXml.toString(), null, SipUtils.getNewFromTag(), null,sipSender.getNewCallIdHeader(sipLayer.getLocalIp(device.getLocalIp()),device.getTransport())); + sipSender.transmitRequest(sipLayer.getLocalIp(device.getLocalIp()), request, eventResult -> { + messageSubscribe.removeSubscribe(messageEvent.getKey()); + callback.run(ErrorCode.ERROR100.getCode(), "失败," + eventResult.msg, null); + },null); + } + + /** + * 远程启动控制命令 + * + * @param device 视频设备 + */ + @Override + public void teleBootCmd(Device device) throws InvalidArgumentException, SipException, ParseException { + + StringBuffer cmdXml = new StringBuffer(200); + String charset = device.getCharset(); + cmdXml.append("\r\n"); + cmdXml.append("\r\n"); + cmdXml.append("DeviceControl\r\n"); + cmdXml.append("" + (int) ((Math.random() * 9 + 1) * 100000) + "\r\n"); + cmdXml.append("" + device.getDeviceId() + "\r\n"); + cmdXml.append("Boot\r\n"); + cmdXml.append("\r\n"); + + + + Request request = headerProvider.createMessageRequest(device, cmdXml.toString(), null, SipUtils.getNewFromTag(), null,sipSender.getNewCallIdHeader(sipLayer.getLocalIp(device.getLocalIp()),device.getTransport())); + sipSender.transmitRequest(sipLayer.getLocalIp(device.getLocalIp()), request); + } + + /** + * 报警布防/撤防命令 + * + * @param device 视频设备 + * @param guardCmdStr "SetGuard"/"ResetGuard" + */ + @Override + public void guardCmd(Device device, String guardCmdStr, ErrorCallback callback) throws InvalidArgumentException, SipException, ParseException { + + String cmdType = "DeviceControl"; + int sn = (int) ((Math.random() * 9 + 1) * 100000); + + StringBuffer cmdXml = new StringBuffer(200); + String charset = device.getCharset(); + cmdXml.append("\r\n"); + cmdXml.append("\r\n"); + cmdXml.append("" + cmdType + "\r\n"); + cmdXml.append("" + sn + "\r\n"); + cmdXml.append("" + device.getDeviceId() + "\r\n"); + cmdXml.append("" + guardCmdStr + "\r\n"); + cmdXml.append("\r\n"); + + MessageEvent messageEvent = MessageEvent.getInstance(cmdType, sn + "", device.getDeviceId(), 1000L, callback); + messageSubscribe.addSubscribe(messageEvent); + + Request request = headerProvider.createMessageRequest(device, cmdXml.toString(), null, SipUtils.getNewFromTag(), null,sipSender.getNewCallIdHeader(sipLayer.getLocalIp(device.getLocalIp()),device.getTransport())); + sipSender.transmitRequest(sipLayer.getLocalIp(device.getLocalIp()), request, eventResult -> { + messageSubscribe.removeSubscribe(messageEvent.getKey()); + callback.run(ErrorCode.ERROR100.getCode(), "失败," + eventResult.msg, null); + }); + } + + /** + * 报警复位命令 + * + * @param device 视频设备 + */ + @Override + public void alarmResetCmd(Device device, String alarmMethod, String alarmType, ErrorCallback callback) throws InvalidArgumentException, SipException, ParseException { + + String cmdType = "DeviceControl"; + int sn = (int) ((Math.random() * 9 + 1) * 100000); + + StringBuffer cmdXml = new StringBuffer(200); + String charset = device.getCharset(); + cmdXml.append("\r\n"); + cmdXml.append("\r\n"); + cmdXml.append("" + cmdType + "\r\n"); + cmdXml.append("" + sn + "\r\n"); + cmdXml.append("" + device.getDeviceId() + "\r\n"); + cmdXml.append("ResetAlarm\r\n"); + if (!ObjectUtils.isEmpty(alarmMethod) || !ObjectUtils.isEmpty(alarmType)) { + cmdXml.append("\r\n"); + } + if (!ObjectUtils.isEmpty(alarmMethod)) { + cmdXml.append("" + alarmMethod + "\r\n"); + } + if (!ObjectUtils.isEmpty(alarmType)) { + cmdXml.append("" + alarmType + "\r\n"); + } + if (!ObjectUtils.isEmpty(alarmMethod) || !ObjectUtils.isEmpty(alarmType)) { + cmdXml.append("\r\n"); + } + cmdXml.append("\r\n"); + + MessageEvent messageEvent = MessageEvent.getInstance(cmdType, sn + "", device.getDeviceId(), 1000L, callback); + messageSubscribe.addSubscribe(messageEvent); + + Request request = headerProvider.createMessageRequest(device, cmdXml.toString(), null, SipUtils.getNewFromTag(), null,sipSender.getNewCallIdHeader(sipLayer.getLocalIp(device.getLocalIp()),device.getTransport())); + sipSender.transmitRequest(sipLayer.getLocalIp(device.getLocalIp()), request, eventResult -> { + messageSubscribe.removeSubscribe(messageEvent.getKey()); + callback.run(ErrorCode.ERROR100.getCode(), "失败," + eventResult.msg, null); + }); + } + + /** + * 强制关键帧命令,设备收到此命令应立刻发送一个IDR帧 + * + * @param device 视频设备 + * @param channelId 预览通道 + */ + @Override + public void iFrameCmd(Device device, String channelId) throws InvalidArgumentException, SipException, ParseException { + + StringBuffer cmdXml = new StringBuffer(200); + String charset = device.getCharset(); + cmdXml.append("\r\n"); + cmdXml.append("\r\n"); + cmdXml.append("DeviceControl\r\n"); + cmdXml.append("" + (int) ((Math.random() * 9 + 1) * 100000) + "\r\n"); + if (ObjectUtils.isEmpty(channelId)) { + cmdXml.append("" + device.getDeviceId() + "\r\n"); + } else { + cmdXml.append("" + channelId + "\r\n"); + } + cmdXml.append("Send\r\n"); + cmdXml.append("\r\n"); + + + + Request request = headerProvider.createMessageRequest(device, cmdXml.toString(), null, SipUtils.getNewFromTag(), null,sipSender.getNewCallIdHeader(sipLayer.getLocalIp(device.getLocalIp()),device.getTransport())); + sipSender.transmitRequest(sipLayer.getLocalIp(device.getLocalIp()), request); + } + + /** + * 看守位控制命令 + * + * @param device 视频设备 + * @param channelId 通道id,非通道则是设备本身 + * @param enabled 看守位使能:1 = 开启,0 = 关闭 + * @param resetTime 自动归位时间间隔,开启看守位时使用,单位:秒(s) + * @param presetIndex 调用预置位编号,开启看守位时使用,取值范围0~255 + */ + @Override + public void homePositionCmd(Device device, String channelId, Boolean enabled, Integer resetTime, Integer presetIndex, ErrorCallback callback) throws InvalidArgumentException, SipException, ParseException { + + String cmdType = "DeviceControl"; + int sn = (int) ((Math.random() * 9 + 1) * 100000); + + StringBuffer cmdXml = new StringBuffer(200); + String charset = device.getCharset(); + cmdXml.append("\r\n"); + cmdXml.append("\r\n"); + cmdXml.append("" + cmdType + "\r\n"); + cmdXml.append("" + sn + "\r\n"); + if (ObjectUtils.isEmpty(channelId)) { + channelId = device.getDeviceId(); + } + cmdXml.append("" + channelId + "\r\n"); + cmdXml.append("\r\n"); + if (enabled) { + cmdXml.append("1\r\n"); + cmdXml.append("" + resetTime + "\r\n"); + cmdXml.append("" + presetIndex + "\r\n"); + } else { + cmdXml.append("0\r\n"); + } + cmdXml.append("\r\n"); + cmdXml.append("\r\n"); + + MessageEvent messageEvent = MessageEvent.getInstance(cmdType, sn + "", channelId, 1000L, callback); + messageSubscribe.addSubscribe(messageEvent); + + Request request = headerProvider.createMessageRequest(device, cmdXml.toString(), null, SipUtils.getNewFromTag(), null,sipSender.getNewCallIdHeader(sipLayer.getLocalIp(device.getLocalIp()),device.getTransport())); + sipSender.transmitRequest(sipLayer.getLocalIp(device.getLocalIp()), request, eventResult -> { + messageSubscribe.removeSubscribe(messageEvent.getKey()); + callback.run(ErrorCode.ERROR100.getCode(), "失败," + eventResult.msg, null); + }); + } + + /** + * 设备配置命令 + * + * @param device 视频设备 + */ + @Override + public void deviceConfigCmd(Device device) { + // TODO Auto-generated method stub + } + + /** + * 设备配置命令:basicParam + */ + @Override + public void deviceBasicConfigCmd(Device device, BasicParam basicParam, ErrorCallback callback) throws InvalidArgumentException, SipException, ParseException { + + int sn = (int) ((Math.random() * 9 + 1) * 100000); + String cmdType = "DeviceConfig"; + StringBuffer cmdXml = new StringBuffer(200); + String charset = device.getCharset(); + cmdXml.append("\r\n"); + cmdXml.append("\r\n"); + cmdXml.append("" + cmdType + "\r\n"); + cmdXml.append("" + sn + "\r\n"); + String channelId = basicParam.getChannelId(); + if (ObjectUtils.isEmpty(channelId)) { + channelId = device.getDeviceId(); + } + cmdXml.append("" + channelId + "\r\n"); + cmdXml.append("\r\n"); + if (!ObjectUtils.isEmpty(basicParam.getName())) { + cmdXml.append("" + basicParam.getName() + "\r\n"); + } + if (NumericUtil.isInteger(basicParam.getExpiration())) { + if (Integer.parseInt(basicParam.getExpiration()) > 0) { + cmdXml.append("" + basicParam.getExpiration() + "\r\n"); + } + } + if (basicParam.getHeartBeatInterval() != null && basicParam.getHeartBeatInterval() > 0) { + cmdXml.append("" + basicParam.getHeartBeatInterval() + "\r\n"); + } + if (basicParam.getHeartBeatCount() != null && basicParam.getHeartBeatCount() > 0) { + cmdXml.append("" + basicParam.getHeartBeatCount() + "\r\n"); + } + cmdXml.append("\r\n"); + cmdXml.append("\r\n"); + + + MessageEvent messageEvent = MessageEvent.getInstance(cmdType, sn + "", channelId, 1000L, callback); + messageSubscribe.addSubscribe(messageEvent); + + Request request = headerProvider.createMessageRequest(device, cmdXml.toString(), null, SipUtils.getNewFromTag(), null,sipSender.getNewCallIdHeader(sipLayer.getLocalIp(device.getLocalIp()),device.getTransport())); + sipSender.transmitRequest(sipLayer.getLocalIp(device.getLocalIp()), request, eventResult -> { + messageSubscribe.removeSubscribe(messageEvent.getKey()); + callback.run(ErrorCode.ERROR100.getCode(), "失败," + eventResult.msg, null); + }); + } + + /** + * 查询设备状态 + * + * @param device 视频设备 + */ + @Override + public void deviceStatusQuery(Device device, ErrorCallback callback) throws InvalidArgumentException, SipException, ParseException { + + String cmdType = "DeviceStatus"; + int sn = (int) ((Math.random() * 9 + 1) * 100000); + + String charset = device.getCharset(); + StringBuffer catalogXml = new StringBuffer(200); + catalogXml.append("\r\n"); + catalogXml.append("\r\n"); + catalogXml.append("" + cmdType + "\r\n"); + catalogXml.append("" + sn + "\r\n"); + catalogXml.append("" + device.getDeviceId() + "\r\n"); + catalogXml.append("\r\n"); + + MessageEvent messageEvent = MessageEvent.getInstance(cmdType, sn + "", device.getDeviceId(), 1000L, callback); + messageSubscribe.addSubscribe(messageEvent); + + Request request = headerProvider.createMessageRequest(device, catalogXml.toString(), null, SipUtils.getNewFromTag(), null,sipSender.getNewCallIdHeader(sipLayer.getLocalIp(device.getLocalIp()),device.getTransport())); + + sipSender.transmitRequest(sipLayer.getLocalIp(device.getLocalIp()), request, eventResult -> { + messageSubscribe.removeSubscribe(messageEvent.getKey()); + callback.run(ErrorCode.ERROR100.getCode(), "失败," + eventResult.msg, null); + }); + } + + /** + * 查询设备信息 + * + * @param device 视频设备 + * @param callback + */ + @Override + public void deviceInfoQuery(Device device, ErrorCallback callback) throws InvalidArgumentException, SipException, ParseException { + + String cmdType = "DeviceInfo"; + String sn = (int) ((Math.random() * 9 + 1) * 100000) + ""; + + StringBuffer catalogXml = new StringBuffer(200); + String charset = device.getCharset(); + catalogXml.append("\r\n"); + catalogXml.append("\r\n"); + catalogXml.append("" + cmdType +"\r\n"); + catalogXml.append("" + sn + "\r\n"); + catalogXml.append("" + device.getDeviceId() + "\r\n"); + catalogXml.append("\r\n"); + + MessageEvent messageEvent = MessageEvent.getInstance(cmdType, sn, device.getDeviceId(), 1000L, callback); + messageSubscribe.addSubscribe(messageEvent); + + Request request = headerProvider.createMessageRequest(device, catalogXml.toString(), SipUtils.getNewViaTag(), SipUtils.getNewFromTag(), null,sipSender.getNewCallIdHeader(sipLayer.getLocalIp(device.getLocalIp()),device.getTransport())); + + sipSender.transmitRequest(sipLayer.getLocalIp(device.getLocalIp()), request, eventResult -> { + messageSubscribe.removeSubscribe(messageEvent.getKey()); + if (callback != null) { + callback.run(ErrorCode.ERROR100.getCode(), "失败," + eventResult.msg, null); + } + }); + + } + + /** + * 查询目录列表 + * + * @param device 视频设备 + */ + @Override + public void catalogQuery(Device device, int sn, SipSubscribe.Event errorEvent) throws SipException, InvalidArgumentException, ParseException { + + StringBuffer catalogXml = new StringBuffer(200); + String charset = device.getCharset(); + catalogXml.append("\r\n"); + catalogXml.append("\r\n"); + catalogXml.append(" Catalog\r\n"); + catalogXml.append(" " + sn + "\r\n"); + catalogXml.append(" " + device.getDeviceId() + "\r\n"); + catalogXml.append("\r\n"); + + Request request = headerProvider.createMessageRequest(device, catalogXml.toString(), SipUtils.getNewViaTag(), SipUtils.getNewFromTag(), null,sipSender.getNewCallIdHeader(sipLayer.getLocalIp(device.getLocalIp()),device.getTransport())); + + sipSender.transmitRequest(sipLayer.getLocalIp(device.getLocalIp()), request, errorEvent); + } + + /** + * 查询录像信息 + * + * @param device 视频设备 + * @param startTime 开始时间,格式要求:yyyy-MM-dd HH:mm:ss + * @param endTime 结束时间,格式要求:yyyy-MM-dd HH:mm:ss + */ + @Override + public void recordInfoQuery(Device device, String channelId, String startTime, String endTime, int sn, Integer secrecy, String type, SipSubscribe.Event okEvent, SipSubscribe.Event errorEvent) throws InvalidArgumentException, SipException, ParseException { + if (secrecy == null) { + secrecy = 0; + } + if (type == null) { + type = "all"; + } + + StringBuffer recordInfoXml = new StringBuffer(200); + String charset = device.getCharset(); + recordInfoXml.append("\r\n"); + recordInfoXml.append("\r\n"); + recordInfoXml.append("RecordInfo\r\n"); + recordInfoXml.append("" + sn + "\r\n"); + recordInfoXml.append("" + channelId + "\r\n"); + if (startTime != null) { + recordInfoXml.append("" + DateUtil.yyyy_MM_dd_HH_mm_ssToISO8601(startTime) + "\r\n"); + } + if (endTime != null) { + recordInfoXml.append("" + DateUtil.yyyy_MM_dd_HH_mm_ssToISO8601(endTime) + "\r\n"); + } + if (secrecy != null) { + recordInfoXml.append(" " + secrecy + " \r\n"); + } + if (type != null) { + // 大华NVR要求必须增加一个值为all的文本元素节点Type + recordInfoXml.append("" + type + "\r\n"); + } + recordInfoXml.append("\r\n"); + + + + Request request = headerProvider.createMessageRequest(device, recordInfoXml.toString(), + SipUtils.getNewViaTag(), SipUtils.getNewFromTag(), null,sipSender.getNewCallIdHeader(sipLayer.getLocalIp(device.getLocalIp()),device.getTransport())); + + sipSender.transmitRequest(sipLayer.getLocalIp(device.getLocalIp()), request, errorEvent, okEvent); + } + + /** + * 查询报警信息 + * + * @param device 视频设备 + * @param startPriority 报警起始级别(可选) + * @param endPriority 报警终止级别(可选) + * @param alarmMethod 报警方式条件(可选) + * @param alarmType 报警类型 + * @param startTime 报警发生起始时间(可选) + * @param endTime 报警发生终止时间(可选) + * @return true = 命令发送成功 + */ + @Override + public void alarmInfoQuery(Device device, String startPriority, String endPriority, String alarmMethod, String alarmType, + String startTime, String endTime, ErrorCallback callback) throws InvalidArgumentException, SipException, ParseException { + + String cmdType = "Alarm"; + String sn = (int) ((Math.random() * 9 + 1) * 100000) + ""; + + StringBuffer cmdXml = new StringBuffer(200); + String charset = device.getCharset(); + cmdXml.append("\r\n"); + cmdXml.append("\r\n"); + cmdXml.append("" + cmdType + "\r\n"); + cmdXml.append("" + sn + "\r\n"); + cmdXml.append("" + device.getDeviceId() + "\r\n"); + if (!ObjectUtils.isEmpty(startPriority)) { + cmdXml.append("" + startPriority + "\r\n"); + } + if (!ObjectUtils.isEmpty(endPriority)) { + cmdXml.append("" + endPriority + "\r\n"); + } + if (!ObjectUtils.isEmpty(alarmMethod)) { + cmdXml.append("" + alarmMethod + "\r\n"); + } + if (!ObjectUtils.isEmpty(alarmType)) { + cmdXml.append("" + alarmType + "\r\n"); + } + if (!ObjectUtils.isEmpty(startTime)) { + cmdXml.append("" + startTime + "\r\n"); + } + if (!ObjectUtils.isEmpty(endTime)) { + cmdXml.append("" + endTime + "\r\n"); + } + cmdXml.append("\r\n"); + + MessageEvent messageEvent = MessageEvent.getInstance(cmdType, sn, device.getDeviceId(), 1000L, callback); + messageSubscribe.addSubscribe(messageEvent); + + Request request = headerProvider.createMessageRequest(device, cmdXml.toString(), null, SipUtils.getNewFromTag(), null,sipSender.getNewCallIdHeader(sipLayer.getLocalIp(device.getLocalIp()),device.getTransport())); + sipSender.transmitRequest(sipLayer.getLocalIp(device.getLocalIp()), request, eventResult -> { + messageSubscribe.removeSubscribe(messageEvent.getKey()); + callback.run(ErrorCode.ERROR100.getCode(), "失败," + eventResult.msg, null); + }); + } + + /** + * 查询设备配置 + * + * @param device 视频设备 + * @param channelId 通道编码(可选) + * @param configType 配置类型: + */ + @Override + public void deviceConfigQuery(Device device, String channelId, String configType, ErrorCallback callback) throws InvalidArgumentException, SipException, ParseException { + + String cmdType = "ConfigDownload"; + int sn = (int) ((Math.random() * 9 + 1) * 100000); + StringBuffer cmdXml = new StringBuffer(200); + String charset = device.getCharset(); + cmdXml.append("\r\n"); + cmdXml.append("\r\n"); + cmdXml.append("" + cmdType + "\r\n"); + cmdXml.append("" + sn + "\r\n"); + if (ObjectUtils.isEmpty(channelId)) { + cmdXml.append("" + device.getDeviceId() + "\r\n"); + } else { + cmdXml.append("" + channelId + "\r\n"); + } + cmdXml.append("" + configType + "\r\n"); + cmdXml.append("\r\n"); + + MessageEvent messageEvent = MessageEvent.getInstance(cmdType, sn + "", channelId, 1000L, callback); + messageSubscribe.addSubscribe(messageEvent); + + Request request = headerProvider.createMessageRequest(device, cmdXml.toString(), null, SipUtils.getNewFromTag(), null,sipSender.getNewCallIdHeader(sipLayer.getLocalIp(device.getLocalIp()),device.getTransport())); + sipSender.transmitRequest(sipLayer.getLocalIp(device.getLocalIp()), request, eventResult -> { + messageSubscribe.removeSubscribe(messageEvent.getKey()); + if (callback != null) { + callback.run(ErrorCode.ERROR100.getCode(), "失败," + eventResult.msg, null); + } + }); + } + + /** + * 查询设备预置位置 + * + * @param device 视频设备 + */ + @Override + public void presetQuery(Device device, String channelId, ErrorCallback> callback) throws InvalidArgumentException, SipException, ParseException { + + String cmdType = "PresetQuery"; + int sn = (int) ((Math.random() * 9 + 1) * 100000); + + StringBuffer cmdXml = new StringBuffer(200); + String charset = device.getCharset(); + cmdXml.append("\r\n"); + cmdXml.append("\r\n"); + cmdXml.append("" + cmdType + "\r\n"); + cmdXml.append("" + sn + "\r\n"); + if (ObjectUtils.isEmpty(channelId)) { + cmdXml.append("" + device.getDeviceId() + "\r\n"); + } else { + cmdXml.append("" + channelId + "\r\n"); + } + cmdXml.append("\r\n"); + + MessageEvent> messageEvent = MessageEvent.getInstance(cmdType, sn + "", channelId, 4000L, callback); + messageSubscribe.addSubscribe(messageEvent); + log.info("[预置位查询] 设备编号: {}, 通道编号: {}, SN: {}", device.getDeviceId(), channelId, sn); + Request request = headerProvider.createMessageRequest(device, cmdXml.toString(), SipUtils.getNewViaTag(), SipUtils.getNewFromTag(), null,sipSender.getNewCallIdHeader(sipLayer.getLocalIp(device.getLocalIp()),device.getTransport())); + sipSender.transmitRequest(sipLayer.getLocalIp(device.getLocalIp()), request, eventResult -> { + messageSubscribe.removeSubscribe(messageEvent.getKey()); + callback.run(ErrorCode.ERROR100.getCode(), "失败," + eventResult.msg, null); + }); + } + + /** + * 查询移动设备位置数据 + * + * @param device 视频设备 + */ + @Override + public void mobilePostitionQuery(Device device, SipSubscribe.Event errorEvent) throws InvalidArgumentException, SipException, ParseException { + + StringBuffer mobilePostitionXml = new StringBuffer(200); + String charset = device.getCharset(); + mobilePostitionXml.append("\r\n"); + mobilePostitionXml.append("\r\n"); + mobilePostitionXml.append("MobilePosition\r\n"); + mobilePostitionXml.append("" + (int) ((Math.random() * 9 + 1) * 100000) + "\r\n"); + mobilePostitionXml.append("" + device.getDeviceId() + "\r\n"); + mobilePostitionXml.append("60\r\n"); + mobilePostitionXml.append("\r\n"); + + + + Request request = headerProvider.createMessageRequest(device, mobilePostitionXml.toString(), SipUtils.getNewViaTag(), SipUtils.getNewFromTag(), null,sipSender.getNewCallIdHeader(sipLayer.getLocalIp(device.getLocalIp()),device.getTransport())); + + sipSender.transmitRequest(sipLayer.getLocalIp(device.getLocalIp()), request, errorEvent); + + } + + /** + * 订阅、取消订阅移动位置 + * + * @param device 视频设备 + * @return true = 命令发送成功 + */ + @Override + public SIPRequest mobilePositionSubscribe(Device device, SipTransactionInfo sipTransactionInfo, SipSubscribe.Event okEvent, SipSubscribe.Event errorEvent) throws InvalidArgumentException, SipException, ParseException { + + StringBuffer subscribePostitionXml = new StringBuffer(200); + String charset = device.getCharset(); + subscribePostitionXml.append("\r\n"); + subscribePostitionXml.append("\r\n"); + subscribePostitionXml.append("MobilePosition\r\n"); + subscribePostitionXml.append("" + (int) ((Math.random() * 9 + 1) * 100000) + "\r\n"); + subscribePostitionXml.append("" + device.getDeviceId() + "\r\n"); + if (device.getSubscribeCycleForMobilePosition() > 0) { + subscribePostitionXml.append("" + device.getMobilePositionSubmissionInterval() + "\r\n"); + }else { + subscribePostitionXml.append("5\r\n"); + } + subscribePostitionXml.append("\r\n"); + + CallIdHeader callIdHeader; + + if (sipTransactionInfo != null) { + callIdHeader = SipFactory.getInstance().createHeaderFactory().createCallIdHeader(sipTransactionInfo.getCallId()); + } else { + callIdHeader = sipSender.getNewCallIdHeader(sipLayer.getLocalIp(device.getLocalIp()),device.getTransport()); + } + + int subscribeCycleForMobilePosition = device.getSubscribeCycleForMobilePosition(); + if (subscribeCycleForMobilePosition > 0) { + // 移动位置订阅有效期不小于 30 秒 + subscribeCycleForMobilePosition = Math.max(subscribeCycleForMobilePosition, 30); + } + SIPRequest request = (SIPRequest) headerProvider.createSubscribeRequest(device, subscribePostitionXml.toString(), sipTransactionInfo, subscribeCycleForMobilePosition, "presence",callIdHeader); //Position;id=" + tm.substring(tm.length() - 4)); + + sipSender.transmitRequest(sipLayer.getLocalIp(device.getLocalIp()), request, errorEvent, okEvent); + return request; + } + + /** + * 订阅、取消订阅报警信息 + * + * @param device 视频设备 + * @param expires 订阅过期时间(0 = 取消订阅) + * @param startPriority 报警起始级别(可选) + * @param endPriority 报警终止级别(可选) + * @param alarmMethod 报警方式条件(可选) + * @param startTime 报警发生起始时间(可选) + * @param endTime 报警发生终止时间(可选) + * @return true = 命令发送成功 + */ + @Override + public void alarmSubscribe(Device device, int expires, String startPriority, String endPriority, String alarmMethod, String startTime, String endTime) throws InvalidArgumentException, SipException, ParseException { + + StringBuffer cmdXml = new StringBuffer(200); + String charset = device.getCharset(); + cmdXml.append("\r\n"); + cmdXml.append("\r\n"); + cmdXml.append("Alarm\r\n"); + cmdXml.append("" + (int) ((Math.random() * 9 + 1) * 100000) + "\r\n"); + cmdXml.append("" + device.getDeviceId() + "\r\n"); + if (!ObjectUtils.isEmpty(startPriority)) { + cmdXml.append("" + startPriority + "\r\n"); + } + if (!ObjectUtils.isEmpty(endPriority)) { + cmdXml.append("" + endPriority + "\r\n"); + } + if (!ObjectUtils.isEmpty(alarmMethod)) { + cmdXml.append("" + alarmMethod + "\r\n"); + } + if (!ObjectUtils.isEmpty(startTime)) { + cmdXml.append("" + startTime + "\r\n"); + } + if (!ObjectUtils.isEmpty(endTime)) { + cmdXml.append("" + endTime + "\r\n"); + } + cmdXml.append("\r\n"); + + + + Request request = headerProvider.createSubscribeRequest(device, cmdXml.toString(), null, expires, "presence",sipSender.getNewCallIdHeader(sipLayer.getLocalIp(device.getLocalIp()),device.getTransport())); + sipSender.transmitRequest(sipLayer.getLocalIp(device.getLocalIp()), request); + + } + + @Override + public SIPRequest catalogSubscribe(Device device, SipTransactionInfo sipTransactionInfo, SipSubscribe.Event okEvent, SipSubscribe.Event errorEvent) throws InvalidArgumentException, SipException, ParseException { + + StringBuffer cmdXml = new StringBuffer(200); + String charset = device.getCharset(); + cmdXml.append("\r\n"); + cmdXml.append("\r\n"); + cmdXml.append("Catalog\r\n"); + cmdXml.append("" + (int) ((Math.random() * 9 + 1) * 100000) + "\r\n"); + cmdXml.append("" + device.getDeviceId() + "\r\n"); + cmdXml.append("\r\n"); + + CallIdHeader callIdHeader; + + if (sipTransactionInfo != null) { + callIdHeader = SipFactory.getInstance().createHeaderFactory().createCallIdHeader(sipTransactionInfo.getCallId()); + } else { + callIdHeader = sipSender.getNewCallIdHeader(sipLayer.getLocalIp(device.getLocalIp()),device.getTransport()); + } + + int subscribeCycleForCatalog = device.getSubscribeCycleForCatalog(); + if (subscribeCycleForCatalog > 0) { + // 目录订阅有效期不小于 30 秒 + subscribeCycleForCatalog = Math.max(subscribeCycleForCatalog, 30); + } + // 有效时间默认为60秒以上 + SIPRequest request = (SIPRequest) headerProvider.createSubscribeRequest(device, cmdXml.toString(), sipTransactionInfo, subscribeCycleForCatalog, "Catalog", + callIdHeader); + sipSender.transmitRequest(sipLayer.getLocalIp(device.getLocalIp()), request, errorEvent, okEvent); + return request; + } + + @Override + public void dragZoomCmd(Device device, String channelId, String cmdString, ErrorCallback callback) throws InvalidArgumentException, SipException, ParseException { + + String cmdType = "DeviceControl"; + int sn = (int) ((Math.random() * 9 + 1) * 100000); + + StringBuffer dragXml = new StringBuffer(200); + String charset = device.getCharset(); + dragXml.append("\r\n"); + dragXml.append("\r\n"); + dragXml.append("" + cmdType + "\r\n"); + dragXml.append("" + sn + "\r\n"); + if (ObjectUtils.isEmpty(channelId)) { + dragXml.append("" + device.getDeviceId() + "\r\n"); + } else { + dragXml.append("" + channelId + "\r\n"); + } + dragXml.append(cmdString); + dragXml.append("\r\n"); + + MessageEvent messageEvent = MessageEvent.getInstance(cmdType, sn + "", channelId, 1000L, callback); + messageSubscribe.addSubscribe(messageEvent); + + Request request = headerProvider.createMessageRequest(device, dragXml.toString(), SipUtils.getNewViaTag(), SipUtils.getNewFromTag(), null,sipSender.getNewCallIdHeader(sipLayer.getLocalIp(device.getLocalIp()),device.getTransport())); + sipSender.transmitRequest(sipLayer.getLocalIp(device.getLocalIp()),request); + } + + + + /** + * 回放暂停 + */ + @Override + public void playPauseCmd(Device device, DeviceChannel channel, StreamInfo streamInfo) throws InvalidArgumentException, ParseException, SipException { + StringBuffer content = new StringBuffer(200); + content.append("PAUSE RTSP/1.0\r\n"); + content.append("CSeq: " + getInfoCseq() + "\r\n"); + content.append("PauseTime: now\r\n"); + + playbackControlCmd(device, channel, streamInfo, content.toString(), null, null); + } + + + /** + * 回放恢复 + */ + @Override + public void playResumeCmd(Device device, DeviceChannel channel, StreamInfo streamInfo) throws InvalidArgumentException, ParseException, SipException { + StringBuffer content = new StringBuffer(200); + content.append("PLAY RTSP/1.0\r\n"); + content.append("CSeq: " + getInfoCseq() + "\r\n"); + content.append("Range: npt=now-\r\n"); + + playbackControlCmd(device, channel, streamInfo, content.toString(), null, null); + } + + /** + * 回放拖动播放 + */ + @Override + public void playSeekCmd(Device device, DeviceChannel channel, StreamInfo streamInfo, long seekTime) throws InvalidArgumentException, ParseException, SipException { + StringBuffer content = new StringBuffer(200); + content.append("PLAY RTSP/1.0\r\n"); + content.append("CSeq: " + getInfoCseq() + "\r\n"); + content.append("Range: npt=" + Math.abs(seekTime) + "-\r\n"); + + playbackControlCmd(device, channel, streamInfo, content.toString(), null, null); + } + + /** + * 回放倍速播放 + */ + @Override + public void playSpeedCmd(Device device, DeviceChannel channel, StreamInfo streamInfo, Double speed) throws InvalidArgumentException, ParseException, SipException { + StringBuffer content = new StringBuffer(200); + content.append("PLAY RTSP/1.0\r\n"); + content.append("CSeq: " + getInfoCseq() + "\r\n"); + content.append("Scale: " + String.format("%.6f", speed) + "\r\n"); + + playbackControlCmd(device, channel, streamInfo, content.toString(), null, null); + } + + private int getInfoCseq() { + return (int) ((Math.random() * 9 + 1) * Math.pow(10, 8)); + } + + @Override + public void playbackControlCmd(Device device, DeviceChannel channel, StreamInfo streamInfo, String content, SipSubscribe.Event errorEvent, SipSubscribe.Event okEvent) throws SipException, InvalidArgumentException, ParseException { + + playbackControlCmd(device, channel, streamInfo.getStream(), content, errorEvent, okEvent); + } + + @Override + public void playbackControlCmd(Device device, DeviceChannel channel, String stream, String content, SipSubscribe.Event errorEvent, SipSubscribe.Event okEvent) throws SipException, InvalidArgumentException, ParseException { + + SsrcTransaction ssrcTransaction = sessionManager.getSsrcTransactionByStream("rtp", stream); + if (ssrcTransaction == null) { + log.info("[回放控制]未找到视频流信息,设备:{}, 流ID: {}", device.getDeviceId(), stream); + return; + } + + SIPRequest request = headerProvider.createInfoRequest(device, channel.getDeviceId(), content, ssrcTransaction.getSipTransactionInfo()); + if (request == null) { + log.info("[回放控制]构建Request信息失败,设备:{}, 流ID: {}", device.getDeviceId(), stream); + return; + } + + sipSender.transmitRequest(sipLayer.getLocalIp(device.getLocalIp()), request, errorEvent, okEvent); + } + + @Override + public void sendAlarmMessage(Device device, DeviceAlarm deviceAlarm) throws InvalidArgumentException, SipException, ParseException { + if (device == null) { + return; + } + log.info("[发送报警通知]设备: {}/{}->{},{}", device.getDeviceId(), deviceAlarm.getChannelId(), + deviceAlarm.getLongitude(), deviceAlarm.getLatitude()); + + String characterSet = device.getCharset(); + StringBuffer deviceStatusXml = new StringBuffer(600); + deviceStatusXml.append("\r\n"); + deviceStatusXml.append("\r\n"); + deviceStatusXml.append("Alarm\r\n"); + deviceStatusXml.append("" + (int) ((Math.random() * 9 + 1) * 100000) + "\r\n"); + deviceStatusXml.append("" + deviceAlarm.getChannelId() + "\r\n"); + deviceStatusXml.append("" + deviceAlarm.getAlarmPriority() + "\r\n"); + deviceStatusXml.append("" + deviceAlarm.getAlarmMethod() + "\r\n"); + deviceStatusXml.append("" + DateUtil.yyyy_MM_dd_HH_mm_ssToISO8601(deviceAlarm.getAlarmTime()) + "\r\n"); + deviceStatusXml.append("" + deviceAlarm.getAlarmDescription() + "\r\n"); + deviceStatusXml.append("" + deviceAlarm.getLongitude() + "\r\n"); + deviceStatusXml.append("" + deviceAlarm.getLatitude() + "\r\n"); + deviceStatusXml.append("\r\n"); + deviceStatusXml.append("" + deviceAlarm.getAlarmType() + "\r\n"); + deviceStatusXml.append("\r\n"); + deviceStatusXml.append("\r\n"); + + + Request request = headerProvider.createMessageRequest(device, deviceStatusXml.toString(), SipUtils.getNewViaTag(), SipUtils.getNewFromTag(), null,sipSender.getNewCallIdHeader(sipLayer.getLocalIp(device.getLocalIp()),device.getTransport())); + sipSender.transmitRequest(sipLayer.getLocalIp(device.getLocalIp()),request); + + + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/cmd/impl/SIPCommanderForPlatform.java b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/cmd/impl/SIPCommanderForPlatform.java new file mode 100755 index 0000000..1df3824 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/cmd/impl/SIPCommanderForPlatform.java @@ -0,0 +1,757 @@ +package com.genersoft.iot.vmp.gb28181.transmit.cmd.impl; + +import com.alibaba.fastjson2.JSON; +import com.genersoft.iot.vmp.common.InviteSessionType; +import com.genersoft.iot.vmp.conf.DynamicTask; +import com.genersoft.iot.vmp.conf.UserSetting; +import com.genersoft.iot.vmp.conf.exception.SsrcTransactionNotFoundException; +import com.genersoft.iot.vmp.gb28181.SipLayer; +import com.genersoft.iot.vmp.gb28181.bean.*; +import com.genersoft.iot.vmp.gb28181.event.SipSubscribe; +import com.genersoft.iot.vmp.gb28181.session.SipInviteSessionManager; +import com.genersoft.iot.vmp.gb28181.transmit.SIPSender; +import com.genersoft.iot.vmp.gb28181.transmit.cmd.ISIPCommanderForPlatform; +import com.genersoft.iot.vmp.gb28181.transmit.cmd.SIPRequestHeaderPlarformProvider; +import com.genersoft.iot.vmp.gb28181.utils.SipUtils; +import com.genersoft.iot.vmp.media.bean.MediaServer; +import com.genersoft.iot.vmp.media.event.hook.Hook; +import com.genersoft.iot.vmp.media.event.hook.HookSubscribe; +import com.genersoft.iot.vmp.media.event.hook.HookType; +import com.genersoft.iot.vmp.media.service.IMediaServerService; +import com.genersoft.iot.vmp.service.bean.GPSMsgInfo; +import com.genersoft.iot.vmp.service.bean.SSRCInfo; +import com.genersoft.iot.vmp.storager.IRedisCatchStorage; +import com.genersoft.iot.vmp.utils.DateUtil; +import com.genersoft.iot.vmp.utils.GitUtil; +import gov.nist.javax.sip.message.MessageFactoryImpl; +import gov.nist.javax.sip.message.SIPRequest; +import gov.nist.javax.sip.message.SIPResponse; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.DependsOn; +import org.springframework.lang.Nullable; +import org.springframework.stereotype.Component; +import org.springframework.util.ObjectUtils; + +import javax.sip.InvalidArgumentException; +import javax.sip.ResponseEvent; +import javax.sip.SipException; +import javax.sip.SipFactory; +import javax.sip.header.CallIdHeader; +import javax.sip.header.WWWAuthenticateHeader; +import javax.sip.message.Request; +import java.text.ParseException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +@Slf4j +@Component +@DependsOn("sipLayer") +public class SIPCommanderForPlatform implements ISIPCommanderForPlatform { + + @Autowired + private SIPRequestHeaderPlarformProvider headerProviderPlatformProvider; + + @Autowired + private IRedisCatchStorage redisCatchStorage; + + @Autowired + private IMediaServerService mediaServerService; + + @Autowired + private SipLayer sipLayer; + + @Autowired + private SIPSender sipSender; + + @Autowired + private HookSubscribe subscribe; + + @Autowired + private UserSetting userSetting; + + + @Autowired + private SipInviteSessionManager sessionManager; + + @Autowired + private DynamicTask dynamicTask; + + @Autowired + private GitUtil gitUtil; + + @Override + public void register(Platform parentPlatform, SipSubscribe.Event errorEvent , SipSubscribe.Event okEvent) throws InvalidArgumentException, ParseException, SipException { + register(parentPlatform, null, null, errorEvent, okEvent, true); + } + + @Override + public void register(Platform parentPlatform, SipTransactionInfo sipTransactionInfo, SipSubscribe.Event errorEvent , SipSubscribe.Event okEvent) throws InvalidArgumentException, ParseException, SipException { + + register(parentPlatform, sipTransactionInfo, null, errorEvent, okEvent, true); + } + + @Override + public void unregister(Platform parentPlatform, SipTransactionInfo sipTransactionInfo, SipSubscribe.Event errorEvent , SipSubscribe.Event okEvent) throws InvalidArgumentException, ParseException, SipException { + register(parentPlatform, sipTransactionInfo, null, errorEvent, okEvent, false); + } + + @Override + public void register(Platform parentPlatform, @Nullable SipTransactionInfo sipTransactionInfo, @Nullable WWWAuthenticateHeader www, + SipSubscribe.Event errorEvent , SipSubscribe.Event okEvent, boolean isRegister) throws SipException, InvalidArgumentException, ParseException { + Request request; + + CallIdHeader callIdHeader = sipSender.getNewCallIdHeader(parentPlatform.getDeviceIp(),parentPlatform.getTransport()); + String fromTag = SipUtils.getNewFromTag(); + String toTag = null; + if (sipTransactionInfo != null ) { + if (sipTransactionInfo.getCallId() != null) { + callIdHeader.setCallId(sipTransactionInfo.getCallId()); + } + if (sipTransactionInfo.getFromTag() != null) { + fromTag = sipTransactionInfo.getFromTag(); + } + if (sipTransactionInfo.getToTag() != null) { + toTag = sipTransactionInfo.getToTag(); + } + } + + if (www == null ) { + request = headerProviderPlatformProvider.createRegisterRequest(parentPlatform, + redisCatchStorage.getCSEQ(), fromTag, + toTag, callIdHeader, isRegister? parentPlatform.getExpires() : 0); + }else { + request = headerProviderPlatformProvider.createRegisterRequest(parentPlatform, fromTag, toTag, www, callIdHeader, isRegister? parentPlatform.getExpires() : 0); + } + + sipSender.transmitRequest(parentPlatform.getDeviceIp(), request, (event)->{ + if (event != null) { + log.info("[国标级联]:{}, 注册失败: {} ", parentPlatform.getServerGBId(), event.msg); + } + if (errorEvent != null ) { + errorEvent.response(event); + } + }, okEvent, 2000L); + } + + @Override + public String keepalive(Platform parentPlatform, SipSubscribe.Event errorEvent , SipSubscribe.Event okEvent) throws SipException, InvalidArgumentException, ParseException { + log.info("[国标级联] 发送心跳, 上级平台编号: {}", parentPlatform.getServerGBId()); + String characterSet = parentPlatform.getCharacterSet(); + StringBuffer keepaliveXml = new StringBuffer(200); + keepaliveXml.append("\r\n") + .append("\r\n") + .append("Keepalive\r\n") + .append("" + (int)((Math.random()*9+1)*100000) + "\r\n") + .append("" + parentPlatform.getDeviceGBId() + "\r\n") + .append("OK\r\n") + .append("\r\n"); + + CallIdHeader callIdHeader = sipSender.getNewCallIdHeader(parentPlatform.getDeviceIp(),parentPlatform.getTransport()); + + Request request = headerProviderPlatformProvider.createMessageRequest( + parentPlatform, + keepaliveXml.toString(), + SipUtils.getNewFromTag(), + SipUtils.getNewViaTag(), + callIdHeader); + + sipSender.transmitRequest(parentPlatform.getDeviceIp(), request, errorEvent, okEvent); + return callIdHeader.getCallId(); + } + + /** + * 向上级回复通道信息 + * @param channel 通道信息 + * @param parentPlatform 平台信息 + */ + @Override + public void catalogQuery(CommonGBChannel channel, Platform parentPlatform, String sn, String fromTag, int size) throws SipException, InvalidArgumentException, ParseException { + + if ( parentPlatform ==null) { + return ; + } + List channels = new ArrayList<>(); + if (channel != null) { + channels.add(channel); + } + String catalogXml = getCatalogXml(channels, sn, parentPlatform, size); + + // callid + CallIdHeader callIdHeader = sipSender.getNewCallIdHeader(parentPlatform.getDeviceIp(),parentPlatform.getTransport()); + + Request request = headerProviderPlatformProvider.createMessageRequest(parentPlatform, catalogXml.toString(), fromTag, SipUtils.getNewViaTag(), callIdHeader); + sipSender.transmitRequest(parentPlatform.getDeviceIp(), request); + + } + + @Override + public void catalogQuery(List channels, Platform parentPlatform, String sn, String fromTag) throws InvalidArgumentException, ParseException, SipException { + if ( parentPlatform ==null) { + return ; + } + sendCatalogResponse(channels, parentPlatform, sn, fromTag, 0, true); + } + private String getCatalogXml(List channels, String sn, Platform platform, int size) { + String characterSet = platform.getCharacterSet(); + StringBuffer catalogXml = new StringBuffer(600); + catalogXml.append("\r\n") + .append("\r\n") + .append("Catalog\r\n") + .append("" +sn + "\r\n") + .append("" + platform.getDeviceGBId() + "\r\n") + .append("" + size + "\r\n") + .append("\r\n"); + if (!channels.isEmpty()) { + for (CommonGBChannel channel : channels) { + catalogXml.append(channel.encode(platform.getDeviceGBId())); + } + } + + catalogXml.append("\r\n"); + catalogXml.append("\r\n"); + return catalogXml.toString(); + } + + private void sendCatalogResponse(List channels, Platform parentPlatform, String sn, String fromTag, int index, boolean sendAfterResponse) throws SipException, InvalidArgumentException, ParseException { + if (index > channels.size()) { + return; + } + String catalogXml; + if (channels.isEmpty()) { + catalogXml = getCatalogXml(Collections.emptyList(), sn, parentPlatform, 0); + }else { + List subChannelList; + if (index + parentPlatform.getCatalogGroup() < channels.size()) { + subChannelList = channels.subList(index, index + parentPlatform.getCatalogGroup()); + }else { + subChannelList = channels.subList(index, channels.size()); + } + catalogXml = getCatalogXml(subChannelList, sn, parentPlatform, channels.size()); + } + // callid + CallIdHeader callIdHeader = sipSender.getNewCallIdHeader(parentPlatform.getDeviceIp(),parentPlatform.getTransport()); + + SIPRequest request = (SIPRequest)headerProviderPlatformProvider.createMessageRequest(parentPlatform, catalogXml, fromTag, SipUtils.getNewViaTag(), callIdHeader); + + String timeoutTaskKey = "catalog_task_" + parentPlatform.getServerGBId() + sn; + + String callId = request.getCallIdHeader().getCallId(); + + log.info("[命令发送] 国标级联{} 目录查询回复: 共{}条,已发送{}条", parentPlatform.getServerGBId(), + channels.size(), Math.min(index + parentPlatform.getCatalogGroup(), channels.size())); + log.debug(catalogXml); + if (sendAfterResponse) { + // 默认按照收到200回复后发送下一条, 如果超时收不到回复,就以30毫秒的间隔直接发送。 + sipSender.transmitRequest(parentPlatform.getDeviceIp(), request, eventResult -> { + if (eventResult.statusCode == -1024) { + // 消息发送超时, 以30毫秒的间隔直接发送 + int indexNext = index + parentPlatform.getCatalogGroup(); + try { + sendCatalogResponse(channels, parentPlatform, sn, fromTag, indexNext, false); + } catch (SipException | InvalidArgumentException | ParseException e) { + log.error("[命令发送失败] 国标级联 目录查询回复: {}", e.getMessage()); + } + return; + } + log.error("[目录推送失败] 国标级联 platform : {}, code: {}, msg: {}, 停止发送", parentPlatform.getServerGBId(), eventResult.statusCode, eventResult.msg); + dynamicTask.stop(timeoutTaskKey); + }, eventResult -> { + dynamicTask.stop(timeoutTaskKey); + int indexNext = index + parentPlatform.getCatalogGroup(); + try { + sendCatalogResponse(channels, parentPlatform, sn, fromTag, indexNext, true); + } catch (SipException | InvalidArgumentException | ParseException e) { + log.error("[命令发送失败] 国标级联 目录查询回复: {}", e.getMessage()); + } + }); + }else { + sipSender.transmitRequest(parentPlatform.getDeviceIp(), request, eventResult -> { + log.error("[目录推送失败] 国标级联 platform : {}, code: {}, msg: {}", parentPlatform.getServerGBId(), eventResult.statusCode, eventResult.msg); + }, null); + dynamicTask.startDelay(timeoutTaskKey, ()->{ + int indexNext = index + parentPlatform.getCatalogGroup(); + try { + sendCatalogResponse(channels, parentPlatform, sn, fromTag, indexNext, false); + } catch (SipException | InvalidArgumentException | ParseException e) { + log.error("[命令发送失败] 国标级联 目录查询回复: {}", e.getMessage()); + } + }, 100); + } + } + + /** + * 向上级回复DeviceInfo查询信息 + * @param parentPlatform 平台信息 + * @param sn + * @param fromTag + * @return + */ + @Override + public void deviceInfoResponse(Platform parentPlatform, Device device, String sn, String fromTag) throws SipException, InvalidArgumentException, ParseException { + if (parentPlatform == null) { + return; + } + String deviceId = device == null ? parentPlatform.getDeviceGBId() : device.getDeviceId(); + String deviceName = device == null ? parentPlatform.getName() : device.getName(); + String manufacturer = device == null ? "WVP-28181-PRO" : device.getManufacturer(); + String model = device == null ? "platform" : device.getModel(); + String firmware = device == null ? gitUtil.getBuildVersion() : device.getFirmware(); + String characterSet = parentPlatform.getCharacterSet(); + StringBuffer deviceInfoXml = new StringBuffer(600); + deviceInfoXml.append("\r\n"); + deviceInfoXml.append("\r\n"); + deviceInfoXml.append("DeviceInfo\r\n"); + deviceInfoXml.append("" +sn + "\r\n"); + deviceInfoXml.append("" + deviceId + "\r\n"); + deviceInfoXml.append("" + deviceName + "\r\n"); + deviceInfoXml.append("" + manufacturer + "\r\n"); + deviceInfoXml.append("" + model + "\r\n"); + deviceInfoXml.append("" + firmware + "\r\n"); + deviceInfoXml.append("OK\r\n"); + deviceInfoXml.append("\r\n"); + + CallIdHeader callIdHeader = sipSender.getNewCallIdHeader(parentPlatform.getDeviceIp(),parentPlatform.getTransport()); + + Request request = headerProviderPlatformProvider.createMessageRequest(parentPlatform, deviceInfoXml.toString(), fromTag, SipUtils.getNewViaTag(), callIdHeader); + sipSender.transmitRequest(parentPlatform.getDeviceIp(), request); + } + + + /** + * 向上级回复DeviceStatus查询信息 + * @param parentPlatform 平台信息 + * @param sn + * @param fromTag + * @return + */ + @Override + public void deviceStatusResponse(Platform parentPlatform, String channelId, String sn, String fromTag, boolean status) throws SipException, InvalidArgumentException, ParseException { + if (parentPlatform == null) { + return ; + } + String statusStr = (status)?"ONLINE":"OFFLINE"; + String characterSet = parentPlatform.getCharacterSet(); + StringBuffer deviceStatusXml = new StringBuffer(600); + deviceStatusXml.append("\r\n") + .append("\r\n") + .append("DeviceStatus\r\n") + .append("" +sn + "\r\n") + .append("" + channelId + "\r\n") + .append("OK\r\n") + .append(""+statusStr+"\r\n") + .append("OK\r\n") + .append("\r\n"); + + CallIdHeader callIdHeader = sipSender.getNewCallIdHeader(parentPlatform.getDeviceIp(),parentPlatform.getTransport()); + + Request request = headerProviderPlatformProvider.createMessageRequest(parentPlatform, deviceStatusXml.toString(), fromTag, SipUtils.getNewViaTag(), callIdHeader); + sipSender.transmitRequest(parentPlatform.getDeviceIp(), request); + } + + @Override + public void sendNotifyMobilePosition(Platform parentPlatform, GPSMsgInfo gpsMsgInfo, CommonGBChannel channel, SubscribeInfo subscribeInfo) throws InvalidArgumentException, ParseException, NoSuchFieldException, SipException, IllegalAccessException { + if (parentPlatform == null) { + return; + } + log.info("[发送 移动位置订阅] {}/{}->{},{}", parentPlatform.getServerGBId(), gpsMsgInfo.getId(), gpsMsgInfo.getLng(), gpsMsgInfo.getLat()); + if (log.isDebugEnabled()) { + log.debug("[发送 移动位置订阅] {}/{}->{},{}", parentPlatform.getServerGBId(), gpsMsgInfo.getId(), gpsMsgInfo.getLng(), gpsMsgInfo.getLat()); + } + + String characterSet = parentPlatform.getCharacterSet(); + StringBuffer deviceStatusXml = new StringBuffer(600); + deviceStatusXml.append("\r\n") + .append("\r\n") + .append("MobilePosition\r\n") + .append("" + (int)((Math.random()*9+1)*100000) + "\r\n") + .append("" + channel.getGbDeviceId() + "\r\n") + .append("\r\n") + .append("" + gpsMsgInfo.getLng() + "\r\n") + .append("" + gpsMsgInfo.getLat() + "\r\n") + .append("" + gpsMsgInfo.getSpeed() + "\r\n") + .append("" + gpsMsgInfo.getDirection() + "\r\n") + .append("" + gpsMsgInfo.getAltitude() + "\r\n") + .append("\r\n"); + + sendNotify(parentPlatform, deviceStatusXml.toString(), subscribeInfo, eventResult -> { + log.error("发送NOTIFY通知消息失败。错误:{} {}", eventResult.statusCode, eventResult.msg); + }, null); + + } + + @Override + public void sendAlarmMessage(Platform parentPlatform, DeviceAlarm deviceAlarm) throws SipException, InvalidArgumentException, ParseException { + if (parentPlatform == null) { + return; + } + log.info("[发送报警通知]平台: {}/{}->{},{}: {}", parentPlatform.getServerGBId(), deviceAlarm.getChannelId(), + deviceAlarm.getLongitude(), deviceAlarm.getLatitude(), JSON.toJSONString(deviceAlarm)); + String characterSet = parentPlatform.getCharacterSet(); + StringBuffer deviceStatusXml = new StringBuffer(600); + deviceStatusXml.append("\r\n") + .append("\r\n") + .append("Alarm\r\n") + .append("" + (int)((Math.random()*9+1)*100000) + "\r\n") + .append("" + deviceAlarm.getChannelId() + "\r\n") + .append("" + deviceAlarm.getAlarmPriority() + "\r\n") + .append("" + deviceAlarm.getAlarmMethod() + "\r\n") + .append("" + DateUtil.yyyy_MM_dd_HH_mm_ssToISO8601(deviceAlarm.getAlarmTime()) + "\r\n") + .append("" + deviceAlarm.getAlarmDescription() + "\r\n") + .append("" + deviceAlarm.getLongitude() + "\r\n") + .append("" + deviceAlarm.getLatitude() + "\r\n") + .append("\r\n") + .append("" + deviceAlarm.getAlarmType() + "\r\n") + .append("\r\n") + .append("\r\n"); + + CallIdHeader callIdHeader = sipSender.getNewCallIdHeader(parentPlatform.getDeviceIp(),parentPlatform.getTransport()); + + Request request = headerProviderPlatformProvider.createMessageRequest(parentPlatform, deviceStatusXml.toString(), SipUtils.getNewFromTag(), SipUtils.getNewViaTag(), callIdHeader); + sipSender.transmitRequest(parentPlatform.getDeviceIp(), request); + + } + + @Override + public void sendNotifyForCatalogAddOrUpdate(String type, Platform parentPlatform, List deviceChannels, SubscribeInfo subscribeInfo, Integer index) throws InvalidArgumentException, ParseException, NoSuchFieldException, SipException, IllegalAccessException { + if (parentPlatform == null || deviceChannels == null || deviceChannels.isEmpty() || subscribeInfo == null) { + return; + } + if (index == null) { + index = 0; + } + if (index >= deviceChannels.size()) { + return; + } + List channels; + if (index + parentPlatform.getCatalogGroup() < deviceChannels.size()) { + channels = deviceChannels.subList(index, index + parentPlatform.getCatalogGroup()); + }else { + channels = deviceChannels.subList(index, deviceChannels.size()); + } + Integer finalIndex = index; + String catalogXmlContent = getCatalogXmlContentForCatalogAddOrUpdate(parentPlatform, channels, + deviceChannels.size(), type, subscribeInfo); + log.info("[发送NOTIFY通知]类型: {},发送数量: {}", type, channels.size()); + sendNotify(parentPlatform, catalogXmlContent, subscribeInfo, eventResult -> { + log.error("发送NOTIFY通知消息失败。错误:{} {}", eventResult.statusCode, eventResult.msg); + log.error(catalogXmlContent); + }, (eventResult -> { + try { + sendNotifyForCatalogAddOrUpdate(type, parentPlatform, deviceChannels, subscribeInfo, + finalIndex + parentPlatform.getCatalogGroup()); + } catch (InvalidArgumentException | ParseException | NoSuchFieldException | SipException | + IllegalAccessException e) { + log.error("[命令发送失败] 国标级联 NOTIFY通知: {}", e.getMessage()); + } + })); + } + + private void sendNotify(Platform parentPlatform, String catalogXmlContent, + SubscribeInfo subscribeInfo, SipSubscribe.Event errorEvent, SipSubscribe.Event okEvent ) + throws SipException, ParseException, InvalidArgumentException { + MessageFactoryImpl messageFactory = (MessageFactoryImpl) SipFactory.getInstance().createMessageFactory(); + String characterSet = parentPlatform.getCharacterSet(); + // 设置编码, 防止中文乱码 + messageFactory.setDefaultContentEncodingCharset(characterSet); + + SIPRequest notifyRequest = headerProviderPlatformProvider.createNotifyRequest(parentPlatform, catalogXmlContent, subscribeInfo); + + sipSender.transmitRequest(parentPlatform.getDeviceIp(), notifyRequest, errorEvent, okEvent); + } + + private String getCatalogXmlContentForCatalogAddOrUpdate(Platform platform, List channels, int sumNum, String type, SubscribeInfo subscribeInfo) { + StringBuffer catalogXml = new StringBuffer(600); + String characterSet = platform.getCharacterSet(); + catalogXml.append("\r\n") + .append("\r\n") + .append("Catalog\r\n") + .append("" + (int) ((Math.random() * 9 + 1) * 100000) + "\r\n") + .append("" + platform.getDeviceGBId() + "\r\n") + .append(""+ sumNum +"\r\n") + .append("\r\n"); + if (!channels.isEmpty()) { + for (CommonGBChannel channel : channels) { + catalogXml.append(channel.encode(type, platform.getDeviceGBId())); + } + } + catalogXml.append("\r\n") + .append("\r\n"); + return catalogXml.toString(); + } + + @Override + public void sendNotifyForCatalogOther(String type, Platform parentPlatform, List deviceChannels, + SubscribeInfo subscribeInfo, Integer index) throws InvalidArgumentException, ParseException, NoSuchFieldException, SipException, IllegalAccessException { + if (parentPlatform == null + || deviceChannels == null + || deviceChannels.size() == 0 + || subscribeInfo == null) { + log.warn("[缺少必要参数]"); + return; + } + + if (index == null) { + index = 0; + } + if (index >= deviceChannels.size()) { + return; + } + List channels; + if (index + parentPlatform.getCatalogGroup() < deviceChannels.size()) { + channels = deviceChannels.subList(index, index + parentPlatform.getCatalogGroup()); + }else { + channels = deviceChannels.subList(index, deviceChannels.size()); + } + log.info("[发送NOTIFY通知]类型: {},发送数量: {}", type, channels.size()); + Integer finalIndex = index; + String catalogXmlContent = getCatalogXmlContentForCatalogOther(parentPlatform, channels, type); + sendNotify(parentPlatform, catalogXmlContent, subscribeInfo, eventResult -> { + log.error("发送NOTIFY通知消息失败。错误:{} {}", eventResult.statusCode, eventResult.msg); + }, eventResult -> { + try { + sendNotifyForCatalogOther(type, parentPlatform, deviceChannels, subscribeInfo, + finalIndex + parentPlatform.getCatalogGroup()); + } catch (InvalidArgumentException | ParseException | NoSuchFieldException | SipException | + IllegalAccessException e) { + log.error("[命令发送失败] 国标级联 NOTIFY通知: {}", e.getMessage()); + } + }); + } + + private String getCatalogXmlContentForCatalogOther(Platform platform, List channels, String type) { + + String characterSet = platform.getCharacterSet(); + StringBuffer catalogXml = new StringBuffer(600); + catalogXml.append("\r\n") + .append("\r\n") + .append("Catalog\r\n") + .append("" + (int) ((Math.random() * 9 + 1) * 100000) + "\r\n") + .append("" + platform.getDeviceGBId() + "\r\n") + .append("1\r\n") + .append("\r\n"); + if (!channels.isEmpty()) { + for (CommonGBChannel channel : channels) { + catalogXml.append(channel.encode(type, platform.getDeviceGBId())); + } + } + catalogXml.append("\r\n") + .append("\r\n"); + return catalogXml.toString(); + } + @Override + public void recordInfo(CommonGBChannel deviceChannel, Platform parentPlatform, String fromTag, RecordInfo recordInfo) throws SipException, InvalidArgumentException, ParseException { + if ( parentPlatform ==null) { + return ; + } + log.info("[国标级联] 发送录像数据通道: {}", recordInfo.getChannelId()); + String characterSet = parentPlatform.getCharacterSet(); + StringBuffer recordXml = new StringBuffer(600); + recordXml.append("\r\n") + .append("\r\n") + .append("RecordInfo\r\n") + .append("" +recordInfo.getSn() + "\r\n") + .append("" + deviceChannel.getGbDeviceId() + "\r\n") + .append("" + recordInfo.getSumNum() + "\r\n"); + if (recordInfo.getRecordList() == null ) { + recordXml.append("\r\n"); + }else { + recordXml.append("\r\n"); + if (recordInfo.getRecordList().size() > 0) { + for (RecordItem recordItem : recordInfo.getRecordList()) { + recordXml.append("\r\n"); + if (deviceChannel != null) { + recordXml.append("" + deviceChannel.getGbDeviceId() + "\r\n") + .append("" + recordItem.getName() + "\r\n") + .append("" + DateUtil.yyyy_MM_dd_HH_mm_ssToISO8601(recordItem.getStartTime()) + "\r\n") + .append("" + DateUtil.yyyy_MM_dd_HH_mm_ssToISO8601(recordItem.getEndTime()) + "\r\n") + .append("" + recordItem.getSecrecy() + "\r\n") + .append("" + recordItem.getType() + "\r\n"); + if (!ObjectUtils.isEmpty(recordItem.getFileSize())) { + recordXml.append("" + recordItem.getFileSize() + "\r\n"); + } + if (!ObjectUtils.isEmpty(recordItem.getFilePath())) { + recordXml.append("" + recordItem.getFilePath() + "\r\n"); + } + } + recordXml.append("\r\n"); + } + } + } + + recordXml.append("\r\n") + .append("\r\n"); + log.debug("[国标级联] 发送录像数据通道:{}, 内容: {}", recordInfo.getChannelId(), recordXml); + // callid + CallIdHeader callIdHeader = sipSender.getNewCallIdHeader(parentPlatform.getDeviceIp(),parentPlatform.getTransport()); + + Request request = headerProviderPlatformProvider.createMessageRequest(parentPlatform, recordXml.toString(), fromTag, SipUtils.getNewViaTag(), callIdHeader); + sipSender.transmitRequest(parentPlatform.getDeviceIp(), request, null, eventResult -> { + log.info("[国标级联] 发送录像数据通道:{}, 发送成功", recordInfo.getChannelId()); + }); + + } + + @Override + public void sendMediaStatusNotify(Platform parentPlatform, SendRtpInfo sendRtpInfo, CommonGBChannel channel) throws SipException, InvalidArgumentException, ParseException { + if (channel == null || parentPlatform == null) { + return; + } + + String characterSet = parentPlatform.getCharacterSet(); + StringBuffer mediaStatusXml = new StringBuffer(200); + mediaStatusXml.append("\r\n") + .append("\r\n") + .append("MediaStatus\r\n") + .append("" + (int)((Math.random()*9+1)*100000) + "\r\n") + .append("" + channel.getGbDeviceId() + "\r\n") + .append("121\r\n") + .append("\r\n"); + + SIPRequest messageRequest = (SIPRequest)headerProviderPlatformProvider.createMessageRequest(parentPlatform, mediaStatusXml.toString(), + sendRtpInfo); + + sipSender.transmitRequest(parentPlatform.getDeviceIp(),messageRequest); + + } + + @Override + public synchronized void streamByeCmd(Platform platform, SendRtpInfo sendRtpItem, CommonGBChannel channel) throws SipException, InvalidArgumentException, ParseException { + if (sendRtpItem == null ) { + log.info("[向上级发送BYE], sendRtpItem 为NULL"); + return; + } + if (platform == null) { + log.info("[向上级发送BYE], platform 为NULL"); + return; + } + log.info("[向上级发送BYE], {}/{}", platform.getServerGBId(), sendRtpItem.getChannelId()); + String mediaServerId = sendRtpItem.getMediaServerId(); + MediaServer mediaServerItem = mediaServerService.getOne(mediaServerId); + if (mediaServerItem != null) { + mediaServerService.releaseSsrc(mediaServerItem.getId(), sendRtpItem.getSsrc()); + mediaServerService.closeRTPServer(mediaServerItem, sendRtpItem.getStream()); + } + SIPRequest byeRequest = headerProviderPlatformProvider.createByeRequest(platform, sendRtpItem, channel); + if (byeRequest == null) { + log.warn("[向上级发送bye]:无法创建 byeRequest"); + } + sipSender.transmitRequest(platform.getDeviceIp(),byeRequest); + } + + @Override + public void streamByeCmd(Platform platform, CommonGBChannel channel, String app, String stream, String callId, SipSubscribe.Event okEvent) throws InvalidArgumentException, SipException, ParseException, SsrcTransactionNotFoundException { + + SsrcTransaction ssrcTransaction = null; + if (callId != null) { + ssrcTransaction = sessionManager.getSsrcTransactionByCallId(callId); + }else if (stream != null) { + ssrcTransaction = sessionManager.getSsrcTransactionByStream(app, stream); + } + if (ssrcTransaction == null) { + throw new SsrcTransactionNotFoundException(platform.getServerGBId(), channel.getGbDeviceId(), callId, stream); + } + + mediaServerService.releaseSsrc(ssrcTransaction.getMediaServerId(), ssrcTransaction.getSsrc()); + mediaServerService.closeRTPServer(ssrcTransaction.getMediaServerId(), ssrcTransaction.getStream()); + sessionManager.removeByStream(ssrcTransaction.getApp(), ssrcTransaction.getStream()); + + Request byteRequest = headerProviderPlatformProvider.createByteRequest(platform, channel.getGbDeviceId(), ssrcTransaction.getSipTransactionInfo()); + sipSender.transmitRequest(sipLayer.getLocalIp(platform.getDeviceIp()), byteRequest, null, okEvent); + } + + @Override + public void broadcastResultCmd(Platform platform, CommonGBChannel deviceChannel, String sn, boolean result, SipSubscribe.Event errorEvent, SipSubscribe.Event okEvent) throws InvalidArgumentException, SipException, ParseException { + if (platform == null || deviceChannel == null) { + return; + } + String characterSet = platform.getCharacterSet(); + StringBuffer mediaStatusXml = new StringBuffer(200); + mediaStatusXml.append("\r\n") + .append("\r\n") + .append("Broadcast\r\n") + .append("" + sn + "\r\n") + .append("" + deviceChannel.getGbDeviceId() + "\r\n") + .append("" + (result?"OK":"ERROR") + "\r\n") + .append("\r\n"); + + CallIdHeader callIdHeader = sipSender.getNewCallIdHeader(platform.getDeviceIp(), platform.getTransport()); + + SIPRequest messageRequest = (SIPRequest)headerProviderPlatformProvider.createMessageRequest(platform, mediaStatusXml.toString(), + SipUtils.getNewFromTag(), SipUtils.getNewViaTag(), callIdHeader); + + sipSender.transmitRequest(platform.getDeviceIp(),messageRequest, errorEvent, okEvent); + } + + @Override + public void broadcastInviteCmd(Platform platform, CommonGBChannel channel,String sourceId, MediaServer mediaServerItem, + SSRCInfo ssrcInfo, HookSubscribe.Event event, SipSubscribe.Event okEvent, + SipSubscribe.Event errorEvent) throws ParseException, SipException, InvalidArgumentException { + String stream = ssrcInfo.getStream(); + + if (platform == null) { + return; + } + + log.info("{} 分配的ZLM为: {} [{}:{}]", stream, mediaServerItem.getId(), mediaServerItem.getIp(), ssrcInfo.getPort()); + Hook hook = Hook.getInstance(HookType.on_media_arrival, "rtp", stream, mediaServerItem.getId()); + subscribe.addSubscribe(hook, (hookData) -> { + if (event != null) { + event.response(hookData); + subscribe.removeSubscribe(hook); + } + }); + String sdpIp = mediaServerItem.getSdpIp(); + + StringBuffer content = new StringBuffer(200); + content.append("v=0\r\n"); + content.append("o=" + platform.getDeviceGBId() + " 0 0 IN IP4 " + sdpIp + "\r\n"); + content.append("s=Play\r\n"); + content.append("u=" + channel.getGbDeviceId() + ":0\r\n"); + content.append("c=IN IP4 " + sdpIp + "\r\n"); + content.append("t=0 0\r\n"); + + if ("TCP-PASSIVE".equalsIgnoreCase(userSetting.getBroadcastForPlatform())) { + content.append("m=audio " + ssrcInfo.getPort() + " TCP/RTP/AVP 8 96\r\n"); + } else if ("TCP-ACTIVE".equalsIgnoreCase(userSetting.getBroadcastForPlatform())) { + content.append("m=audio " + ssrcInfo.getPort() + " TCP/RTP/AVP 8 96\r\n"); + } else if ("UDP".equalsIgnoreCase(userSetting.getBroadcastForPlatform())) { + content.append("m=audio " + ssrcInfo.getPort() + " RTP/AVP 8 96\r\n"); + } + + content.append("a=recvonly\r\n"); + content.append("a=rtpmap:8 PCMA/8000\r\n"); + content.append("a=rtpmap:96 PS/90000\r\n"); + if ("TCP-PASSIVE".equalsIgnoreCase(userSetting.getBroadcastForPlatform())) { + content.append("a=setup:passive\r\n"); + content.append("a=connection:new\r\n"); + }else if ("TCP-ACTIVE".equalsIgnoreCase(userSetting.getBroadcastForPlatform())) { + content.append("a=setup:active\r\n"); + content.append("a=connection:new\r\n"); + } + + content.append("y=" + ssrcInfo.getSsrc() + "\r\n");//ssrc + // f字段:f= v/编码格式/分辨率/帧率/码率类型/码率大小a/编码格式/码率大小/采样率 + content.append("f=v/2/5/25/1/4096a/1/8/1\r\n"); + CallIdHeader callIdHeader = sipSender.getNewCallIdHeader(sipLayer.getLocalIp(platform.getDeviceIp()), platform.getTransport()); + + Request request = headerProviderPlatformProvider.createInviteRequest(platform, sourceId, channel.getGbDeviceId(), + content.toString(), SipUtils.getNewViaTag(), SipUtils.getNewFromTag(), ssrcInfo.getSsrc(), + callIdHeader); + sipSender.transmitRequest(sipLayer.getLocalIp(platform.getDeviceIp()), request, (e -> { + sessionManager.removeByStream(ssrcInfo.getApp(), ssrcInfo.getStream()); + mediaServerService.releaseSsrc(mediaServerItem.getId(), ssrcInfo.getSsrc()); + subscribe.removeSubscribe(hook); + errorEvent.response(e); + }), e -> { + ResponseEvent responseEvent = (ResponseEvent) e.event; + SIPResponse response = (SIPResponse) responseEvent.getResponse(); + SsrcTransaction ssrcTransaction = SsrcTransaction.buildForPlatform(platform.getServerGBId(), channel.getGbId(), + callIdHeader.getCallId(), ssrcInfo.getApp(), stream, ssrcInfo.getSsrc(), mediaServerItem.getId(), response, InviteSessionType.BROADCAST); + sessionManager.put(ssrcTransaction); + okEvent.response(e); + }); + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/ISIPRequestProcessor.java b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/ISIPRequestProcessor.java new file mode 100755 index 0000000..8e79941 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/ISIPRequestProcessor.java @@ -0,0 +1,14 @@ +package com.genersoft.iot.vmp.gb28181.transmit.event.request; + +import javax.sip.RequestEvent; + +/** + * @description: 对SIP事件进行处理,包括request, response, timeout, ioException, transactionTerminated,dialogTerminated + * @author: panlinlin + * @date: 2021年11月5日 15:47 + */ +public interface ISIPRequestProcessor { + + void process(RequestEvent event); + +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/SIPRequestProcessorParent.java b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/SIPRequestProcessorParent.java new file mode 100755 index 0000000..9a2ff38 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/SIPRequestProcessorParent.java @@ -0,0 +1,234 @@ +package com.genersoft.iot.vmp.gb28181.transmit.event.request; + +import com.genersoft.iot.vmp.gb28181.bean.Platform; +import com.genersoft.iot.vmp.gb28181.transmit.SIPSender; +import com.genersoft.iot.vmp.gb28181.utils.SipUtils; +import com.genersoft.iot.vmp.utils.IpPortUtil; +import com.google.common.primitives.Bytes; +import gov.nist.javax.sip.message.SIPRequest; +import gov.nist.javax.sip.message.SIPResponse; +import lombok.extern.slf4j.Slf4j; +import org.dom4j.Document; +import org.dom4j.DocumentException; +import org.dom4j.Element; +import org.dom4j.io.SAXReader; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.util.ObjectUtils; + +import javax.sip.*; +import javax.sip.address.Address; +import javax.sip.address.SipURI; +import javax.sip.header.ContentTypeHeader; +import javax.sip.header.ExpiresHeader; +import javax.sip.header.HeaderFactory; +import javax.sip.message.MessageFactory; +import javax.sip.message.Request; +import javax.sip.message.Response; +import java.io.ByteArrayInputStream; +import java.text.ParseException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * @description:处理接收IPCamera发来的SIP协议请求消息 + * @author: songww + * @date: 2020年5月3日 下午4:42:22 + */ +@Slf4j +public abstract class SIPRequestProcessorParent { + + @Autowired + private SIPSender sipSender; + + public HeaderFactory getHeaderFactory() { + try { + return SipFactory.getInstance().createHeaderFactory(); + } catch (PeerUnavailableException e) { + log.error("未处理的异常 ", e); + } + return null; + } + + public MessageFactory getMessageFactory() { + try { + return SipFactory.getInstance().createMessageFactory(); + } catch (PeerUnavailableException e) { + log.error("未处理的异常 ", e); + } + return null; + } + + class ResponseAckExtraParam{ + String content; + ContentTypeHeader contentTypeHeader; + SipURI sipURI; + int expires = -1; + } + + /*** + * 回复状态码 + * 100 trying + * 200 OK + * 400 + * 404 + */ + public SIPResponse responseAck(SIPRequest sipRequest, int statusCode) throws SipException, InvalidArgumentException, ParseException { + return responseAck(sipRequest, statusCode, null); + } + + public SIPResponse responseAck(SIPRequest sipRequest, int statusCode, String msg) throws SipException, InvalidArgumentException, ParseException { + return responseAck(sipRequest, statusCode, msg, null); + } + + + public SIPResponse responseAck(SIPRequest sipRequest, int statusCode, String msg, ResponseAckExtraParam responseAckExtraParam) throws SipException, InvalidArgumentException, ParseException { + if (sipRequest.getToHeader().getTag() == null) { + sipRequest.getToHeader().setTag(SipUtils.getNewTag()); + } + SIPResponse response = (SIPResponse)getMessageFactory().createResponse(statusCode, sipRequest); + response.setStatusCode(statusCode); + if (msg != null) { + response.setReasonPhrase(msg); + } + + if (responseAckExtraParam != null) { + if (responseAckExtraParam.sipURI != null && sipRequest.getMethod().equals(Request.INVITE)) { + log.debug("responseSdpAck SipURI: {}:{}", responseAckExtraParam.sipURI.getHost(), responseAckExtraParam.sipURI.getPort()); + Address concatAddress = SipFactory.getInstance().createAddressFactory().createAddress( + SipFactory.getInstance().createAddressFactory().createSipURI(responseAckExtraParam.sipURI.getUser(), IpPortUtil.concatenateIpAndPort(responseAckExtraParam.sipURI.getHost(), String.valueOf(responseAckExtraParam.sipURI.getPort())) + )); + response.addHeader(SipFactory.getInstance().createHeaderFactory().createContactHeader(concatAddress)); + } + if (responseAckExtraParam.contentTypeHeader != null) { + response.setContent(responseAckExtraParam.content, responseAckExtraParam.contentTypeHeader); + } + + if (sipRequest.getMethod().equals(Request.SUBSCRIBE)) { + if (responseAckExtraParam.expires == -1) { + log.error("[参数不全] 2xx的SUBSCRIBE回复,必须设置Expires header"); + }else { + ExpiresHeader expiresHeader = SipFactory.getInstance().createHeaderFactory().createExpiresHeader(responseAckExtraParam.expires); + response.addHeader(expiresHeader); + } + } + }else { + if (sipRequest.getMethod().equals(Request.SUBSCRIBE)) { + log.error("[参数不全] 2xx的SUBSCRIBE回复,必须设置Expires header"); + } + } + + // 发送response + sipSender.transmitRequest(sipRequest.getLocalAddress().getHostAddress(), response); + + return response; + } + + + + /** + * 回复带sdp的200 + */ + public SIPResponse responseSdpAck(SIPRequest request, String sdp, Platform platform) throws SipException, InvalidArgumentException, ParseException { + + ContentTypeHeader contentTypeHeader = SipFactory.getInstance().createHeaderFactory().createContentTypeHeader("APPLICATION", "SDP"); + + // 兼容国标中的使用编码@域名作为RequestURI的情况 + SipURI sipURI = (SipURI)request.getRequestURI(); + if (sipURI.getPort() == -1) { + sipURI = SipFactory.getInstance().createAddressFactory().createSipURI(platform.getServerGBId(), IpPortUtil.concatenateIpAndPort(platform.getServerIp(), String.valueOf(platform.getServerPort()))); + } + ResponseAckExtraParam responseAckExtraParam = new ResponseAckExtraParam(); + responseAckExtraParam.contentTypeHeader = contentTypeHeader; + responseAckExtraParam.content = sdp; + responseAckExtraParam.sipURI = sipURI; + + SIPResponse sipResponse = responseAck(request, Response.OK, null, responseAckExtraParam); + + + return sipResponse; + } + + /** + * 回复带xml的200 + */ + public SIPResponse responseXmlAck(SIPRequest request, String xml, Platform platform, Integer expires) throws SipException, InvalidArgumentException, ParseException { + ContentTypeHeader contentTypeHeader = SipFactory.getInstance().createHeaderFactory().createContentTypeHeader("Application", "MANSCDP+xml"); + + SipURI sipURI = (SipURI)request.getRequestURI(); + if (sipURI.getPort() == -1) { + sipURI = SipFactory.getInstance().createAddressFactory().createSipURI(platform.getServerGBId(), IpPortUtil.concatenateIpAndPort(platform.getServerIp(), String.valueOf(platform.getServerPort()))); + } + ResponseAckExtraParam responseAckExtraParam = new ResponseAckExtraParam(); + responseAckExtraParam.contentTypeHeader = contentTypeHeader; + responseAckExtraParam.content = xml; + responseAckExtraParam.sipURI = sipURI; + responseAckExtraParam.expires = expires; + return responseAck(request, Response.OK, null, responseAckExtraParam); + } + + public Element getRootElement(RequestEvent evt) throws DocumentException { + return getRootElement(evt, "gb2312"); + } + public Element getRootElement(RequestEvent evt, String charset) throws DocumentException { + + byte[] rawContent = evt.getRequest().getRawContent(); + if (evt.getRequest().getContentLength().getContentLength() == 0 + || rawContent == null + || rawContent.length == 0 + || ObjectUtils.isEmpty(new String(rawContent))) { + return null; + } + + if (charset == null) { + charset = "gb2312"; + } + SAXReader reader = new SAXReader(); + reader.setEncoding(charset); + // 对海康出现的未转义字符做处理。 + String[] destStrArray = new String[]{"<",">","&","'","""}; + // 或许可扩展兼容其他字符 + char despChar = '&'; + byte destBye = (byte) despChar; + List result = new ArrayList<>(); + for (int i = 0; i < rawContent.length; i++) { + if (rawContent[i] == destBye) { + boolean resul = false; + for (String destStr : destStrArray) { + if (i + destStr.length() <= rawContent.length) { + byte[] bytes = Arrays.copyOfRange(rawContent, i, i + destStr.length()); + resul = resul || (Arrays.equals(bytes,destStr.getBytes())); + } + } + if (resul) { + result.add(rawContent[i]); + } + }else { + result.add(rawContent[i]); + } + } + byte[] bytesResult = Bytes.toArray(result); + + Document xml; + try { + xml = reader.read(new ByteArrayInputStream(bytesResult)); + }catch (DocumentException e) { + log.warn("[xml解析异常]: 原文如下: \r\n{}", new String(bytesResult)); + log.warn("[xml解析异常]: 原文如下: 尝试兼容性处理"); + String[] xmlLineArray = new String(bytesResult).split("\\r?\\n"); + + // 兼容海康的address字段带有<破换xml结构导致无法解析xml的问题 + StringBuilder stringBuilder = new StringBuilder(); + for (String s : xmlLineArray) { + if (s.startsWith("{}", fromUserId); + SendRtpInfo sendRtpItem = sendRtpServerService.queryByCallId(callIdHeader.getCallId()); + if (sendRtpItem == null) { + log.warn("[收到ACK]:未找到来自{},callId: {}", fromUserId, callIdHeader.getCallId()); + return; + } + // tcp主动时,此时是级联下级平台,在回复200ok时,本地已经请求zlm开启监听,跳过下面步骤 + if (sendRtpItem.isTcpActive()) { + log.info("收到ACK,rtp/{} TCP主动方式等收到上级连接后开始发流", sendRtpItem.getStream()); + return; + } + MediaServer mediaServer = mediaServerService.getOne(sendRtpItem.getMediaServerId()); + log.info("收到ACK,rtp/{}开始向上级推流, 目标={}:{},SSRC={}, 协议:{}", + sendRtpItem.getStream(), + sendRtpItem.getIp(), + sendRtpItem.getPort(), + sendRtpItem.getSsrc(), + sendRtpItem.isTcp()?(sendRtpItem.isTcpActive()?"TCP主动":"TCP被动"):"UDP" + ); + Platform parentPlatform = platformService.queryPlatformByServerGBId(fromUserId); + + if (parentPlatform != null) { + DeviceChannel deviceChannel = deviceChannelService.getOneForSourceById(sendRtpItem.getChannelId()); + if (!userSetting.getServerId().equals(sendRtpItem.getServerId())) { + WVPResult wvpResult = redisRpcService.startSendRtp(callIdHeader.getCallId(), sendRtpItem); + if (wvpResult.getCode() == 0) { + redisCatchStorage.sendPlatformStartPlayMsg(sendRtpItem, deviceChannel, parentPlatform); + } + } else { + try { + if (mediaServer != null) { + if (sendRtpItem.isTcpActive()) { + mediaServerService.startSendRtpPassive(mediaServer,sendRtpItem, null); + } else { + mediaServerService.startSendRtp(mediaServer, sendRtpItem); + } + }else { + // mediaInfo 在集群的其他wvp里 + + } + + redisCatchStorage.sendPlatformStartPlayMsg(sendRtpItem, deviceChannel, parentPlatform); + }catch (ControllerException e) { + log.error("RTP推流失败: {}", e.getMessage()); + playService.startSendRtpStreamFailHand(sendRtpItem, parentPlatform, callIdHeader); + } + } + }else { + Device device = deviceService.getDeviceByDeviceId(fromUserId); + if (device == null) { + log.warn("[收到ACK]:来自{},目标为({})的推流信息为找到流体服务[{}]信息",fromUserId, toUserId, sendRtpItem.getMediaServerId()); + return; + } + // 设置为收到ACK后发送语音的设备已经在发送200OK开始发流了 + if (!device.isBroadcastPushAfterAck()) { + return; + } + if (mediaServer == null) { + log.warn("[收到ACK]:来自{},目标为({})的推流信息为找到流体服务[{}]信息",fromUserId, toUserId, sendRtpItem.getMediaServerId()); + return; + } + try { + if (sendRtpItem.isTcpActive()) { + mediaServerService.startSendRtpPassive(mediaServer, sendRtpItem, null); + } else { + mediaServerService.startSendRtp(mediaServer, sendRtpItem); + } + }catch (ControllerException e) { + log.error("RTP推流失败: {}", e.getMessage()); + playService.startSendRtpStreamFailHand(sendRtpItem, null, callIdHeader); + } + } + } + +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/ByeRequestProcessor.java b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/ByeRequestProcessor.java new file mode 100755 index 0000000..bacf504 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/ByeRequestProcessor.java @@ -0,0 +1,258 @@ +package com.genersoft.iot.vmp.gb28181.transmit.event.request.impl; + +import com.genersoft.iot.vmp.common.InviteInfo; +import com.genersoft.iot.vmp.common.InviteSessionType; +import com.genersoft.iot.vmp.conf.UserSetting; +import com.genersoft.iot.vmp.conf.exception.SsrcTransactionNotFoundException; +import com.genersoft.iot.vmp.gb28181.bean.*; +import com.genersoft.iot.vmp.gb28181.service.*; +import com.genersoft.iot.vmp.gb28181.session.AudioBroadcastManager; +import com.genersoft.iot.vmp.gb28181.session.SipInviteSessionManager; +import com.genersoft.iot.vmp.gb28181.transmit.SIPProcessorObserver; +import com.genersoft.iot.vmp.gb28181.transmit.cmd.ISIPCommander; +import com.genersoft.iot.vmp.gb28181.transmit.event.request.ISIPRequestProcessor; +import com.genersoft.iot.vmp.gb28181.transmit.event.request.SIPRequestProcessorParent; +import com.genersoft.iot.vmp.media.bean.MediaInfo; +import com.genersoft.iot.vmp.media.bean.MediaServer; +import com.genersoft.iot.vmp.media.service.IMediaServerService; +import com.genersoft.iot.vmp.service.ISendRtpServerService; +import com.genersoft.iot.vmp.service.redisMsg.IRedisRpcService; +import com.genersoft.iot.vmp.storager.IRedisCatchStorage; +import gov.nist.javax.sip.message.SIPRequest; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import javax.sip.InvalidArgumentException; +import javax.sip.RequestEvent; +import javax.sip.SipException; +import javax.sip.header.CallIdHeader; +import javax.sip.message.Response; +import java.text.ParseException; + +/** + * SIP命令类型: BYE请求 + */ +@Slf4j +@Component +public class ByeRequestProcessor extends SIPRequestProcessorParent implements InitializingBean, ISIPRequestProcessor { + + private final String method = "BYE"; + + @Autowired + private ISIPCommander cmder; + + @Autowired + private ISendRtpServerService sendRtpServerService; + + @Autowired + private IRedisCatchStorage redisCatchStorage; + + @Autowired + private IInviteStreamService inviteStreamService; + + @Autowired + private IPlatformService platformService; + + @Autowired + private IDeviceService deviceService; + + @Autowired + private IDeviceChannelService deviceChannelService; + + @Autowired + private AudioBroadcastManager audioBroadcastManager; + + @Autowired + private IGbChannelService channelService; + + @Autowired + private IMediaServerService mediaServerService; + + @Autowired + private SIPProcessorObserver sipProcessorObserver; + + @Autowired + private SipInviteSessionManager sessionManager; + + @Autowired + private IPlayService playService; + + @Autowired + private UserSetting userSetting; + + @Autowired + private IRedisRpcService redisRpcService; + + + @Override + public void afterPropertiesSet() throws Exception { + // 添加消息处理的订阅 + sipProcessorObserver.addRequestProcessor(method, this); + } + + /** + * 处理BYE请求 + */ + @Override + public void process(RequestEvent evt) { + SIPRequest request = (SIPRequest) evt.getRequest(); + try { + responseAck(request, Response.OK); + } catch (SipException | InvalidArgumentException | ParseException e) { + log.error("[回复BYE信息失败],{}", e.getMessage()); + } + CallIdHeader callIdHeader = (CallIdHeader)evt.getRequest().getHeader(CallIdHeader.NAME); + SendRtpInfo sendRtpItem = sendRtpServerService.queryByCallId(callIdHeader.getCallId()); + + // 收流端发送的停止 + if (sendRtpItem != null){ + CommonGBChannel channel = channelService.getOne(sendRtpItem.getChannelId()); + log.info("[收到bye] 来自{},停止通道:{}, 类型: {}, callId: {}", sendRtpItem.getTargetId(), channel.getGbDeviceId(), sendRtpItem.getPlayType(), callIdHeader.getCallId()); + + String streamId = sendRtpItem.getStream(); + log.info("[收到bye] 停止推流:{}, 媒体节点: {}", streamId, sendRtpItem.getMediaServerId()); + + if (sendRtpItem.getPlayType().equals(InviteStreamType.PUSH)) { + // 不是本平台的就发送redis消息让其他wvp停止发流 + Platform platform = platformService.queryPlatformByServerGBId(sendRtpItem.getTargetId()); + if (platform != null) { + redisCatchStorage.sendPlatformStopPlayMsg(sendRtpItem, platform, channel); + if (!userSetting.getServerId().equals(sendRtpItem.getServerId())) { + redisRpcService.stopSendRtp(sendRtpItem.getCallId()); + sendRtpServerService.deleteByCallId(sendRtpItem.getCallId()); + }else { + MediaServer mediaServer = mediaServerService.getOne(sendRtpItem.getMediaServerId()); + sendRtpServerService.deleteByCallId(callIdHeader.getCallId()); + if (mediaServer != null) { + mediaServerService.stopSendRtp(mediaServer, sendRtpItem.getApp(), sendRtpItem.getStream(), sendRtpItem.getSsrc()); + if (userSetting.getUseCustomSsrcForParentInvite()) { + mediaServerService.releaseSsrc(mediaServer.getId(), sendRtpItem.getSsrc()); + } + } + } + }else { + log.info("[上级平台停止观看] 未找到平台{}的信息,发送redis消息失败", sendRtpItem.getTargetId()); + } + }else { + MediaServer mediaInfo = mediaServerService.getOne(sendRtpItem.getMediaServerId()); + sendRtpServerService.delete(sendRtpItem); + mediaServerService.stopSendRtp(mediaInfo, sendRtpItem.getApp(), sendRtpItem.getStream(), sendRtpItem.getSsrc()); + if (userSetting.getUseCustomSsrcForParentInvite()) { + mediaServerService.releaseSsrc(mediaInfo.getId(), sendRtpItem.getSsrc()); + } + } + if (sendRtpItem.getServerId().equals(userSetting.getServerId())) { + MediaServer mediaServer = mediaServerService.getOne(sendRtpItem.getMediaServerId()); + if (mediaServer != null) { + AudioBroadcastCatch audioBroadcastCatch = audioBroadcastManager.get(sendRtpItem.getChannelId()); + if (audioBroadcastCatch != null && audioBroadcastCatch.getSipTransactionInfo().getCallId().equals(callIdHeader.getCallId())) { + // 来自上级平台的停止对讲 + log.info("[停止对讲] 来自上级,平台:{}, 通道:{}", sendRtpItem.getTargetId(), sendRtpItem.getChannelId()); + audioBroadcastManager.del(sendRtpItem.getChannelId()); + } + + MediaInfo mediaInfo = mediaServerService.getMediaInfo(mediaServer, sendRtpItem.getApp(), streamId); + + if (mediaInfo != null && mediaInfo.getReaderCount() <= 0) { + log.info("[收到bye] {} 无其它观看者,通知设备停止推流", streamId); + if (sendRtpItem.getPlayType().equals(InviteStreamType.PLAY)) { + Device device = deviceService.getDeviceByDeviceId(sendRtpItem.getTargetId()); + if (device == null) { + log.info("[收到bye] {} 通知设备停止推流时未找到设备信息", streamId); + return; + } + DeviceChannel deviceChannel = deviceChannelService.getOneForSourceById(sendRtpItem.getChannelId()); + if (deviceChannel == null) { + log.info("[收到bye] {} 通知设备停止推流时未找到通道信息", streamId); + return; + } + try { + log.info("[停止点播] {}/{}", sendRtpItem.getTargetId(), sendRtpItem.getChannelId()); + cmder.streamByeCmd(device, deviceChannel.getDeviceId(), sendRtpItem.getApp(), sendRtpItem.getStream(), null, null); + } catch (InvalidArgumentException | ParseException | SipException | + SsrcTransactionNotFoundException e) { + log.error("[收到bye] {} 无其它观看者,通知设备停止推流, 发送BYE失败 {}",streamId, e.getMessage()); + } + } + } + } + } else { + // TODO 流再其他wvp上时应该通知这个wvp停止推流和发送BYE + + } + } + // 可能是设备发送的停止 + SsrcTransaction ssrcTransaction = sessionManager.getSsrcTransactionByCallId(callIdHeader.getCallId()); + if (ssrcTransaction == null) { + return; + } + log.info("[收到bye] 来自:{}, 通道: {}, 类型: {}", ssrcTransaction.getDeviceId(), ssrcTransaction.getChannelId(), ssrcTransaction.getType()); + // TODO 结束点播 避免等待 + + if (ssrcTransaction.getPlatformId() != null ) { + Platform platform = platformService.queryPlatformByServerGBId(ssrcTransaction.getPlatformId()); + if (ssrcTransaction.getType().equals(InviteSessionType.BROADCAST)) { + log.info("[收到bye] 上级停止语音对讲,来自:{}, 通道已停止推流: {}", ssrcTransaction.getPlatformId(), ssrcTransaction.getChannelId()); + CommonGBChannel channel = channelService.getOne(ssrcTransaction.getChannelId()); + if (channel == null) { + log.info("[收到bye] 未找到通道,上级:{}, 通道:{}", ssrcTransaction.getPlatformId(), ssrcTransaction.getChannelId()); + return; + } + String mediaServerId = ssrcTransaction.getMediaServerId(); + platformService.stopBroadcast(platform, channel, ssrcTransaction.getApp(), ssrcTransaction.getStream(), false, + mediaServerService.getOne(mediaServerId)); + DeviceChannel deviceChannel = deviceChannelService.getOneForSourceById(channel.getGbId()); + Device device = deviceService.getDevice(channel.getDataDeviceId()); + playService.stopAudioBroadcast(device, deviceChannel); + } + + }else { + Device device = deviceService.getDeviceByDeviceId(ssrcTransaction.getDeviceId()); + if (device == null) { + log.info("[收到bye] 未找到设备:{} ", ssrcTransaction.getDeviceId()); + return; + } + DeviceChannel channel = deviceChannelService.getOneForSourceById(ssrcTransaction.getChannelId()); + if (channel == null) { + log.info("[收到bye] 未找到通道,设备:{}, 通道:{}", ssrcTransaction.getDeviceId(), ssrcTransaction.getChannelId()); + return; + } + switch (ssrcTransaction.getType()){ + case PLAY: + case PLAYBACK: + case DOWNLOAD: + InviteInfo inviteInfo = inviteStreamService.getInviteInfoByDeviceAndChannel(InviteSessionType.PLAY, channel.getId()); + if (inviteInfo != null) { + deviceChannelService.stopPlay(channel.getId()); + inviteStreamService.removeInviteInfo(inviteInfo); + if (inviteInfo.getStreamInfo() != null) { + mediaServerService.closeRTPServer(inviteInfo.getStreamInfo().getMediaServer(), inviteInfo.getStreamInfo().getStream()); + } + } + break; + case BROADCAST: + case TALK: + // 查找来源的对讲设备,发送停止 + Device sourceDevice = deviceService.getDeviceByChannelId(ssrcTransaction.getChannelId()); + AudioBroadcastCatch audioBroadcastCatch = audioBroadcastManager.get(channel.getId()); + if (sourceDevice != null) { + playService.stopAudioBroadcast(sourceDevice, channel); + } + if (audioBroadcastCatch != null) { + // 来自上级平台的停止对讲 + log.info("[停止对讲] 来自上级,平台:{}, 通道:{}", ssrcTransaction.getDeviceId(), channel.getDeviceId()); + audioBroadcastManager.del(channel.getId()); + } + break; + } + // 释放ssrc + MediaServer mediaServerItem = mediaServerService.getOne(ssrcTransaction.getMediaServerId()); + if (mediaServerItem != null) { + mediaServerService.releaseSsrc(mediaServerItem.getId(), ssrcTransaction.getSsrc()); + } + sessionManager.removeByCallId(ssrcTransaction.getCallId()); + } + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/CancelRequestProcessor.java b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/CancelRequestProcessor.java new file mode 100755 index 0000000..b04352a --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/CancelRequestProcessor.java @@ -0,0 +1,40 @@ +package com.genersoft.iot.vmp.gb28181.transmit.event.request.impl; + +import com.genersoft.iot.vmp.gb28181.transmit.SIPProcessorObserver; +import com.genersoft.iot.vmp.gb28181.transmit.event.request.ISIPRequestProcessor; +import com.genersoft.iot.vmp.gb28181.transmit.event.request.SIPRequestProcessorParent; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import javax.sip.RequestEvent; + +/** + * SIP命令类型: CANCEL请求 + */ +@Component +public class CancelRequestProcessor extends SIPRequestProcessorParent implements InitializingBean, ISIPRequestProcessor { + + private final String method = "CANCEL"; + + @Autowired + private SIPProcessorObserver sipProcessorObserver; + + @Override + public void afterPropertiesSet() throws Exception { + // 添加消息处理的订阅 + sipProcessorObserver.addRequestProcessor(method, this); + } + + /** + * 处理CANCEL请求 + * + * @param evt 事件 + */ + @Override + public void process(RequestEvent evt) { + // TODO 优先级99 Cancel Request消息实现,此消息一般为级联消息,上级给下级发送请求取消指令 + + } + +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/InviteRequestProcessor.java b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/InviteRequestProcessor.java new file mode 100755 index 0000000..6daf121 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/InviteRequestProcessor.java @@ -0,0 +1,659 @@ +package com.genersoft.iot.vmp.gb28181.transmit.event.request.impl; + +import com.genersoft.iot.vmp.common.InviteSessionType; +import com.genersoft.iot.vmp.common.VideoManagerConstants; +import com.genersoft.iot.vmp.common.enums.ChannelDataType; +import com.genersoft.iot.vmp.conf.DynamicTask; +import com.genersoft.iot.vmp.conf.SipConfig; +import com.genersoft.iot.vmp.conf.UserSetting; +import com.genersoft.iot.vmp.conf.exception.ControllerException; +import com.genersoft.iot.vmp.gb28181.bean.*; +import com.genersoft.iot.vmp.gb28181.service.*; +import com.genersoft.iot.vmp.gb28181.session.AudioBroadcastManager; +import com.genersoft.iot.vmp.gb28181.session.SSRCFactory; +import com.genersoft.iot.vmp.gb28181.session.SipInviteSessionManager; +import com.genersoft.iot.vmp.gb28181.transmit.SIPProcessorObserver; +import com.genersoft.iot.vmp.gb28181.transmit.cmd.ISIPCommanderForPlatform; +import com.genersoft.iot.vmp.gb28181.transmit.event.request.ISIPRequestProcessor; +import com.genersoft.iot.vmp.gb28181.transmit.event.request.SIPRequestProcessorParent; +import com.genersoft.iot.vmp.gb28181.utils.SipUtils; +import com.genersoft.iot.vmp.media.bean.MediaServer; +import com.genersoft.iot.vmp.media.service.IMediaServerService; +import com.genersoft.iot.vmp.service.ISendRtpServerService; +import com.genersoft.iot.vmp.service.bean.InviteErrorCode; +import com.genersoft.iot.vmp.storager.IRedisCatchStorage; +import gov.nist.javax.sdp.TimeDescriptionImpl; +import gov.nist.javax.sdp.fields.TimeField; +import gov.nist.javax.sdp.fields.URIField; +import gov.nist.javax.sip.message.SIPRequest; +import gov.nist.javax.sip.message.SIPResponse; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.ObjectUtils; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import javax.sdp.*; +import javax.sip.InvalidArgumentException; +import javax.sip.RequestEvent; +import javax.sip.SipException; +import javax.sip.header.CallIdHeader; +import javax.sip.message.Response; +import java.text.ParseException; +import java.util.List; +import java.util.Vector; + +/** + * SIP命令类型: INVITE请求 + */ +@Slf4j +@SuppressWarnings("rawtypes") +@Component +public class InviteRequestProcessor extends SIPRequestProcessorParent implements InitializingBean, ISIPRequestProcessor { + + private final String method = "INVITE"; + + @Autowired + private ISIPCommanderForPlatform cmderFroPlatform; + + @Autowired + private IDeviceChannelService deviceChannelService; + + @Autowired + private IDeviceService deviceService; + + @Autowired + private IGbChannelService channelService; + + @Autowired + private IGbChannelPlayService channelPlayService; + + @Autowired + private ISendRtpServerService sendRtpServerService; + + @Autowired + private IRedisCatchStorage redisCatchStorage; + + @Autowired + private IMediaServerService mediaServerService; + + @Autowired + private DynamicTask dynamicTask; + + @Autowired + private IPlayService playService; + + @Autowired + private IPlatformService platformService; + + @Autowired + private AudioBroadcastManager audioBroadcastManager; + + @Autowired + private SIPProcessorObserver sipProcessorObserver; + + @Autowired + private SipConfig config; + + @Autowired + private SipInviteSessionManager sessionManager; + + @Autowired + private UserSetting userSetting; + + @Autowired + private SSRCFactory ssrcFactory; + + + @Override + public void afterPropertiesSet() throws Exception { + // 添加消息处理的订阅 + sipProcessorObserver.addRequestProcessor(method, this); + } + + /** + * 处理invite请求 + * + * @param evt 请求消息 + */ + @Override + public void process(RequestEvent evt) { + + SIPRequest request = (SIPRequest)evt.getRequest(); + try { + InviteMessageInfo inviteInfo = decode(evt); + + // 查询请求是否来自上级平台\设备 + Platform platform = platformService.queryPlatformByServerGBId(inviteInfo.getRequesterId()); + if (platform == null) { + inviteFromDeviceHandle(request, inviteInfo); + } else { + // 查询平台下是否有该通道 + CommonGBChannel channel= channelService.queryOneWithPlatform(platform.getId(), inviteInfo.getTargetChannelId()); + if (channel == null) { + log.info("[上级INVITE] 通道不存在,返回404: {}", inviteInfo.getTargetChannelId()); + try { + // 通道不存在,发404,资源不存在 + responseAck(request, Response.NOT_FOUND); + } catch (SipException | InvalidArgumentException | ParseException e) { + log.error("[命令发送失败] invite 通道不存在: {}", e.getMessage()); + } + return; + } + log.info("[上级INVITE] 平台:{}, 通道:{}({}), 收流地址:{}:{},收流方式:{}, 点播类型:{}, SSRC:{}", + platform.getName(), channel.getGbName(), channel.getGbDeviceId(), inviteInfo.getIp(), + inviteInfo.getPort(), inviteInfo.isTcp()?(inviteInfo.isTcpActive()?"TCP主动":"TCP被动"): "UDP", + inviteInfo.getSessionName(), inviteInfo.getSsrc()); + if(!userSetting.getUseCustomSsrcForParentInvite() && ObjectUtils.isEmpty(inviteInfo.getSsrc())) { + log.warn("[上级INVITE] 点播失败, 上级未携带SSRC, 并且本级未设置使用自定义SSRC"); + // 通道存在,发100,TRYING + try { + responseAck(request, Response.BAD_REQUEST); + } catch (SipException | InvalidArgumentException | ParseException e) { + log.error("[命令发送失败] 上级INVITE TRYING: {}", e.getMessage()); + } + return; + } + // 通道存在,发100,TRYING + try { + responseAck(request, Response.TRYING); + } catch (SipException | InvalidArgumentException | ParseException e) { + log.error("[命令发送失败] 上级INVITE TRYING: {}", e.getMessage()); + } + + channelPlayService.startInvite(channel, inviteInfo, platform, ((code, msg, streamInfo) -> { + if (code != InviteErrorCode.SUCCESS.getCode()) { + try { + responseAck(request, Response.BUSY_HERE , msg); + } catch (SipException | InvalidArgumentException | ParseException e) { + log.error("[命令发送失败] 上级INVITE 点播失败: {}", e.getMessage()); + } + }else { + // 点播成功, TODO 可以在此处检测cancel命令是否存在,存在则不发送 + if (userSetting.getUseCustomSsrcForParentInvite()) { + // 上级平台点播时不使用上级平台指定的ssrc,使用自定义的ssrc,参考国标文档-点播外域设备媒体流SSRC处理方式 + MediaServer mediaServer = mediaServerService.getOne(streamInfo.getMediaServer().getId()); + if (mediaServer != null) { + String ssrc = "Play".equalsIgnoreCase(inviteInfo.getSessionName()) + ? ssrcFactory.getPlaySsrc(streamInfo.getMediaServer().getId()) + : ssrcFactory.getPlayBackSsrc(streamInfo.getMediaServer().getId()); + inviteInfo.setSsrc(ssrc); + } + } + // 构建sendRTP内容 + SendRtpInfo sendRtpItem = sendRtpServerService.createSendRtpInfo(streamInfo.getMediaServer(), + inviteInfo.getIp(), inviteInfo.getPort(), inviteInfo.getSsrc(), platform.getServerGBId(), + streamInfo.getApp(), streamInfo.getStream(), + channel.getGbId(), inviteInfo.isTcp(), platform.isRtcp()); + if (inviteInfo.isTcp() && inviteInfo.isTcpActive()) { + sendRtpItem.setTcpActive(true); + } + sendRtpItem.setStatus(1); + sendRtpItem.setCallId(inviteInfo.getCallId()); + + sendRtpItem.setPlayTypeByChannelDataType(channel.getDataType(), inviteInfo.getSessionName()); + sendRtpItem.setServerId(streamInfo.getServerId()); + sendRtpServerService.update(sendRtpItem); + String sdpIp = streamInfo.getMediaServer().getSdpIp(); + if (!ObjectUtils.isEmpty(platform.getSendStreamIp())) { + sdpIp = platform.getSendStreamIp(); + } + String content = createSendSdp(sendRtpItem, inviteInfo, sdpIp); + // 超时未收到Ack应该回复bye,当前等待时间为10秒 + dynamicTask.startDelay(inviteInfo.getCallId(), () -> { + log.info("[Ack ] 等待超时, {}/{}", inviteInfo.getCallId(), channel.getGbDeviceId()); + mediaServerService.releaseSsrc(streamInfo.getMediaServer().getId(), sendRtpItem.getSsrc()); + // 回复bye + sendBye(platform, inviteInfo.getCallId()); + }, 60 * 1000); + try { + responseSdpAck(request, content, platform); + } catch (SipException | InvalidArgumentException | ParseException e) { + log.error("[命令发送失败] 上级INVITE 发送 200(SDP): {}", e.getMessage()); + } + + // tcp主动模式,回复sdp后开启监听 + if (sendRtpItem.isTcpActive()) { + MediaServer mediaServer = mediaServerService.getOne(sendRtpItem.getMediaServerId()); + try { + mediaServerService.startSendRtpPassive(mediaServer, sendRtpItem, 5); + DeviceChannel deviceChannel = deviceChannelService.getOneForSourceById(sendRtpItem.getChannelId()); + if (deviceChannel != null) { + redisCatchStorage.sendPlatformStartPlayMsg(sendRtpItem, deviceChannel, platform); + } + }catch (ControllerException e) { + log.warn("[上级INVITE] tcp主动模式 发流失败", e); + sendBye(platform, inviteInfo.getCallId()); + } + } + } + })); + } + } catch (SdpException e) { + // 参数不全, 发400,请求错误 + try { + responseAck(request, Response.BAD_REQUEST); + } catch (SipException | InvalidArgumentException | ParseException sendException) { + log.error("[命令发送失败] invite BAD_REQUEST: {}", sendException.getMessage()); + } + } catch (InviteDecodeException e) { + try { + responseAck(request, e.getCode(), e.getMsg()); + } catch (SipException | InvalidArgumentException | ParseException sendException) { + log.error("[命令发送失败] invite BAD_REQUEST: {}", sendException.getMessage()); + } + }catch (PlayException e) { + try { + responseAck(request, e.getCode(), e.getMsg()); + } catch (SipException | InvalidArgumentException | ParseException sendException) { + log.error("[命令发送失败] invite 点播失败: {}", sendException.getMessage()); + } + }catch (Exception e) { + log.error("[Invite处理异常] ", e); + try { + responseAck(request, Response.SERVER_INTERNAL_ERROR, ""); + } catch (SipException | InvalidArgumentException | ParseException sendException) { + log.error("[命令发送失败] invite 点播失败: {}", sendException.getMessage()); + } + } + } + + private InviteMessageInfo decode(RequestEvent evt) throws SdpException { + + InviteMessageInfo inviteInfo = new InviteMessageInfo(); + SIPRequest request = (SIPRequest)evt.getRequest(); + String[] channelIdArrayFromSub = SipUtils.getChannelIdFromRequest(request); + + // 解析sdp消息, 使用jainsip 自带的sdp解析方式 + String contentString = new String(request.getRawContent()); + Gb28181Sdp gb28181Sdp = SipUtils.parseSDP(contentString); + SessionDescription sdp = gb28181Sdp.getBaseSdb(); + String sessionName = sdp.getSessionName().getValue(); + String channelIdFromSdp = null; + if(StringUtils.equalsIgnoreCase("Playback", sessionName)){ + URIField uriField = (URIField)sdp.getURI(); + channelIdFromSdp = uriField.getURI().split(":")[0]; + } + final String channelId = StringUtils.isNotBlank(channelIdFromSdp) ? channelIdFromSdp : + (channelIdArrayFromSub != null? channelIdArrayFromSub[0]: null); + String requesterId = SipUtils.getUserIdFromFromHeader(request); + CallIdHeader callIdHeader = (CallIdHeader) request.getHeader(CallIdHeader.NAME); + + if (requesterId == null || channelId == null) { + log.warn("[解析INVITE消息] 无法从请求中获取到来源id,返回400错误"); + throw new InviteDecodeException(Response.BAD_REQUEST, "request decode fail"); + } + log.info("[INVITE] 来源ID: {}, callId: {}, 来自:{}:{}", + requesterId, callIdHeader.getCallId(), request.getRemoteAddress(), request.getRemotePort()); + inviteInfo.setRequesterId(requesterId); + inviteInfo.setTargetChannelId(channelId); + if (channelIdArrayFromSub != null && channelIdArrayFromSub.length == 2) { + inviteInfo.setSourceChannelId(channelIdArrayFromSub[1]); + } + inviteInfo.setSessionName(sessionName); + inviteInfo.setSsrc(gb28181Sdp.getSsrc()); + inviteInfo.setCallId(callIdHeader.getCallId()); + + // 如果是录像回放,则会存在录像的开始时间与结束时间 + Long startTime = null; + Long stopTime = null; + if (sdp.getTimeDescriptions(false) != null && !sdp.getTimeDescriptions(false).isEmpty()) { + TimeDescriptionImpl timeDescription = (TimeDescriptionImpl) (sdp.getTimeDescriptions(false).get(0)); + TimeField startTimeFiled = (TimeField) timeDescription.getTime(); + startTime = startTimeFiled.getStartTime(); + stopTime = startTimeFiled.getStopTime(); + } + // 获取支持的格式 + Vector mediaDescriptions = sdp.getMediaDescriptions(true); + // 查看是否支持PS 负载96 + //String ip = null; + int port = -1; + boolean mediaTransmissionTCP = false; + Boolean tcpActive = null; + for (Object description : mediaDescriptions) { + MediaDescription mediaDescription = (MediaDescription) description; + Media media = mediaDescription.getMedia(); + + Vector mediaFormats = media.getMediaFormats(false); + if (mediaFormats.contains("96") || mediaFormats.contains("8")) { + port = media.getMediaPort(); + //String mediaType = media.getMediaType(); + String protocol = media.getProtocol(); + + // 区分TCP发流还是udp, 当前默认udp + if ("TCP/RTP/AVP".equalsIgnoreCase(protocol)) { + String setup = mediaDescription.getAttribute("setup"); + if (setup != null) { + mediaTransmissionTCP = true; + if ("active".equalsIgnoreCase(setup)) { + tcpActive = true; + } else if ("passive".equalsIgnoreCase(setup)) { + tcpActive = false; + } + } + } + break; + } + } + if (port == -1) { + log.info("[解析INVITE消息] 不支持的媒体格式,返回415"); + throw new InviteDecodeException(Response.UNSUPPORTED_MEDIA_TYPE, "unsupported media type"); + } + inviteInfo.setTcp(mediaTransmissionTCP); + inviteInfo.setTcpActive(tcpActive != null? tcpActive: false); + inviteInfo.setStartTime(startTime); + inviteInfo.setStopTime(stopTime); + + Vector sdpMediaDescriptions = sdp.getMediaDescriptions(true); + MediaDescription mediaDescription = null; + String downloadSpeed = "1"; + if (!sdpMediaDescriptions.isEmpty()) { + mediaDescription = (MediaDescription) sdpMediaDescriptions.get(0); + } + if (mediaDescription != null) { + downloadSpeed = mediaDescription.getAttribute("downloadspeed"); + } + inviteInfo.setIp(sdp.getConnection().getAddress()); + inviteInfo.setPort(port); + inviteInfo.setDownloadSpeed(downloadSpeed); + + return inviteInfo; + + } + + private String createSendSdp(SendRtpInfo sendRtpItem, InviteMessageInfo inviteInfo, String sdpIp) { + StringBuilder content = new StringBuilder(200); + content.append("v=0\r\n"); + content.append("o=" + inviteInfo.getTargetChannelId() + " 0 0 IN IP4 " + sdpIp + "\r\n"); + content.append("s=" + inviteInfo.getSessionName() + "\r\n"); + content.append("c=IN IP4 " + sdpIp + "\r\n"); + if ("Playback".equalsIgnoreCase(inviteInfo.getSessionName())) { + content.append("t=" + inviteInfo.getStartTime() + " " + inviteInfo.getStopTime() + "\r\n"); + } else { + content.append("t=0 0\r\n"); + } + if (sendRtpItem.isTcp()) { + content.append("m=video " + sendRtpItem.getLocalPort() + " TCP/RTP/AVP 96\r\n"); + if (!sendRtpItem.isTcpActive()) { + content.append("a=setup:active\r\n"); + } else { + content.append("a=setup:passive\r\n"); + } + }else { + content.append("m=video " + sendRtpItem.getLocalPort() + " RTP/AVP 96\r\n"); + } + content.append("a=sendonly\r\n"); + content.append("a=rtpmap:96 PS/90000\r\n"); + content.append("y=" + sendRtpItem.getSsrc() + "\r\n"); + content.append("f=\r\n"); + return content.toString(); + } + + private void sendBye(Platform platform, String callId) { + try { + SendRtpInfo sendRtpItem = sendRtpServerService.queryByCallId(callId); + if (sendRtpItem == null) { + return; + } + CommonGBChannel channel = channelService.getOne(sendRtpItem.getChannelId()); + if (channel == null) { + return; + } + cmderFroPlatform.streamByeCmd(platform, sendRtpItem, channel); + } catch (SipException | InvalidArgumentException | ParseException e) { + log.error("[命令发送失败] 上级INVITE 发送BYE: {}", e.getMessage()); + } + } + + public void inviteFromDeviceHandle(SIPRequest request, InviteMessageInfo inviteInfo) { + + if (inviteInfo.getSourceChannelId() == null) { + log.warn("来自设备的Invite请求,无法从请求信息中确定请求来自的通道,已忽略,requesterId: {}", inviteInfo.getRequesterId()); + try { + responseAck(request, Response.FORBIDDEN); + } catch (SipException | InvalidArgumentException | ParseException e) { + log.error("[命令发送失败] 来自设备的Invite请求,无法从请求信息中确定所属设备 FORBIDDEN: {}", e.getMessage()); + } + return; + } + // 非上级平台请求,查询是否设备请求(通常为接收语音广播的设备) + Device device = redisCatchStorage.getDevice(inviteInfo.getRequesterId()); + // 判断requesterId是设备还是通道 + if (device == null) { + device = deviceService.getDeviceBySourceChannelDeviceId(inviteInfo.getRequesterId()); + } + if (device == null) { + // 检查channelID是否可用 + device = deviceService.getDeviceBySourceChannelDeviceId(inviteInfo.getSourceChannelId()); + } + + if (device == null) { + log.warn("来自设备的Invite请求,无法从请求信息中确定所属设备,已忽略,requesterId: {}/{}", inviteInfo.getRequesterId(), + inviteInfo.getSourceChannelId()); + try { + responseAck(request, Response.FORBIDDEN); + } catch (SipException | InvalidArgumentException | ParseException e) { + log.error("[命令发送失败] 来自设备的Invite请求,无法从请求信息中确定所属设备 FORBIDDEN: {}", e.getMessage()); + } + return; + } + DeviceChannel deviceChannel = deviceChannelService.getOne(device.getDeviceId(), inviteInfo.getSourceChannelId()); + if (deviceChannel == null) { + List audioBroadcastCatchList = audioBroadcastManager.getByDeviceId(device.getDeviceId()); + if (audioBroadcastCatchList.isEmpty()) { + log.warn("来自设备的Invite请求,无法从请求信息中确定所属通道,已忽略,requesterId: {}/{}", inviteInfo.getRequesterId(), inviteInfo.getSourceChannelId()); + try { + responseAck(request, Response.FORBIDDEN); + } catch (SipException | InvalidArgumentException | ParseException e) { + log.error("[命令发送失败] 来自设备的Invite请求,无法从请求信息中确定所属设备 FORBIDDEN: {}", e.getMessage()); + } + return; + }else { + deviceChannel = deviceChannelService.getOneForSourceById(audioBroadcastCatchList.get(0).getChannelId()); + } + } + AudioBroadcastCatch broadcastCatch = audioBroadcastManager.get(deviceChannel.getId()); + if (broadcastCatch == null) { + log.warn("来自设备的Invite请求非语音广播,已忽略,requesterId: {}/{}", inviteInfo.getRequesterId(), inviteInfo.getSourceChannelId()); + try { + responseAck(request, Response.FORBIDDEN); + } catch (SipException | InvalidArgumentException | ParseException e) { + log.error("[命令发送失败] 来自设备的Invite请求非语音广播 FORBIDDEN: {}", e.getMessage()); + } + return; + } + log.info("收到设备" + inviteInfo.getRequesterId() + "的语音广播Invite请求"); + String key = VideoManagerConstants.BROADCAST_WAITE_INVITE + device.getDeviceId(); + if (!SipUtils.isFrontEnd(device.getDeviceId())) { + key += broadcastCatch.getChannelId(); + } + dynamicTask.stop(key); + try { + responseAck(request, Response.TRYING); + } catch (SipException | InvalidArgumentException | ParseException e) { + log.error("[命令发送失败] invite BAD_REQUEST: {}", e.getMessage()); + playService.stopAudioBroadcast(device, deviceChannel); + return; + } + String contentString = new String(request.getRawContent()); + + try { + Gb28181Sdp gb28181Sdp = SipUtils.parseSDP(contentString); + SessionDescription sdp = gb28181Sdp.getBaseSdb(); + // 获取支持的格式 + Vector mediaDescriptions = sdp.getMediaDescriptions(true); + + // 查看是否支持PS 负载96 + int port = -1; + boolean mediaTransmissionTCP = false; + Boolean tcpActive = null; + for (int i = 0; i < mediaDescriptions.size(); i++) { + MediaDescription mediaDescription = (MediaDescription) mediaDescriptions.get(i); + Media media = mediaDescription.getMedia(); + + Vector mediaFormats = media.getMediaFormats(false); +// if (mediaFormats.contains("8")) { + port = media.getMediaPort(); + String protocol = media.getProtocol(); + // 区分TCP发流还是udp, 当前默认udp + if ("TCP/RTP/AVP".equals(protocol)) { + String setup = mediaDescription.getAttribute("setup"); + if (setup != null) { + mediaTransmissionTCP = true; + if ("active".equals(setup)) { + tcpActive = true; + } else if ("passive".equals(setup)) { + tcpActive = false; + } + } + } + break; +// } + } + if (port == -1) { + log.info("不支持的媒体格式,返回415"); + // 回复不支持的格式 + try { + responseAck(request, Response.UNSUPPORTED_MEDIA_TYPE); // 不支持的格式,发415 + } catch (SipException | InvalidArgumentException | ParseException e) { + log.error("[命令发送失败] invite 不支持的媒体格式: {}", e.getMessage()); + playService.stopAudioBroadcast(device, deviceChannel); + return; + } + return; + } + String addressStr = sdp.getOrigin().getAddress(); + log.info("设备{}请求语音流,地址:{}:{},ssrc:{}, {}", inviteInfo.getRequesterId(), addressStr, port, gb28181Sdp.getSsrc(), + mediaTransmissionTCP ? (tcpActive ? "TCP主动" : "TCP被动") : "UDP"); + + MediaServer mediaServerItem = broadcastCatch.getMediaServerItem(); + if (mediaServerItem == null) { + log.warn("未找到语音喊话使用的zlm"); + try { + responseAck(request, Response.BUSY_HERE); + } catch (SipException | InvalidArgumentException | ParseException e) { + log.error("[命令发送失败] invite 未找到可用的zlm: {}", e.getMessage()); + playService.stopAudioBroadcast(device, deviceChannel); + } + return; + } + log.info("设备{}请求语音流, 收流地址:{}:{},ssrc:{}, {}, 对讲方式:{}", inviteInfo.getRequesterId(), addressStr, port, gb28181Sdp.getSsrc(), + mediaTransmissionTCP ? (tcpActive ? "TCP主动" : "TCP被动") : "UDP", sdp.getSessionName().getValue()); + CallIdHeader callIdHeader = (CallIdHeader) request.getHeader(CallIdHeader.NAME); + + SendRtpInfo sendRtpItem = sendRtpServerService.createSendRtpInfo(mediaServerItem, addressStr, port, gb28181Sdp.getSsrc(), inviteInfo.getRequesterId(), + device.getDeviceId(), deviceChannel.getId(), + mediaTransmissionTCP, false); + + if (sendRtpItem == null) { + log.warn("服务器端口资源不足"); + try { + responseAck(request, Response.BUSY_HERE); + } catch (SipException | InvalidArgumentException | ParseException e) { + log.error("[命令发送失败] invite 服务器端口资源不足: {}", e.getMessage()); + playService.stopAudioBroadcast(device, deviceChannel); + return; + } + return; + } + + sendRtpItem.setPlayType(InviteStreamType.BROADCAST); + sendRtpItem.setCallId(callIdHeader.getCallId()); + sendRtpItem.setStatus(1); + sendRtpItem.setApp(broadcastCatch.getApp()); + sendRtpItem.setStream(broadcastCatch.getStream()); + sendRtpItem.setPt(8); + sendRtpItem.setUsePs(false); + sendRtpItem.setRtcp(false); + sendRtpItem.setOnlyAudio(true); + sendRtpItem.setTcp(mediaTransmissionTCP); + if (tcpActive != null) { + sendRtpItem.setTcpActive(tcpActive); + } + + sendRtpServerService.update(sendRtpItem); + + Boolean streamReady = mediaServerService.isStreamReady(mediaServerItem, broadcastCatch.getApp(), broadcastCatch.getStream()); + if (streamReady) { + sendOk(device, deviceChannel, sendRtpItem, sdp, request, mediaServerItem, mediaTransmissionTCP, gb28181Sdp.getSsrc()); + } else { + log.warn("[语音通话], 未发现待推送的流,app={},stream={}", broadcastCatch.getApp(), broadcastCatch.getStream()); + try { + responseAck(request, Response.GONE); + } catch (SipException | InvalidArgumentException | ParseException e) { + log.error("[命令发送失败] 语音通话 回复410失败, {}", e.getMessage()); + return; + } + playService.stopAudioBroadcast(device, deviceChannel); + } + } catch (SdpException e) { + log.error("[SDP解析异常]", e); + playService.stopAudioBroadcast(device, deviceChannel); + } + } + + SIPResponse sendOk(Device device, DeviceChannel channel, SendRtpInfo sendRtpItem, SessionDescription sdp, SIPRequest request, MediaServer mediaServerItem, boolean mediaTransmissionTCP, String ssrc) { + SIPResponse sipResponse = null; + try { + sendRtpItem.setStatus(2); + sendRtpServerService.update(sendRtpItem); + StringBuffer content = new StringBuffer(200); + content.append("v=0\r\n"); + content.append("o=" + config.getId() + " " + sdp.getOrigin().getSessionId() + " " + sdp.getOrigin().getSessionVersion() + " IN IP4 " + mediaServerItem.getSdpIp() + "\r\n"); + content.append("s=Play\r\n"); + content.append("c=IN IP4 " + mediaServerItem.getSdpIp() + "\r\n"); + content.append("t=0 0\r\n"); + + if (mediaTransmissionTCP) { + content.append("m=audio " + sendRtpItem.getLocalPort() + " TCP/RTP/AVP 8\r\n"); + } else { + content.append("m=audio " + sendRtpItem.getLocalPort() + " RTP/AVP 8\r\n"); + } + + content.append("a=rtpmap:8 PCMA/8000/1\r\n"); + + content.append("a=sendonly\r\n"); + if (sendRtpItem.isTcp()) { + content.append("a=connection:new\r\n"); + if (!sendRtpItem.isTcpActive()) { + content.append("a=setup:active\r\n"); + } else { + content.append("a=setup:passive\r\n"); + } + } + content.append("y=" + ssrc + "\r\n"); + content.append("f=v/////a/1/8/1\r\n"); + + Platform parentPlatform = new Platform(); + parentPlatform.setServerIp(device.getIp()); + parentPlatform.setServerPort(device.getPort()); + parentPlatform.setServerGBId(device.getDeviceId()); + + sipResponse = responseSdpAck(request, content.toString(), parentPlatform); + + AudioBroadcastCatch audioBroadcastCatch = audioBroadcastManager.get(sendRtpItem.getChannelId()); + + audioBroadcastCatch.setStatus(AudioBroadcastCatchStatus.Ok); + audioBroadcastCatch.setSipTransactionInfoByRequest(sipResponse); + audioBroadcastManager.update(audioBroadcastCatch); + SsrcTransaction ssrcTransaction = SsrcTransaction.buildForDevice(device.getDeviceId(), sendRtpItem.getChannelId(), + request.getCallIdHeader().getCallId(), sendRtpItem.getApp(), sendRtpItem.getStream(), sendRtpItem.getSsrc(), sendRtpItem.getMediaServerId(), sipResponse, InviteSessionType.BROADCAST); + sessionManager.put(ssrcTransaction); + // 开启发流,大华在收到200OK后就会开始建立连接 + if (sendRtpItem.isTcpActive() || !device.isBroadcastPushAfterAck()) { + if (sendRtpItem.isTcpActive()) { + log.info("[语音喊话] 监听端口等待设备连接后推流"); + }else { + log.info("[语音喊话] 回复200OK后发现 BroadcastPushAfterAck为False,现在开始推流"); + } + + playService.startPushStream(sendRtpItem, channel, sipResponse, parentPlatform, request.getCallIdHeader()); + } + + } catch (SipException | InvalidArgumentException | ParseException | SdpParseException e) { + log.error("[命令发送失败] 语音喊话 回复200OK(SDP): {}", e.getMessage()); + } + return sipResponse; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/NotifyRequestForCatalogProcessor.java b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/NotifyRequestForCatalogProcessor.java new file mode 100755 index 0000000..6c8ac8f --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/NotifyRequestForCatalogProcessor.java @@ -0,0 +1,321 @@ +package com.genersoft.iot.vmp.gb28181.transmit.event.request.impl; + +import com.genersoft.iot.vmp.conf.UserSetting; +import com.genersoft.iot.vmp.gb28181.bean.*; +import com.genersoft.iot.vmp.gb28181.event.EventPublisher; +import com.genersoft.iot.vmp.gb28181.event.channel.ChannelEvent; +import com.genersoft.iot.vmp.gb28181.event.subscribe.catalog.CatalogEvent; +import com.genersoft.iot.vmp.gb28181.service.IDeviceChannelService; +import com.genersoft.iot.vmp.gb28181.service.IGbChannelService; +import com.genersoft.iot.vmp.gb28181.transmit.event.request.SIPRequestProcessorParent; +import com.genersoft.iot.vmp.gb28181.utils.SipUtils; +import com.genersoft.iot.vmp.storager.IRedisCatchStorage; +import com.genersoft.iot.vmp.utils.Coordtransform; +import com.genersoft.iot.vmp.utils.DateUtil; +import lombok.extern.slf4j.Slf4j; +import org.dom4j.DocumentException; +import org.dom4j.Element; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +import javax.sip.RequestEvent; +import javax.sip.header.FromHeader; +import java.lang.reflect.InvocationTargetException; +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.concurrent.ConcurrentLinkedQueue; + +/** + * SIP命令类型: NOTIFY请求中的目录请求处理 + */ +@Slf4j +@Component +public class NotifyRequestForCatalogProcessor extends SIPRequestProcessorParent { + + private final ConcurrentLinkedQueue channelList = new ConcurrentLinkedQueue<>(); + + private final ConcurrentLinkedQueue taskQueue = new ConcurrentLinkedQueue<>(); + + @Autowired + private UserSetting userSetting; + + @Autowired + private EventPublisher eventPublisher; + + @Autowired + private IRedisCatchStorage redisCatchStorage; + + @Autowired + private IDeviceChannelService deviceChannelService; + + @Autowired + private IGbChannelService channelService; + +// @Scheduled(fixedRate = 2000) //每400毫秒执行一次 +// public void showSize(){ +// log.warn("[notify-目录订阅] 待处理消息数量: {}", taskQueue.size() ); +// } + + public void process(RequestEvent evt) { + if (taskQueue.size() >= userSetting.getMaxNotifyCountQueue()) { + log.error("[notify-目录订阅] 待处理消息队列已满 {},返回486 BUSY_HERE,消息不做处理", userSetting.getMaxNotifyCountQueue()); + return; + } + taskQueue.offer(new HandlerCatchData(evt, null, null)); + } + + @Scheduled(fixedDelay = 400) //每400毫秒执行一次 + public void executeTaskQueue(){ + if (taskQueue.isEmpty()) { + return; + } + List handlerCatchDataList = new ArrayList<>(); + int size = taskQueue.size(); + for (int i = 0; i < size; i++) { + HandlerCatchData poll = taskQueue.poll(); + if (poll != null) { + handlerCatchDataList.add(poll); + } + } + if (handlerCatchDataList.isEmpty()) { + return; + } + for (HandlerCatchData take : handlerCatchDataList) { + if (take == null) { + continue; + } + RequestEvent evt = take.getEvt(); + try { + FromHeader fromHeader = (FromHeader) evt.getRequest().getHeader(FromHeader.NAME); + String deviceId = SipUtils.getUserIdFromFromHeader(fromHeader); + + Device device = redisCatchStorage.getDevice(deviceId); + if (device == null || !device.isOnLine()) { + log.warn("[收到目录订阅]:{}, 但是设备已经离线", (device != null ? device.getDeviceId() : "")); + continue; + } + Element rootElement = getRootElement(evt, device.getCharset()); + if (rootElement == null) { + log.warn("[ 收到目录订阅 ] content cannot be null, {}", evt.getRequest()); + continue; + } + Element deviceListElement = rootElement.element("DeviceList"); + if (deviceListElement == null) { + log.warn("[ 收到目录订阅 ] content cannot be null, {}", evt.getRequest()); + continue; + } + Iterator deviceListIterator = deviceListElement.elementIterator(); + if (deviceListIterator != null) { + + // 遍历DeviceList + while (deviceListIterator.hasNext()) { + Element itemDevice = deviceListIterator.next(); + CatalogChannelEvent catalogChannelEvent = null; + try { + catalogChannelEvent = CatalogChannelEvent.decode(itemDevice); + if (catalogChannelEvent.getChannel() == null) { + log.info("[解析CatalogChannelEvent]成功:但是解析通道信息失败, 原文如下: \n{}", new String(evt.getRequest().getRawContent())); + continue; + } + catalogChannelEvent.getChannel().setDataDeviceId(device.getId()); + if (catalogChannelEvent.getChannel().getLongitude() != null + && catalogChannelEvent.getChannel().getLatitude() != null + && catalogChannelEvent.getChannel().getLongitude() > 0 + && catalogChannelEvent.getChannel().getLatitude() > 0) { + if (device.checkWgs84()) { + catalogChannelEvent.getChannel().setGbLongitude(catalogChannelEvent.getChannel().getLongitude()); + catalogChannelEvent.getChannel().setGbLatitude(catalogChannelEvent.getChannel().getLatitude()); + }else { + Double[] wgs84Position = Coordtransform.GCJ02ToWGS84(catalogChannelEvent.getChannel().getLongitude(), catalogChannelEvent.getChannel().getLatitude()); + catalogChannelEvent.getChannel().setGbLongitude(wgs84Position[0]); + catalogChannelEvent.getChannel().setGbLatitude(wgs84Position[1]); + } + } + } catch (InvocationTargetException | NoSuchMethodException | InstantiationException | + IllegalAccessException e) { + log.error("[解析CatalogChannelEvent]失败,", e); + log.error("[解析CatalogChannelEvent]失败原文: \n{}", new String(evt.getRequest().getRawContent(), Charset.forName(device.getCharset()))); + continue; + } + if (log.isDebugEnabled()){ + log.debug("[收到目录订阅]:{}/{}-{}", device.getDeviceId(), + catalogChannelEvent.getChannel().getDeviceId(), catalogChannelEvent.getEvent()); + } + DeviceChannel channel = catalogChannelEvent.getChannel(); + switch (catalogChannelEvent.getEvent()) { + case CatalogEvent.ON: + // 上线 + log.info("[收到通道上线通知] 来自设备: {}, 通道 {}", device.getDeviceId(), catalogChannelEvent.getChannel().getDeviceId()); + channel.setStatus("ON"); + channelList.add(NotifyCatalogChannel.getInstance(NotifyCatalogChannel.Type.STATUS_CHANGED, channel)); + + if (userSetting.getDeviceStatusNotify()) { + // 发送redis消息 + redisCatchStorage.sendDeviceOrChannelStatus(device.getDeviceId(), catalogChannelEvent.getChannel().getDeviceId(), true); + } + break; + case CatalogEvent.OFF: + // 离线 + log.info("[收到通道离线通知] 来自设备: {}, 通道 {}", device.getDeviceId(), catalogChannelEvent.getChannel().getDeviceId()); + if (userSetting.getRefuseChannelStatusChannelFormNotify()) { + log.info("[收到通道离线通知] 但是平台已配置拒绝此消息,来自设备: {}, 通道 {}", device.getDeviceId(), catalogChannelEvent.getChannel().getDeviceId()); + } else { + channel.setStatus("OFF"); + channelList.add(NotifyCatalogChannel.getInstance(NotifyCatalogChannel.Type.STATUS_CHANGED, channel)); + if (userSetting.getDeviceStatusNotify()) { + // 发送redis消息 + redisCatchStorage.sendDeviceOrChannelStatus(device.getDeviceId(), catalogChannelEvent.getChannel().getDeviceId(), false); + } + } + break; + case CatalogEvent.VLOST: + // 视频丢失 + log.info("[收到通道视频丢失通知] 来自设备: {}, 通道 {}", device.getDeviceId(), catalogChannelEvent.getChannel().getDeviceId()); + if (userSetting.getRefuseChannelStatusChannelFormNotify()) { + log.info("[收到通道视频丢失通知] 但是平台已配置拒绝此消息,来自设备: {}, 通道 {}", device.getDeviceId(), catalogChannelEvent.getChannel().getDeviceId()); + } else { + channel.setStatus("OFF"); + channelList.add(NotifyCatalogChannel.getInstance(NotifyCatalogChannel.Type.STATUS_CHANGED, channel)); + + if (userSetting.getDeviceStatusNotify()) { + // 发送redis消息 + redisCatchStorage.sendDeviceOrChannelStatus(device.getDeviceId(), catalogChannelEvent.getChannel().getDeviceId(), false); + } + } + break; + case CatalogEvent.DEFECT: + // 故障 + log.info("[收到通道视频故障通知] 来自设备: {}, 通道 {}", device.getDeviceId(), catalogChannelEvent.getChannel().getDeviceId()); + if (userSetting.getRefuseChannelStatusChannelFormNotify()) { + log.info("[收到通道视频故障通知] 但是平台已配置拒绝此消息,来自设备: {}, 通道 {}", device.getDeviceId(), catalogChannelEvent.getChannel().getDeviceId()); + } else { + channel.setStatus("OFF"); + channelList.add(NotifyCatalogChannel.getInstance(NotifyCatalogChannel.Type.STATUS_CHANGED, channel)); + + if (userSetting.getDeviceStatusNotify()) { + // 发送redis消息 + redisCatchStorage.sendDeviceOrChannelStatus(device.getDeviceId(), catalogChannelEvent.getChannel().getDeviceId(), false); + } + } + break; + case CatalogEvent.ADD: + // 增加 + log.info("[收到增加通道通知] 来自设备: {}, 通道 {}", device.getDeviceId(), catalogChannelEvent.getChannel().getDeviceId()); + // 判断此通道是否存在 + DeviceChannel deviceChannel = deviceChannelService.getOneForSource(device.getId(), catalogChannelEvent.getChannel().getDeviceId()); + if (deviceChannel != null) { + log.info("[增加通道] 已存在,不发送通知只更新,设备: {}, 通道 {}", device.getDeviceId(), catalogChannelEvent.getChannel().getDeviceId()); + channel.setId(deviceChannel.getId()); + channel.setHasAudio(deviceChannel.isHasAudio()); + channel.setUpdateTime(DateUtil.getNow()); + channelList.add(NotifyCatalogChannel.getInstance(NotifyCatalogChannel.Type.UPDATE, channel)); + + } else { + catalogChannelEvent.getChannel().setUpdateTime(DateUtil.getNow()); + catalogChannelEvent.getChannel().setCreateTime(DateUtil.getNow()); + channelList.add(NotifyCatalogChannel.getInstance(NotifyCatalogChannel.Type.ADD, channel)); + + if (userSetting.getDeviceStatusNotify()) { + // 发送redis消息 + redisCatchStorage.sendChannelAddOrDelete(device.getDeviceId(), catalogChannelEvent.getChannel().getDeviceId(), true); + } + } + + break; + case CatalogEvent.DEL: + // 删除 + log.info("[收到删除通道通知] 来自设备: {}, 通道 {}", device.getDeviceId(), catalogChannelEvent.getChannel().getDeviceId()); + channelList.add(NotifyCatalogChannel.getInstance(NotifyCatalogChannel.Type.DELETE, channel)); + + if (userSetting.getDeviceStatusNotify()) { + // 发送redis消息 + redisCatchStorage.sendChannelAddOrDelete(device.getDeviceId(), catalogChannelEvent.getChannel().getDeviceId(), false); + } + break; + case CatalogEvent.UPDATE: + // 更新 + log.info("[收到更新通道通知] 来自设备: {}, 通道 {}", device.getDeviceId(), catalogChannelEvent.getChannel().getDeviceId()); + // 判断此通道是否存在 + DeviceChannel deviceChannelForUpdate = deviceChannelService.getOneForSource(device.getId(), catalogChannelEvent.getChannel().getDeviceId()); + if (deviceChannelForUpdate != null) { + channel.setId(deviceChannelForUpdate.getId()); + channel.setHasAudio(deviceChannelForUpdate.isHasAudio()); + channel.setUpdateTime(DateUtil.getNow()); + channel.setUpdateTime(DateUtil.getNow()); + channelList.add(NotifyCatalogChannel.getInstance(NotifyCatalogChannel.Type.UPDATE, channel)); + + } else { + catalogChannelEvent.getChannel().setCreateTime(DateUtil.getNow()); + catalogChannelEvent.getChannel().setUpdateTime(DateUtil.getNow()); + channelList.add(NotifyCatalogChannel.getInstance(NotifyCatalogChannel.Type.ADD, channel)); + + if (userSetting.getDeviceStatusNotify()) { + // 发送redis消息 + redisCatchStorage.sendChannelAddOrDelete(device.getDeviceId(), catalogChannelEvent.getChannel().getDeviceId(), true); + } + } + break; + default: + log.warn("[ NotifyCatalog ] event not found : {}", catalogChannelEvent.getEvent()); + + } + } + } + + } catch (DocumentException e) { + log.error("未处理的异常 ", e); + } + } + if (!channelList.isEmpty()) { + executeSave(); + } + } + + @Transactional + public void executeSave() { + int size = channelList.size(); + List channelListForSave = new ArrayList<>(); + for (int i = 0; i < size; i++) { + channelListForSave.add(channelList.poll()); + } + + for (NotifyCatalogChannel notifyCatalogChannel : channelListForSave) { + try { + switch (notifyCatalogChannel.getType()) { + case STATUS_CHANGED: + deviceChannelService.updateChannelStatusForNotify(notifyCatalogChannel.getChannel()); + CommonGBChannel channelForStatus = channelService.queryCommonChannelByDeviceChannel(notifyCatalogChannel.getChannel()); + if ("ON".equals(notifyCatalogChannel.getChannel().getStatus()) ) { + eventPublisher.channelEventPublish(channelForStatus, ChannelEvent.ChannelEventMessageType.ON); + }else { + eventPublisher.channelEventPublish(channelForStatus, ChannelEvent.ChannelEventMessageType.OFF); + } + break; + case ADD: + deviceChannelService.addChannel(notifyCatalogChannel.getChannel()); + CommonGBChannel channelForAdd = channelService.getOne(notifyCatalogChannel.getChannel().getId()); + eventPublisher.channelEventPublish(channelForAdd, ChannelEvent.ChannelEventMessageType.ADD); + break; + case UPDATE: + CommonGBChannel oldCommonChannel = channelService.getOne(notifyCatalogChannel.getChannel().getId()); + deviceChannelService.updateChannelForNotify(notifyCatalogChannel.getChannel()); + CommonGBChannel channel = channelService.getOne(oldCommonChannel.getGbId()); + eventPublisher.channelEventPublishForUpdate(channel, oldCommonChannel); + break; + case DELETE: + CommonGBChannel oldCommonChannelForDelete = channelService.queryCommonChannelByDeviceChannel(notifyCatalogChannel.getChannel()); + deviceChannelService.deleteForNotify(notifyCatalogChannel.getChannel()); + eventPublisher.channelEventPublish(oldCommonChannelForDelete, ChannelEvent.ChannelEventMessageType.DEL); + break; + } + }catch (Exception e) { + log.error("[存储收到的通道-异常]类型:{},编号:{}", notifyCatalogChannel.getType(), + notifyCatalogChannel.getChannel().getDeviceId(), e); + } + } + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/NotifyRequestForMobilePositionProcessor.java b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/NotifyRequestForMobilePositionProcessor.java new file mode 100755 index 0000000..5bcf206 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/NotifyRequestForMobilePositionProcessor.java @@ -0,0 +1,182 @@ +package com.genersoft.iot.vmp.gb28181.transmit.event.request.impl; + +import com.genersoft.iot.vmp.conf.UserSetting; +import com.genersoft.iot.vmp.gb28181.bean.Device; +import com.genersoft.iot.vmp.gb28181.bean.DeviceChannel; +import com.genersoft.iot.vmp.gb28181.bean.HandlerCatchData; +import com.genersoft.iot.vmp.gb28181.bean.MobilePosition; +import com.genersoft.iot.vmp.gb28181.event.EventPublisher; +import com.genersoft.iot.vmp.gb28181.service.IDeviceChannelService; +import com.genersoft.iot.vmp.gb28181.transmit.event.request.SIPRequestProcessorParent; +import com.genersoft.iot.vmp.gb28181.utils.NumericUtil; +import com.genersoft.iot.vmp.gb28181.utils.SipUtils; +import com.genersoft.iot.vmp.service.IMobilePositionService; +import com.genersoft.iot.vmp.storager.IRedisCatchStorage; +import com.genersoft.iot.vmp.utils.DateUtil; +import lombok.extern.slf4j.Slf4j; +import org.dom4j.DocumentException; +import org.dom4j.Element; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; +import org.springframework.util.ObjectUtils; + +import javax.sip.RequestEvent; +import javax.sip.header.FromHeader; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ConcurrentLinkedQueue; + +/** + * SIP命令类型: NOTIFY请求中的移动位置请求处理 + */ +@Slf4j +@Component +public class NotifyRequestForMobilePositionProcessor extends SIPRequestProcessorParent { + + private final ConcurrentLinkedQueue taskQueue = new ConcurrentLinkedQueue<>(); + + @Autowired + private UserSetting userSetting; + + @Autowired + private EventPublisher eventPublisher; + + @Autowired + private IRedisCatchStorage redisCatchStorage; + + @Autowired + private IDeviceChannelService deviceChannelService; + + @Autowired + private IMobilePositionService mobilePositionService; + + public void process(RequestEvent evt) { + + if (taskQueue.size() >= userSetting.getMaxNotifyCountQueue()) { + log.error("[notify-移动位置] 待处理消息队列已满 {},返回486 BUSY_HERE,消息不做处理", userSetting.getMaxNotifyCountQueue()); + return; + } + taskQueue.offer(new HandlerCatchData(evt, null, null)); + } + + @Scheduled(fixedDelay = 200) //每200毫秒执行一次 + public void executeTaskQueue() { + if (taskQueue.isEmpty()) { + return; + } + List handlerCatchDataList = new ArrayList<>(); + while (!taskQueue.isEmpty()) { + handlerCatchDataList.add(taskQueue.poll()); + } + if (handlerCatchDataList.isEmpty()) { + return; + } + for (HandlerCatchData take : handlerCatchDataList) { + if (take == null) { + continue; + } + RequestEvent evt = take.getEvt(); + try { + FromHeader fromHeader = (FromHeader) evt.getRequest().getHeader(FromHeader.NAME); + String deviceId = SipUtils.getUserIdFromFromHeader(fromHeader); + long startTime = System.currentTimeMillis(); + // 回复 200 OK + Element rootElement = getRootElement(evt); + if (rootElement == null) { + log.error("处理MobilePosition移动位置Notify时未获取到消息体,{}", evt.getRequest()); + continue; + } + Device device = redisCatchStorage.getDevice(deviceId); + if (device == null) { + log.error("处理MobilePosition移动位置Notify时未获取到device,{}", deviceId); + continue; + } + MobilePosition mobilePosition = new MobilePosition(); + mobilePosition.setDeviceId(device.getDeviceId()); + mobilePosition.setDeviceName(device.getName()); + mobilePosition.setCreateTime(DateUtil.getNow()); + + DeviceChannel deviceChannel = null; + List elements = rootElement.elements(); + readDocument: for (Element element : elements) { + switch (element.getName()){ + case "DeviceID": + String channelId = element.getStringValue(); + deviceChannel = deviceChannelService.getOne(device.getDeviceId(), channelId); + if (deviceChannel != null) { + mobilePosition.setChannelId(deviceChannel.getId()); + mobilePosition.setChannelDeviceId(deviceChannel.getDeviceId()); + }else { + log.error("[notify-移动位置] 未找到通道 {}/{}", device.getDeviceId(), channelId); + break readDocument; + } + break; + case "Time": + String timeVal = element.getStringValue(); + if (ObjectUtils.isEmpty(timeVal)) { + mobilePosition.setTime(DateUtil.getNow()); + } else { + mobilePosition.setTime(SipUtils.parseTime(timeVal)); + } + break; + case "Longitude": + mobilePosition.setLongitude(Double.parseDouble(element.getStringValue())); + break; + case "Latitude": + mobilePosition.setLatitude(Double.parseDouble(element.getStringValue())); + break; + case "Speed": + String speedVal = element.getStringValue(); + if (NumericUtil.isDouble(speedVal)) { + mobilePosition.setSpeed(Double.parseDouble(speedVal)); + } else { + mobilePosition.setSpeed(0.0); + } + break; + case "Direction": + String directionVal = element.getStringValue(); + if (NumericUtil.isDouble(directionVal)) { + mobilePosition.setDirection(Double.parseDouble(directionVal)); + } else { + mobilePosition.setDirection(0.0); + } + break; + case "Altitude": + String altitudeVal = element.getStringValue(); + if (NumericUtil.isDouble(altitudeVal)) { + mobilePosition.setAltitude(Double.parseDouble(altitudeVal)); + } else { + mobilePosition.setAltitude(0.0); + } + break; + + } + } + if (deviceChannel == null) { + continue; + } + + log.info("[收到移动位置订阅通知]:{}/{}->{}.{}, 时间: {}", mobilePosition.getDeviceId(), mobilePosition.getChannelId(), + mobilePosition.getLongitude(), mobilePosition.getLatitude(), System.currentTimeMillis() - startTime); + mobilePosition.setReportSource("Mobile Position"); + + mobilePositionService.add(mobilePosition); + // 向关联了该通道并且开启移动位置订阅的上级平台发送移动位置订阅消息 + try { + eventPublisher.mobilePositionEventPublish(mobilePosition); + }catch (Exception e) { + log.error("[MobilePositionEvent] 发送失败: ", e); + } + } catch (DocumentException e) { + log.error("[收到移动位置订阅通知] 文档解析异常: \r\n{}", evt.getRequest(), e); + } catch ( Exception e) { + log.error("[收到移动位置订阅通知] 异常: ", e); + } + } + } +// @Scheduled(fixedRate = 10000) +// public void execute(){ +// logger.debug("[待处理Notify-移动位置订阅消息数量]: {}", taskQueue.size()); +// } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/NotifyRequestProcessor.java b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/NotifyRequestProcessor.java new file mode 100755 index 0000000..15184e2 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/NotifyRequestProcessor.java @@ -0,0 +1,184 @@ +package com.genersoft.iot.vmp.gb28181.transmit.event.request.impl; + +import com.genersoft.iot.vmp.conf.SipConfig; +import com.genersoft.iot.vmp.gb28181.bean.*; +import com.genersoft.iot.vmp.gb28181.event.EventPublisher; +import com.genersoft.iot.vmp.gb28181.transmit.SIPProcessorObserver; +import com.genersoft.iot.vmp.gb28181.transmit.event.request.ISIPRequestProcessor; +import com.genersoft.iot.vmp.gb28181.transmit.event.request.SIPRequestProcessorParent; +import com.genersoft.iot.vmp.gb28181.utils.NumericUtil; +import com.genersoft.iot.vmp.gb28181.utils.SipUtils; +import com.genersoft.iot.vmp.gb28181.utils.XmlUtil; +import com.genersoft.iot.vmp.gb28181.service.IDeviceChannelService; +import com.genersoft.iot.vmp.storager.IRedisCatchStorage; +import com.genersoft.iot.vmp.utils.DateUtil; +import gov.nist.javax.sip.message.SIPRequest; +import lombok.extern.slf4j.Slf4j; +import org.dom4j.DocumentException; +import org.dom4j.Element; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import javax.sip.InvalidArgumentException; +import javax.sip.RequestEvent; +import javax.sip.SipException; +import javax.sip.header.FromHeader; +import javax.sip.message.Response; +import java.text.ParseException; + +/** + * SIP命令类型: NOTIFY请求,这是作为上级发送订阅请求后,设备才会响应的 + */ +@Slf4j +@Component +public class NotifyRequestProcessor extends SIPRequestProcessorParent implements InitializingBean, ISIPRequestProcessor { + + @Autowired + private SipConfig sipConfig; + + @Autowired + private IRedisCatchStorage redisCatchStorage; + + @Autowired + private EventPublisher publisher; + + private final String method = "NOTIFY"; + + @Autowired + private SIPProcessorObserver sipProcessorObserver; + + @Autowired + private IDeviceChannelService deviceChannelService; + + @Autowired + private NotifyRequestForCatalogProcessor notifyRequestForCatalogProcessor; + + @Autowired + private NotifyRequestForMobilePositionProcessor notifyRequestForMobilePositionProcessor; + + @Override + public void afterPropertiesSet() throws Exception { + // 添加消息处理的订阅 + sipProcessorObserver.addRequestProcessor(method, this); + } + + @Override + public void process(RequestEvent evt) { + try { + responseAck((SIPRequest) evt.getRequest(), Response.OK, null, null); + Element rootElement = getRootElement(evt); + if (rootElement == null) { + log.error("处理NOTIFY消息时未获取到消息体,{}", evt.getRequest()); + responseAck((SIPRequest) evt.getRequest(), Response.OK, null, null); + return; + } + String cmd = XmlUtil.getText(rootElement, "CmdType"); + + if (CmdType.CATALOG.equals(cmd)) { + notifyRequestForCatalogProcessor.process(evt); + } else if (CmdType.ALARM.equals(cmd)) { + processNotifyAlarm(evt); + } else if (CmdType.MOBILE_POSITION.equals(cmd)) { + notifyRequestForMobilePositionProcessor.process(evt); + } else { + log.info("接收到消息:" + cmd); + } + } catch (SipException | InvalidArgumentException | ParseException e) { + log.error("未处理的异常 ", e); + } catch (DocumentException e) { + throw new RuntimeException(e); + } + + } + /*** + * 处理alarm设备报警Notify + */ + private void processNotifyAlarm(RequestEvent evt) { + if (!sipConfig.isAlarm()) { + return; + } + try { + FromHeader fromHeader = (FromHeader) evt.getRequest().getHeader(FromHeader.NAME); + String deviceId = SipUtils.getUserIdFromFromHeader(fromHeader); + + Element rootElement = getRootElement(evt); + if (rootElement == null) { + log.error("处理alarm设备报警Notify时未获取到消息体{}", evt.getRequest()); + return; + } + Element deviceIdElement = rootElement.element("DeviceID"); + String channelId = deviceIdElement.getText().toString(); + + Device device = redisCatchStorage.getDevice(deviceId); + if (device == null) { + log.warn("[ NotifyAlarm ] 未找到设备:{}", deviceId); + return; + } + rootElement = getRootElement(evt, device.getCharset()); + if (rootElement == null) { + log.warn("[ NotifyAlarm ] content cannot be null, {}", evt.getRequest()); + return; + } + DeviceAlarm deviceAlarm = new DeviceAlarm(); + deviceAlarm.setDeviceId(deviceId); + deviceAlarm.setDeviceName(device.getName()); + deviceAlarm.setAlarmPriority(XmlUtil.getText(rootElement, "AlarmPriority")); + deviceAlarm.setAlarmMethod(XmlUtil.getText(rootElement, "AlarmMethod")); + String alarmTime = XmlUtil.getText(rootElement, "AlarmTime"); + if (alarmTime == null) { + log.warn("[ NotifyAlarm ] AlarmTime cannot be null"); + return; + } + deviceAlarm.setAlarmTime(DateUtil.ISO8601Toyyyy_MM_dd_HH_mm_ss(alarmTime)); + if (XmlUtil.getText(rootElement, "AlarmDescription") == null) { + deviceAlarm.setAlarmDescription(""); + } else { + deviceAlarm.setAlarmDescription(XmlUtil.getText(rootElement, "AlarmDescription")); + } + if (NumericUtil.isDouble(XmlUtil.getText(rootElement, "Longitude"))) { + deviceAlarm.setLongitude(Double.parseDouble(XmlUtil.getText(rootElement, "Longitude"))); + } else { + deviceAlarm.setLongitude(0.00); + } + if (NumericUtil.isDouble(XmlUtil.getText(rootElement, "Latitude"))) { + deviceAlarm.setLatitude(Double.parseDouble(XmlUtil.getText(rootElement, "Latitude"))); + } else { + deviceAlarm.setLatitude(0.00); + } + log.info("[收到Notify-Alarm]:{}/{}", device.getDeviceId(), deviceAlarm.getChannelId()); + if ("4".equals(deviceAlarm.getAlarmMethod())) { + DeviceChannel deviceChannel = deviceChannelService.getOne(device.getDeviceId(), channelId); + if (deviceChannel == null) { + log.warn("[解析报警通知] 未找到通道:{}/{}", device.getDeviceId(), channelId); + }else { + MobilePosition mobilePosition = new MobilePosition(); + mobilePosition.setChannelId(deviceChannel.getId()); + mobilePosition.setChannelDeviceId(deviceChannel.getDeviceId()); + mobilePosition.setCreateTime(DateUtil.getNow()); + mobilePosition.setDeviceId(deviceAlarm.getDeviceId()); + mobilePosition.setTime(deviceAlarm.getAlarmTime()); + mobilePosition.setLongitude(deviceAlarm.getLongitude()); + mobilePosition.setLatitude(deviceAlarm.getLatitude()); + mobilePosition.setReportSource("GPS Alarm"); + + // 更新device channel 的经纬度 + deviceChannel.setLongitude(mobilePosition.getLongitude()); + deviceChannel.setLatitude(mobilePosition.getLatitude()); + deviceChannel.setGpsTime(mobilePosition.getTime()); + + deviceChannelService.updateChannelGPS(device, deviceChannel, mobilePosition); + } + } + + // 回复200 OK + if (redisCatchStorage.deviceIsOnline(deviceId)) { + publisher.deviceAlarmEventPublish(deviceAlarm); + } + } catch (DocumentException e) { + log.error("未处理的异常 ", e); + } + } + + +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/RegisterRequestProcessor.java b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/RegisterRequestProcessor.java new file mode 100755 index 0000000..575d27c --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/RegisterRequestProcessor.java @@ -0,0 +1,243 @@ +package com.genersoft.iot.vmp.gb28181.transmit.event.request.impl; + +import com.genersoft.iot.vmp.conf.SipConfig; +import com.genersoft.iot.vmp.conf.UserSetting; +import com.genersoft.iot.vmp.gb28181.auth.DigestServerAuthenticationHelper; +import com.genersoft.iot.vmp.gb28181.bean.Device; +import com.genersoft.iot.vmp.gb28181.bean.GbSipDate; +import com.genersoft.iot.vmp.common.RemoteAddressInfo; +import com.genersoft.iot.vmp.gb28181.bean.SipTransactionInfo; +import com.genersoft.iot.vmp.gb28181.service.IDeviceService; +import com.genersoft.iot.vmp.gb28181.transmit.SIPProcessorObserver; +import com.genersoft.iot.vmp.gb28181.transmit.SIPSender; +import com.genersoft.iot.vmp.gb28181.transmit.event.request.ISIPRequestProcessor; +import com.genersoft.iot.vmp.gb28181.transmit.event.request.SIPRequestProcessorParent; +import com.genersoft.iot.vmp.gb28181.utils.SipUtils; +import com.genersoft.iot.vmp.utils.DateUtil; +import com.genersoft.iot.vmp.utils.IpPortUtil; +import gov.nist.javax.sip.address.AddressImpl; +import gov.nist.javax.sip.address.SipUri; +import gov.nist.javax.sip.header.SIPDateHeader; +import gov.nist.javax.sip.message.SIPRequest; +import gov.nist.javax.sip.message.SIPResponse; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import org.springframework.util.ObjectUtils; + +import javax.sip.RequestEvent; +import javax.sip.SipException; +import javax.sip.header.AuthorizationHeader; +import javax.sip.header.ContactHeader; +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; +import java.util.Calendar; +import java.util.Locale; + +/** + * SIP命令类型: REGISTER请求 + */ +@Slf4j +@Component +public class RegisterRequestProcessor extends SIPRequestProcessorParent implements InitializingBean, ISIPRequestProcessor { + + public final String method = "REGISTER"; + + @Autowired + private SipConfig sipConfig; + + @Autowired + private SIPProcessorObserver sipProcessorObserver; + + @Autowired + private IDeviceService deviceService; + + @Autowired + private SIPSender sipSender; + + @Autowired + private UserSetting userSetting; + + @Override + public void afterPropertiesSet() throws Exception { + // 添加消息处理的订阅 + sipProcessorObserver.addRequestProcessor(method, this); + } + + /** + * 收到注册请求 处理 + */ + @Override + public void process(RequestEvent evt) { + try { + SIPRequest request = (SIPRequest) evt.getRequest(); + Response response = null; + boolean passwordCorrect = false; + // 注册标志 + boolean registerFlag = true; + if (request.getExpires().getExpires() == 0) { + // 注销成功 + registerFlag = false; + } + FromHeader fromHeader = (FromHeader) request.getHeader(FromHeader.NAME); + AddressImpl address = (AddressImpl) fromHeader.getAddress(); + SipUri uri = (SipUri) address.getURI(); + String deviceId = uri.getUser(); + + Device device = deviceService.getDeviceByDeviceId(deviceId); + + RemoteAddressInfo remoteAddressInfo = SipUtils.getRemoteAddressFromRequest(request, + userSetting.getSipUseSourceIpAsRemoteAddress()); + String requestAddress = remoteAddressInfo.getIp() + ":" + remoteAddressInfo.getPort(); + String title = registerFlag ? "[注册请求]" : "[注销请求]"; + log.info(title + "设备:{}, 开始处理: {}", deviceId, requestAddress); + if (device != null && + device.getSipTransactionInfo() != null && + request.getCallIdHeader().getCallId().equals(device.getSipTransactionInfo().getCallId())) { + log.info(title + "设备:{}, 注册续订: {}", device.getDeviceId(), device.getDeviceId()); + if (registerFlag) { + device.setExpires(request.getExpires().getExpires()); + device.setIp(remoteAddressInfo.getIp()); + device.setPort(remoteAddressInfo.getPort()); + device.setHostAddress(IpPortUtil.concatenateIpAndPort(remoteAddressInfo.getIp(), String.valueOf(remoteAddressInfo.getPort()))); + + device.setLocalIp(request.getLocalAddress().getHostAddress()); + Response registerOkResponse = getRegisterOkResponse(request); + // 判断TCP还是UDP + ViaHeader reqViaHeader = (ViaHeader) request.getHeader(ViaHeader.NAME); + String transport = reqViaHeader.getTransport(); + device.setTransport("TCP".equalsIgnoreCase(transport) ? "TCP" : "UDP"); + sipSender.transmitRequest(request.getLocalAddress().getHostAddress(), registerOkResponse); + device.setRegisterTime(DateUtil.getNow()); + deviceService.online(device, null); + } else { + deviceService.offline(deviceId, "主动注销"); + } + return; + } + String password = (device != null && !ObjectUtils.isEmpty(device.getPassword())) ? device.getPassword() : sipConfig.getPassword(); + AuthorizationHeader authHead = (AuthorizationHeader) request.getHeader(AuthorizationHeader.NAME); + if (authHead == null && !ObjectUtils.isEmpty(password)) { + log.info(title + " 设备:{}, 回复401: {}", deviceId, requestAddress); + response = getMessageFactory().createResponse(Response.UNAUTHORIZED, request); + new DigestServerAuthenticationHelper().generateChallenge(getHeaderFactory(), response, sipConfig.getDomain()); + sipSender.transmitRequest(request.getLocalAddress().getHostAddress(), response); + return; + } + + // 校验密码是否正确 + passwordCorrect = ObjectUtils.isEmpty(password) || + new DigestServerAuthenticationHelper().doAuthenticatePlainTextPassword(request, password); + + if (!passwordCorrect) { + // 注册失败 + response = getMessageFactory().createResponse(Response.FORBIDDEN, request); + response.setReasonPhrase("wrong password"); + log.info(title + " 设备:{}, 密码/SIP服务器ID错误, 回复403: {}", deviceId, requestAddress); + sipSender.transmitRequest(request.getLocalAddress().getHostAddress(), response); + return; + } + + // 携带授权头并且密码正确 + response = getMessageFactory().createResponse(Response.OK, request); + // 如果主动禁用了Date头,则不添加 + if (!userSetting.isDisableDateHeader()) { + // 添加date头 + SIPDateHeader dateHeader = new SIPDateHeader(); + // 使用自己修改的 + GbSipDate gbSipDate = new GbSipDate(Calendar.getInstance(Locale.ENGLISH).getTimeInMillis()); + dateHeader.setDate(gbSipDate); + response.addHeader(dateHeader); + } + + if (request.getExpires() == null) { + response = getMessageFactory().createResponse(Response.BAD_REQUEST, request); + sipSender.transmitRequest(request.getLocalAddress().getHostAddress(), response); + return; + } + // 添加Contact头 + response.addHeader(request.getHeader(ContactHeader.NAME)); + // 添加Expires头 + response.addHeader(request.getExpires()); + + if (device == null) { + device = new Device(); + device.setStreamMode("TCP-PASSIVE"); + device.setCharset("GB2312"); + device.setGeoCoordSys("WGS84"); + device.setMediaServerId("auto"); + device.setDeviceId(deviceId); + device.setOnLine(false); + } else { + if (ObjectUtils.isEmpty(device.getStreamMode())) { + device.setStreamMode("TCP-PASSIVE"); + } + if (ObjectUtils.isEmpty(device.getCharset())) { + device.setCharset("GB2312"); + } + if (ObjectUtils.isEmpty(device.getGeoCoordSys())) { + device.setGeoCoordSys("WGS84"); + } + } + device.setServerId(userSetting.getServerId()); + device.setIp(remoteAddressInfo.getIp()); + device.setPort(remoteAddressInfo.getPort()); + device.setHostAddress(IpPortUtil.concatenateIpAndPort(remoteAddressInfo.getIp(), String.valueOf(remoteAddressInfo.getPort()))); + device.setLocalIp(request.getLocalAddress().getHostAddress()); + if (request.getExpires().getExpires() == 0) { + // 注销成功 + registerFlag = false; + } else { + // 注册成功 + device.setExpires(request.getExpires().getExpires()); + registerFlag = true; + // 判断TCP还是UDP + ViaHeader reqViaHeader = (ViaHeader) request.getHeader(ViaHeader.NAME); + String transport = reqViaHeader.getTransport(); + device.setTransport("TCP".equalsIgnoreCase(transport) ? "TCP" : "UDP"); + } + + sipSender.transmitRequest(request.getLocalAddress().getHostAddress(), response); + // 注册成功 + // 保存到redis + if (registerFlag) { + log.info("[注册成功] deviceId: {}->{}", deviceId, requestAddress); + device.setRegisterTime(DateUtil.getNow()); + SipTransactionInfo sipTransactionInfo = new SipTransactionInfo((SIPResponse) response); + deviceService.online(device, sipTransactionInfo); + } else { + log.info("[注销成功] deviceId: {}->{}", deviceId, requestAddress); + deviceService.offline(deviceId, "主动注销"); + } + } catch (SipException | NoSuchAlgorithmException | ParseException e) { + log.error("未处理的异常 ", e); + } + } + + private Response getRegisterOkResponse(Request request) throws ParseException { + // 携带授权头并且密码正确 + Response response = getMessageFactory().createResponse(Response.OK, request); + // 如果主动禁用了Date头,则不添加 + if (!userSetting.isDisableDateHeader()) { + // 添加date头 + SIPDateHeader dateHeader = new SIPDateHeader(); + // 使用自己修改的 + GbSipDate gbSipDate = new GbSipDate(Calendar.getInstance(Locale.ENGLISH).getTimeInMillis()); + dateHeader.setDate(gbSipDate); + response.addHeader(dateHeader); + } + + // 添加Contact头 + response.addHeader(request.getHeader(ContactHeader.NAME)); + // 添加Expires头 + response.addHeader(request.getExpires()); + + return response; + + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/SubscribeRequestProcessor.java b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/SubscribeRequestProcessor.java new file mode 100755 index 0000000..12d84b7 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/SubscribeRequestProcessor.java @@ -0,0 +1,212 @@ +package com.genersoft.iot.vmp.gb28181.transmit.event.request.impl; + +import com.genersoft.iot.vmp.gb28181.bean.*; +import com.genersoft.iot.vmp.gb28181.service.IPlatformService; +import com.genersoft.iot.vmp.gb28181.transmit.SIPProcessorObserver; +import com.genersoft.iot.vmp.gb28181.transmit.SIPSender; +import com.genersoft.iot.vmp.gb28181.transmit.event.request.ISIPRequestProcessor; +import com.genersoft.iot.vmp.gb28181.transmit.event.request.SIPRequestProcessorParent; +import com.genersoft.iot.vmp.gb28181.utils.SipUtils; +import com.genersoft.iot.vmp.gb28181.utils.XmlUtil; +import gov.nist.javax.sip.message.SIPRequest; +import gov.nist.javax.sip.message.SIPResponse; +import lombok.extern.slf4j.Slf4j; +import org.dom4j.DocumentException; +import org.dom4j.Element; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import javax.sip.InvalidArgumentException; +import javax.sip.RequestEvent; +import javax.sip.SipException; +import javax.sip.header.EventHeader; +import javax.sip.header.ExpiresHeader; +import javax.sip.message.Response; +import java.text.ParseException; + +/** + * SIP命令类型: SUBSCRIBE请求 + * @author lin + */ +@Slf4j +@Component +public class SubscribeRequestProcessor extends SIPRequestProcessorParent implements InitializingBean, ISIPRequestProcessor { + + private final String method = "SUBSCRIBE"; + + @Autowired + private SIPProcessorObserver sipProcessorObserver; + + @Autowired + private SubscribeHolder subscribeHolder; + + @Autowired + private SIPSender sipSender; + + + @Autowired + private IPlatformService platformService; + + @Override + public void afterPropertiesSet() throws Exception { + // 添加消息处理的订阅 + sipProcessorObserver.addRequestProcessor(method, this); + } + + /** + * 处理SUBSCRIBE请求 + * + * @param evt 事件 + */ + @Override + public void process(RequestEvent evt) { + SIPRequest request = (SIPRequest) evt.getRequest(); + try { + Element rootElement = getRootElement(evt); + if (rootElement == null) { + log.error("处理SUBSCRIBE请求 未获取到消息体{}", evt.getRequest()); + responseAck(request, Response.BAD_REQUEST); + return; + } + ExpiresHeader expires = request.getExpires(); + if (expires == null) { + log.error("处理SUBSCRIBE请求 未获取到ExpiresHeader{}", evt.getRequest()); + responseAck(request, Response.BAD_REQUEST, "missing expires"); + return; + } + String platformId = SipUtils.getUserIdFromFromHeader(request); + String cmd = XmlUtil.getText(rootElement, "CmdType"); + log.info("[收到订阅请求] 类型: {}, 来自: {}", cmd, platformId); + if (CmdType.MOBILE_POSITION.equals(cmd)) { + processNotifyMobilePosition(request, rootElement); +// } else if (CmdType.ALARM.equals(cmd)) { +// logger.info("接收到Alarm订阅"); +// processNotifyAlarm(serverTransaction, rootElement); + } else if (CmdType.CATALOG.equals(cmd)) { + processNotifyCatalogList(request, rootElement); + } else { + log.info("接收到消息:" + cmd); + + Response response = getMessageFactory().createResponse(200, request); + if (response != null) { + ExpiresHeader expireHeader = getHeaderFactory().createExpiresHeader(30); + response.setExpires(expireHeader); + } + log.info("response : " + response); + sipSender.transmitRequest(request.getLocalAddress().getHostAddress(), response); + } + } catch (ParseException | SipException | InvalidArgumentException | DocumentException e) { + log.error("未处理的异常 ", e); + } + + } + + /** + * 处理移动位置订阅消息 + */ + private void processNotifyMobilePosition(SIPRequest request, Element rootElement) throws SipException { + if (request == null) { + return; + } + String platformId = SipUtils.getUserIdFromFromHeader(request); + String deviceId = XmlUtil.getText(rootElement, "DeviceID"); + Platform platform = platformService.queryPlatformByServerGBId(platformId); + if (platform == null) { + return; + } + + String sn = XmlUtil.getText(rootElement, "SN"); + log.info("[回复上级的移动位置订阅请求]: {}", platformId); + StringBuilder resultXml = new StringBuilder(200); + resultXml.append("\r\n") + .append("\r\n") + .append("MobilePosition\r\n") + .append("").append(sn).append("\r\n") + .append("").append(deviceId).append("\r\n") + .append("OK\r\n") + .append("\r\n"); + + + + try { + int expires = request.getExpires().getExpires(); + SIPResponse response = responseXmlAck(request, resultXml.toString(), platform, expires); + + SubscribeInfo subscribeInfo = SubscribeInfo.getInstance(response, platformId, expires, + (EventHeader)request.getHeader(EventHeader.NAME)); + if (subscribeInfo.getExpires() > 0) { + // GPS上报时间间隔 + String interval = XmlUtil.getText(rootElement, "Interval"); + if (interval == null) { + subscribeInfo.setGpsInterval(5); + }else { + subscribeInfo.setGpsInterval(Integer.parseInt(interval)); + } + subscribeInfo.setSn(sn); + } + if (subscribeInfo.getExpires() == 0) { + subscribeHolder.removeMobilePositionSubscribe(platformId); + }else { + subscribeInfo.setTransactionInfo(new SipTransactionInfo(response)); + subscribeHolder.putMobilePositionSubscribe(platformId, subscribeInfo, ()->{ + platformService.sendNotifyMobilePosition(platformId); + }); + } + + } catch (SipException | InvalidArgumentException | ParseException e) { + log.error("未处理的异常 ", e); + } + } + + private void processNotifyAlarm(RequestEvent evt, Element rootElement) { + + } + + private void processNotifyCatalogList(SIPRequest request, Element rootElement) throws SipException { + if (request == null) { + log.info("[处理目录订阅] 发现request为NUll。已忽略"); + return; + } + String platformId = SipUtils.getUserIdFromFromHeader(request); + String deviceId = XmlUtil.getText(rootElement, "DeviceID"); + Platform platform = platformService.queryPlatformByServerGBId(platformId); + if (platform == null){ + log.info("[处理目录订阅] 未找到平台 {}。已忽略", platformId); + return; + } + + String sn = XmlUtil.getText(rootElement, "SN"); + log.info("[回复上级的目录订阅请求]: {}/{}", platformId, deviceId); + StringBuilder resultXml = new StringBuilder(200); + resultXml.append("\r\n") + .append("\r\n") + .append("Catalog\r\n") + .append("").append(sn).append("\r\n") + .append("").append(deviceId).append("\r\n") + .append("OK\r\n") + .append("\r\n"); + + try { + int expires = request.getExpires().getExpires(); + Platform parentPlatform = platformService.queryPlatformByServerGBId(platformId); + SIPResponse response = responseXmlAck(request, resultXml.toString(), parentPlatform, expires); + + SubscribeInfo subscribeInfo = SubscribeInfo.getInstance(response, platformId, expires, + (EventHeader)request.getHeader(EventHeader.NAME)); + + if (subscribeInfo.getExpires() == 0) { + subscribeHolder.removeCatalogSubscribe(platformId); + }else { + subscribeInfo.setTransactionInfo(new SipTransactionInfo(response)); + subscribeHolder.putCatalogSubscribe(platformId, subscribeInfo); + } + } catch (SipException | InvalidArgumentException | ParseException e) { + log.error("未处理的异常 ", e); + } + if (subscribeHolder.getCatalogSubscribe(platformId) == null + && platform.getAutoPushChannel() != null && platform.getAutoPushChannel()) { + platformService.addSimulatedSubscribeInfo(platform); + } + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/info/InfoRequestProcessor.java b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/info/InfoRequestProcessor.java new file mode 100755 index 0000000..02408aa --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/info/InfoRequestProcessor.java @@ -0,0 +1,154 @@ +package com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.info; + +import com.genersoft.iot.vmp.common.enums.ChannelDataType; +import com.genersoft.iot.vmp.gb28181.bean.*; +import com.genersoft.iot.vmp.gb28181.event.SipSubscribe; +import com.genersoft.iot.vmp.gb28181.service.*; +import com.genersoft.iot.vmp.gb28181.session.SipInviteSessionManager; +import com.genersoft.iot.vmp.gb28181.transmit.SIPProcessorObserver; +import com.genersoft.iot.vmp.gb28181.transmit.cmd.impl.SIPCommander; +import com.genersoft.iot.vmp.gb28181.transmit.event.request.ISIPRequestProcessor; +import com.genersoft.iot.vmp.gb28181.transmit.event.request.SIPRequestProcessorParent; +import com.genersoft.iot.vmp.service.ISendRtpServerService; +import com.genersoft.iot.vmp.storager.IRedisCatchStorage; +import gov.nist.javax.sip.message.SIPRequest; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import javax.sip.InvalidArgumentException; +import javax.sip.RequestEvent; +import javax.sip.SipException; +import javax.sip.header.CallIdHeader; +import javax.sip.header.ContentTypeHeader; +import javax.sip.message.Response; +import java.text.ParseException; + +/** + * INFO 一般用于国标级联时的回放控制 + */ +@Slf4j +@Component +public class InfoRequestProcessor extends SIPRequestProcessorParent implements InitializingBean, ISIPRequestProcessor { + + private final String method = "INFO"; + + @Autowired + private SIPProcessorObserver sipProcessorObserver; + + @Autowired + private IPlatformService platformService; + + @Autowired + private SipSubscribe sipSubscribe; + + @Autowired + private IRedisCatchStorage redisCatchStorage; + + @Autowired + private IInviteStreamService inviteStreamService; + + @Autowired + private IDeviceService deviceService; + + @Autowired + private IGbChannelService channelService; + + @Autowired + private IDeviceChannelService deviceChannelService; + + @Autowired + private SIPCommander cmder; + + @Autowired + private SipInviteSessionManager sessionManager; + + @Autowired + private ISendRtpServerService sendRtpServerService; + + @Override + public void afterPropertiesSet() throws Exception { + // 添加消息处理的订阅 + sipProcessorObserver.addRequestProcessor(method, this); + } + + @Override + public void process(RequestEvent evt) { + SIPRequest request = (SIPRequest) evt.getRequest(); + CallIdHeader callIdHeader = request.getCallIdHeader(); + // 先从会话内查找 + try { + SendRtpInfo sendRtpInfo = sendRtpServerService.queryByCallId(callIdHeader.getCallId()); + if (sendRtpInfo == null || !sendRtpInfo.isSendToPlatform()) { + // 不存在则回复404 + log.warn("[INFO 消息] 事务未找到, callID: {}", callIdHeader.getCallId()); + responseAck(request, Response.NOT_FOUND, "transaction not found"); + return; + } + // 查询上级平台是否存在 + Platform platform = platformService.queryPlatformByServerGBId(sendRtpInfo.getTargetId()); + if (platform == null || !platform.isStatus()) { + // 不存在则回复404 + log.warn("[INFO 消息] 平台未找到或者已离线: 平台: {}", sendRtpInfo.getTargetId()); + responseAck(request, Response.NOT_FOUND, "platform "+ sendRtpInfo.getTargetId() +" not found or offline"); + return; + } + CommonGBChannel channel = channelService.getOne(sendRtpInfo.getChannelId()); + if (channel == null) { + // 不存在则回复404 + log.warn("[INFO 消息] 通道不存在: 通道ID: {}", sendRtpInfo.getChannelId()); + responseAck(request, Response.NOT_FOUND, "channel not found or offline"); + return; + } + // 判断通道类型 + if (channel.getDataType() != ChannelDataType.GB28181) { + // 非国标通道不支持录像回放控制 + log.warn("[INFO 消息] 非国标通道不支持录像回放控制: 通道ID: {}", sendRtpInfo.getChannelId()); + responseAck(request, Response.FORBIDDEN, ""); + return; + } + + // 根据通道ID,获取所属设备 + Device device = deviceService.getDevice(channel.getDataDeviceId()); + if (device == null) { + // 不存在则回复404 + log.warn("[INFO 消息] 通道所属设备不存在, 通道ID: {}", sendRtpInfo.getChannelId()); + responseAck(request, Response.NOT_FOUND, "platform "+ sendRtpInfo.getChannelId() +" not found or offline"); + return; + } + // 获取通道的原始信息 + DeviceChannel deviceChannel = deviceChannelService.getOneForSourceById(sendRtpInfo.getChannelId()); + // 向原始通道转发控制消息 + ContentTypeHeader header = (ContentTypeHeader)evt.getRequest().getHeader(ContentTypeHeader.NAME); + String contentType = header.getContentType(); + String contentSubType = header.getContentSubType(); + if ("Application".equalsIgnoreCase(contentType) && "MANSRTSP".equalsIgnoreCase(contentSubType)) { + log.info("[INFO 消息] 平台: {}->{}({})/{}", platform.getServerGBId(), device.getName(), + device.getDeviceId(), deviceChannel.getId()); + // 不解析协议, 直接转发给对应的设备 + cmder.playbackControlCmd(device, deviceChannel, sendRtpInfo.getStream(), new String(evt.getRequest().getRawContent()), eventResult -> { + // 失败的回复 + try { + responseAck(request, eventResult.statusCode, eventResult.msg); + } catch (SipException | InvalidArgumentException | ParseException e) { + log.error("[命令发送失败] 国标级联 录像控制: {}", e.getMessage()); + } + }, eventResult -> { + // 成功的回复 + try { + responseAck(request, eventResult.statusCode); + } catch (SipException | InvalidArgumentException | ParseException e) { + log.error("[命令发送失败] 国标级联 录像控制: {}", e.getMessage()); + } + }); + } + } catch (SipException e) { + log.warn("SIP 回复错误", e); + } catch (InvalidArgumentException e) { + log.warn("参数无效", e); + } catch (ParseException e) { + log.warn("SIP回复时解析异常", e); + } + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/IMessageHandler.java b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/IMessageHandler.java new file mode 100755 index 0000000..085f921 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/IMessageHandler.java @@ -0,0 +1,23 @@ +package com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message; + +import com.genersoft.iot.vmp.gb28181.bean.Device; +import com.genersoft.iot.vmp.gb28181.bean.Platform; +import org.dom4j.Element; + +import javax.sip.RequestEvent; + +public interface IMessageHandler { + /** + * 处理来自设备的信息 + * @param evt + * @param device + */ + void handForDevice(RequestEvent evt, Device device, Element element); + + /** + * 处理来自平台的信息 + * @param evt + * @param parentPlatform + */ + void handForPlatform(RequestEvent evt, Platform parentPlatform, Element element); +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/MessageHandlerAbstract.java b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/MessageHandlerAbstract.java new file mode 100755 index 0000000..7a743d1 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/MessageHandlerAbstract.java @@ -0,0 +1,93 @@ +package com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message; + +import com.genersoft.iot.vmp.gb28181.bean.Device; +import com.genersoft.iot.vmp.gb28181.bean.Platform; +import com.genersoft.iot.vmp.gb28181.event.MessageSubscribe; +import com.genersoft.iot.vmp.gb28181.event.sip.MessageEvent; +import com.genersoft.iot.vmp.gb28181.service.IPlatformService; +import com.genersoft.iot.vmp.gb28181.transmit.event.request.SIPRequestProcessorParent; +import com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.query.cmd.CatalogQueryMessageHandler; +import com.genersoft.iot.vmp.vmanager.bean.ErrorCode; +import gov.nist.javax.sip.message.SIPRequest; +import lombok.extern.slf4j.Slf4j; +import org.dom4j.Element; +import org.springframework.beans.factory.annotation.Autowired; + +import javax.sip.InvalidArgumentException; +import javax.sip.RequestEvent; +import javax.sip.SipException; +import javax.sip.message.Response; +import java.text.ParseException; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import static com.genersoft.iot.vmp.gb28181.utils.XmlUtil.getText; + +@Slf4j +public abstract class MessageHandlerAbstract extends SIPRequestProcessorParent implements IMessageHandler{ + + public Map messageHandlerMap = new ConcurrentHashMap<>(); + + @Autowired + private IPlatformService platformService; + + @Autowired + private MessageSubscribe messageSubscribe; + + public void addHandler(String cmdType, IMessageHandler messageHandler) { + messageHandlerMap.put(cmdType, messageHandler); + } + + @Override + public void handForDevice(RequestEvent evt, Device device, Element element) { + String cmd = getText(element, "CmdType"); + if (cmd == null) { + try { + responseAck((SIPRequest) evt.getRequest(), Response.OK); + } catch (SipException | InvalidArgumentException | ParseException e) { + log.error("[命令发送失败] 回复200 OK: {}", e.getMessage()); + } + return; + } + IMessageHandler messageHandler = messageHandlerMap.get(cmd); + + if (messageHandler != null) { + //两个国标平台互相级联时由于上一步判断导致本该在平台处理的消息 放到了设备的处理逻辑 + //所以对目录查询单独做了校验 + if(messageHandler instanceof CatalogQueryMessageHandler){ + Platform parentPlatform = platformService.queryPlatformByServerGBId(device.getDeviceId()); + messageHandler.handForPlatform(evt, parentPlatform, element); + return; + } + messageHandler.handForDevice(evt, device, element); + }else { + handMessageEvent(element, null); + } + } + + @Override + public void handForPlatform(RequestEvent evt, Platform parentPlatform, Element element) { + String cmd = getText(element, "CmdType"); + IMessageHandler messageHandler = messageHandlerMap.get(cmd); + if (messageHandler != null) { + messageHandler.handForPlatform(evt, parentPlatform, element); + } + } + + + public void handMessageEvent(Element element, Object data) { + + String cmd = getText(element, "CmdType"); + String sn = getText(element, "SN"); + MessageEvent subscribe = (MessageEvent)messageSubscribe.getSubscribe(cmd + sn); + if (subscribe != null && subscribe.getCallback() != null) { + String result = getText(element, "Result"); + if (result == null || "OK".equalsIgnoreCase(result) || data != null) { + subscribe.getCallback().run(ErrorCode.SUCCESS.getCode(), ErrorCode.SUCCESS.getMsg(), data); + }else { + subscribe.getCallback().run(ErrorCode.ERROR100.getCode(), ErrorCode.ERROR100.getMsg(), result); + } + messageSubscribe.removeSubscribe(cmd + sn); + } + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/MessageRequestProcessor.java b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/MessageRequestProcessor.java new file mode 100755 index 0000000..a8c1062 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/MessageRequestProcessor.java @@ -0,0 +1,143 @@ +package com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message; + +import com.genersoft.iot.vmp.gb28181.bean.Device; +import com.genersoft.iot.vmp.gb28181.bean.DeviceNotFoundEvent; +import com.genersoft.iot.vmp.gb28181.bean.Platform; +import com.genersoft.iot.vmp.gb28181.bean.SsrcTransaction; +import com.genersoft.iot.vmp.gb28181.event.SipSubscribe; +import com.genersoft.iot.vmp.gb28181.event.sip.SipEvent; +import com.genersoft.iot.vmp.gb28181.service.IPlatformService; +import com.genersoft.iot.vmp.gb28181.session.SipInviteSessionManager; +import com.genersoft.iot.vmp.gb28181.transmit.SIPProcessorObserver; +import com.genersoft.iot.vmp.gb28181.transmit.event.request.ISIPRequestProcessor; +import com.genersoft.iot.vmp.gb28181.transmit.event.request.SIPRequestProcessorParent; +import com.genersoft.iot.vmp.gb28181.utils.SipUtils; +import com.genersoft.iot.vmp.storager.IRedisCatchStorage; +import gov.nist.javax.sip.message.SIPRequest; +import lombok.extern.slf4j.Slf4j; +import org.dom4j.DocumentException; +import org.dom4j.Element; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import javax.sip.InvalidArgumentException; +import javax.sip.RequestEvent; +import javax.sip.SipException; +import javax.sip.header.CSeqHeader; +import javax.sip.header.CallIdHeader; +import javax.sip.message.Response; +import java.text.ParseException; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +@Slf4j +@Component +public class MessageRequestProcessor extends SIPRequestProcessorParent implements InitializingBean, ISIPRequestProcessor { + + private final String method = "MESSAGE"; + + private static final Map messageHandlerMap = new ConcurrentHashMap<>(); + + @Autowired + private SIPProcessorObserver sipProcessorObserver; + + @Autowired + private IPlatformService platformService; + + @Autowired + private SipSubscribe sipSubscribe; + + @Autowired + private IRedisCatchStorage redisCatchStorage; + + @Autowired + private SipInviteSessionManager sessionManager; + + @Override + public void afterPropertiesSet() throws Exception { + // 添加消息处理的订阅 + sipProcessorObserver.addRequestProcessor(method, this); + } + + public void addHandler(String name, IMessageHandler handler) { + messageHandlerMap.put(name, handler); + } + + @Override + public void process(RequestEvent evt) { + SIPRequest sipRequest = (SIPRequest)evt.getRequest(); +// logger.info("接收到消息:" + evt.getRequest()); + String deviceId = SipUtils.getUserIdFromFromHeader(evt.getRequest()); + CallIdHeader callIdHeader = sipRequest.getCallIdHeader(); + CSeqHeader cSeqHeader = sipRequest.getCSeqHeader(); + // 先从会话内查找 + SsrcTransaction ssrcTransaction = sessionManager.getSsrcTransactionByCallId(callIdHeader.getCallId()); + // 兼容海康 媒体通知 消息from字段不是设备ID的问题 + if (ssrcTransaction != null) { + deviceId = ssrcTransaction.getDeviceId(); + } + SIPRequest request = (SIPRequest) evt.getRequest(); + // 查询设备是否存在 + Device device = redisCatchStorage.getDevice(deviceId); + // 查询上级平台是否存在 + Platform parentPlatform = platformService.queryPlatformByServerGBId(deviceId); + try { + if (device != null && parentPlatform != null) { + String hostAddress = request.getRemoteAddress().getHostAddress(); + int remotePort = request.getRemotePort(); + if (device.getHostAddress().equals(hostAddress + ":" + remotePort)) { + parentPlatform = null; + }else { + device = null; + } + } + if (device == null && parentPlatform == null) { + // 不存在则回复404 + responseAck(request, Response.NOT_FOUND, "device "+ deviceId +" not found"); + log.warn("[设备未找到 ]deviceId: {}, callId: {}", deviceId, callIdHeader.getCallId()); + SipEvent sipEvent = sipSubscribe.getSubscribe(callIdHeader.getCallId() + cSeqHeader.getSeqNumber()); + if (sipEvent != null && sipEvent.getErrorEvent() != null){ + DeviceNotFoundEvent deviceNotFoundEvent = new DeviceNotFoundEvent(callIdHeader.getCallId()); + SipSubscribe.EventResult eventResult = new SipSubscribe.EventResult(deviceNotFoundEvent); + sipEvent.getErrorEvent().response(eventResult); + } + }else { + Element rootElement; + try { + rootElement = getRootElement(evt); + if (rootElement == null) { + log.error("处理MESSAGE请求 未获取到消息体{}", evt.getRequest()); + responseAck(request, Response.BAD_REQUEST, "content is null"); + return; + } + String name = rootElement.getName(); + IMessageHandler messageHandler = messageHandlerMap.get(name); + if (messageHandler != null) { + if (device != null) { + messageHandler.handForDevice(evt, device, rootElement); + }else { // 由于上面已经判断都为null则直接返回,所以这里device和parentPlatform必有一个不为null + messageHandler.handForPlatform(evt, parentPlatform, rootElement); + } + }else { + // 不支持的message + // 不存在则回复415 + responseAck(request, Response.UNSUPPORTED_MEDIA_TYPE, "Unsupported message type, must Control/Notify/Query/Response"); + } + } catch (DocumentException e) { + log.warn("解析XML消息内容异常", e); + // 不存在则回复404 + responseAck(request, Response.BAD_REQUEST, e.getMessage()); + } + } + } catch (SipException e) { + log.warn("SIP 回复错误", e); + } catch (InvalidArgumentException e) { + log.warn("参数无效", e); + } catch (ParseException e) { + log.warn("SIP回复时解析异常", e); + } + } + + +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/control/ControlMessageHandler.java b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/control/ControlMessageHandler.java new file mode 100755 index 0000000..235a477 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/control/ControlMessageHandler.java @@ -0,0 +1,27 @@ +package com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.control; + +import com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.MessageHandlerAbstract; +import com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.MessageRequestProcessor; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +/** + * 命令类型: 控制命令 + * 命令类型: 设备控制: 远程启动, 录像控制(TODO), 报警布防/撤防命令(TODO), 报警复位命令(TODO), + * 强制关键帧命令(TODO), 拉框放大/缩小控制命令(TODO), 看守位控制(TODO), 报警复位(TODO) + * 命令类型: 设备配置: SVAC编码配置(TODO), 音频参数(TODO), SVAC解码配置(TODO) + */ +@Component +public class ControlMessageHandler extends MessageHandlerAbstract implements InitializingBean { + + private final String messageType = "Control"; + + @Autowired + private MessageRequestProcessor messageRequestProcessor; + + @Override + public void afterPropertiesSet() throws Exception { + messageRequestProcessor.addHandler(messageType, this); + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/control/cmd/DeviceControlQueryMessageHandler.java b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/control/cmd/DeviceControlQueryMessageHandler.java new file mode 100755 index 0000000..e22e136 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/control/cmd/DeviceControlQueryMessageHandler.java @@ -0,0 +1,639 @@ +package com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.control.cmd; + +import com.genersoft.iot.vmp.common.enums.ChannelDataType; +import com.genersoft.iot.vmp.common.enums.DeviceControlType; +import com.genersoft.iot.vmp.gb28181.bean.*; +import com.genersoft.iot.vmp.gb28181.event.SipSubscribe; +import com.genersoft.iot.vmp.gb28181.service.IDeviceChannelService; +import com.genersoft.iot.vmp.gb28181.service.IDeviceService; +import com.genersoft.iot.vmp.gb28181.service.IGbChannelControlService; +import com.genersoft.iot.vmp.gb28181.service.IGbChannelService; +import com.genersoft.iot.vmp.gb28181.transmit.cmd.impl.SIPCommander; +import com.genersoft.iot.vmp.gb28181.transmit.event.request.SIPRequestProcessorParent; +import com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.IMessageHandler; +import com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.control.ControlMessageHandler; +import com.genersoft.iot.vmp.vmanager.bean.ErrorCode; +import gov.nist.javax.sip.message.SIPRequest; +import lombok.extern.slf4j.Slf4j; +import org.dom4j.Element; +import org.springframework.beans.factory.InitializingBean; +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.SipException; +import javax.sip.address.SipURI; +import javax.sip.message.Response; +import java.text.ParseException; +import java.util.List; + +import static com.genersoft.iot.vmp.gb28181.utils.XmlUtil.getText; +import static com.genersoft.iot.vmp.gb28181.utils.XmlUtil.loadElement; + +@Slf4j +@Component +public class DeviceControlQueryMessageHandler extends SIPRequestProcessorParent implements InitializingBean, IMessageHandler { + + private final String cmdType = "DeviceControl"; + + @Autowired + private ControlMessageHandler controlMessageHandler; + + @Autowired + private IGbChannelService channelService; + + @Autowired + private IGbChannelControlService channelControlService; + + @Autowired + private IDeviceService deviceService; + + @Autowired + private IDeviceChannelService deviceChannelService; + + @Autowired + private SIPCommander cmder; + + @Override + public void afterPropertiesSet() throws Exception { + controlMessageHandler.addHandler(cmdType, this); + } + + @Override + public void handForDevice(RequestEvent evt, Device device, Element element) { + + } + + @Override + public void handForPlatform(RequestEvent evt, Platform platform, Element rootElement) { + + SIPRequest request = (SIPRequest) evt.getRequest(); + + // 此处是上级发出的DeviceControl指令 + String targetGBId = ((SipURI) request.getToHeader().getAddress().getURI()).getUser(); + String channelId = getText(rootElement, "DeviceID"); + // 远程启动功能 + if (!ObjectUtils.isEmpty(getText(rootElement, "TeleBoot"))) { + // 拒绝远程启动命令 + log.warn("[deviceControl] 远程启动命令, 禁用,不允许上级平台随意重启下级平台"); + try { + responseAck(request, Response.FORBIDDEN); + } catch (SipException | InvalidArgumentException | ParseException e) { + log.error("[命令发送失败] 错误信息: {}", e.getMessage()); + } + return; + } + DeviceControlType deviceControlType = DeviceControlType.typeOf(rootElement); + + CommonGBChannel channel = channelService.queryOneWithPlatform(platform.getId(), channelId); + if (channel == null) { + log.warn("[deviceControl] 未找到通道, 平台: {}({}),通道编号:{}", platform.getName(), + platform.getServerGBId(), channelId); + try { + responseAck(request, Response.NOT_FOUND, "channel not found"); + } catch (SipException | InvalidArgumentException | ParseException e) { + log.error("[命令发送失败] 错误信息: {}", e.getMessage()); + } + return; + } + log.info("[deviceControl] 命令: {}, 平台: {}({})->{}", deviceControlType, platform.getName(), + platform.getServerGBId(), channel.getGbId()); + + if (!ObjectUtils.isEmpty(deviceControlType)) { + switch (deviceControlType) { + case PTZ: + handlePtzCmd(channel, rootElement, request, DeviceControlType.PTZ); + break; + case ALARM: + handleAlarmCmd(channel, rootElement, request); + break; + case GUARD: + handleGuardCmd(channel, rootElement, request, DeviceControlType.GUARD); + break; + case RECORD: + handleRecordCmd(channel, rootElement, request, DeviceControlType.RECORD); + break; + case I_FRAME: + handleIFameCmd(channel, request); + break; + case TELE_BOOT: + handleTeleBootCmd(channel, request); + break; + case DRAG_ZOOM_IN: + handleDragZoom(channel, rootElement, request, DeviceControlType.DRAG_ZOOM_IN); + break; + case DRAG_ZOOM_OUT: + handleDragZoom(channel, rootElement, request, DeviceControlType.DRAG_ZOOM_OUT); + break; + case HOME_POSITION: + handleHomePositionCmd(channel, rootElement, request, DeviceControlType.HOME_POSITION); + break; + default: + break; + } + } + } + + /** + * 处理云台指令 + */ + private void handlePtzCmd(CommonGBChannel channel, Element rootElement, SIPRequest request, DeviceControlType type) { + if (channel.getDataType() == ChannelDataType.GB28181) { + + deviceChannelService.handlePtzCmd(channel.getDataDeviceId(), channel.getGbId(), rootElement, type, ((code, msg, data) -> { + try { + responseAck(request, code, msg); + } catch (InvalidArgumentException | SipException | ParseException exception) { + log.error("[命令发送失败] 云台指令: {}", exception.getMessage()); + } + })); + }else { + // 解析云台控制参数 + String cmdString = getText(rootElement, type.getVal()); + IFrontEndControlCode frontEndControlCode = FrontEndCode.decode(cmdString); + if (frontEndControlCode == null) { + log.info("[INFO 消息] 不支持的控制方式"); + try { + responseAck(request, Response.FORBIDDEN, ""); + } catch (InvalidArgumentException | SipException | ParseException exception) { + log.error("[命令发送失败] 云台指令: {}", exception.getMessage()); + } + return; + } + switch (frontEndControlCode.getType()){ + case PTZ: + channelControlService.ptz(channel, (FrontEndControlCodeForPTZ)frontEndControlCode, ((code, msg, data) -> { + try { + if (code == ErrorCode.SUCCESS.getCode()) { + responseAck(request, Response.OK); + }else { + responseAck(request, Response.FORBIDDEN); + } + } catch (InvalidArgumentException | SipException | ParseException exception) { + log.error("[命令发送失败] 云台指令: {}", exception.getMessage()); + } + })); + break; + case FI: + channelControlService.fi(channel, (FrontEndControlCodeForFI) frontEndControlCode, ((code, msg, data) -> { + try { + if (code == ErrorCode.SUCCESS.getCode()) { + responseAck(request, Response.OK); + }else { + responseAck(request, Response.FORBIDDEN); + } + } catch (InvalidArgumentException | SipException | ParseException exception) { + log.error("[命令发送失败] FI指令: {}", exception.getMessage()); + } + })); + break; + case PRESET: + channelControlService.preset(channel, (FrontEndControlCodeForPreset) frontEndControlCode, ((code, msg, data) -> { + try { + if (code == ErrorCode.SUCCESS.getCode()) { + responseAck(request, Response.OK); + }else { + responseAck(request, Response.FORBIDDEN); + } + } catch (InvalidArgumentException | SipException | ParseException exception) { + log.error("[命令发送失败] 预置位指令: {}", exception.getMessage()); + } + })); + break; + case TOUR: + channelControlService.tour(channel, (FrontEndControlCodeForTour) frontEndControlCode, ((code, msg, data) -> { + try { + if (code == ErrorCode.SUCCESS.getCode()) { + responseAck(request, Response.OK); + }else { + responseAck(request, Response.FORBIDDEN); + } + } catch (InvalidArgumentException | SipException | ParseException exception) { + log.error("[命令发送失败] 巡航指令: {}", exception.getMessage()); + } + })); + break; + case SCAN: + channelControlService.scan(channel, (FrontEndControlCodeForScan) frontEndControlCode, ((code, msg, data) -> { + try { + if (code == ErrorCode.SUCCESS.getCode()) { + responseAck(request, Response.OK); + }else { + responseAck(request, Response.FORBIDDEN); + } + } catch (InvalidArgumentException | SipException | ParseException exception) { + log.error("[命令发送失败] 扫描指令: {}", exception.getMessage()); + } + })); + break; + case AUXILIARY: + channelControlService.auxiliary(channel, (FrontEndControlCodeForAuxiliary) frontEndControlCode, ((code, msg, data) -> { + try { + if (code == ErrorCode.SUCCESS.getCode()) { + responseAck(request, Response.OK); + }else { + responseAck(request, Response.FORBIDDEN); + } + } catch (InvalidArgumentException | SipException | ParseException exception) { + log.error("[命令发送失败] 辅助开关指令: {}", exception.getMessage()); + } + })); + break; + default: + log.info("[INFO 消息] 设备不支持的控制方式"); + try { + responseAck(request, Response.FORBIDDEN, ""); + } catch (InvalidArgumentException | SipException | ParseException exception) { + log.error("[命令发送失败] 云台指令: {}", exception.getMessage()); + } + } + } + } + + /** + * 处理强制关键帧 + */ + private void handleIFameCmd(CommonGBChannel channel, SIPRequest request) { + if (channel.getDataType() != ChannelDataType.GB28181) { + // 只支持国标的云台控制 + log.warn("[INFO 消息] 只支持国标的处理强制关键帧, 通道ID: {}", channel.getGbId()); + try { + responseAck(request, Response.FORBIDDEN, ""); + } catch (SipException | InvalidArgumentException | ParseException e) { + log.error("[命令发送失败] 错误信息: {}", e.getMessage()); + } + return; + } + // 根据通道ID,获取所属设备 + Device device = deviceService.getDevice(channel.getDataDeviceId()); + if (device == null) { + // 不存在则回复404 + log.warn("[INFO 消息] 通道所属设备不存在, 通道ID: {}", channel.getGbId()); + try { + responseAck(request, Response.NOT_FOUND, "device not found"); + } catch (SipException | InvalidArgumentException | ParseException e) { + log.error("[命令发送失败] 错误信息: {}", e.getMessage()); + } + return; + } + + DeviceChannel deviceChannel = deviceChannelService.getOneForSourceById(channel.getGbId()); + if (deviceChannel == null) { + log.warn("[deviceControl] 未找到设备原始通道, 设备: {}({}),通道编号:{}", device.getName(), + device.getDeviceId(), channel.getGbId()); + try { + responseAck(request, Response.NOT_FOUND, "channel not found"); + } catch (SipException | InvalidArgumentException | ParseException e) { + log.error("[命令发送失败] 错误信息: {}", e.getMessage()); + } + return; + } + log.info("[deviceControl] 命令: 强制关键帧, 设备: {}({}), 通道{}({}", device.getName(), device.getDeviceId(), + deviceChannel.getName(), deviceChannel.getDeviceId()); + try { + cmder.iFrameCmd(device, deviceChannel.getDeviceId()); + responseAck(request, Response.OK); + } catch (InvalidArgumentException | SipException | ParseException e) { + log.error("[命令发送失败] 强制关键帧: {}", e.getMessage()); + } + } + + /** + * 处理重启命令 + */ + private void handleTeleBootCmd(CommonGBChannel channel, SIPRequest request) { + if (channel.getDataType() != ChannelDataType.GB28181) { + // 只支持国标的云台控制 + log.warn("[INFO 消息] 只支持国标的重启命令, 通道ID: {}", channel.getGbId()); + try { + responseAck(request, Response.FORBIDDEN, ""); + } catch (SipException | InvalidArgumentException | ParseException e) { + log.error("[命令发送失败] 错误信息: {}", e.getMessage()); + } + return; + } + // 根据通道ID,获取所属设备 + Device device = deviceService.getDevice(channel.getDataDeviceId()); + if (device == null) { + // 不存在则回复404 + log.warn("[INFO 消息] 通道所属设备不存在, 通道ID: {}", channel.getGbId()); + try { + responseAck(request, Response.NOT_FOUND, "device not found"); + } catch (SipException | InvalidArgumentException | ParseException e) { + log.error("[命令发送失败] 错误信息: {}", e.getMessage()); + } + return; + } + try { + cmder.teleBootCmd(device); + responseAck(request, Response.OK); + } catch (InvalidArgumentException | SipException | ParseException e) { + log.error("[命令发送失败] 重启: {}", e.getMessage()); + } + + } + + /** + * 处理拉框控制 + */ + private void handleDragZoom(CommonGBChannel channel, Element rootElement, SIPRequest request, DeviceControlType type) { + if (channel.getDataType() != ChannelDataType.GB28181) { + // 只支持国标的云台控制 + log.warn("[deviceControl-DragZoom] 只支持国标的拉框控制, 通道ID: {}", channel.getGbId()); + try { + responseAck(request, Response.FORBIDDEN, ""); + } catch (SipException | InvalidArgumentException | ParseException e) { + log.error("[命令发送失败] 错误信息: {}", e.getMessage()); + } + return; + } + // 根据通道ID,获取所属设备 + Device device = deviceService.getDevice(channel.getDataDeviceId()); + if (device == null) { + // 不存在则回复404 + log.warn("[deviceControl-DragZoom] 通道所属设备不存在, 通道ID: {}", channel.getGbId()); + try { + responseAck(request, Response.NOT_FOUND, "device not found"); + } catch (SipException | InvalidArgumentException | ParseException e) { + log.error("[命令发送失败] 错误信息: {}", e.getMessage()); + } + return; + } + + DeviceChannel deviceChannel = deviceChannelService.getOneForSourceById(channel.getGbId()); + if (deviceChannel == null) { + log.warn("[deviceControl-DragZoom] 未找到设备原始通道, 设备: {}({}),通道编号:{}", device.getName(), + device.getDeviceId(), channel.getGbId()); + try { + responseAck(request, Response.NOT_FOUND, "channel not found"); + } catch (SipException | InvalidArgumentException | ParseException e) { + log.error("[命令发送失败] 错误信息: {}", e.getMessage()); + } + return; + } + log.info("[deviceControl] 命令: {}, 设备: {}({}), 通道{}({}", type, device.getName(), device.getDeviceId(), + deviceChannel.getName(), deviceChannel.getDeviceId()); + try { + DragZoomRequest dragZoomRequest = loadElement(rootElement, DragZoomRequest.class); + DragZoomParam dragZoom = dragZoomRequest.getDragZoomIn(); + if (dragZoom == null) { + dragZoom = dragZoomRequest.getDragZoomOut(); + } + StringBuffer cmdXml = new StringBuffer(200); + cmdXml.append("<" + type.getVal() + ">\r\n"); + cmdXml.append("" + dragZoom.getLength() + "\r\n"); + cmdXml.append("" + dragZoom.getWidth() + "\r\n"); + cmdXml.append("" + dragZoom.getMidPointX() + "\r\n"); + cmdXml.append("" + dragZoom.getMidPointY() + "\r\n"); + cmdXml.append("" + dragZoom.getLengthX() + "\r\n"); + cmdXml.append("" + dragZoom.getLengthY() + "\r\n"); + cmdXml.append("\r\n"); + cmder.dragZoomCmd(device, deviceChannel.getDeviceId(), cmdXml.toString(), (code, msg, data) -> { + + }); + responseAck(request, Response.OK); + } catch (Exception e) { + log.error("[命令发送失败] 拉框控制: {}", e.getMessage()); + } + + } + + /** + * 处理看守位命令 + */ + private void handleHomePositionCmd(CommonGBChannel channel, Element rootElement, SIPRequest request, DeviceControlType type) { + if (channel.getDataType() != ChannelDataType.GB28181) { + // 只支持国标的云台控制 + log.warn("[INFO 消息] 只支持国标的看守位命令, 通道ID: {}", channel.getGbId()); + try { + responseAck(request, Response.FORBIDDEN, ""); + } catch (SipException | InvalidArgumentException | ParseException e) { + log.error("[命令发送失败] 错误信息: {}", e.getMessage()); + } + return; + } + // 根据通道ID,获取所属设备 + Device device = deviceService.getDevice(channel.getDataDeviceId()); + if (device == null) { + // 不存在则回复404 + log.warn("[INFO 消息] 通道所属设备不存在, 通道ID: {}", channel.getGbId()); + try { + responseAck(request, Response.NOT_FOUND, "device not found"); + } catch (SipException | InvalidArgumentException | ParseException e) { + log.error("[命令发送失败] 错误信息: {}", e.getMessage()); + } + return; + } + + DeviceChannel deviceChannel = deviceChannelService.getOneForSourceById(channel.getGbId()); + if (deviceChannel == null) { + log.warn("[deviceControl] 未找到设备原始通道, 设备: {}({}),通道编号:{}", device.getName(), + device.getDeviceId(), channel.getGbId()); + try { + responseAck(request, Response.NOT_FOUND, "channel not found"); + } catch (SipException | InvalidArgumentException | ParseException e) { + log.error("[命令发送失败] 错误信息: {}", e.getMessage()); + } + return; + } + log.info("[deviceControl] 命令: {}, 设备: {}({}), 通道{}({}", type, device.getName(), device.getDeviceId(), + deviceChannel.getName(), deviceChannel.getDeviceId()); + try { + HomePositionRequest homePosition = loadElement(rootElement, HomePositionRequest.class); + //获取整个消息主体,我们只需要修改请求头即可 + HomePositionRequest.HomePosition info = homePosition.getHomePosition(); + cmder.homePositionCmd(device, deviceChannel.getDeviceId(), !"0".equals(info.getEnabled()), Integer.parseInt(info.getResetTime()), Integer.parseInt(info.getPresetIndex()), (code, msg, data) -> { + if (code == ErrorCode.SUCCESS.getCode()) { + onOk(request); + }else { + onError(request, code, msg); + } + }); + } catch (Exception e) { + log.error("[命令发送失败] 看守位设置: {}", e.getMessage()); + } + } + + /** + * 处理告警消息 + */ + private void handleAlarmCmd(CommonGBChannel channel, Element rootElement, SIPRequest request) { + if (channel.getDataType() != ChannelDataType.GB28181) { + // 只支持国标的云台控制 + log.warn("[INFO 消息] 只支持国标的告警消息, 通道ID: {}", channel.getGbId()); + try { + responseAck(request, Response.FORBIDDEN, ""); + } catch (SipException | InvalidArgumentException | ParseException e) { + log.error("[命令发送失败] 错误信息: {}", e.getMessage()); + } + return; + } + // 根据通道ID,获取所属设备 + Device device = deviceService.getDevice(channel.getDataDeviceId()); + if (device == null) { + // 不存在则回复404 + log.warn("[INFO 消息] 通道所属设备不存在, 通道ID: {}", channel.getGbId()); + try { + responseAck(request, Response.NOT_FOUND, "device not found"); + } catch (SipException | InvalidArgumentException | ParseException e) { + log.error("[命令发送失败] 错误信息: {}", e.getMessage()); + } + return; + } + //告警方法 + String alarmMethod = ""; + //告警类型 + String alarmType = ""; + List info = rootElement.elements("Info"); + if (info != null) { + for (Element element : info) { + alarmMethod = getText(element, "AlarmMethod"); + alarmType = getText(element, "AlarmType"); + } + } + try { + cmder.alarmResetCmd(device, alarmMethod, alarmType, (code, msg, data) -> { + if (code == ErrorCode.SUCCESS.getCode()) { + onOk(request); + }else { + onError(request, code, msg); + } + }); + } catch (InvalidArgumentException | SipException | ParseException e) { + log.error("[命令发送失败] 告警消息: {}", e.getMessage()); + } + } + + /** + * 处理录像控制 + */ + private void handleRecordCmd(CommonGBChannel channel, Element rootElement, SIPRequest request, DeviceControlType type) { + if (channel.getDataType() != ChannelDataType.GB28181) { + // 只支持国标的云台控制 + log.warn("[INFO 消息] 只支持国标的息录像控制, 通道ID: {}", channel.getGbId()); + try { + responseAck(request, Response.FORBIDDEN, ""); + } catch (SipException | InvalidArgumentException | ParseException e) { + log.error("[命令发送失败] 错误信息: {}", e.getMessage()); + } + return; + } + // 根据通道ID,获取所属设备 + Device device = deviceService.getDevice(channel.getDataDeviceId()); + if (device == null) { + // 不存在则回复404 + log.warn("[INFO 消息] 通道所属设备不存在, 通道ID: {}", channel.getGbId()); + try { + responseAck(request, Response.NOT_FOUND, "device not found"); + } catch (SipException | InvalidArgumentException | ParseException e) { + log.error("[命令发送失败] 错误信息: {}", e.getMessage()); + } + return; + } + + DeviceChannel deviceChannel = deviceChannelService.getOneForSourceById(channel.getGbId()); + if (deviceChannel == null) { + // 拒绝远程启动命令 + log.warn("[deviceControl] 未找到设备原始通道, 设备: {}({}),通道编号:{}", device.getName(), + device.getDeviceId(), channel.getGbId()); + try { + responseAck(request, Response.NOT_FOUND, "channel not found"); + } catch (SipException | InvalidArgumentException | ParseException e) { + log.error("[命令发送失败] 错误信息: {}", e.getMessage()); + } + return; + } + log.info("[deviceControl] 命令: {}, 设备: {}({}), 通道{}({}", type, device.getName(), device.getDeviceId(), + deviceChannel.getName(), deviceChannel.getDeviceId()); + //获取整个消息主体,我们只需要修改请求头即可 + String cmdString = getText(rootElement, type.getVal()); + try { + cmder.recordCmd(device, deviceChannel.getDeviceId(), cmdString, (code, msg, data) -> { + if (code == ErrorCode.SUCCESS.getCode()) { + onOk(request); + }else { + onError(request, code, msg); + } + }); + } catch (InvalidArgumentException | SipException | ParseException e) { + log.error("[命令发送失败] 录像控制: {}", e.getMessage()); + } + } + + /** + * 处理报警布防/撤防命令 + */ + private void handleGuardCmd(CommonGBChannel channel, Element rootElement, SIPRequest request, DeviceControlType type) { + if (channel.getDataType() != ChannelDataType.GB28181) { + // 只支持国标的云台控制 + log.warn("[INFO 消息] 只支持国标的报警布防/撤防命令, 通道ID: {}", channel.getGbId()); + try { + responseAck(request, Response.FORBIDDEN, ""); + } catch (SipException | InvalidArgumentException | ParseException e) { + log.error("[命令发送失败] 错误信息: {}", e.getMessage()); + } + return; + } + // 根据通道ID,获取所属设备 + Device device = deviceService.getDevice(channel.getDataDeviceId()); + if (device == null) { + // 不存在则回复404 + log.warn("[INFO 消息] 通道所属设备不存在, 通道ID: {}", channel.getGbId()); + try { + responseAck(request, Response.NOT_FOUND, "device not found"); + } catch (SipException | InvalidArgumentException | ParseException e) { + log.error("[命令发送失败] 错误信息: {}", e.getMessage()); + } + return; + } + //获取整个消息主体,我们只需要修改请求头即可 + String cmdString = getText(rootElement, type.getVal()); + try { + cmder.guardCmd(device, cmdString,(code, msg, data) -> { + if (code == ErrorCode.SUCCESS.getCode()) { + onOk(request); + }else { + onError(request, code, msg); + } + }); + } catch (InvalidArgumentException | SipException | ParseException e) { + log.error("[命令发送失败] 布防/撤防命令: {}", e.getMessage()); + } + } + + + + + /** + * 错误响应处理 + * + */ + private void onError(SIPRequest request, Integer code, String msg) { + // 失败的回复 + try { + responseAck(request, code, msg); + } catch (SipException | InvalidArgumentException | ParseException e) { + log.error("[命令发送失败] 回复: {}", e.getMessage()); + } + } + + private void onError(SIPRequest request, SipSubscribe.EventResult errorResult) { + onError(request, errorResult.statusCode, errorResult.msg); + } + + /** + * 成功响应处理 + * + * @param request 请求 + */ + private void onOk(SIPRequest request) { + // 成功的回复 + try { + responseAck(request, Response.OK); + } catch (SipException | InvalidArgumentException | ParseException e) { + log.error("[命令发送失败] 回复: {}", e.getMessage()); + } + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/notify/NotifyMessageHandler.java b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/notify/NotifyMessageHandler.java new file mode 100755 index 0000000..bb34189 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/notify/NotifyMessageHandler.java @@ -0,0 +1,26 @@ +package com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.notify; + +import com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.MessageHandlerAbstract; +import com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.MessageRequestProcessor; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +/** + * 命令类型: 通知命令, 参看 A.2.5 通知命令 + * 命令类型: 状态信息(心跳)报送, 报警通知, 媒体通知, 移动设备位置数据,语音广播通知(TODO), 设备预置位(TODO) + * @author lin + */ +@Component +public class NotifyMessageHandler extends MessageHandlerAbstract implements InitializingBean { + + private final String messageType = "Notify"; + + @Autowired + private MessageRequestProcessor messageRequestProcessor; + + @Override + public void afterPropertiesSet() throws Exception { + messageRequestProcessor.addHandler(messageType, this); + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/notify/cmd/AlarmNotifyMessageHandler.java b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/notify/cmd/AlarmNotifyMessageHandler.java new file mode 100755 index 0000000..5cb5ac9 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/notify/cmd/AlarmNotifyMessageHandler.java @@ -0,0 +1,276 @@ +package com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.notify.cmd; + +import com.alibaba.fastjson2.JSON; +import com.alibaba.fastjson2.JSONObject; +import com.genersoft.iot.vmp.conf.SipConfig; +import com.genersoft.iot.vmp.conf.UserSetting; +import com.genersoft.iot.vmp.gb28181.bean.*; +import com.genersoft.iot.vmp.gb28181.event.EventPublisher; +import com.genersoft.iot.vmp.gb28181.service.IDeviceAlarmService; +import com.genersoft.iot.vmp.gb28181.service.IDeviceChannelService; +import com.genersoft.iot.vmp.gb28181.transmit.event.request.SIPRequestProcessorParent; +import com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.IMessageHandler; +import com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.notify.NotifyMessageHandler; +import com.genersoft.iot.vmp.gb28181.utils.NumericUtil; +import com.genersoft.iot.vmp.gb28181.utils.XmlUtil; +import com.genersoft.iot.vmp.storager.IRedisCatchStorage; +import com.genersoft.iot.vmp.utils.DateUtil; +import gov.nist.javax.sip.message.SIPRequest; +import lombok.extern.slf4j.Slf4j; +import org.dom4j.Element; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; +import org.springframework.util.ObjectUtils; + +import javax.sip.InvalidArgumentException; +import javax.sip.RequestEvent; +import javax.sip.SipException; +import javax.sip.message.Response; +import java.text.ParseException; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ConcurrentLinkedQueue; + +import static com.genersoft.iot.vmp.gb28181.utils.XmlUtil.getText; + +/** + * 报警事件的处理,参考:9.4 + */ +@Slf4j +@Component +public class AlarmNotifyMessageHandler extends SIPRequestProcessorParent implements InitializingBean, IMessageHandler { + + private final String cmdType = "Alarm"; + + @Autowired + private NotifyMessageHandler notifyMessageHandler; + + @Autowired + private EventPublisher publisher; + + @Autowired + private UserSetting userSetting; + + @Autowired + private SipConfig sipConfig; + + @Autowired + private IRedisCatchStorage redisCatchStorage; + + @Autowired + private IDeviceAlarmService deviceAlarmService; + + @Autowired + private IDeviceChannelService deviceChannelService; + + private final ConcurrentLinkedQueue taskQueue = new ConcurrentLinkedQueue<>(); + + @Override + public void afterPropertiesSet() throws Exception { + notifyMessageHandler.addHandler(cmdType, this); + } + + @Override + public void handForDevice(RequestEvent evt, Device device, Element rootElement) { + if (taskQueue.size() >= userSetting.getMaxNotifyCountQueue()) { + log.error("[Alarm] 待处理消息队列已满 {},返回486 BUSY_HERE,消息不做处理", userSetting.getMaxNotifyCountQueue()); + return; + } + taskQueue.offer(new SipMsgInfo(evt, device, rootElement)); + } + + @Scheduled(fixedDelay = 200) + public void executeTaskQueue() { + if (taskQueue.isEmpty()) { + return; + } + List handlerCatchDataList = new ArrayList<>(); + int size = taskQueue.size(); + for (int i = 0; i < size; i++) { + SipMsgInfo poll = taskQueue.poll(); + if (poll != null) { + handlerCatchDataList.add(poll); + } + } + if (handlerCatchDataList.isEmpty()) { + return; + } + for (SipMsgInfo sipMsgInfo : handlerCatchDataList) { + if (sipMsgInfo == null) { + continue; + } + RequestEvent evt = sipMsgInfo.getEvt(); + // 回复200 OK + try { + responseAck((SIPRequest) evt.getRequest(), Response.OK); + } catch (SipException | InvalidArgumentException | ParseException e) { + log.error("[命令发送失败] 报警通知回复: {}", e.getMessage()); + } + try { + Device device = sipMsgInfo.getDevice(); + Element deviceIdElement = sipMsgInfo.getRootElement().element("DeviceID"); + String channelId = deviceIdElement.getText(); + + DeviceAlarm deviceAlarm = new DeviceAlarm(); + deviceAlarm.setCreateTime(DateUtil.getNow()); + deviceAlarm.setDeviceId(sipMsgInfo.getDevice().getDeviceId()); + deviceAlarm.setDeviceName(sipMsgInfo.getDevice().getName()); + deviceAlarm.setChannelId(channelId); + deviceAlarm.setAlarmPriority(getText(sipMsgInfo.getRootElement(), "AlarmPriority")); + deviceAlarm.setAlarmMethod(getText(sipMsgInfo.getRootElement(), "AlarmMethod")); + String alarmTime = XmlUtil.getText(sipMsgInfo.getRootElement(), "AlarmTime"); + if (alarmTime == null) { + continue; + } + deviceAlarm.setAlarmTime(DateUtil.ISO8601Toyyyy_MM_dd_HH_mm_ss(alarmTime)); + String alarmDescription = getText(sipMsgInfo.getRootElement(), "AlarmDescription"); + if (alarmDescription == null) { + deviceAlarm.setAlarmDescription(""); + } else { + deviceAlarm.setAlarmDescription(alarmDescription); + } + String longitude = getText(sipMsgInfo.getRootElement(), "Longitude"); + if (longitude != null && NumericUtil.isDouble(longitude)) { + deviceAlarm.setLongitude(Double.parseDouble(longitude)); + } else { + deviceAlarm.setLongitude(0.00); + } + String latitude = getText(sipMsgInfo.getRootElement(), "Latitude"); + if (latitude != null && NumericUtil.isDouble(latitude)) { + deviceAlarm.setLatitude(Double.parseDouble(latitude)); + } else { + deviceAlarm.setLatitude(0.00); + } + + if (!ObjectUtils.isEmpty(deviceAlarm.getAlarmMethod()) && deviceAlarm.getAlarmMethod().contains(DeviceAlarmMethod.GPS.getVal() + "")) { + DeviceChannel deviceChannel = deviceChannelService.getOne(device.getDeviceId(), channelId); + if (deviceChannel == null) { + log.warn("[解析报警消息] 未找到通道:{}/{}", device.getDeviceId(), channelId); + } else { + MobilePosition mobilePosition = new MobilePosition(); + mobilePosition.setCreateTime(DateUtil.getNow()); + mobilePosition.setDeviceId(deviceAlarm.getDeviceId()); + mobilePosition.setChannelId(deviceChannel.getId()); + mobilePosition.setChannelDeviceId(deviceChannel.getDeviceId()); + mobilePosition.setTime(deviceAlarm.getAlarmTime()); + mobilePosition.setLongitude(deviceAlarm.getLongitude()); + mobilePosition.setLatitude(deviceAlarm.getLatitude()); + mobilePosition.setReportSource("GPS Alarm"); + + // 更新device channel 的经纬度 + deviceChannel.setLongitude(mobilePosition.getLongitude()); + deviceChannel.setLatitude(mobilePosition.getLatitude()); + deviceChannel.setGpsTime(mobilePosition.getTime()); + + deviceChannelService.updateChannelGPS(device, deviceChannel, mobilePosition); + } + } + if (!ObjectUtils.isEmpty(deviceAlarm.getDeviceId())) { + if (deviceAlarm.getAlarmMethod().contains(DeviceAlarmMethod.Video.getVal() + "")) { + deviceAlarm.setAlarmType(getText(sipMsgInfo.getRootElement().element("Info"), "AlarmType")); + } + } + if (log.isDebugEnabled()) { + log.debug("[收到报警通知]设备:{}, 内容:{}", device.getDeviceId(), JSON.toJSONString(deviceAlarm)); + } + // 作者自用判断,其他小伙伴需要此消息可以自行修改,但是不要提在pr里 + if (DeviceAlarmMethod.Other.getVal() == Integer.parseInt(deviceAlarm.getAlarmMethod())) { + // 发送给平台的报警信息。 发送redis通知 + log.info("[发送给平台的报警信息]内容:{}", JSONObject.toJSONString(deviceAlarm)); + AlarmChannelMessage alarmChannelMessage = new AlarmChannelMessage(); + if (deviceAlarm.getAlarmMethod() != null) { + alarmChannelMessage.setAlarmSn(Integer.parseInt(deviceAlarm.getAlarmMethod())); + } + alarmChannelMessage.setAlarmDescription(deviceAlarm.getAlarmDescription()); + if (deviceAlarm.getAlarmType() != null) { + alarmChannelMessage.setAlarmType(Integer.parseInt(deviceAlarm.getAlarmType())); + } + alarmChannelMessage.setGbId(channelId); + redisCatchStorage.sendAlarmMsg(alarmChannelMessage); + continue; + } + + log.debug("存储报警信息、报警分类"); + // 存储报警信息、报警分类 + if (sipConfig.isAlarm()) { + deviceAlarmService.add(deviceAlarm); + } + + if (redisCatchStorage.deviceIsOnline(sipMsgInfo.getDevice().getDeviceId())) { + publisher.deviceAlarmEventPublish(deviceAlarm); + } + } catch (Exception e) { + log.error("未处理的异常 ", e); + log.warn("[收到报警通知] 发现未处理的异常, {}\r\n{}", e.getMessage(), evt.getRequest()); + } + } + } + + @Override + public void handForPlatform(RequestEvent evt, Platform parentPlatform, Element rootElement) { + log.info("收到来自平台[{}]的报警通知", parentPlatform.getServerGBId()); + // 回复200 OK + try { + responseAck((SIPRequest) evt.getRequest(), Response.OK); + } catch (SipException | InvalidArgumentException | ParseException e) { + log.error("[命令发送失败] 国标级联 报警通知回复: {}", e.getMessage()); + } + Element deviceIdElement = rootElement.element("DeviceID"); + String channelId = deviceIdElement.getText(); + + + DeviceAlarm deviceAlarm = new DeviceAlarm(); + deviceAlarm.setCreateTime(DateUtil.getNow()); + deviceAlarm.setDeviceId(parentPlatform.getServerGBId()); + deviceAlarm.setDeviceName(parentPlatform.getName()); + deviceAlarm.setChannelId(channelId); + deviceAlarm.setAlarmPriority(getText(rootElement, "AlarmPriority")); + deviceAlarm.setAlarmMethod(getText(rootElement, "AlarmMethod")); + String alarmTime = XmlUtil.getText(rootElement, "AlarmTime"); + if (alarmTime == null) { + return; + } + deviceAlarm.setAlarmTime(DateUtil.ISO8601Toyyyy_MM_dd_HH_mm_ss(alarmTime)); + String alarmDescription = getText(rootElement, "AlarmDescription"); + if (alarmDescription == null) { + deviceAlarm.setAlarmDescription(""); + } else { + deviceAlarm.setAlarmDescription(alarmDescription); + } + String longitude = getText(rootElement, "Longitude"); + if (longitude != null && NumericUtil.isDouble(longitude)) { + deviceAlarm.setLongitude(Double.parseDouble(longitude)); + } else { + deviceAlarm.setLongitude(0.00); + } + String latitude = getText(rootElement, "Latitude"); + if (latitude != null && NumericUtil.isDouble(latitude)) { + deviceAlarm.setLatitude(Double.parseDouble(latitude)); + } else { + deviceAlarm.setLatitude(0.00); + } + + if (!ObjectUtils.isEmpty(deviceAlarm.getAlarmMethod())) { + + if (deviceAlarm.getAlarmMethod().contains(DeviceAlarmMethod.Video.getVal() + "")) { + deviceAlarm.setAlarmType(getText(rootElement.element("Info"), "AlarmType")); + } + } + + if (channelId.equals(parentPlatform.getDeviceGBId())) { + // 发送给平台的报警信息。 发送redis通知 + AlarmChannelMessage alarmChannelMessage = new AlarmChannelMessage(); + if (deviceAlarm.getAlarmMethod() != null) { + alarmChannelMessage.setAlarmSn(Integer.parseInt(deviceAlarm.getAlarmMethod())); + } + alarmChannelMessage.setAlarmDescription(deviceAlarm.getAlarmDescription()); + alarmChannelMessage.setGbId(channelId); + if (deviceAlarm.getAlarmType() != null) { + alarmChannelMessage.setAlarmType(Integer.parseInt(deviceAlarm.getAlarmType())); + } + redisCatchStorage.sendAlarmMsg(alarmChannelMessage); + } + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/notify/cmd/BroadcastNotifyMessageHandler.java b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/notify/cmd/BroadcastNotifyMessageHandler.java new file mode 100644 index 0000000..d185408 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/notify/cmd/BroadcastNotifyMessageHandler.java @@ -0,0 +1,212 @@ +package com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.notify.cmd; + +import com.genersoft.iot.vmp.common.enums.ChannelDataType; +import com.genersoft.iot.vmp.conf.exception.ControllerException; +import com.genersoft.iot.vmp.gb28181.bean.*; +import com.genersoft.iot.vmp.gb28181.service.*; +import com.genersoft.iot.vmp.gb28181.session.AudioBroadcastManager; +import com.genersoft.iot.vmp.gb28181.transmit.cmd.ISIPCommanderForPlatform; +import com.genersoft.iot.vmp.gb28181.transmit.event.request.SIPRequestProcessorParent; +import com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.IMessageHandler; +import com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.notify.NotifyMessageHandler; +import com.genersoft.iot.vmp.media.bean.MediaServer; +import com.genersoft.iot.vmp.media.service.IMediaServerService; +import com.genersoft.iot.vmp.service.ISendRtpServerService; +import gov.nist.javax.sip.message.SIPRequest; +import lombok.extern.slf4j.Slf4j; +import org.dom4j.Element; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import javax.sip.InvalidArgumentException; +import javax.sip.RequestEvent; +import javax.sip.SipException; +import javax.sip.message.Response; +import java.text.ParseException; + +/** + * 语音喊话请求 + */ +@Slf4j +@Component +public class BroadcastNotifyMessageHandler extends SIPRequestProcessorParent implements InitializingBean, IMessageHandler { + + private final static String cmdType = "Broadcast"; + + @Autowired + private NotifyMessageHandler notifyMessageHandler; + + @Autowired + private IGbChannelService channelService; + + @Autowired + private ISIPCommanderForPlatform commanderForPlatform; + + @Autowired + private IMediaServerService mediaServerService; + + @Autowired + private IPlayService playService; + + @Autowired + private IDeviceService deviceService; + + @Autowired + private IDeviceChannelService deviceChannelService; + + @Autowired + private IPlatformService platformService; + + @Autowired + private AudioBroadcastManager audioBroadcastManager; + + @Autowired + private ISendRtpServerService sendRtpServerService; + + @Override + public void afterPropertiesSet() throws Exception { + notifyMessageHandler.addHandler(cmdType, this); + } + + @Override + public void handForDevice(RequestEvent evt, Device device, Element element) { + + } + + @Override + public void handForPlatform(RequestEvent evt, Platform platform, Element rootElement) { + // 来自上级平台的语音喊话请求 + SIPRequest request = (SIPRequest) evt.getRequest(); + try { + Element snElement = rootElement.element("SN"); + if (snElement == null) { + responseAck(request, Response.BAD_REQUEST, "sn must not null"); + return; + } + String sn = snElement.getText(); + Element targetIDElement = rootElement.element("TargetID"); + if (targetIDElement == null) { + responseAck(request, Response.BAD_REQUEST, "TargetID must not null"); + return; + } + String targetId = targetIDElement.getText(); + + Element sourceIdElement = rootElement.element("SourceID"); + String sourceId; + if (sourceIdElement != null) { + sourceId = sourceIdElement.getText(); + }else { + sourceId = targetId; + } + log.info("[国标级联 语音喊话] platform: {}, channel: {}", platform.getServerGBId(), targetId); + + CommonGBChannel channel = channelService.queryOneWithPlatform(platform.getId(), targetId); + if (channel == null) { + log.warn("[国标级联 语音喊话] 未找到通道 platform: {}, channel: {}", platform.getServerGBId(), targetId); + responseAck(request, Response.NOT_FOUND, "TargetID not found"); + return; + } + if (channel.getDataType() != ChannelDataType.GB28181) { + // 只支持国标的语音喊话 + log.warn("[INFO 消息] 只支持国标的语音喊话命令, 通道ID: {}", channel.getGbId()); + try { + responseAck(request, Response.FORBIDDEN, ""); + } catch (SipException | InvalidArgumentException | ParseException e) { + log.error("[命令发送失败] 错误信息: {}", e.getMessage()); + } + return; + } + // 向下级发送语音的喊话请求 + Device device = deviceService.getDevice(channel.getDataDeviceId()); + if (device == null) { + responseAck(request, Response.NOT_FOUND, "device not found"); + return; + } + DeviceChannel deviceChannel = deviceChannelService.getOneForSourceById(channel.getGbId()); + if (deviceChannel == null) { + responseAck(request, Response.NOT_FOUND, "channel not found"); + return; + } + responseAck(request, Response.OK); + + // 查看语音通道是否已经建立并且已经在使用 + if (playService.audioBroadcastInUse(device, deviceChannel)) { + commanderForPlatform.broadcastResultCmd(platform, channel, sn, false,null, null); + return; + } + + MediaServer mediaServerForMinimumLoad = mediaServerService.getMediaServerForMinimumLoad(null); + commanderForPlatform.broadcastResultCmd(platform, channel, sn, true, eventResult->{ + log.info("[国标级联] 语音喊话 回复失败 platform: {}, 错误:{}/{}", platform.getServerGBId(), eventResult.statusCode, eventResult.msg); + }, eventResult->{ + // 消息发送成功, 向上级发送invite,获取推流 + try { + platformService.broadcastInvite(platform, channel, sourceId, mediaServerForMinimumLoad, (hookData)->{ + // 上级平台推流成功 + AudioBroadcastCatch broadcastCatch = audioBroadcastManager.get(channel.getGbId()); + if (broadcastCatch != null ) { + + if (playService.audioBroadcastInUse(device, deviceChannel)) { + log.info("[国标级联] 语音喊话 设备正在使用中 platform: {}, channel: {}", + platform.getServerGBId(), channel.getGbDeviceId()); + // 查看语音通道已经建立且已经占用 回复BYE + platformService.stopBroadcast(platform, channel, hookData.getApp(), hookData.getStream(), true, hookData.getMediaServer()); + }else { + // 查看语音通道已经建立但是未占用 + broadcastCatch.setApp(hookData.getApp()); + broadcastCatch.setStream(hookData.getStream()); + broadcastCatch.setMediaServerItem(hookData.getMediaServer()); + audioBroadcastManager.update(broadcastCatch); + // 推流到设备 + SendRtpInfo sendRtpItem = sendRtpServerService.queryByStream(hookData.getStream(), targetId); + if (sendRtpItem == null) { + log.warn("[国标级联] 语音喊话 异常,未找到发流信息, channelId: {}, stream: {}", targetId, hookData.getStream()); + log.info("[国标级联] 语音喊话 重新开始,channelId: {}, stream: {}", targetId, hookData.getStream()); + try { + playService.audioBroadcastCmd(device, deviceChannel, hookData.getMediaServer(), hookData.getApp(), hookData.getStream(), 60, true, msg -> { + log.info("[语音喊话] 通道建立成功, device: {}, channel: {}", device.getDeviceId(), targetId); + }); + } catch (SipException | InvalidArgumentException | ParseException e) { + log.info("[消息发送失败] 国标级联 语音喊话 platform: {}", platform.getServerGBId()); + } + }else { + // 发流 + try { + mediaServerService.startSendRtp(hookData.getMediaServer(), sendRtpItem); + }catch (ControllerException e) { + log.info("[语音喊话] 推流失败, 结果: {}", e.getMessage()); + return; + } + log.info("[语音喊话] 自动推流成功, device: {}, channel: {}", device.getDeviceId(), targetId); + } + } + }else { + try { + playService.audioBroadcastCmd(device, deviceChannel, hookData.getMediaServer(), hookData.getApp(), hookData.getStream(), 60, true, msg -> { + log.info("[语音喊话] 通道建立成功, device: {}, channel: {}", device.getDeviceId(), targetId); + }); + } catch (SipException | InvalidArgumentException | ParseException e) { + log.info("[消息发送失败] 国标级联 语音喊话 platform: {}", platform.getServerGBId()); + } + } + + }, eventResultForBroadcastInvite -> { + // 收到错误 + log.info("[国标级联-语音喊话] 与下级通道建立失败 device: {}, channel: {}, 错误:{}/{}", device.getDeviceId(), + targetId, eventResultForBroadcastInvite.statusCode, eventResultForBroadcastInvite.msg); + }, (code, msg)->{ + // 超时 + log.info("[国标级联-语音喊话] 与下级通道建立超时 device: {}, channel: {}, 错误:{}/{}", device.getDeviceId(), + targetId, code, msg); + }); + } catch (SipException | InvalidArgumentException | ParseException e) { + log.info("[消息发送失败] 国标级联 语音喊话 invite消息 platform: {}", platform.getServerGBId()); + } + }); + } catch (SipException | InvalidArgumentException | ParseException e) { + log.info("[消息发送失败] 国标级联 语音喊话 platform: {}", platform.getServerGBId()); + } + + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/notify/cmd/KeepaliveNotifyMessageHandler.java b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/notify/cmd/KeepaliveNotifyMessageHandler.java new file mode 100755 index 0000000..e0cabc7 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/notify/cmd/KeepaliveNotifyMessageHandler.java @@ -0,0 +1,144 @@ +package com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.notify.cmd; + +import com.genersoft.iot.vmp.common.RemoteAddressInfo; +import com.genersoft.iot.vmp.conf.DynamicTask; +import com.genersoft.iot.vmp.conf.UserSetting; +import com.genersoft.iot.vmp.gb28181.bean.Device; +import com.genersoft.iot.vmp.gb28181.bean.Platform; +import com.genersoft.iot.vmp.gb28181.bean.SipMsgInfo; +import com.genersoft.iot.vmp.gb28181.service.IDeviceService; +import com.genersoft.iot.vmp.gb28181.task.deviceStatus.DeviceStatusTaskRunner; +import com.genersoft.iot.vmp.gb28181.transmit.event.request.SIPRequestProcessorParent; +import com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.IMessageHandler; +import com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.notify.NotifyMessageHandler; +import com.genersoft.iot.vmp.gb28181.utils.SipUtils; +import com.genersoft.iot.vmp.utils.DateUtil; +import com.genersoft.iot.vmp.utils.IpPortUtil; +import gov.nist.javax.sip.message.SIPRequest; +import lombok.extern.slf4j.Slf4j; +import org.dom4j.Element; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +import javax.sip.InvalidArgumentException; +import javax.sip.RequestEvent; +import javax.sip.SipException; +import javax.sip.message.Response; +import java.text.ParseException; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ConcurrentLinkedQueue; + +/** + * 状态信息(心跳)报送 + */ +@Slf4j +@Component +public class KeepaliveNotifyMessageHandler extends SIPRequestProcessorParent implements InitializingBean, IMessageHandler { + + + private final static String cmdType = "Keepalive"; + + private final ConcurrentLinkedQueue taskQueue = new ConcurrentLinkedQueue<>(); + + @Autowired + private NotifyMessageHandler notifyMessageHandler; + + @Autowired + private IDeviceService deviceService; + + @Autowired + private DeviceStatusTaskRunner statusTaskRunner; + + @Autowired + private UserSetting userSetting; + + @Autowired + private DynamicTask dynamicTask; + + @Override + public void afterPropertiesSet() throws Exception { + notifyMessageHandler.addHandler(cmdType, this); + } + + @Override + public void handForDevice(RequestEvent evt, Device device, Element rootElement) { + if (taskQueue.size() >= userSetting.getMaxNotifyCountQueue()) { + log.error("[心跳] 待处理消息队列已满 {},返回486 BUSY_HERE,消息不做处理", userSetting.getMaxNotifyCountQueue()); + return; + } + taskQueue.offer(new SipMsgInfo(evt, device, rootElement)); + } + + @Scheduled(fixedDelay = 100) + public void executeTaskQueue() { + if (taskQueue.isEmpty()) { + return; + } + List handlerCatchDataList = new ArrayList<>(); + int size = taskQueue.size(); + for (int i = 0; i < size; i++) { + SipMsgInfo poll = taskQueue.poll(); + if (poll != null) { + handlerCatchDataList.add(poll); + } + } + if (handlerCatchDataList.isEmpty()) { + return; + } + List deviceListForUpdate = new ArrayList<>(); + for (SipMsgInfo sipMsgInfo : handlerCatchDataList) { + if (sipMsgInfo == null) { + continue; + } + RequestEvent evt = sipMsgInfo.getEvt(); + // 回复200 OK + try { + responseAck((SIPRequest) evt.getRequest(), Response.OK); + } catch (SipException | InvalidArgumentException | ParseException e) { + log.error("[命令发送失败] 心跳回复: {}", e.getMessage()); + } + Device device = sipMsgInfo.getDevice(); + SIPRequest request = (SIPRequest) evt.getRequest(); + + RemoteAddressInfo remoteAddressInfo = SipUtils.getRemoteAddressFromRequest(request, userSetting.getSipUseSourceIpAsRemoteAddress()); + if (device.getIp() == null || !device.getIp().equalsIgnoreCase(remoteAddressInfo.getIp()) || device.getPort() != remoteAddressInfo.getPort()) { + log.info("[收到心跳] 地址变化, {}({}), {}:{}->{}", device.getName(), device.getDeviceId(), remoteAddressInfo.getIp(), remoteAddressInfo.getPort(), request.getLocalAddress().getHostAddress()); + device.setPort(remoteAddressInfo.getPort()); + device.setHostAddress(IpPortUtil.concatenateIpAndPort(remoteAddressInfo.getIp(), String.valueOf(remoteAddressInfo.getPort()))); + device.setIp(remoteAddressInfo.getIp()); + device.setLocalIp(request.getLocalAddress().getHostAddress()); + } + + device.setKeepaliveTime(DateUtil.getNow()); + + if (device.isOnLine()) { + deviceListForUpdate.add(device); + long expiresTime = Math.min(device.getExpires(), device.getHeartBeatInterval() * device.getHeartBeatCount()) * 1000L; + if (statusTaskRunner.containsKey(device.getDeviceId())) { + statusTaskRunner.updateDelay(device.getDeviceId(), expiresTime + System.currentTimeMillis()); + } + } else { + if (userSetting.getGbDeviceOnline() == 1) { + // 对于已经离线的设备判断他的注册是否已经过期 + deviceService.online(device, null); + } + } + } + if (!deviceListForUpdate.isEmpty()) { + deviceService.updateDeviceList(deviceListForUpdate); + } + } + + @Override + public void handForPlatform(RequestEvent evt, Platform parentPlatform, Element element) { + // 个别平台保活不回复200OK会判定离线 + try { + responseAck((SIPRequest) evt.getRequest(), Response.OK); + } catch (SipException | InvalidArgumentException | ParseException e) { + log.error("[命令发送失败] 心跳回复: {}", e.getMessage()); + } + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/notify/cmd/MediaStatusNotifyMessageHandler.java b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/notify/cmd/MediaStatusNotifyMessageHandler.java new file mode 100755 index 0000000..41fa636 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/notify/cmd/MediaStatusNotifyMessageHandler.java @@ -0,0 +1,134 @@ +package com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.notify.cmd; + +import com.genersoft.iot.vmp.common.InviteInfo; +import com.genersoft.iot.vmp.common.InviteSessionType; +import com.genersoft.iot.vmp.gb28181.bean.*; +import com.genersoft.iot.vmp.gb28181.service.*; +import com.genersoft.iot.vmp.gb28181.session.SipInviteSessionManager; +import com.genersoft.iot.vmp.gb28181.transmit.cmd.impl.SIPCommander; +import com.genersoft.iot.vmp.gb28181.transmit.cmd.impl.SIPCommanderForPlatform; +import com.genersoft.iot.vmp.gb28181.transmit.event.request.SIPRequestProcessorParent; +import com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.IMessageHandler; +import com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.notify.NotifyMessageHandler; +import com.genersoft.iot.vmp.media.event.hook.Hook; +import com.genersoft.iot.vmp.media.event.hook.HookSubscribe; +import com.genersoft.iot.vmp.media.event.hook.HookType; +import com.genersoft.iot.vmp.service.ISendRtpServerService; +import gov.nist.javax.sip.message.SIPRequest; +import lombok.extern.slf4j.Slf4j; +import org.dom4j.Element; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import javax.sip.InvalidArgumentException; +import javax.sip.RequestEvent; +import javax.sip.SipException; +import javax.sip.header.CallIdHeader; +import javax.sip.message.Response; +import java.text.ParseException; + +import static com.genersoft.iot.vmp.gb28181.utils.XmlUtil.getText; + +/** + * 媒体通知 + */ +@Slf4j +@Component +public class MediaStatusNotifyMessageHandler extends SIPRequestProcessorParent implements InitializingBean, IMessageHandler { + + private final String cmdType = "MediaStatus"; + + @Autowired + private NotifyMessageHandler notifyMessageHandler; + + @Autowired + private SIPCommander cmder; + + @Autowired + private SIPCommanderForPlatform sipCommanderFroPlatform; + + @Autowired + private IPlatformChannelService platformChannelService; + + @Autowired + private IPlatformService platformService; + + @Autowired + private HookSubscribe subscribe; + + @Autowired + private IInviteStreamService inviteStreamService; + + @Autowired + private SipInviteSessionManager sessionManager; + + @Autowired + private IDeviceChannelService deviceChannelService; + + @Autowired + private IPlayService playService; + + @Autowired + private ISendRtpServerService sendRtpServerService; + + @Override + public void afterPropertiesSet() throws Exception { + notifyMessageHandler.addHandler(cmdType, this); + } + + @Override + public void handForDevice(RequestEvent evt, Device device, Element rootElement) { + // 回复200 OK + try { + responseAck((SIPRequest) evt.getRequest(), Response.OK); + } catch (SipException | InvalidArgumentException | ParseException e) { + log.error("[命令发送失败] 国标级联 录像流推送完毕,回复200OK: {}", e.getMessage()); + } + CallIdHeader callIdHeader = (CallIdHeader)evt.getRequest().getHeader(CallIdHeader.NAME); + String NotifyType =getText(rootElement, "NotifyType"); + if ("121".equals(NotifyType)){ + log.info("[录像流]推送完毕,收到关流通知"); + + SsrcTransaction ssrcTransaction = sessionManager.getSsrcTransactionByCallId(callIdHeader.getCallId()); + if (ssrcTransaction != null) { + log.info("[录像流]推送完毕,关流通知, device: {}, channelId: {}", ssrcTransaction.getDeviceId(), ssrcTransaction.getChannelId()); + InviteInfo inviteInfo = inviteStreamService.getInviteInfo(InviteSessionType.DOWNLOAD, ssrcTransaction.getChannelId(), ssrcTransaction.getStream()); + if (inviteInfo != null) { + playService.stop(inviteInfo); + } + // 去除监听流注销自动停止下载的监听 + Hook hook = Hook.getInstance(HookType.on_media_arrival, "rtp", ssrcTransaction.getStream(), ssrcTransaction.getMediaServerId()); + subscribe.removeSubscribe(hook); + if (ssrcTransaction.getPlatformId() != null) { + // 如果级联播放,需要给上级发送此通知 TODO 多个上级同时观看一个下级 可能存在停错的问题,需要将点播CallId进行上下级绑定 + SendRtpInfo sendRtpInfo = sendRtpServerService.queryByChannelId(ssrcTransaction.getChannelId(), ssrcTransaction.getPlatformId()); + if (sendRtpInfo != null) { + Platform parentPlatform = platformService.queryPlatformByServerGBId(sendRtpInfo.getTargetId()); + if (parentPlatform == null) { + log.warn("[级联消息发送]:发送MediaStatus发现上级平台{}不存在", sendRtpInfo.getTargetId()); + return; + } + CommonGBChannel channel = platformChannelService.queryChannelByPlatformIdAndChannelId(parentPlatform.getId(), sendRtpInfo.getChannelId()); + if (channel == null) { + log.warn("[级联消息发送]:发送MediaStatus发现通道{}不存在", sendRtpInfo.getChannelId()); + return; + } + try { + sipCommanderFroPlatform.sendMediaStatusNotify(parentPlatform, sendRtpInfo, channel); + } catch (SipException | InvalidArgumentException | ParseException e) { + log.error("[命令发送失败] 国标级联 录像播放完毕: {}", e.getMessage()); + } + } + } + }else { + log.info("[录像流]推送完毕,关流通知, 但是未找到对应的下载信息"); + } + } + } + + @Override + public void handForPlatform(RequestEvent evt, Platform parentPlatform, Element element) { + + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/notify/cmd/MobilePositionNotifyMessageHandler.java b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/notify/cmd/MobilePositionNotifyMessageHandler.java new file mode 100755 index 0000000..ec65dc8 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/notify/cmd/MobilePositionNotifyMessageHandler.java @@ -0,0 +1,141 @@ +package com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.notify.cmd; + +import com.genersoft.iot.vmp.gb28181.bean.*; +import com.genersoft.iot.vmp.gb28181.service.IDeviceChannelService; +import com.genersoft.iot.vmp.gb28181.transmit.event.request.SIPRequestProcessorParent; +import com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.IMessageHandler; +import com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.notify.NotifyMessageHandler; +import com.genersoft.iot.vmp.gb28181.utils.NumericUtil; +import com.genersoft.iot.vmp.gb28181.utils.SipUtils; +import com.genersoft.iot.vmp.utils.DateUtil; +import gov.nist.javax.sip.message.SIPRequest; +import lombok.extern.slf4j.Slf4j; +import org.dom4j.DocumentException; +import org.dom4j.Element; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; +import org.springframework.stereotype.Component; +import org.springframework.util.ObjectUtils; + +import javax.sip.InvalidArgumentException; +import javax.sip.RequestEvent; +import javax.sip.SipException; +import javax.sip.message.Response; +import java.text.ParseException; +import java.util.concurrent.ConcurrentLinkedQueue; + +import static com.genersoft.iot.vmp.gb28181.utils.XmlUtil.getText; + +/** + * 移动设备位置数据通知,设备主动发起,不需要上级订阅 + */ +@Slf4j +@Component +public class MobilePositionNotifyMessageHandler extends SIPRequestProcessorParent implements InitializingBean, IMessageHandler { + + private final String cmdType = "MobilePosition"; + + @Autowired + private NotifyMessageHandler notifyMessageHandler; + + @Autowired + private IDeviceChannelService deviceChannelService; + + private ConcurrentLinkedQueue taskQueue = new ConcurrentLinkedQueue<>(); + + @Qualifier("taskExecutor") + @Autowired + private ThreadPoolTaskExecutor taskExecutor; + + @Override + public void afterPropertiesSet() throws Exception { + notifyMessageHandler.addHandler(cmdType, this); + } + + @Override + public void handForDevice(RequestEvent evt, Device device, Element rootElement) { + + boolean isEmpty = taskQueue.isEmpty(); + taskQueue.offer(new SipMsgInfo(evt, device, rootElement)); + // 回复200 OK + try { + responseAck((SIPRequest) evt.getRequest(), Response.OK); + } catch (SipException | InvalidArgumentException | ParseException e) { + log.error("[命令发送失败] 移动位置通知回复: {}", e.getMessage()); + } + if (isEmpty) { + taskExecutor.execute(() -> { + while (!taskQueue.isEmpty()) { + SipMsgInfo sipMsgInfo = taskQueue.poll(); + try { + Element rootElementAfterCharset = getRootElement(sipMsgInfo.getEvt(), sipMsgInfo.getDevice().getCharset()); + if (rootElementAfterCharset == null) { + log.warn("[移动位置通知] {}处理失败,未识别到信息体", device.getDeviceId()); + continue; + } + String channelId = getText(rootElementAfterCharset, "DeviceID"); + DeviceChannel deviceChannel = deviceChannelService.getOne(device.getDeviceId(), channelId); + if (deviceChannel == null) { + log.warn("[解析移动位置通知] 未找到通道:{}/{}", device.getDeviceId(), channelId); + continue; + } + + MobilePosition mobilePosition = new MobilePosition(); + mobilePosition.setCreateTime(DateUtil.getNow()); + if (!ObjectUtils.isEmpty(sipMsgInfo.getDevice().getName())) { + mobilePosition.setDeviceName(sipMsgInfo.getDevice().getName()); + } + mobilePosition.setDeviceId(sipMsgInfo.getDevice().getDeviceId()); + + mobilePosition.setChannelId(deviceChannel.getId()); + mobilePosition.setChannelDeviceId(deviceChannel.getDeviceId()); + String time = getText(rootElementAfterCharset, "Time"); + if (ObjectUtils.isEmpty(time)){ + mobilePosition.setTime(DateUtil.getNow()); + }else { + mobilePosition.setTime(SipUtils.parseTime(time)); + } + mobilePosition.setLongitude(Double.parseDouble(getText(rootElementAfterCharset, "Longitude"))); + mobilePosition.setLatitude(Double.parseDouble(getText(rootElementAfterCharset, "Latitude"))); + if (NumericUtil.isDouble(getText(rootElementAfterCharset, "Speed"))) { + mobilePosition.setSpeed(Double.parseDouble(getText(rootElementAfterCharset, "Speed"))); + } else { + mobilePosition.setSpeed(0.0); + } + if (NumericUtil.isDouble(getText(rootElementAfterCharset, "Direction"))) { + mobilePosition.setDirection(Double.parseDouble(getText(rootElementAfterCharset, "Direction"))); + } else { + mobilePosition.setDirection(0.0); + } + if (NumericUtil.isDouble(getText(rootElementAfterCharset, "Altitude"))) { + mobilePosition.setAltitude(Double.parseDouble(getText(rootElementAfterCharset, "Altitude"))); + } else { + mobilePosition.setAltitude(0.0); + } + mobilePosition.setReportSource("Mobile Position"); + + // 更新device channel 的经纬度 + deviceChannel.setLongitude(mobilePosition.getLongitude()); + deviceChannel.setLatitude(mobilePosition.getLatitude()); + deviceChannel.setGpsTime(mobilePosition.getTime()); + + deviceChannelService.updateChannelGPS(device, deviceChannel, mobilePosition); + + } catch (DocumentException e) { + log.error("未处理的异常 ", e); + } catch (Exception e) { + log.warn("[移动位置通知] 发现未处理的异常, \r\n{}", evt.getRequest()); + log.error("[移动位置通知] 异常内容: ", e); + } + } + }); + } + } + + @Override + public void handForPlatform(RequestEvent evt, Platform parentPlatform, Element element) { + + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/query/QueryMessageHandler.java b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/query/QueryMessageHandler.java new file mode 100755 index 0000000..9a29955 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/query/QueryMessageHandler.java @@ -0,0 +1,25 @@ +package com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.query; + +import com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.MessageHandlerAbstract; +import com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.MessageRequestProcessor; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +/** + * 命令类型: 查询指令 + * 命令类型: 设备状态, 设备目录信息, 设备信息, 文件目录检索(TODO), 报警(TODO), 设备配置(TODO), 设备预置位(TODO), 移动设备位置数据(TODO) + */ +@Component +public class QueryMessageHandler extends MessageHandlerAbstract implements InitializingBean { + + private final String messageType = "Query"; + + @Autowired + private MessageRequestProcessor messageRequestProcessor; + + @Override + public void afterPropertiesSet() throws Exception { + messageRequestProcessor.addHandler(messageType, this); + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/query/cmd/AlarmQueryMessageHandler.java b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/query/cmd/AlarmQueryMessageHandler.java new file mode 100755 index 0000000..adcfb86 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/query/cmd/AlarmQueryMessageHandler.java @@ -0,0 +1,51 @@ +package com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.query.cmd; + +import com.genersoft.iot.vmp.gb28181.bean.Device; +import com.genersoft.iot.vmp.gb28181.bean.Platform; +import com.genersoft.iot.vmp.gb28181.transmit.event.request.SIPRequestProcessorParent; +import com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.IMessageHandler; +import com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.query.QueryMessageHandler; +import gov.nist.javax.sip.message.SIPRequest; +import lombok.extern.slf4j.Slf4j; +import org.dom4j.Element; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import javax.sip.InvalidArgumentException; +import javax.sip.RequestEvent; +import javax.sip.SipException; +import javax.sip.message.Response; +import java.text.ParseException; + +@Slf4j +@Component +public class AlarmQueryMessageHandler extends SIPRequestProcessorParent implements InitializingBean, IMessageHandler { + + private final String cmdType = "Alarm"; + + @Autowired + private QueryMessageHandler queryMessageHandler; + + @Override + public void afterPropertiesSet() throws Exception { + queryMessageHandler.addHandler(cmdType, this); + } + + @Override + public void handForDevice(RequestEvent evt, Device device, Element element) { + + } + + @Override + public void handForPlatform(RequestEvent evt, Platform parentPlatform, Element rootElement) { + + log.info("不支持alarm查询"); + try { + responseAck((SIPRequest) evt.getRequest(), Response.NOT_FOUND, "not support alarm query"); + } catch (SipException | InvalidArgumentException | ParseException e) { + log.error("[命令发送失败] 国标级联 alarm查询回复200OK: {}", e.getMessage()); + } + + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/query/cmd/CatalogQueryMessageHandler.java b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/query/cmd/CatalogQueryMessageHandler.java new file mode 100755 index 0000000..dbe2b5e --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/query/cmd/CatalogQueryMessageHandler.java @@ -0,0 +1,85 @@ +package com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.query.cmd; + +import com.genersoft.iot.vmp.gb28181.bean.CommonGBChannel; +import com.genersoft.iot.vmp.gb28181.bean.Device; +import com.genersoft.iot.vmp.gb28181.bean.Platform; +import com.genersoft.iot.vmp.gb28181.service.IGbChannelService; +import com.genersoft.iot.vmp.gb28181.service.IPlatformChannelService; +import com.genersoft.iot.vmp.gb28181.transmit.cmd.impl.SIPCommanderForPlatform; +import com.genersoft.iot.vmp.gb28181.transmit.event.request.SIPRequestProcessorParent; +import com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.IMessageHandler; +import com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.query.QueryMessageHandler; +import gov.nist.javax.sip.message.SIPRequest; +import lombok.extern.slf4j.Slf4j; +import org.dom4j.Element; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import javax.sip.InvalidArgumentException; +import javax.sip.RequestEvent; +import javax.sip.SipException; +import javax.sip.header.FromHeader; +import javax.sip.message.Response; +import java.text.ParseException; +import java.util.Collections; +import java.util.List; + +@Slf4j +@Component +public class CatalogQueryMessageHandler extends SIPRequestProcessorParent implements InitializingBean, IMessageHandler { + + private final String cmdType = "Catalog"; + + @Autowired + private QueryMessageHandler queryMessageHandler; + + @Autowired + private IGbChannelService channelService; + + @Autowired + private IPlatformChannelService platformChannelService; + + @Autowired + private SIPCommanderForPlatform cmderFroPlatform; + + + @Override + public void afterPropertiesSet() throws Exception { + queryMessageHandler.addHandler(cmdType, this); + } + + @Override + public void handForDevice(RequestEvent evt, Device device, Element element) { + try { + // 回复200 OK + responseAck((SIPRequest) evt.getRequest(), Response.FORBIDDEN); + } catch (SipException | InvalidArgumentException | ParseException ignored) {} + } + + @Override + public void handForPlatform(RequestEvent evt, Platform platform, Element rootElement) { + + FromHeader fromHeader = (FromHeader) evt.getRequest().getHeader(FromHeader.NAME); + try { + // 回复200 OK + responseAck((SIPRequest) evt.getRequest(), Response.OK); + } catch (SipException | InvalidArgumentException | ParseException e) { + log.error("[命令发送失败] 国标级联 目录查询回复200OK: {}", e.getMessage()); + } + Element snElement = rootElement.element("SN"); + String sn = snElement.getText(); + List channelList = platformChannelService.queryByPlatform(platform); + + try { + if (!channelList.isEmpty()) { + cmderFroPlatform.catalogQuery(channelList, platform, sn, fromHeader.getTag()); + }else { + // 回复无通道 + cmderFroPlatform.catalogQuery(Collections.emptyList(), platform, sn, fromHeader.getTag()); + } + } catch (SipException | InvalidArgumentException | ParseException e) { + log.error("[命令发送失败] 国标级联 目录查询回复: {}", e.getMessage()); + } + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/query/cmd/DeviceInfoQueryMessageHandler.java b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/query/cmd/DeviceInfoQueryMessageHandler.java new file mode 100755 index 0000000..648ea67 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/query/cmd/DeviceInfoQueryMessageHandler.java @@ -0,0 +1,136 @@ +package com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.query.cmd; + +import com.genersoft.iot.vmp.common.enums.ChannelDataType; +import com.genersoft.iot.vmp.gb28181.bean.CommonGBChannel; +import com.genersoft.iot.vmp.gb28181.bean.Device; +import com.genersoft.iot.vmp.gb28181.bean.Platform; +import com.genersoft.iot.vmp.gb28181.service.IDeviceChannelService; +import com.genersoft.iot.vmp.gb28181.service.IDeviceService; +import com.genersoft.iot.vmp.gb28181.service.IGbChannelService; +import com.genersoft.iot.vmp.gb28181.transmit.cmd.impl.SIPCommanderForPlatform; +import com.genersoft.iot.vmp.gb28181.transmit.event.request.SIPRequestProcessorParent; +import com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.IMessageHandler; +import com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.query.QueryMessageHandler; +import gov.nist.javax.sip.message.SIPRequest; +import lombok.extern.slf4j.Slf4j; +import org.dom4j.Element; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import javax.sip.InvalidArgumentException; +import javax.sip.RequestEvent; +import javax.sip.SipException; +import javax.sip.header.FromHeader; +import javax.sip.message.Response; +import java.text.ParseException; + +import static com.genersoft.iot.vmp.gb28181.utils.XmlUtil.getText; + +@Slf4j +@Component +public class DeviceInfoQueryMessageHandler extends SIPRequestProcessorParent implements InitializingBean, IMessageHandler { + + private final String cmdType = "DeviceInfo"; + + @Autowired + private QueryMessageHandler queryMessageHandler; + + @Autowired + private SIPCommanderForPlatform cmderFroPlatform; + + @Autowired + private IDeviceService deviceService; + + @Autowired + private IGbChannelService channelService; + + @Autowired + private IDeviceChannelService deviceChannelService; + + @Override + public void afterPropertiesSet() throws Exception { + queryMessageHandler.addHandler(cmdType, this); + } + + @Override + public void handForDevice(RequestEvent evt, Device device, Element rootElement) { + + } + + @Override + public void handForPlatform(RequestEvent evt, Platform platform, Element rootElement) { + log.info("[DeviceInfo查询]消息"); + SIPRequest request = (SIPRequest) evt.getRequest(); + FromHeader fromHeader = (FromHeader) evt.getRequest().getHeader(FromHeader.NAME); + + String sn = rootElement.element("SN").getText(); + + /*根据WVP原有的数据结构,设备和通道是分开放置,设备信息都是存放在设备表里,通道表里的设备信息不可作为真实信息处理 + 大部分NVR/IPC设备对他的通道信息实现都是返回默认的值没有什么参考价值。NVR/IPC通道我们统一使用设备表的设备信息来作为返回。 + 我们这里使用查询数据库的方式来实现这个设备信息查询的功能,在其他地方对设备信息更新达到正确的目的。*/ + + String channelId = getText(rootElement, "DeviceID"); + // 查询这是通道id还是设备id + if (platform.getDeviceGBId().equals(channelId)) { + // id指向平台的国标编号,那么就是查询平台的信息 + try { + cmderFroPlatform.deviceInfoResponse(platform, null, sn, fromHeader.getTag()); + } catch (SipException | InvalidArgumentException | ParseException e) { + log.error("[命令发送失败] 国标级联 DeviceInfo查询回复: {}", e.getMessage()); + } + return; + } + CommonGBChannel channel = channelService.queryOneWithPlatform(platform.getId(), channelId); + if (channel == null) { + // 不存在则回复404 + log.warn("[DeviceInfo] 通道不存在: 通道编号: {}", channelId); + try { + responseAck(request, Response.NOT_FOUND, "channel not found or offline"); + } catch (SipException | InvalidArgumentException | ParseException e) { + log.error("[命令发送失败] DeviceInfo查询回复: {}", e.getMessage()); + return; + } + return; + } + // 判断通道类型 + if (channel.getDataType() != ChannelDataType.GB28181) { + // 非国标通道不支持录像回放控制 + log.warn("[DeviceInfo] 非国标通道不支持录像回放控制: 通道ID: {}", channel.getGbId()); + try { + responseAck(request, Response.FORBIDDEN, ""); + } catch (SipException | InvalidArgumentException | ParseException e) { + log.error("[命令发送失败] DeviceInfo查询回复: {}", e.getMessage()); + return; + } + return; + } + + // 根据通道ID,获取所属设备 + Device device = deviceService.getDevice(channel.getDataDeviceId()); + if (device == null) { + // 不存在则回复404 + log.warn("[DeviceInfo] 通道所属设备不存在, 通道ID: {}", channel.getDataDeviceId()); + + try { + responseAck(request, Response.NOT_FOUND, "device not found "); + } catch (SipException | InvalidArgumentException | ParseException e) { + log.error("[命令发送失败] DeviceInfo查询回复: {}", e.getMessage()); + return; + } + return; + } + try { + // 回复200 OK + responseAck((SIPRequest) evt.getRequest(), Response.OK); + } catch (SipException | InvalidArgumentException | ParseException e) { + log.error("[命令发送失败] DeviceInfo查询回复: {}", e.getMessage()); + return; + } + try { + cmderFroPlatform.deviceInfoResponse(platform, device, sn, fromHeader.getTag()); + } catch (SipException | InvalidArgumentException | ParseException e) { + log.error("[命令发送失败] 国标级联 DeviceInfo查询回复: {}", e.getMessage()); + } + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/query/cmd/DeviceStatusQueryMessageHandler.java b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/query/cmd/DeviceStatusQueryMessageHandler.java new file mode 100755 index 0000000..2b3b8fc --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/query/cmd/DeviceStatusQueryMessageHandler.java @@ -0,0 +1,76 @@ +package com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.query.cmd; + +import com.genersoft.iot.vmp.gb28181.bean.CommonGBChannel; +import com.genersoft.iot.vmp.gb28181.bean.Device; +import com.genersoft.iot.vmp.gb28181.bean.Platform; +import com.genersoft.iot.vmp.gb28181.service.IGbChannelService; +import com.genersoft.iot.vmp.gb28181.transmit.cmd.ISIPCommanderForPlatform; +import com.genersoft.iot.vmp.gb28181.transmit.event.request.SIPRequestProcessorParent; +import com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.IMessageHandler; +import com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.query.QueryMessageHandler; +import gov.nist.javax.sip.message.SIPRequest; +import lombok.extern.slf4j.Slf4j; +import org.dom4j.Element; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import javax.sip.InvalidArgumentException; +import javax.sip.RequestEvent; +import javax.sip.SipException; +import javax.sip.header.FromHeader; +import javax.sip.message.Response; +import java.text.ParseException; + +import static com.genersoft.iot.vmp.gb28181.utils.XmlUtil.getText; + +@Slf4j +@Component +public class DeviceStatusQueryMessageHandler extends SIPRequestProcessorParent implements InitializingBean, IMessageHandler { + + private final String cmdType = "DeviceStatus"; + + @Autowired + private QueryMessageHandler queryMessageHandler; + + @Autowired + private IGbChannelService channelService; + + @Autowired + private ISIPCommanderForPlatform cmderFroPlatform; + + @Override + public void afterPropertiesSet() throws Exception { + queryMessageHandler.addHandler(cmdType, this); + } + + @Override + public void handForDevice(RequestEvent evt, Device device, Element element) { + + } + + @Override + public void handForPlatform(RequestEvent evt, Platform parentPlatform, Element rootElement) { + + log.info("接收到DeviceStatus查询消息"); + FromHeader fromHeader = (FromHeader) evt.getRequest().getHeader(FromHeader.NAME); + // 回复200 OK + try { + responseAck((SIPRequest) evt.getRequest(), Response.OK); + } catch (SipException | InvalidArgumentException | ParseException e) { + log.error("[命令发送失败] 国标级联 DeviceStatus查询回复200OK: {}", e.getMessage()); + } + String sn = rootElement.element("SN").getText(); + String channelId = getText(rootElement, "DeviceID"); + CommonGBChannel channel= channelService.queryOneWithPlatform(parentPlatform.getId(), channelId); + if (channel ==null){ + log.error("[平台没有该通道的使用权限]:platformId"+parentPlatform.getServerGBId()+" deviceID:"+channelId); + return; + } + try { + cmderFroPlatform.deviceStatusResponse(parentPlatform, channelId, sn, fromHeader.getTag(), "ON".equalsIgnoreCase(channel.getGbStatus())); + } catch (SipException | InvalidArgumentException | ParseException e) { + log.error("[命令发送失败] 国标级联 DeviceStatus查询回复: {}", e.getMessage()); + } + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/query/cmd/RecordInfoQueryMessageHandler.java b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/query/cmd/RecordInfoQueryMessageHandler.java new file mode 100755 index 0000000..b0b82ef --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/query/cmd/RecordInfoQueryMessageHandler.java @@ -0,0 +1,212 @@ +package com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.query.cmd; + +import com.genersoft.iot.vmp.common.enums.ChannelDataType; +import com.genersoft.iot.vmp.gb28181.bean.*; +import com.genersoft.iot.vmp.gb28181.event.record.RecordInfoEventListener; +import com.genersoft.iot.vmp.gb28181.service.IDeviceChannelService; +import com.genersoft.iot.vmp.gb28181.service.IDeviceService; +import com.genersoft.iot.vmp.gb28181.service.IGbChannelPlayService; +import com.genersoft.iot.vmp.gb28181.service.IGbChannelService; +import com.genersoft.iot.vmp.gb28181.transmit.cmd.impl.SIPCommander; +import com.genersoft.iot.vmp.gb28181.transmit.cmd.impl.SIPCommanderForPlatform; +import com.genersoft.iot.vmp.gb28181.transmit.event.request.SIPRequestProcessorParent; +import com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.IMessageHandler; +import com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.query.QueryMessageHandler; +import com.genersoft.iot.vmp.utils.DateUtil; +import gov.nist.javax.sip.message.SIPRequest; +import lombok.extern.slf4j.Slf4j; +import org.dom4j.Element; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import javax.sip.InvalidArgumentException; +import javax.sip.RequestEvent; +import javax.sip.SipException; +import javax.sip.message.Response; +import java.text.ParseException; +import java.util.ArrayList; +import java.util.List; + +@Slf4j +@Component +public class RecordInfoQueryMessageHandler extends SIPRequestProcessorParent implements InitializingBean, IMessageHandler { + + private final String cmdType = "RecordInfo"; + + @Autowired + private QueryMessageHandler queryMessageHandler; + + @Autowired + private IGbChannelService channelService; + + @Autowired + private IGbChannelPlayService playService; + + @Autowired + private IDeviceService deviceService; + + @Autowired + private IDeviceChannelService deviceChannelService; + + @Autowired + private SIPCommanderForPlatform cmderFroPlatform; + + @Autowired + private SIPCommander commander; + + @Autowired + private RecordInfoEventListener recordInfoEventListener; + + @Override + public void afterPropertiesSet() throws Exception { + queryMessageHandler.addHandler(cmdType, this); + } + + @Override + public void handForDevice(RequestEvent evt, Device device, Element element) { + + } + + @Override + public void handForPlatform(RequestEvent evt, Platform platform, Element rootElement) { + + SIPRequest request = (SIPRequest) evt.getRequest(); + Element snElement = rootElement.element("SN"); + int sn = Integer.parseInt(snElement.getText()); + Element deviceIDElement = rootElement.element("DeviceID"); + String channelId = deviceIDElement.getText(); + Element startTimeElement = rootElement.element("StartTime"); + String startTime = null; + if (startTimeElement != null) { + startTime = startTimeElement.getText(); + } + Element endTimeElement = rootElement.element("EndTime"); + String endTime = null; + if (endTimeElement != null) { + endTime = endTimeElement.getText(); + } + Element secrecyElement = rootElement.element("Secrecy"); + int secrecy = 0; + if (secrecyElement != null) { + secrecy = Integer.parseInt(secrecyElement.getText().trim()); + } + String type = "all"; + Element typeElement = rootElement.element("Type"); + if (typeElement != null) { + type = typeElement.getText(); + } + + // 向国标设备请求录像数据 + CommonGBChannel channel = channelService.queryOneWithPlatform(platform.getId(), channelId); + if (channel == null) { + log.info("[平台查询录像记录] 未找到通道 {}/{}", platform.getName(), channelId ); + try { + responseAck(request, Response.BAD_REQUEST); + } catch (SipException | InvalidArgumentException | ParseException e) { + log.error("[命令发送失败] [平台查询录像记录] 未找到通道: {}", e.getMessage()); + } + return; + } + if (channel.getDataType() == ChannelDataType.GB28181) { + Device device = deviceService.getDevice(channel.getDataDeviceId()); + if (device == null) { + log.warn("[平台查询录像记录] 未找到通道对应的设备 {}/{}", platform.getName(), channelId ); + try { + responseAck(request, Response.BAD_REQUEST); + } catch (SipException | InvalidArgumentException | ParseException e) { + log.error("[命令发送失败] [平台查询录像记录] 未找到通道对应的设备: {}", e.getMessage()); + } + return; + } + // 获取通道的原始信息 + DeviceChannel deviceChannel = deviceChannelService.getOneForSourceById(channel.getGbId()); + // 接收录像数据 + recordInfoEventListener.addEndEventHandler(device.getDeviceId(), deviceChannel.getDeviceId(), (recordInfo)->{ + try { + log.info("[国标级联] 录像查询收到数据, 通道: {},准备转发===", channelId); + cmderFroPlatform.recordInfo(channel, platform, request.getFromTag(), recordInfo); + } catch (SipException | InvalidArgumentException | ParseException e) { + log.error("[命令发送失败] 国标级联 回复录像数据: {}", e.getMessage()); + } + }); + try { + commander.recordInfoQuery(device, deviceChannel.getDeviceId(), DateUtil.ISO8601Toyyyy_MM_dd_HH_mm_ss(startTime), + DateUtil.ISO8601Toyyyy_MM_dd_HH_mm_ss(endTime), sn, secrecy, type, (eventResult -> { + // 回复200 OK + try { + responseAck(request, Response.OK); + } catch (SipException | InvalidArgumentException | ParseException e) { + log.error("[命令发送失败] 录像查询回复: {}", e.getMessage()); + } + }),(eventResult -> { + // 查询失败 + try { + responseAck(request, eventResult.statusCode, eventResult.msg); + } catch (SipException | InvalidArgumentException | ParseException e) { + log.error("[命令发送失败] 录像查询回复: {}", e.getMessage()); + } + })); + } catch (InvalidArgumentException | ParseException | SipException e) { + log.error("[命令发送失败] 录像查询: {}", e.getMessage()); + } + }else { + // 回复200 OK + try { + responseAck(request, Response.OK); + } catch (SipException | InvalidArgumentException | ParseException e) { + log.error("[命令发送失败] 录像查询回复: {}", e.getMessage()); + } + + playService.queryRecord(channel, DateUtil.ISO8601Toyyyy_MM_dd_HH_mm_ss(startTime), + DateUtil.ISO8601Toyyyy_MM_dd_HH_mm_ss(endTime), + (code, msg, commonRecordInfoList) -> { + RecordInfo recordInfo = new RecordInfo(); + recordInfo.setSumNum(commonRecordInfoList.size()); + recordInfo.setChannelId(channelId); + recordInfo.setSn(sn + ""); + List recordList = new ArrayList<>(commonRecordInfoList.size()); + for (int i = 0; i < commonRecordInfoList.size(); i++) { + CommonRecordInfo commonRecordInfo = commonRecordInfoList.get(i); + RecordItem recordItem = new RecordItem(); + recordItem.setDeviceId(channelId); + recordItem.setName(commonRecordInfo.getStartTime()); + recordItem.setFilePath("/" + commonRecordInfo.getStartTime()); + recordItem.setAddress("/" + commonRecordInfo.getStartTime()); + recordItem.setStartTime(commonRecordInfo.getStartTime()); + recordItem.setEndTime(commonRecordInfo.getEndTime()); + recordItem.setSecrecy(0); + recordItem.setRecorderId(""); + recordItem.setType(""); + recordItem.setFileSize(commonRecordInfo.getFileSize()); + recordList.add(recordItem); + } + recordInfo.setRecordList(recordList); + + try { + log.info("[国标级联] 录像查询收到数据, 通道: {},准备转发===", channelId); + cmderFroPlatform.recordInfo(channel, platform, request.getFromTag(), recordInfo); + } catch (SipException | InvalidArgumentException | ParseException e) { + log.error("[命令发送失败] 国标级联 回复录像数据: {}", e.getMessage()); + } + }); + } +// +// +// +// +// +// +// +// if (channel.getDataType() != ChannelDataType.GB28181) { +// log.info("[平台查询录像记录] 只支持查询国标28181的录像数据 {}/{}", platform.getName(), channelId ); +// try { +// responseAck(request, Response.NOT_IMPLEMENTED); // 回复未实现 +// } catch (SipException | InvalidArgumentException | ParseException e) { +// log.error("[命令发送失败] 平台查询录像记录: {}", e.getMessage()); +// } +// return; +// } + + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/response/ResponseMessageHandler.java b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/response/ResponseMessageHandler.java new file mode 100755 index 0000000..163288e --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/response/ResponseMessageHandler.java @@ -0,0 +1,36 @@ +package com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.response; + +import com.genersoft.iot.vmp.gb28181.bean.Device; +import com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.MessageHandlerAbstract; +import com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.MessageRequestProcessor; +import org.dom4j.Element; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import javax.sip.RequestEvent; + +/** + * 命令类型: 请求动作的应答 + * 命令类型: 设备控制, 报警通知, 设备目录信息查询, 目录信息查询, 目录收到, 设备信息查询, 设备状态信息查询 ...... + */ +@Component +public class ResponseMessageHandler extends MessageHandlerAbstract implements InitializingBean { + + private final String messageType = "Response"; + + @Autowired + private MessageRequestProcessor messageRequestProcessor; + + + + @Override + public void afterPropertiesSet() throws Exception { + messageRequestProcessor.addHandler(messageType, this); + } + + @Override + public void handForDevice(RequestEvent evt, Device device, Element element) { + super.handForDevice(evt, device, element); + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/response/cmd/AlarmResponseMessageHandler.java b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/response/cmd/AlarmResponseMessageHandler.java new file mode 100755 index 0000000..ffbe907 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/response/cmd/AlarmResponseMessageHandler.java @@ -0,0 +1,61 @@ +package com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.response.cmd; + +import com.alibaba.fastjson2.JSONObject; +import com.genersoft.iot.vmp.gb28181.bean.Device; +import com.genersoft.iot.vmp.gb28181.bean.Platform; +import com.genersoft.iot.vmp.gb28181.transmit.callback.DeferredResultHolder; +import com.genersoft.iot.vmp.gb28181.transmit.event.request.SIPRequestProcessorParent; +import com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.IMessageHandler; +import com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.response.ResponseMessageHandler; +import com.genersoft.iot.vmp.gb28181.utils.XmlUtil; +import gov.nist.javax.sip.message.SIPRequest; +import lombok.extern.slf4j.Slf4j; +import org.dom4j.Element; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import javax.sip.InvalidArgumentException; +import javax.sip.RequestEvent; +import javax.sip.SipException; +import javax.sip.message.Response; +import java.text.ParseException; + +@Slf4j +@Component +public class AlarmResponseMessageHandler extends SIPRequestProcessorParent implements InitializingBean, IMessageHandler { + + private final String cmdType = "Alarm"; + + @Autowired + private ResponseMessageHandler responseMessageHandler; + + @Autowired + private DeferredResultHolder deferredResultHolder; + + @Override + public void afterPropertiesSet() throws Exception { + responseMessageHandler.addHandler(cmdType, this); + } + + @Override + public void handForDevice(RequestEvent evt, Device device, Element rootElement) { + // 回复200 OK + try { + responseAck((SIPRequest) evt.getRequest(), Response.OK); + } catch (SipException | InvalidArgumentException | ParseException e) { + log.error("[命令发送失败] 目录查询回复: {}", e.getMessage()); + } + JSONObject json = new JSONObject(); + XmlUtil.node2Json(rootElement, json); + if (log.isDebugEnabled()) { + log.debug(json.toJSONString()); + } + responseMessageHandler.handMessageEvent(rootElement, null); + } + + @Override + public void handForPlatform(RequestEvent evt, Platform parentPlatform, Element element) { + + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/response/cmd/BroadcastResponseMessageHandler.java b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/response/cmd/BroadcastResponseMessageHandler.java new file mode 100755 index 0000000..2567b76 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/response/cmd/BroadcastResponseMessageHandler.java @@ -0,0 +1,99 @@ +package com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.response.cmd; + +import com.genersoft.iot.vmp.gb28181.bean.*; +import com.genersoft.iot.vmp.gb28181.service.IDeviceChannelService; +import com.genersoft.iot.vmp.gb28181.service.IPlayService; +import com.genersoft.iot.vmp.gb28181.session.AudioBroadcastManager; +import com.genersoft.iot.vmp.gb28181.transmit.event.request.SIPRequestProcessorParent; +import com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.IMessageHandler; +import com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.response.ResponseMessageHandler; +import gov.nist.javax.sip.message.SIPRequest; +import lombok.extern.slf4j.Slf4j; +import org.dom4j.Element; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import javax.sip.InvalidArgumentException; +import javax.sip.RequestEvent; +import javax.sip.SipException; +import javax.sip.message.Response; +import java.text.ParseException; + +import static com.genersoft.iot.vmp.gb28181.utils.XmlUtil.getText; + +@Slf4j +@Component +public class BroadcastResponseMessageHandler extends SIPRequestProcessorParent implements InitializingBean, IMessageHandler { + + private final String cmdType = "Broadcast"; + + @Autowired + private ResponseMessageHandler responseMessageHandler; + + @Autowired + private IDeviceChannelService deviceChannelService; + + @Autowired + private AudioBroadcastManager audioBroadcastManager; + + @Autowired + private IPlayService playService; + + @Override + public void afterPropertiesSet() throws Exception { + responseMessageHandler.addHandler(cmdType, this); + } + + @Override + public void handForDevice(RequestEvent evt, Device device, Element rootElement) { + + SIPRequest request = (SIPRequest) evt.getRequest(); + try { + String channelId = getText(rootElement, "DeviceID"); + DeviceChannel channel = null; + if (!channelId.equals(device.getDeviceId())) { + channel = deviceChannelService.getOneBySourceId(device.getId(), channelId); + }else { + channel = deviceChannelService.getBroadcastChannel(device.getId()); + } + if (channel == null) { + log.info("[语音广播]回复: 未找到通道{}/{}", device.getDeviceId(), channelId ); + // 回复410 + responseAck((SIPRequest) evt.getRequest(), Response.NOT_FOUND); + return; + } + if (!audioBroadcastManager.exit(channel.getId())) { + // 回复410 + responseAck((SIPRequest) evt.getRequest(), Response.BUSY_HERE); + return; + } + String result = getText(rootElement, "Result"); + Element infoElement = rootElement.element("Info"); + String reason = null; + if (infoElement != null) { + reason = getText(infoElement, "Reason"); + } + log.info("[语音广播]回复:{}, {}/{}", reason == null? result : result + ": " + reason, device.getDeviceId(), channelId ); + + // 回复200 OK + responseAck(request, Response.OK); + if (result.equalsIgnoreCase("OK")) { + AudioBroadcastCatch audioBroadcastCatch = audioBroadcastManager.get(channel.getId()); + audioBroadcastCatch.setStatus(AudioBroadcastCatchStatus.WaiteInvite); + audioBroadcastManager.update(audioBroadcastCatch); + }else { + playService.stopAudioBroadcast(device, channel); + } + } catch (ParseException | SipException | InvalidArgumentException e) { + log.error("[命令发送失败] 国标级联 语音喊话: {}", e.getMessage()); + } + } + + @Override + public void handForPlatform(RequestEvent evt, Platform parentPlatform, Element element) { + + } + + +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/response/cmd/CatalogResponseMessageHandler.java b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/response/cmd/CatalogResponseMessageHandler.java new file mode 100755 index 0000000..738430a --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/response/cmd/CatalogResponseMessageHandler.java @@ -0,0 +1,245 @@ +package com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.response.cmd; + +import com.genersoft.iot.vmp.conf.SipConfig; +import com.genersoft.iot.vmp.gb28181.bean.*; +import com.genersoft.iot.vmp.gb28181.service.IDeviceChannelService; +import com.genersoft.iot.vmp.gb28181.service.IGroupService; +import com.genersoft.iot.vmp.gb28181.service.IRegionService; +import com.genersoft.iot.vmp.gb28181.session.CatalogDataManager; +import com.genersoft.iot.vmp.gb28181.transmit.event.request.SIPRequestProcessorParent; +import com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.IMessageHandler; +import com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.response.ResponseMessageHandler; +import com.genersoft.iot.vmp.utils.Coordtransform; +import gov.nist.javax.sip.message.SIPRequest; +import lombok.extern.slf4j.Slf4j; +import org.dom4j.DocumentException; +import org.dom4j.Element; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +import javax.sip.InvalidArgumentException; +import javax.sip.RequestEvent; +import javax.sip.SipException; +import javax.sip.message.Response; +import java.text.ParseException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.concurrent.ConcurrentLinkedQueue; + +/** + * 目录查询的回复 + */ +@Slf4j +@Component +public class CatalogResponseMessageHandler extends SIPRequestProcessorParent implements InitializingBean, IMessageHandler { + + private final String cmdType = "Catalog"; + + @Autowired + private ResponseMessageHandler responseMessageHandler; + + private final ConcurrentLinkedQueue taskQueue = new ConcurrentLinkedQueue<>(); + + @Autowired + private IDeviceChannelService deviceChannelService; + + @Autowired + private IRegionService regionService; + + @Autowired + private IGroupService groupService; + + @Autowired + private CatalogDataManager catalogDataCatch; + + @Autowired + private SipConfig sipConfig; + + @Override + public void afterPropertiesSet() throws Exception { + responseMessageHandler.addHandler(cmdType, this); + } + + @Override + public void handForDevice(RequestEvent evt, Device device, Element element) { + taskQueue.offer(new HandlerCatchData(evt, device, element)); + // 回复200 OK + try { + responseAck((SIPRequest) evt.getRequest(), Response.OK); + } catch (SipException | InvalidArgumentException | ParseException e) { + log.error("[命令发送失败] 目录查询回复: {}", e.getMessage()); + } + } + + @Scheduled(fixedDelay = 50) + @Transactional + public void executeTaskQueue(){ + if (taskQueue.isEmpty()) { + return; + } + List handlerCatchDataList = new ArrayList<>(); + int size = taskQueue.size(); + for (int i = 0; i < size; i++) { + HandlerCatchData poll = taskQueue.poll(); + if (poll != null) { + handlerCatchDataList.add(poll); + } + } + if (handlerCatchDataList.isEmpty()) { + return; + } + for (HandlerCatchData take : handlerCatchDataList) { + if (take == null) { + continue; + } + RequestEvent evt = take.getEvt(); + int sn = 0; + // 全局异常捕获,保证下一条可以得到处理 + try { + Element rootElement = null; + try { + rootElement = getRootElement(take.getEvt(), take.getDevice().getCharset()); + } catch (DocumentException e) { + log.error("[xml解析] 失败: ", e); + continue; + } + if (rootElement == null) { + log.warn("[ 收到通道 ] content cannot be null, {}", evt.getRequest()); + continue; + } + Element deviceListElement = rootElement.element("DeviceList"); + Element sumNumElement = rootElement.element("SumNum"); + Element snElement = rootElement.element("SN"); + + sn = Integer.parseInt(snElement.getText()); + int sumNum = Integer.parseInt(sumNumElement.getText()); + + if (sumNum == 0) { + log.info("[收到通道]设备:{}的: 0个", take.getDevice().getDeviceId()); + // 数据已经完整接收 + deviceChannelService.cleanChannelsForDevice(take.getDevice().getId()); + // 推送空数据,不然无法及时结束 + catalogDataCatch.put(take.getDevice().getDeviceId(), sn, 0, take.getDevice(), + Collections.emptyList(), Collections.emptyList(), Collections.emptyList()); + catalogDataCatch.setChannelSyncEnd(take.getDevice().getDeviceId(), sn, null); + return; + } else { + Iterator deviceListIterator = deviceListElement.elementIterator(); + if (deviceListIterator != null) { + List channelList = new ArrayList<>(); + List regionList = new ArrayList<>(); + List groupList = new ArrayList<>(); + // 遍历DeviceList + while (deviceListIterator.hasNext()) { + Element itemDevice = deviceListIterator.next(); + Element channelDeviceElement = itemDevice.element("DeviceID"); + if (channelDeviceElement == null) { + // 总数减一, 避免最后总数不对 无法确定问题 + continue; + } + // 从xml解析内容到 DeviceChannel 对象 + DeviceChannel channel = DeviceChannel.decode(itemDevice); + if (channel.getDeviceId() == null) { + log.info("[收到目录订阅]:但是解析失败 {}", new String(evt.getRequest().getRawContent())); + continue; + } + channel.setDataDeviceId(take.getDevice().getId()); + if (channel.getParentId() != null && channel.getParentId().equals(sipConfig.getId())) { + channel.setParentId(null); + } + // 解析通道类型 + if (channel.getDeviceId().length() <= 8) { + // 行政区划 + Region region = Region.getInstance(channel); + regionList.add(region); + channel.setChannelType(1); + }else if (channel.getDeviceId().length() == 20){ + // 业务分组/虚拟组织 + Group group = Group.getInstance(channel); + if (group != null) { + channel.setParental(1); + channel.setChannelType(2); + groupList.add(group); + } + if (channel.getLongitude() != null && channel.getLatitude() != null && channel.getLongitude() > 0 && channel.getLatitude() > 0) { + Double[] wgs84Position = Coordtransform.GCJ02ToWGS84(channel.getLongitude(), channel.getLatitude()); + channel.setGbLongitude(wgs84Position[0]); + channel.setGbLatitude(wgs84Position[1]); + } + } + channelList.add(channel); + } + + catalogDataCatch.put(take.getDevice().getDeviceId(), sn, sumNum, take.getDevice(), + channelList, regionList, groupList); + log.info("[收到通道]设备: {} -> {}个,{}/{}", take.getDevice().getDeviceId(), channelList.size(), catalogDataCatch.size(take.getDevice().getDeviceId(), sn), sumNum); + } + } + } catch (Exception e) { + log.warn("[收到通道] 发现未处理的异常, \r\n{}", evt.getRequest()); + log.error("[收到通道] 异常内容: ", e); + } finally { + String deviceId = take.getDevice().getDeviceId(); + if (catalogDataCatch.size(deviceId, sn) > 0 + && catalogDataCatch.size(deviceId, sn) == catalogDataCatch.sumNum(deviceId, sn)) { + // 数据已经完整接收, 此时可能存在某个设备离线变上线的情况,但是考虑到性能,此处不做处理, + // 目前支持设备通道上线通知时和设备上线时向上级通知 + boolean resetChannelsResult = saveData(take.getDevice(), sn); + if (!resetChannelsResult) { + String errorMsg = "接收成功,写入失败,共" + catalogDataCatch.sumNum(deviceId, sn) + "条,已接收" + catalogDataCatch.getDeviceChannelList(take.getDevice().getDeviceId(), sn).size() + "条"; + catalogDataCatch.setChannelSyncEnd(deviceId, sn, errorMsg); + } else { + catalogDataCatch.setChannelSyncEnd(deviceId, sn, null); + } + } + } + } + } + + @Transactional + public boolean saveData(Device device, int sn) { + + boolean result = true; + List deviceChannelList = catalogDataCatch.getDeviceChannelList(device.getDeviceId(), sn); + if (deviceChannelList != null && !deviceChannelList.isEmpty()) { + result &= deviceChannelService.resetChannels(device.getId(), deviceChannelList); + } + + List regionList = catalogDataCatch.getRegionList(device.getDeviceId(), sn); + if ( regionList!= null && !regionList.isEmpty()) { + result &= regionService.batchAdd(regionList); + } + + List groupList = catalogDataCatch.getGroupList(device.getDeviceId(), sn); + if (groupList != null && !groupList.isEmpty()) { + result &= groupService.batchAdd(groupList); + } + return result; + } + + @Override + public void handForPlatform(RequestEvent evt, Platform parentPlatform, Element rootElement) { + + } + + public SyncStatus getChannelSyncProgress(String deviceId) { + return catalogDataCatch.getSyncStatus(deviceId); + } + + public boolean isSyncRunning(String deviceId) { + return catalogDataCatch.isSyncRunning(deviceId); + } + + public void setChannelSyncReady(Device device, int sn) { + catalogDataCatch.addReady(device, sn); + } + + public void setChannelSyncEnd(String deviceId, int sn, String errorMsg) { + catalogDataCatch.setChannelSyncEnd(deviceId, sn, errorMsg); + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/response/cmd/ConfigDownloadResponseMessageHandler.java b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/response/cmd/ConfigDownloadResponseMessageHandler.java new file mode 100755 index 0000000..da2526c --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/response/cmd/ConfigDownloadResponseMessageHandler.java @@ -0,0 +1,103 @@ +package com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.response.cmd; + +import com.alibaba.fastjson2.JSONObject; +import com.genersoft.iot.vmp.gb28181.bean.Device; +import com.genersoft.iot.vmp.gb28181.bean.Platform; +import com.genersoft.iot.vmp.gb28181.service.IDeviceService; +import com.genersoft.iot.vmp.gb28181.transmit.callback.DeferredResultHolder; +import com.genersoft.iot.vmp.gb28181.transmit.event.request.SIPRequestProcessorParent; +import com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.IMessageHandler; +import com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.response.ResponseMessageHandler; +import com.genersoft.iot.vmp.gb28181.utils.XmlUtil; +import gov.nist.javax.sip.message.SIPRequest; +import lombok.extern.slf4j.Slf4j; +import org.dom4j.Element; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import javax.sip.InvalidArgumentException; +import javax.sip.RequestEvent; +import javax.sip.SipException; +import javax.sip.message.Response; +import java.text.ParseException; + +@Slf4j +@Component +public class ConfigDownloadResponseMessageHandler extends SIPRequestProcessorParent implements InitializingBean, IMessageHandler { + + private final String cmdType = "ConfigDownload"; + + @Autowired + private ResponseMessageHandler responseMessageHandler; + + @Autowired + private DeferredResultHolder deferredResultHolder; + + @Autowired + private IDeviceService deviceService; + + @Override + public void afterPropertiesSet() throws Exception { + responseMessageHandler.addHandler(cmdType, this); + } + + + @Override + public void handForDevice(RequestEvent evt, Device device, Element element) { + try { + // 回复200 OK + responseAck((SIPRequest) evt.getRequest(), Response.OK); + } catch (SipException | InvalidArgumentException | ParseException e) { + log.error("[命令发送失败] 设备配置查询: {}", e.getMessage()); + } + // 此处是对本平台发出DeviceControl指令的应答 + JSONObject json = new JSONObject(); + XmlUtil.node2Json(element, json); + if (log.isDebugEnabled()) { + log.debug(json.toJSONString()); + } + JSONObject jsonObject = new JSONObject(); + if (json.get("BasicParam") != null) { + jsonObject.put("BasicParam", json.getJSONObject("BasicParam")); + } + if (json.get("VideoParamOpt") != null) { + jsonObject.put("VideoParamOpt", json.getJSONObject("VideoParamOpt")); + } + if (json.get("SVACEncodeConfig") != null) { + jsonObject.put("SVACEncodeConfig", json.getJSONObject("SVACEncodeConfig")); + } + if (json.get("SVACDecodeConfig") != null) { + jsonObject.put("SVACDecodeConfig", json.getJSONObject("SVACDecodeConfig")); + } + + responseMessageHandler.handMessageEvent(element, jsonObject); + + JSONObject basicParam = json.getJSONObject("BasicParam"); + if (basicParam != null) { + Integer heartBeatInterval = basicParam.getInteger("HeartBeatInterval"); + Integer heartBeatCount = basicParam.getInteger("HeartBeatCount"); + Integer positionCapability = basicParam.getInteger("PositionCapability"); + + // 只在值不为 null 时才设置,避免覆盖默认值 + if (heartBeatInterval != null) { + device.setHeartBeatInterval(heartBeatInterval); + } + if (heartBeatCount != null) { + device.setHeartBeatCount(heartBeatCount); + } + if (positionCapability != null) { + device.setPositionCapability(positionCapability); + } + + deviceService.updateDeviceHeartInfo(device); + } + + } + + @Override + public void handForPlatform(RequestEvent evt, Platform parentPlatform, Element element) { + // 不会收到上级平台的心跳信息 + + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/response/cmd/DeviceInfoResponseMessageHandler.java b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/response/cmd/DeviceInfoResponseMessageHandler.java new file mode 100755 index 0000000..24e7f58 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/response/cmd/DeviceInfoResponseMessageHandler.java @@ -0,0 +1,95 @@ +package com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.response.cmd; + +import com.genersoft.iot.vmp.gb28181.bean.Device; +import com.genersoft.iot.vmp.gb28181.bean.Platform; +import com.genersoft.iot.vmp.gb28181.service.IDeviceService; +import com.genersoft.iot.vmp.gb28181.transmit.event.request.SIPRequestProcessorParent; +import com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.IMessageHandler; +import com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.response.ResponseMessageHandler; +import gov.nist.javax.sip.message.SIPRequest; +import lombok.extern.slf4j.Slf4j; +import org.dom4j.DocumentException; +import org.dom4j.Element; +import org.springframework.beans.factory.InitializingBean; +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.SipException; +import javax.sip.message.Response; +import java.text.ParseException; + +import static com.genersoft.iot.vmp.gb28181.utils.XmlUtil.getText; + +/** + * @author lin + */ +@Slf4j +@Component +public class DeviceInfoResponseMessageHandler extends SIPRequestProcessorParent implements InitializingBean, IMessageHandler { + + private final String cmdType = "DeviceInfo"; + + @Autowired + private ResponseMessageHandler responseMessageHandler; + + + @Autowired + private IDeviceService deviceService; + + @Override + public void afterPropertiesSet() throws Exception { + responseMessageHandler.addHandler(cmdType, this); + } + + @Override + public void handForDevice(RequestEvent evt, Device device, Element rootElement) { + log.debug("接收到DeviceInfo应答消息"); + // 检查设备是否存在, 不存在则不回复 + if (device == null || !device.isOnLine()) { + log.warn("[接收到DeviceInfo应答消息,但是设备已经离线]:" + (device != null ? device.getDeviceId():"" )); + return; + } + SIPRequest request = (SIPRequest) evt.getRequest(); + try { + rootElement = getRootElement(evt, device.getCharset()); + + if (rootElement == null) { + log.warn("[ 接收到DeviceInfo应答消息 ] content cannot be null, {}", evt.getRequest()); + try { + responseAck((SIPRequest) evt.getRequest(), Response.BAD_REQUEST); + } catch (SipException | InvalidArgumentException | ParseException e) { + log.error("[命令发送失败] DeviceInfo应答消息 BAD_REQUEST: {}", e.getMessage()); + } + return; + } + device.setName(getText(rootElement, "DeviceName")); + + device.setManufacturer(getText(rootElement, "Manufacturer")); + device.setModel(getText(rootElement, "Model")); + device.setFirmware(getText(rootElement, "Firmware")); + if (ObjectUtils.isEmpty(device.getStreamMode())) { + device.setStreamMode("TCP-PASSIVE"); + } + deviceService.updateDevice(device); + responseMessageHandler.handMessageEvent(rootElement, device); + + } catch (DocumentException e) { + throw new RuntimeException(e); + } + try { + // 回复200 OK + responseAck(request, Response.OK); + } catch (SipException | InvalidArgumentException | ParseException e) { + log.error("[命令发送失败] DeviceInfo应答消息 200: {}", e.getMessage()); + } + + } + + @Override + public void handForPlatform(RequestEvent evt, Platform parentPlatform, Element rootElement) { + + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/response/cmd/DeviceStatusResponseMessageHandler.java b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/response/cmd/DeviceStatusResponseMessageHandler.java new file mode 100755 index 0000000..affd22f --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/response/cmd/DeviceStatusResponseMessageHandler.java @@ -0,0 +1,70 @@ +package com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.response.cmd; + +import com.alibaba.fastjson2.JSONObject; +import com.genersoft.iot.vmp.gb28181.bean.Device; +import com.genersoft.iot.vmp.gb28181.bean.Platform; +import com.genersoft.iot.vmp.gb28181.service.IDeviceService; +import com.genersoft.iot.vmp.gb28181.transmit.event.request.SIPRequestProcessorParent; +import com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.IMessageHandler; +import com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.response.ResponseMessageHandler; +import com.genersoft.iot.vmp.gb28181.utils.XmlUtil; +import gov.nist.javax.sip.message.SIPRequest; +import lombok.extern.slf4j.Slf4j; +import org.dom4j.Element; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import javax.sip.InvalidArgumentException; +import javax.sip.RequestEvent; +import javax.sip.SipException; +import javax.sip.message.Response; +import java.text.ParseException; + +@Slf4j +@Component +public class DeviceStatusResponseMessageHandler extends SIPRequestProcessorParent implements InitializingBean, IMessageHandler { + + private final String cmdType = "DeviceStatus"; + + @Autowired + private ResponseMessageHandler responseMessageHandler; + + @Autowired + private IDeviceService deviceService; + + @Override + public void afterPropertiesSet() throws Exception { + responseMessageHandler.addHandler(cmdType, this); + } + + @Override + public void handForDevice(RequestEvent evt, Device device, Element element) { + log.info("接收到DeviceStatus应答消息"); + // 检查设备是否存在, 不存在则不回复 + if (device == null) { + return; + } + // 回复200 OK + try { + responseAck((SIPRequest) evt.getRequest(), Response.OK); + } catch (SipException | InvalidArgumentException | ParseException e) { + log.error("[命令发送失败] 国标级联 设备状态应答回复200OK: {}", e.getMessage()); + } + Element onlineElement = element.element("Online"); + JSONObject json = new JSONObject(); + XmlUtil.node2Json(element, json); + if (log.isDebugEnabled()) { + log.debug(json.toJSONString()); + } + String text = onlineElement.getText(); + responseMessageHandler.handMessageEvent(element, text); + + } + + @Override + public void handForPlatform(RequestEvent evt, Platform parentPlatform, Element rootElement) { + + + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/response/cmd/MobilePositionResponseMessageHandler.java b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/response/cmd/MobilePositionResponseMessageHandler.java new file mode 100755 index 0000000..cc72c02 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/response/cmd/MobilePositionResponseMessageHandler.java @@ -0,0 +1,141 @@ +package com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.response.cmd; + +import com.genersoft.iot.vmp.gb28181.bean.Device; +import com.genersoft.iot.vmp.gb28181.bean.DeviceChannel; +import com.genersoft.iot.vmp.gb28181.bean.MobilePosition; +import com.genersoft.iot.vmp.gb28181.bean.Platform; +import com.genersoft.iot.vmp.gb28181.transmit.callback.DeferredResultHolder; +import com.genersoft.iot.vmp.gb28181.transmit.callback.RequestMessage; +import com.genersoft.iot.vmp.gb28181.transmit.event.request.SIPRequestProcessorParent; +import com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.IMessageHandler; +import com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.response.ResponseMessageHandler; +import com.genersoft.iot.vmp.gb28181.utils.NumericUtil; +import com.genersoft.iot.vmp.gb28181.utils.SipUtils; +import com.genersoft.iot.vmp.gb28181.service.IDeviceChannelService; +import com.genersoft.iot.vmp.utils.DateUtil; +import gov.nist.javax.sip.message.SIPRequest; +import lombok.extern.slf4j.Slf4j; +import org.dom4j.DocumentException; +import org.dom4j.Element; +import org.springframework.beans.factory.InitializingBean; +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.SipException; +import javax.sip.message.Response; +import java.text.ParseException; + +import static com.genersoft.iot.vmp.gb28181.utils.XmlUtil.getText; + +/** + * 移动设备位置数据查询回复 + * @author lin + */ +@Slf4j +@Component +public class MobilePositionResponseMessageHandler extends SIPRequestProcessorParent implements InitializingBean, IMessageHandler { + + private final String cmdType = "MobilePosition"; + + @Autowired + private ResponseMessageHandler responseMessageHandler; + + @Autowired + private IDeviceChannelService deviceChannelService; + + @Autowired + private DeferredResultHolder resultHolder; + + @Override + public void afterPropertiesSet() throws Exception { + responseMessageHandler.addHandler(cmdType, this); + } + + @Override + public void handForDevice(RequestEvent evt, Device device, Element rootElement) { + SIPRequest request = (SIPRequest) evt.getRequest(); + + try { + rootElement = getRootElement(evt, device.getCharset()); + if (rootElement == null) { + log.warn("[ 移动设备位置数据查询回复 ] content cannot be null, {}", evt.getRequest()); + try { + responseAck(request, Response.BAD_REQUEST); + } catch (SipException | InvalidArgumentException | ParseException e) { + log.error("[命令发送失败] 移动设备位置数据查询 BAD_REQUEST: {}", e.getMessage()); + } + return; + } + String channelId = getText(rootElement, "DeviceID"); + DeviceChannel deviceChannel = deviceChannelService.getOne(device.getDeviceId(), channelId); + if (deviceChannel == null) { + log.warn("[解析报警消息] 未找到通道:{}/{}", device.getDeviceId(), channelId); + }else { + MobilePosition mobilePosition = new MobilePosition(); + mobilePosition.setCreateTime(DateUtil.getNow()); + if (!ObjectUtils.isEmpty(device.getName())) { + mobilePosition.setDeviceName(device.getName()); + } + mobilePosition.setDeviceId(device.getDeviceId()); + mobilePosition.setChannelId(deviceChannel.getId()); + mobilePosition.setChannelDeviceId(deviceChannel.getDeviceId()); + //兼容ISO 8601格式时间 + String time = getText(rootElement, "Time"); + if (ObjectUtils.isEmpty(time)){ + mobilePosition.setTime(DateUtil.getNow()); + }else { + mobilePosition.setTime(SipUtils.parseTime(time)); + } + mobilePosition.setLongitude(Double.parseDouble(getText(rootElement, "Longitude"))); + mobilePosition.setLatitude(Double.parseDouble(getText(rootElement, "Latitude"))); + if (NumericUtil.isDouble(getText(rootElement, "Speed"))) { + mobilePosition.setSpeed(Double.parseDouble(getText(rootElement, "Speed"))); + } else { + mobilePosition.setSpeed(0.0); + } + if (NumericUtil.isDouble(getText(rootElement, "Direction"))) { + mobilePosition.setDirection(Double.parseDouble(getText(rootElement, "Direction"))); + } else { + mobilePosition.setDirection(0.0); + } + if (NumericUtil.isDouble(getText(rootElement, "Altitude"))) { + mobilePosition.setAltitude(Double.parseDouble(getText(rootElement, "Altitude"))); + } else { + mobilePosition.setAltitude(0.0); + } + mobilePosition.setReportSource("Mobile Position"); + + // 更新device channel 的经纬度 + deviceChannel.setLongitude(mobilePosition.getLongitude()); + deviceChannel.setLatitude(mobilePosition.getLatitude()); + deviceChannel.setGpsTime(mobilePosition.getTime()); + + deviceChannelService.updateChannelGPS(device, deviceChannel, mobilePosition); + + String key = DeferredResultHolder.CALLBACK_CMD_MOBILE_POSITION + device.getDeviceId(); + RequestMessage msg = new RequestMessage(); + msg.setKey(key); + msg.setData(mobilePosition); + resultHolder.invokeAllResult(msg); + } + + //回复 200 OK + try { + responseAck(request, Response.OK); + } catch (SipException | InvalidArgumentException | ParseException e) { + log.error("[命令发送失败] 移动设备位置数据查询 200: {}", e.getMessage()); + } + + } catch (DocumentException e) { + log.error("未处理的异常 ", e); + } + } + + @Override + public void handForPlatform(RequestEvent evt, Platform parentPlatform, Element element) { + + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/response/cmd/PresetQueryResponseMessageHandler.java b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/response/cmd/PresetQueryResponseMessageHandler.java new file mode 100755 index 0000000..7984e69 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/response/cmd/PresetQueryResponseMessageHandler.java @@ -0,0 +1,178 @@ +package com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.response.cmd; + +import com.genersoft.iot.vmp.gb28181.bean.Device; +import com.genersoft.iot.vmp.gb28181.bean.MessageResponseTask; +import com.genersoft.iot.vmp.gb28181.bean.Platform; +import com.genersoft.iot.vmp.gb28181.bean.Preset; +import com.genersoft.iot.vmp.gb28181.transmit.event.request.SIPRequestProcessorParent; +import com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.IMessageHandler; +import com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.response.ResponseMessageHandler; +import gov.nist.javax.sip.message.SIPRequest; +import lombok.extern.slf4j.Slf4j; +import org.dom4j.DocumentException; +import org.dom4j.Element; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +import javax.sip.InvalidArgumentException; +import javax.sip.RequestEvent; +import javax.sip.SipException; +import javax.sip.message.Response; +import java.text.ParseException; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.DelayQueue; +import java.util.concurrent.TimeUnit; + +import static com.genersoft.iot.vmp.gb28181.utils.XmlUtil.getText; + +/** + * 设备预置位查询应答 + */ +@Slf4j +@Component +public class PresetQueryResponseMessageHandler extends SIPRequestProcessorParent implements InitializingBean, IMessageHandler { + + private final String cmdType = "PresetQuery"; + + @Autowired + private ResponseMessageHandler responseMessageHandler; + + private final Map> mesageMap = new ConcurrentHashMap<>(); + + private final DelayQueue> delayQueue = new DelayQueue<>(); + + + @Override + public void afterPropertiesSet() throws Exception { + responseMessageHandler.addHandler(cmdType, this); + } + + @Override + public void handForDevice(RequestEvent evt, Device device, Element element) { + + SIPRequest request = (SIPRequest) evt.getRequest(); + + try { + Element rootElement = getRootElement(evt, device.getCharset()); + + if (rootElement == null) { + log.warn("[ 设备预置位查询应答 ] content cannot be null, {}", evt.getRequest()); + try { + responseAck(request, Response.BAD_REQUEST); + } catch (InvalidArgumentException | ParseException | SipException e) { + log.error("[命令发送失败] 设备预置位查询应答处理: {}", e.getMessage()); + } + return; + } + Element presetListNumElement = rootElement.element("PresetList"); + Element snElement = rootElement.element("SN"); + //该字段可能为通道或则设备的id + if (snElement == null || presetListNumElement == null) { + try { + responseAck(request, Response.BAD_REQUEST, "xml error"); + } catch (InvalidArgumentException | ParseException | SipException e) { + log.error("[命令发送失败] 设备预置位查询应答处理: {}", e.getMessage()); + } + return; + } + int num = Integer.parseInt(presetListNumElement.attributeValue("Num")); + List presetQuerySipReqList = new ArrayList<>(); + if (num > 0) { + for (Iterator presetIterator = presetListNumElement.elementIterator(); presetIterator.hasNext(); ) { + Element itemListElement = presetIterator.next(); + Preset presetQuerySipReq = new Preset(); + for (Iterator itemListIterator = itemListElement.elementIterator(); itemListIterator.hasNext(); ) { + // 遍历item + Element itemOne = itemListIterator.next(); + String name = itemOne.getName(); + String textTrim = itemOne.getTextTrim(); + if ("PresetID".equalsIgnoreCase(name)) { + presetQuerySipReq.setPresetId(textTrim); + } else { + presetQuerySipReq.setPresetName(textTrim); + } + } + presetQuerySipReqList.add(presetQuerySipReq); + } + } + String sn = getText(element, "SN"); + addCatch(cmdType + "_" + sn, num, rootElement, presetQuerySipReqList); + try { + responseAck(request, Response.OK); + } catch (InvalidArgumentException | ParseException | SipException e) { + log.error("[命令发送失败] 设备预置位查询应答处理: {}", e.getMessage()); + } + } catch (DocumentException e) { + log.error("[解析xml]失败: ", e); + } + } + + private void addCatch(String key, int sumNum, Element rootElement, List presetQuerySipReqList) { + if (presetQuerySipReqList.size() == sumNum) { + responseMessageHandler.handMessageEvent(rootElement, presetQuerySipReqList); + if (mesageMap.containsKey(key)) { + MessageResponseTask messageResponseTask = mesageMap.get(key); + mesageMap.remove(key); + boolean remove = delayQueue.remove(messageResponseTask); + if (!remove) { + log.info("[移除预置位查询任务] 从延时队列内移除失败: {}", key); + } + } + }else { + if (mesageMap.containsKey(key)) { + MessageResponseTask messageResponseTask = mesageMap.get(key); + List data = messageResponseTask.getData(); + data.addAll(presetQuerySipReqList); + if (data.size() == sumNum) { + responseMessageHandler.handMessageEvent(rootElement, data); + mesageMap.remove(key); + boolean remove = delayQueue.remove(messageResponseTask); + if (!remove) { + log.info("[移除预置位查询任务] 从延时队列内移除失败: {}", key); + } + return; + } + messageResponseTask.setDelayTime(System.currentTimeMillis() + 1000); + }else { + MessageResponseTask messageResponseTask = new MessageResponseTask<>(); + messageResponseTask.setElement(rootElement); + messageResponseTask.setData(presetQuerySipReqList); + messageResponseTask.setDelayTime(System.currentTimeMillis() + 1000); + messageResponseTask.setKey(key); + mesageMap.put(key, messageResponseTask); + delayQueue.offer(messageResponseTask); + } + } + } + + // 处理过期的缓存 + @Scheduled(fixedDelay = 500, timeUnit = TimeUnit.MILLISECONDS) + public void expirationCheck(){ + while (!delayQueue.isEmpty()) { + MessageResponseTask take = null; + try { + take = delayQueue.take(); + try { + responseMessageHandler.handMessageEvent(take.getElement(), take.getData()); + mesageMap.remove(take.getKey()); + }catch (Exception e) { + log.error("[预置位查询到期] {} 到期处理时出现异常", take.getKey()); + } + } catch (InterruptedException e) { + log.error("[设备订阅任务] ", e); + } + } + } + + @Override + public void handForPlatform(RequestEvent evt, Platform parentPlatform, Element rootElement) { + + } + +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/response/cmd/RecordInfoResponseMessageHandler.java b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/response/cmd/RecordInfoResponseMessageHandler.java new file mode 100755 index 0000000..0ab97e8 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/response/cmd/RecordInfoResponseMessageHandler.java @@ -0,0 +1,191 @@ +package com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.response.cmd; + +import com.genersoft.iot.vmp.common.VideoManagerConstants; +import com.genersoft.iot.vmp.gb28181.bean.Device; +import com.genersoft.iot.vmp.gb28181.bean.Platform; +import com.genersoft.iot.vmp.gb28181.bean.RecordInfo; +import com.genersoft.iot.vmp.gb28181.bean.RecordItem; +import com.genersoft.iot.vmp.gb28181.event.EventPublisher; +import com.genersoft.iot.vmp.gb28181.event.record.RecordInfoEndEvent; +import com.genersoft.iot.vmp.gb28181.event.record.RecordInfoEvent; +import com.genersoft.iot.vmp.gb28181.transmit.callback.DeferredResultHolder; +import com.genersoft.iot.vmp.gb28181.transmit.callback.RequestMessage; +import com.genersoft.iot.vmp.gb28181.transmit.event.request.SIPRequestProcessorParent; +import com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.IMessageHandler; +import com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.response.ResponseMessageHandler; +import com.genersoft.iot.vmp.utils.DateUtil; +import com.genersoft.iot.vmp.utils.UJson; +import gov.nist.javax.sip.message.SIPRequest; +import lombok.extern.slf4j.Slf4j; +import org.dom4j.Element; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; +import org.springframework.stereotype.Component; +import org.springframework.util.ObjectUtils; + +import javax.sip.InvalidArgumentException; +import javax.sip.RequestEvent; +import javax.sip.SipException; +import javax.sip.message.Response; +import java.text.ParseException; +import java.util.*; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +import static com.genersoft.iot.vmp.gb28181.utils.XmlUtil.getText; + +/** + * @author lin + */ +@Slf4j +@Component +public class RecordInfoResponseMessageHandler extends SIPRequestProcessorParent implements InitializingBean, IMessageHandler { + + private final String cmdType = "RecordInfo"; + + @Autowired + private ResponseMessageHandler responseMessageHandler; + + @Autowired + private ApplicationEventPublisher applicationEventPublisher; + + @Autowired + private RedisTemplate redisTemplate; + + private Long recordInfoTtl = 1800L; + + @Override + public void afterPropertiesSet() throws Exception { + responseMessageHandler.addHandler(cmdType, this); + } + + @Override + public void handForDevice(RequestEvent evt, Device device, Element rootElement) { + try { + // 回复200 OK + responseAck((SIPRequest) evt.getRequest(), Response.OK); + }catch (SipException | InvalidArgumentException | ParseException e) { + log.error("[命令发送失败] 国标级联 国标录像: {}", e.getMessage()); + } + try { + String sn = getText(rootElement, "SN"); + String channelId = getText(rootElement, "DeviceID"); + RecordInfo recordInfo = new RecordInfo(); + recordInfo.setChannelId(channelId); + recordInfo.setDeviceId(device.getDeviceId()); + recordInfo.setSn(sn); + recordInfo.setName(getText(rootElement, "Name")); + String sumNumStr = getText(rootElement, "SumNum"); + int sumNum = 0; + if (!ObjectUtils.isEmpty(sumNumStr)) { + sumNum = Integer.parseInt(sumNumStr); + } + recordInfo.setSumNum(sumNum); + Element recordListElement = rootElement.element("RecordList"); + if (recordListElement == null || sumNum == 0) { + log.info("无录像数据"); + recordInfo.setCount(sumNum); + recordInfoEventPush(recordInfo); + recordInfoEndEventPush(recordInfo); + } else { + Iterator recordListIterator = recordListElement.elementIterator(); + if (recordListIterator != null) { + List recordList = new ArrayList<>(); + // 遍历DeviceList + while (recordListIterator.hasNext()) { + Element itemRecord = recordListIterator.next(); + Element recordElement = itemRecord.element("DeviceID"); + if (recordElement == null) { + log.info("记录为空,下一个..."); + continue; + } + RecordItem record = new RecordItem(); + record.setDeviceId(getText(itemRecord, "DeviceID")); + record.setName(getText(itemRecord, "Name")); + record.setFilePath(getText(itemRecord, "FilePath")); + record.setFileSize(getText(itemRecord, "FileSize")); + record.setAddress(getText(itemRecord, "Address")); + + String startTimeStr = getText(itemRecord, "StartTime"); + record.setStartTime(DateUtil.ISO8601Toyyyy_MM_dd_HH_mm_ss(startTimeStr)); + + String endTimeStr = getText(itemRecord, "EndTime"); + record.setEndTime(DateUtil.ISO8601Toyyyy_MM_dd_HH_mm_ss(endTimeStr)); + + record.setSecrecy(itemRecord.element("Secrecy") == null ? 0 + : Integer.parseInt(getText(itemRecord, "Secrecy"))); + record.setType(getText(itemRecord, "Type")); + record.setRecorderId(getText(itemRecord, "RecorderID")); + recordList.add(record); + } + Map map = recordList.stream() + .filter(record -> record.getDeviceId() != null) + .collect(Collectors.toMap(record -> record.getStartTime()+ record.getEndTime(), UJson::writeJson)); + // 获取任务结果数据 + String resKey = VideoManagerConstants.REDIS_RECORD_INFO_RES_PRE + channelId + sn; + redisTemplate.opsForHash().putAll(resKey, map); + redisTemplate.expire(resKey, recordInfoTtl, TimeUnit.SECONDS); + String resCountKey = VideoManagerConstants.REDIS_RECORD_INFO_RES_COUNT_PRE + channelId + sn; + Long incr = redisTemplate.opsForValue().increment(resCountKey, map.size()); + if (incr == null) { + incr = 0L; + } + redisTemplate.expire(resCountKey, recordInfoTtl, TimeUnit.SECONDS); + recordInfo.setRecordList(recordList); + recordInfo.setCount(Math.toIntExact(incr)); + recordInfoEventPush(recordInfo); + if (incr < sumNum) { + return; + } + // 已接收完成 + List resList = redisTemplate.opsForHash().entries(resKey).values().stream().map(e -> UJson.readJson(e.toString(), RecordItem.class)).collect(Collectors.toList()); + if (resList.size() < sumNum) { + return; + } + recordInfo.setRecordList(resList); + recordInfoEndEventPush(recordInfo); + } + } + } catch (Exception e) { + log.error("[国标录像] 发现未处理的异常, \r\n{}", evt.getRequest()); + log.error("[国标录像] 异常内容: ", e); + } + } + + @Override + public void handForPlatform(RequestEvent evt, Platform parentPlatform, Element element) { + + } + + private void recordInfoEventPush(RecordInfo recordInfo) { + if (recordInfo == null) { + return; + } + if(recordInfo.getRecordList() != null) { + Collections.sort(recordInfo.getRecordList()); + }else{ + recordInfo.setRecordList(new ArrayList<>()); + } + RecordInfoEvent outEvent = new RecordInfoEvent(this); + outEvent.setRecordInfo(recordInfo); + applicationEventPublisher.publishEvent(outEvent); + } + + private void recordInfoEndEventPush(RecordInfo recordInfo) { + if (recordInfo == null) { + return; + } + if(recordInfo.getRecordList() != null) { + Collections.sort(recordInfo.getRecordList()); + }else{ + recordInfo.setRecordList(new ArrayList<>()); + } + RecordInfoEndEvent outEvent = new RecordInfoEndEvent(this); + outEvent.setRecordInfo(recordInfo); + applicationEventPublisher.publishEvent(outEvent); + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/response/ISIPResponseProcessor.java b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/response/ISIPResponseProcessor.java new file mode 100755 index 0000000..ffa93f2 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/response/ISIPResponseProcessor.java @@ -0,0 +1,18 @@ +package com.genersoft.iot.vmp.gb28181.transmit.event.response; + +import org.springframework.scheduling.annotation.Async; + +import javax.sip.ResponseEvent; + +/** + * @description:处理接收IPCamera发来的SIP协议响应消息 + * @author: swwheihei + * @date: 2020年5月3日 下午4:42:22 + */ +public interface ISIPResponseProcessor { + + + void process(ResponseEvent evt); + + +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/response/SIPResponseProcessorAbstract.java b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/response/SIPResponseProcessorAbstract.java new file mode 100755 index 0000000..9f2a10b --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/response/SIPResponseProcessorAbstract.java @@ -0,0 +1,8 @@ +package com.genersoft.iot.vmp.gb28181.transmit.event.response; + +import org.springframework.beans.factory.InitializingBean; + +public abstract class SIPResponseProcessorAbstract implements InitializingBean, ISIPResponseProcessor { + + +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/response/impl/ByeResponseProcessor.java b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/response/impl/ByeResponseProcessor.java new file mode 100755 index 0000000..17c928d --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/response/impl/ByeResponseProcessor.java @@ -0,0 +1,39 @@ +package com.genersoft.iot.vmp.gb28181.transmit.event.response.impl; + +import com.genersoft.iot.vmp.gb28181.transmit.SIPProcessorObserver; +import com.genersoft.iot.vmp.gb28181.transmit.event.response.SIPResponseProcessorAbstract; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import javax.sip.ResponseEvent; + +/** + * @description: BYE请求响应器 + * @author: swwheihei + * @date: 2020年5月3日 下午5:32:05 + */ +@Component +public class ByeResponseProcessor extends SIPResponseProcessorAbstract { + + private final String method = "BYE"; + + @Autowired + private SIPProcessorObserver sipProcessorObserver; + + @Override + public void afterPropertiesSet() throws Exception { + // 添加消息处理的订阅 + sipProcessorObserver.addResponseProcessor(method, this); + } + /** + * 处理BYE响应 + * + * @param evt + */ + @Override + public void process(ResponseEvent evt) { + // TODO Auto-generated method stub + } + + +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/response/impl/CancelResponseProcessor.java b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/response/impl/CancelResponseProcessor.java new file mode 100755 index 0000000..a5f80d8 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/response/impl/CancelResponseProcessor.java @@ -0,0 +1,39 @@ +package com.genersoft.iot.vmp.gb28181.transmit.event.response.impl; + +import com.genersoft.iot.vmp.gb28181.transmit.SIPProcessorObserver; +import com.genersoft.iot.vmp.gb28181.transmit.event.response.SIPResponseProcessorAbstract; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import javax.sip.ResponseEvent; + +/** + * @description: CANCEL响应处理器 + * @author: panlinlin + * @date: 2021年11月5日 16:35 + */ +@Component +public class CancelResponseProcessor extends SIPResponseProcessorAbstract { + + private final String method = "CANCEL"; + + @Autowired + private SIPProcessorObserver sipProcessorObserver; + + @Override + public void afterPropertiesSet() throws Exception { + // 添加消息处理的订阅 + sipProcessorObserver.addResponseProcessor(method, this); + } + /** + * 处理CANCEL响应 + * + * @param evt + */ + @Override + public void process(ResponseEvent evt) { + // TODO Auto-generated method stub + + } + +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/response/impl/InviteResponseProcessor.java b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/response/impl/InviteResponseProcessor.java new file mode 100755 index 0000000..82c5375 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/response/impl/InviteResponseProcessor.java @@ -0,0 +1,90 @@ +package com.genersoft.iot.vmp.gb28181.transmit.event.response.impl; + +import com.genersoft.iot.vmp.gb28181.bean.Gb28181Sdp; +import com.genersoft.iot.vmp.gb28181.transmit.SIPProcessorObserver; +import com.genersoft.iot.vmp.gb28181.transmit.SIPSender; +import com.genersoft.iot.vmp.gb28181.transmit.cmd.SIPRequestHeaderProvider; +import com.genersoft.iot.vmp.gb28181.transmit.event.response.SIPResponseProcessorAbstract; +import com.genersoft.iot.vmp.gb28181.utils.SipUtils; +import com.genersoft.iot.vmp.utils.IpPortUtil; +import gov.nist.javax.sip.ResponseEventExt; +import gov.nist.javax.sip.message.SIPResponse; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import javax.sdp.SdpParseException; +import javax.sdp.SessionDescription; +import javax.sip.InvalidArgumentException; +import javax.sip.ResponseEvent; +import javax.sip.SipException; +import javax.sip.SipFactory; +import javax.sip.address.SipURI; +import javax.sip.message.Request; +import javax.sip.message.Response; +import java.text.ParseException; + + +/** + * @description: 处理INVITE响应 + * @author: panlinlin + * @date: 2021年11月5日 16:40 + */ +@Slf4j +@Component +public class InviteResponseProcessor extends SIPResponseProcessorAbstract { + + private final String method = "INVITE"; + + @Autowired + private SIPProcessorObserver sipProcessorObserver; + + @Autowired + private SIPSender sipSender; + + @Autowired + private SIPRequestHeaderProvider headerProvider; + + @Override + public void afterPropertiesSet() throws Exception { + // 添加消息处理的订阅 + sipProcessorObserver.addResponseProcessor(method, this); + } + + + + /** + * 处理invite响应 + * + * @param evt 响应消息 + * @throws ParseException + */ + @Override + public void process(ResponseEvent evt ){ + log.debug("接收到消息:" + evt.getResponse()); + try { + SIPResponse response = (SIPResponse)evt.getResponse(); + int statusCode = response.getStatusCode(); + // trying不会回复 + if (statusCode == Response.TRYING) { + } + // 成功响应 + // 下发ack + if (statusCode == Response.OK) { + ResponseEventExt event = (ResponseEventExt)evt; + + String contentString = new String(response.getRawContent()); + Gb28181Sdp gb28181Sdp = SipUtils.parseSDP(contentString); + SessionDescription sdp = gb28181Sdp.getBaseSdb(); + SipURI requestUri = SipFactory.getInstance().createAddressFactory().createSipURI(sdp.getOrigin().getUsername(), IpPortUtil.concatenateIpAndPort(event.getRemoteIpAddress(), String.valueOf(event.getRemotePort()))); + Request reqAck = headerProvider.createAckRequest(response.getLocalAddress().getHostAddress(), requestUri, response); + + log.info("[回复ack] {}-> {}:{} ", sdp.getOrigin().getUsername(), event.getRemoteIpAddress(), event.getRemotePort()); + sipSender.transmitRequest( response.getLocalAddress().getHostAddress(), reqAck); + } + } catch (InvalidArgumentException | ParseException | SipException | SdpParseException e) { + log.info("[点播回复ACK],异常:", e ); + } + } + +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/response/impl/RegisterResponseProcessor.java b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/response/impl/RegisterResponseProcessor.java new file mode 100755 index 0000000..1f73370 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/response/impl/RegisterResponseProcessor.java @@ -0,0 +1,99 @@ +package com.genersoft.iot.vmp.gb28181.transmit.event.response.impl; + +import com.genersoft.iot.vmp.gb28181.bean.Platform; +import com.genersoft.iot.vmp.gb28181.bean.SipTransactionInfo; +import com.genersoft.iot.vmp.gb28181.event.SipSubscribe; +import com.genersoft.iot.vmp.gb28181.event.sip.SipEvent; +import com.genersoft.iot.vmp.gb28181.service.IPlatformService; +import com.genersoft.iot.vmp.gb28181.transmit.SIPProcessorObserver; +import com.genersoft.iot.vmp.gb28181.transmit.cmd.ISIPCommanderForPlatform; +import com.genersoft.iot.vmp.gb28181.transmit.event.response.SIPResponseProcessorAbstract; +import com.genersoft.iot.vmp.storager.IRedisCatchStorage; +import gov.nist.javax.sip.message.SIPResponse; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import javax.sip.InvalidArgumentException; +import javax.sip.ResponseEvent; +import javax.sip.SipException; +import javax.sip.header.WWWAuthenticateHeader; +import javax.sip.message.Response; +import java.text.ParseException; + +/** + * @description:Register响应处理器 + * @author: swwheihei + * @date: 2020年5月3日 下午5:32:23 + */ +@Slf4j +@Component +public class RegisterResponseProcessor extends SIPResponseProcessorAbstract { + + private final String method = "REGISTER"; + + @Autowired + private ISIPCommanderForPlatform sipCommanderForPlatform; + + @Autowired + private IRedisCatchStorage redisCatchStorage; + + @Autowired + private SIPProcessorObserver sipProcessorObserver; + + @Autowired + private IPlatformService platformService; + + @Autowired + private SipSubscribe sipSubscribe; + + @Override + public void afterPropertiesSet() throws Exception { + // 添加消息处理的订阅 + sipProcessorObserver.addResponseProcessor(method, this); + } + + /** + * 处理Register响应 + * + * @param evt 事件 + */ + @Override + public void process(ResponseEvent evt) { + SIPResponse response = (SIPResponse)evt.getResponse(); + String callId = response.getCallIdHeader().getCallId(); + long seqNumber = response.getCSeqHeader().getSeqNumber(); + SipEvent subscribe = sipSubscribe.getSubscribe(callId + seqNumber); + if (subscribe == null || subscribe.getSipTransactionInfo() == null || subscribe.getSipTransactionInfo().getUser() == null) { + return; + } + + String action = subscribe.getSipTransactionInfo().getExpires() > 0 ? "注册" : "注销"; + String platFormServerGbId = subscribe.getSipTransactionInfo().getUser(); + + log.info("[国标级联]{} {}响应 {} ", action, response.getStatusCode(), platFormServerGbId); + Platform platform = platformService.queryPlatformByServerGBId(platFormServerGbId); + if (platform == null) { + log.warn("[国标级联]收到 来自{}的 {} 回复 {}, 但是平台信息未查询到!!!", platFormServerGbId, action, response.getStatusCode()); + return; + } + + if (response.getStatusCode() == Response.UNAUTHORIZED) { + WWWAuthenticateHeader www = (WWWAuthenticateHeader)response.getHeader(WWWAuthenticateHeader.NAME); + SipTransactionInfo sipTransactionInfo = new SipTransactionInfo(response); + try { + sipCommanderForPlatform.register(platform, sipTransactionInfo, www, null, null, subscribe.getSipTransactionInfo().getExpires() > 0); + } catch (SipException | InvalidArgumentException | ParseException e) { + log.error("[命令发送失败] 国标级联 再次注册: {}", e.getMessage()); + } + }else if (response.getStatusCode() == Response.OK){ + if (subscribe.getSipTransactionInfo().getExpires() > 0) { + SipTransactionInfo sipTransactionInfo = new SipTransactionInfo(response); + platformService.online(platform, sipTransactionInfo); + }else { + platformService.offline(platform); + } + } + } + +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/utils/Coordtransform.java b/src/main/java/com/genersoft/iot/vmp/gb28181/utils/Coordtransform.java new file mode 100644 index 0000000..5c12ff6 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/utils/Coordtransform.java @@ -0,0 +1,126 @@ +package com.genersoft.iot.vmp.gb28181.utils; + +/** + * 坐标转换 + * 一个提供了百度坐标(BD09)、国测局坐标(火星坐标,GCJ02)、和WGS84坐标系之间的转换的工具类 + * 参考https://github.com/wandergis/coordtransform 写的Java版本 + * @author Xinconan + * @date 2016-03-18 + * @url https://github.com/xinconan/coordtransform + */ +public class Coordtransform { + + private static double x_PI = 3.14159265358979324 * 3000.0 / 180.0; + private static double PI = 3.1415926535897932384626; + private static double a = 6378245.0; + private static double ee = 0.00669342162296594323; + + /** + * 百度坐标系 (BD-09) 与 火星坐标系 (GCJ-02)的转换 + * 即 百度 转 谷歌、高德 + * @param bd_lon + * @param bd_lat + * @return Double[lon,lat] + */ + public static Double[] BD09ToGCJ02(Double bd_lon,Double bd_lat){ + double x = bd_lon - 0.0065; + double y = bd_lat - 0.006; + double z = Math.sqrt(x * x + y * y) - 0.00002 * Math.sin(y * x_PI); + double theta = Math.atan2(y, x) - 0.000003 * Math.cos(x * x_PI); + Double[] arr = new Double[2]; + arr[0] = z * Math.cos(theta); + arr[1] = z * Math.sin(theta); + return arr; + } + + /** + * 火星坐标系 (GCJ-02) 与百度坐标系 (BD-09) 的转换 + * 即谷歌、高德 转 百度 + * @param gcj_lon + * @param gcj_lat + * @return Double[lon,lat] + */ + public static Double[] GCJ02ToBD09(Double gcj_lon,Double gcj_lat){ + double z = Math.sqrt(gcj_lon * gcj_lon + gcj_lat * gcj_lat) + 0.00002 * Math.sin(gcj_lat * x_PI); + double theta = Math.atan2(gcj_lat, gcj_lon) + 0.000003 * Math.cos(gcj_lon * x_PI); + Double[] arr = new Double[2]; + arr[0] = z * Math.cos(theta) + 0.0065; + arr[1] = z * Math.sin(theta) + 0.006; + return arr; + } + + /** + * WGS84转GCJ02 + * @param wgs_lon + * @param wgs_lat + * @return Double[lon,lat] + */ + public static Double[] WGS84ToGCJ02(Double wgs_lon,Double wgs_lat){ + if(outOfChina(wgs_lon, wgs_lat)){ + return new Double[]{wgs_lon,wgs_lat}; + } + double dlat = transformlat(wgs_lon - 105.0, wgs_lat - 35.0); + double dlng = transformlng(wgs_lon - 105.0, wgs_lat - 35.0); + double radlat = wgs_lat / 180.0 * PI; + double magic = Math.sin(radlat); + magic = 1 - ee * magic * magic; + double sqrtmagic = Math.sqrt(magic); + dlat = (dlat * 180.0) / ((a * (1 - ee)) / (magic * sqrtmagic) * PI); + dlng = (dlng * 180.0) / (a / sqrtmagic * Math.cos(radlat) * PI); + Double[] arr = new Double[2]; + arr[0] = wgs_lon + dlng; + arr[1] = wgs_lat + dlat; + return arr; + } + + /** + * GCJ02转WGS84 + * @param gcj_lon + * @param gcj_lat + * @return Double[lon,lat] + */ + public static Double[] GCJ02ToWGS84(Double gcj_lon,Double gcj_lat){ + if(outOfChina(gcj_lon, gcj_lat)){ + return new Double[]{gcj_lon,gcj_lat}; + } + double dlat = transformlat(gcj_lon - 105.0, gcj_lat - 35.0); + double dlng = transformlng(gcj_lon - 105.0, gcj_lat - 35.0); + double radlat = gcj_lat / 180.0 * PI; + double magic = Math.sin(radlat); + magic = 1 - ee * magic * magic; + double sqrtmagic = Math.sqrt(magic); + dlat = (dlat * 180.0) / ((a * (1 - ee)) / (magic * sqrtmagic) * PI); + dlng = (dlng * 180.0) / (a / sqrtmagic * Math.cos(radlat) * PI); + double mglat = gcj_lat + dlat; + double mglng = gcj_lon + dlng; + return new Double[]{gcj_lon * 2 - mglng, gcj_lat * 2 - mglat}; + } + + private static Double transformlat(double lng, double lat) { + double ret = -100.0 + 2.0 * lng + 3.0 * lat + 0.2 * lat * lat + 0.1 * lng * lat + 0.2 * Math.sqrt(Math.abs(lng)); + ret += (20.0 * Math.sin(6.0 * lng * PI) + 20.0 * Math.sin(2.0 * lng * PI)) * 2.0 / 3.0; + ret += (20.0 * Math.sin(lat * PI) + 40.0 * Math.sin(lat / 3.0 * PI)) * 2.0 / 3.0; + ret += (160.0 * Math.sin(lat / 12.0 * PI) + 320 * Math.sin(lat * PI / 30.0)) * 2.0 / 3.0; + return ret; + } + + private static Double transformlng(double lng,double lat) { + double ret = 300.0 + lng + 2.0 * lat + 0.1 * lng * lng + 0.1 * lng * lat + 0.1 * Math.sqrt(Math.abs(lng)); + ret += (20.0 * Math.sin(6.0 * lng * PI) + 20.0 * Math.sin(2.0 * lng * PI)) * 2.0 / 3.0; + ret += (20.0 * Math.sin(lng * PI) + 40.0 * Math.sin(lng / 3.0 * PI)) * 2.0 / 3.0; + ret += (150.0 * Math.sin(lng / 12.0 * PI) + 300.0 * Math.sin(lng / 30.0 * PI)) * 2.0 / 3.0; + return ret; + } + + /** + * outOfChina + * @描述: 判断是否在国内,不在国内则不做偏移 + * @param lng + * @param lat + * @return {boolean} + */ + private static boolean outOfChina(Double lng,Double lat) { + return (lng < 72.004 || lng > 137.8347) || ((lat < 0.8293 || lat > 55.8271) || false); + }; + +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/utils/MessageElement.java b/src/main/java/com/genersoft/iot/vmp/gb28181/utils/MessageElement.java new file mode 100644 index 0000000..f94237c --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/utils/MessageElement.java @@ -0,0 +1,17 @@ +package com.genersoft.iot.vmp.gb28181.utils; + +import java.lang.annotation.*; + +/** + * @author gaofuwang + * @version 1.0 + * @date 2022/6/28 14:58 + */ +@Target({ElementType.FIELD}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface MessageElement { + String value(); + + String subVal() default ""; +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/utils/MessageElementForCatalog.java b/src/main/java/com/genersoft/iot/vmp/gb28181/utils/MessageElementForCatalog.java new file mode 100644 index 0000000..7abd7b3 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/utils/MessageElementForCatalog.java @@ -0,0 +1,17 @@ +package com.genersoft.iot.vmp.gb28181.utils; + +import java.lang.annotation.*; + +/** + * @author gaofuwang + * @version 1.0 + * @date 2022/6/28 14:58 + */ +@Target({ElementType.FIELD}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface MessageElementForCatalog { + String[] value(); + + String subVal() default ""; +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/utils/NumericUtil.java b/src/main/java/com/genersoft/iot/vmp/gb28181/utils/NumericUtil.java new file mode 100644 index 0000000..f2b84b2 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/utils/NumericUtil.java @@ -0,0 +1,40 @@ +package com.genersoft.iot.vmp.gb28181.utils; + +/** + * 数值格式判断和处理 + * @author lawrencehj + * @date 2021年1月27日 + */ +public class NumericUtil { + /** + * 判断是否Double格式 + * @param str + * @return true/false + */ + public static boolean isDouble(String str) { + try { + Double num2 = Double.valueOf(str); +// logger.debug(num2 + " is a valid numeric string!"); + return true; + } catch (Exception e) { +// logger.debug(str + " is an invalid numeric string!"); + return false; + } + } + + /** + * 判断是否Double格式 + * @param str + * @return true/false + */ + public static boolean isInteger(String str) { + try { + int num2 = Integer.valueOf(str); +// logger.debug(num2 + " is an integer!"); + return true; + } catch (Exception e) { +// logger.debug(str + " is not an integer!"); + return false; + } + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/utils/SipUtils.java b/src/main/java/com/genersoft/iot/vmp/gb28181/utils/SipUtils.java new file mode 100644 index 0000000..90a7e2a --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/utils/SipUtils.java @@ -0,0 +1,267 @@ +package com.genersoft.iot.vmp.gb28181.utils; + +import com.genersoft.iot.vmp.gb28181.bean.Gb28181Sdp; +import com.genersoft.iot.vmp.common.RemoteAddressInfo; +import com.genersoft.iot.vmp.utils.DateUtil; +import com.genersoft.iot.vmp.utils.GitUtil; +import gov.nist.javax.sip.address.AddressImpl; +import gov.nist.javax.sip.address.SipUri; +import gov.nist.javax.sip.header.Subject; +import gov.nist.javax.sip.message.SIPRequest; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.RandomStringUtils; +import org.springframework.util.ObjectUtils; + +import javax.sdp.SdpFactory; +import javax.sdp.SdpParseException; +import javax.sdp.SessionDescription; +import javax.sip.PeerUnavailableException; +import javax.sip.SipFactory; +import javax.sip.header.FromHeader; +import javax.sip.header.SubjectHeader; +import javax.sip.header.UserAgentHeader; +import javax.sip.message.Request; +import java.text.ParseException; +import java.time.LocalDateTime; +import java.time.format.DateTimeParseException; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +/** + * @author panlinlin + * @version 1.0.0 + * @description JAIN SIP的工具类 + * @createTime 2021年09月27日 15:12:00 + */ +@Slf4j +public class SipUtils { + + public static String getUserIdFromFromHeader(Request request) { + FromHeader fromHeader = (FromHeader)request.getHeader(FromHeader.NAME); + return getUserIdFromFromHeader(fromHeader); + } + /** + * 从subject读取channelId + * */ + public static String[] getChannelIdFromRequest(Request request) { + SubjectHeader subject = (Subject)request.getHeader("subject"); + if (subject == null) { + // 如果缺失subject + return null; + } + String[] result = new String[2]; + String subjectStr = subject.getSubject(); + if (subjectStr.indexOf(",") > 0) { + String[] subjectSplit = subjectStr.split(","); + result[0] = subjectSplit[0].split(":")[0]; + result[1] = subjectSplit[1].split(":")[0]; + }else { + result[0] = subjectStr.split(":")[0]; + } + return result; + } + + public static String getUserIdFromFromHeader(FromHeader fromHeader) { + AddressImpl address = (AddressImpl)fromHeader.getAddress(); + SipUri uri = (SipUri) address.getURI(); + return uri.getUser(); + } + + public static String getNewViaTag() { + return "z9hG4bK" + RandomStringUtils.randomNumeric(10); + } + + public static UserAgentHeader createUserAgentHeader(GitUtil gitUtil) throws PeerUnavailableException, ParseException { + List agentParam = new ArrayList<>(); + agentParam.add("WVP-Pro "); + if (gitUtil != null ) { + if (!ObjectUtils.isEmpty(gitUtil.getBuildVersion())) { + agentParam.add("v"); + agentParam.add(gitUtil.getBuildVersion() + "."); + } + if (!ObjectUtils.isEmpty(gitUtil.getCommitTime())) { + agentParam.add(gitUtil.getCommitTime()); + } + } + return SipFactory.getInstance().createHeaderFactory().createUserAgentHeader(agentParam); + } + + public static String getNewFromTag(){ + return UUID.randomUUID().toString().replace("-", ""); + +// return getNewTag(); + } + + public static String getNewTag(){ + return String.valueOf(System.currentTimeMillis()); + } + + + /** + * 云台指令码计算 + * + * @param leftRight 镜头左移右移 0:停止 1:左移 2:右移 + * @param upDown 镜头上移下移 0:停止 1:上移 2:下移 + * @param inOut 镜头放大缩小 0:停止 1:缩小 2:放大 + * @param moveSpeed 镜头移动速度 默认 0XFF (0-255) + * @param zoomSpeed 镜头缩放速度 默认 0X1 (0-255) + */ + public static String cmdString(int leftRight, int upDown, int inOut, int moveSpeed, int zoomSpeed) { + int cmdCode = 0; + if (leftRight == 2) { + cmdCode|=0x01; // 右移 + } else if(leftRight == 1) { + cmdCode|=0x02; // 左移 + } + if (upDown == 2) { + cmdCode|=0x04; // 下移 + } else if(upDown == 1) { + cmdCode|=0x08; // 上移 + } + if (inOut == 2) { + cmdCode |= 0x10; // 放大 + } else if(inOut == 1) { + cmdCode |= 0x20; // 缩小 + } + StringBuilder builder = new StringBuilder("A50F01"); + String strTmp; + strTmp = String.format("%02X", cmdCode); + builder.append(strTmp, 0, 2); + strTmp = String.format("%02X", moveSpeed); + builder.append(strTmp, 0, 2); + builder.append(strTmp, 0, 2); + + //优化zoom低倍速下的变倍速率 + if ((zoomSpeed > 0) && (zoomSpeed <16)) + { + zoomSpeed = 16; + } + strTmp = String.format("%X", zoomSpeed); + builder.append(strTmp, 0, 1).append("0"); + //计算校验码 + int checkCode = (0XA5 + 0X0F + 0X01 + cmdCode + moveSpeed + moveSpeed + (zoomSpeed /*<< 4*/ & 0XF0)) % 0X100; + strTmp = String.format("%02X", checkCode); + builder.append(strTmp, 0, 2); + return builder.toString(); + } + + public static String getNewCallId() { + return (int) Math.floor(Math.random() * 1000000000) + ""; + } + + public static int getTypeCodeFromGbCode(String deviceId) { + if (ObjectUtils.isEmpty(deviceId)) { + return 0; + } + return Integer.parseInt(deviceId.substring(10, 13)); + } + + /** + * 判断是否是前端外围设备 + * @param deviceId + * @return + */ + public static boolean isFrontEnd(String deviceId) { + int typeCodeFromGbCode = getTypeCodeFromGbCode(deviceId); + return typeCodeFromGbCode > 130 && typeCodeFromGbCode < 199; + } + /** + * 从请求中获取设备ip地址和端口号 + * @param request 请求 + * @param sipUseSourceIpAsRemoteAddress false 从via中获取地址, true 直接获取远程地址 + * @return 地址信息 + */ + public static RemoteAddressInfo getRemoteAddressFromRequest(SIPRequest request, boolean sipUseSourceIpAsRemoteAddress) { + + String remoteAddress; + int remotePort; + if (sipUseSourceIpAsRemoteAddress) { + remoteAddress = request.getPeerPacketSourceAddress().getHostAddress(); + remotePort = request.getPeerPacketSourcePort(); + + }else { + // 判断RPort是否改变,改变则说明路由nat信息变化,修改设备信息 + // 获取到通信地址等信息 + remoteAddress = request.getTopmostViaHeader().getReceived(); + remotePort = request.getTopmostViaHeader().getRPort(); + // 解析本地地址替代 + if (ObjectUtils.isEmpty(remoteAddress) || remotePort == -1) { + if (request.getPeerPacketSourceAddress() != null) { + remoteAddress = request.getPeerPacketSourceAddress().getHostAddress(); + }else { + remoteAddress = request.getRemoteAddress().getHostAddress(); + } + if( request.getPeerPacketSourcePort() > 0) { + remotePort = request.getPeerPacketSourcePort(); + }else { + remotePort = request.getRemotePort(); + } + } + } + + return new RemoteAddressInfo(remoteAddress, remotePort); + } + + public static Gb28181Sdp parseSDP(String sdpStr) throws SdpParseException { + + // jainSip不支持y= f=字段, 移除以解析。 + int ssrcIndex = sdpStr.indexOf("y="); + int mediaDescriptionIndex = sdpStr.indexOf("f="); + // 检查是否有y字段 + SessionDescription sdp; + String ssrc = null; + String mediaDescription = null; + if (mediaDescriptionIndex == 0 && ssrcIndex == 0) { + sdp = SdpFactory.getInstance().createSessionDescription(sdpStr); + }else { + String lines[] = sdpStr.split("\\r?\\n"); + StringBuilder sdpBuffer = new StringBuilder(); + for (String line : lines) { + if (line.trim().startsWith("y=")) { + ssrc = line.substring(2); + }else if (line.trim().startsWith("f=")) { + mediaDescription = line.substring(2); + }else { + sdpBuffer.append(line.trim()).append("\r\n"); + } + } + sdp = SdpFactory.getInstance().createSessionDescription(sdpBuffer.toString()); + } + return Gb28181Sdp.getInstance(sdp, ssrc, mediaDescription); + } + + public static String getSsrcFromSdp(String sdpStr) { + + // jainSip不支持y= f=字段, 移除以解析。 + int ssrcIndex = sdpStr.indexOf("y="); + if (ssrcIndex == 0) { + return null; + } + String lines[] = sdpStr.split("\\r?\\n"); + for (String line : lines) { + if (line.trim().startsWith("y=")) { + return line.substring(2); + } + } + return null; + } + + public static String parseTime(String timeStr) { + if (ObjectUtils.isEmpty(timeStr)){ + return null; + } + LocalDateTime localDateTime; + try { + localDateTime = LocalDateTime.parse(timeStr); + }catch (DateTimeParseException e) { + try { + localDateTime = LocalDateTime.parse(timeStr, DateUtil.formatterISO8601); + }catch (DateTimeParseException e2) { + log.error("[格式化时间] 无法格式化时间: {}", timeStr); + return null; + } + } + return localDateTime.format(DateUtil.formatter); + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/utils/VectorTileCatch.java b/src/main/java/com/genersoft/iot/vmp/gb28181/utils/VectorTileCatch.java new file mode 100644 index 0000000..88c27de --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/utils/VectorTileCatch.java @@ -0,0 +1,95 @@ +package com.genersoft.iot.vmp.gb28181.utils; + +import com.genersoft.iot.vmp.gb28181.bean.CommonGBChannel; +import com.genersoft.iot.vmp.gb28181.bean.VectorTileSource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; +import org.springframework.util.ConcurrentReferenceHashMap; + +import java.util.List; +import java.util.Map; +import java.util.concurrent.DelayQueue; +import java.util.concurrent.TimeUnit; + +@Slf4j +@Component +public class VectorTileCatch { + + private final Map vectorTileMap = new ConcurrentReferenceHashMap<>(); + private final DelayQueue delayQueue = new DelayQueue<>(); + + public void addVectorTile(String id, String key, byte[] content) { + VectorTileSource vectorTileSource = vectorTileMap.get(id); + if (vectorTileSource == null) { + vectorTileSource = new VectorTileSource(); + vectorTileSource.setId(id); + vectorTileMap.put(id, vectorTileSource); + delayQueue.offer(vectorTileSource); + } + + vectorTileSource.getVectorTileMap().put(key, content); + } + + public byte[] getVectorTile(String id, String key) { + if (!vectorTileMap.containsKey(id)) { + return null; + } + return vectorTileMap.get(id).getVectorTileMap().get(key); + } + + public void addSource(String id, List channelList) { + VectorTileSource vectorTileSource = vectorTileMap.get(id); + if (vectorTileSource == null) { + vectorTileSource = new VectorTileSource(); + vectorTileSource.setId(id); + vectorTileMap.put(id, vectorTileSource); + delayQueue.offer(vectorTileSource); + } + vectorTileMap.get(id).getChannelList().addAll(channelList); + } + + + public void remove(String id) { + VectorTileSource vectorTileSource = vectorTileMap.get(id); + if (vectorTileSource != null) { + delayQueue.remove(vectorTileSource); + } + vectorTileMap.remove(id); + } + + public List getChannelList(String id) { + if (!vectorTileMap.containsKey(id)) { + return null; + } + return vectorTileMap.get(id).getChannelList(); + } + + public void save(String id) { + if (!vectorTileMap.containsKey(id)) { + return; + } + VectorTileSource vectorTileSource = vectorTileMap.get(id); + if (vectorTileSource == null) { + return; + } + vectorTileMap.remove(id); + delayQueue.remove(vectorTileSource); + vectorTileMap.put("DEFAULT", vectorTileSource); + } + + + // 缓存数据过期检查 + @Scheduled(fixedDelay = 30, timeUnit = TimeUnit.MINUTES) + public void expirationCheck(){ + while (!delayQueue.isEmpty()) { + try { + VectorTileSource vectorTileSource = delayQueue.take(); + vectorTileMap.remove(vectorTileSource.getId()); + } catch (InterruptedException e) { + log.error("[清理过期的抽稀数据] ", e); + } + } + } + +} diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/utils/XmlUtil.java b/src/main/java/com/genersoft/iot/vmp/gb28181/utils/XmlUtil.java new file mode 100644 index 0000000..4656670 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/utils/XmlUtil.java @@ -0,0 +1,731 @@ +package com.genersoft.iot.vmp.gb28181.utils; + +import com.alibaba.fastjson2.JSONArray; +import com.alibaba.fastjson2.JSONObject; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.math.NumberUtils; +import org.dom4j.Attribute; +import org.dom4j.Document; +import org.dom4j.DocumentException; +import org.dom4j.Element; +import org.dom4j.io.SAXReader; +import org.springframework.util.ObjectUtils; +import org.springframework.util.ReflectionUtils; + +import javax.sip.RequestEvent; +import javax.sip.message.Request; +import java.io.ByteArrayInputStream; +import java.io.StringReader; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.*; + +/** + * 基于dom4j的工具包 + */ +@Slf4j +public class XmlUtil { + + /** + * 解析XML为Document对象 + */ + public static Element parseXml(String xml) { + Document document = null; + // + StringReader sr = new StringReader(xml); + SAXReader saxReader = new SAXReader(); + try { + document = saxReader.read(sr); + } catch (DocumentException e) { + log.error("解析失败", e); + } + return null == document ? null : document.getRootElement(); + } + + /** + * 获取element对象的text的值 + * + * @param em 节点的对象 + * @param tag 节点的tag + * @return 节点 + */ + public static String getText(Element em, String tag) { + if (null == em) { + return null; + } + Element e = em.element(tag); + // + return null == e ? null : e.getText().trim(); + } + + /** + * 获取element对象的text的值 + * + * @param em 节点的对象 + * @param tag 节点的tag + * @return 节点 + */ + public static Double getDouble(Element em, String tag) { + if (null == em) { + return null; + } + Element e = em.element(tag); + if (null == e) { + return null; + } + String text = e.getText().trim(); + if (ObjectUtils.isEmpty(text) || !NumberUtils.isParsable(text)) { + return null; + } + return Double.parseDouble(text); + } + + /** + * 获取element对象的text的值 + * + * @param em 节点的对象 + * @param tag 节点的tag + * @return 节点 + */ + public static Integer getInteger(Element em, String tag) { + if (null == em) { + return null; + } + Element e = em.element(tag); + if (null == e) { + return null; + } + String text = e.getText().trim(); + if (ObjectUtils.isEmpty(text) || !NumberUtils.isParsable(text)) { + return null; + } + return Integer.parseInt(text); + } + + /** + * 递归解析xml节点,适用于 多节点数据 + * + * @param node node + * @param nodeName nodeName + * @return List> + */ + public static List> listNodes(Element node, String nodeName) { + if (null == node) { + return null; + } + // 初始化返回 + List> listMap = new ArrayList>(); + // 首先获取当前节点的所有属性节点 + List list = node.attributes(); + + Map map = null; + // 遍历属性节点 + for (Attribute attribute : list) { + if (nodeName.equals(node.getName())) { + if (null == map) { + map = new HashMap(); + listMap.add(map); + } + // 取到的节点属性放到map中 + map.put(attribute.getName(), attribute.getValue()); + } + + } + // 遍历当前节点下的所有节点 ,nodeName 要解析的节点名称 + // 使用递归 + Iterator iterator = node.elementIterator(); + while (iterator.hasNext()) { + Element e = iterator.next(); + listMap.addAll(listNodes(e, nodeName)); + } + return listMap; + } + + /** + * xml转json + * + * @param element + * @param json + */ + public static void node2Json(Element element, JSONObject json) { + // 如果是属性 + for (Object o : element.attributes()) { + Attribute attr = (Attribute) o; + if (!ObjectUtils.isEmpty(attr.getValue())) { + json.put("@" + attr.getName(), attr.getValue()); + } + } + List chdEl = element.elements(); + if (chdEl.isEmpty() && !ObjectUtils.isEmpty(element.getText())) {// 如果没有子元素,只有一个值 + json.put(element.getName(), element.getText()); + } + + for (Element e : chdEl) { // 有子元素 + if (!e.elements().isEmpty()) { // 子元素也有子元素 + JSONObject chdjson = new JSONObject(); + node2Json(e, chdjson); + Object o = json.get(e.getName()); + if (o != null) { + JSONArray jsona = null; + if (o instanceof JSONObject) { // 如果此元素已存在,则转为jsonArray + JSONObject jsono = (JSONObject) o; + json.remove(e.getName()); + jsona = new JSONArray(); + jsona.add(jsono); + jsona.add(chdjson); + } + if (o instanceof JSONArray) { + jsona = (JSONArray) o; + jsona.add(chdjson); + } + json.put(e.getName(), jsona); + } else { + if (!chdjson.isEmpty()) { + json.put(e.getName(), chdjson); + } + } + } else { // 子元素没有子元素 + for (Object o : element.attributes()) { + Attribute attr = (Attribute) o; + if (!ObjectUtils.isEmpty(attr.getValue())) { + json.put("@" + attr.getName(), attr.getValue()); + } + } + if (!e.getText().isEmpty()) { + json.put(e.getName(), e.getText()); + } + } + } + } + public static Element getRootElement(RequestEvent evt) throws DocumentException { + + return getRootElement(evt, "gb2312"); + } + + public static Element getRootElement(RequestEvent evt, String charset) throws DocumentException { + Request request = evt.getRequest(); + return getRootElement(request.getRawContent(), charset); + } + + public static Element getRootElement(byte[] content, String charset) throws DocumentException { + if (charset == null) { + charset = "gb2312"; + } + SAXReader reader = new SAXReader(); + reader.setEncoding(charset); + Document xml = reader.read(new ByteArrayInputStream(content)); + return xml.getRootElement(); + } + + private enum ChannelType{ + CivilCode, BusinessGroup,VirtualOrganization,Other + } + +// public static DeviceChannel channelContentHandler(Element itemDevice, Device device, String event){ +// DeviceChannel deviceChannel = new DeviceChannel(); +// deviceChannel.setDeviceId(device.getDeviceId()); +// Element channdelIdElement = itemDevice.element("DeviceID"); +// if (channdelIdElement == null) { +// logger.warn("解析Catalog消息时发现缺少 DeviceID"); +// return null; +// } +// String channelId = channdelIdElement.getTextTrim(); +// if (ObjectUtils.isEmpty(channelId)) { +// logger.warn("解析Catalog消息时发现缺少 DeviceID"); +// return null; +// } +// deviceChannel.setDeviceId(channelId); +// if (event != null && !event.equals(CatalogEvent.ADD) && !event.equals(CatalogEvent.UPDATE)) { +// // 除了ADD和update情况下需要识别全部内容, +// return deviceChannel; +// } +// Element nameElement = itemDevice.element("Name"); +// // 当通道名称为空时,设置通道名称为通道编码,避免级联时因通道名称为空导致上级接收通道失败 +// if (nameElement != null && StringUtils.isNotBlank(nameElement.getText())) { +// deviceChannel.setName(nameElement.getText()); +// } else { +// deviceChannel.setName(channelId); +// } +// if(channelId.length() <= 8) { +// deviceChannel.setHasAudio(false); +// CivilCodePo parentCode = CivilCodeUtil.INSTANCE.getParentCode(channelId); +// if (parentCode != null) { +// deviceChannel.setParentId(parentCode.getCode()); +// deviceChannel.setCivilCode(parentCode.getCode()); +// }else { +// logger.warn("[xml解析] 无法确定行政区划{}的上级行政区划", channelId); +// } +// deviceChannel.setStatus("ON"); +// return deviceChannel; +// }else { +// if(channelId.length() != 20) { +// logger.warn("[xml解析] 失败,编号不符合国标28181定义: {}", channelId); +// return null; +// } +// +// int code = Integer.parseInt(channelId.substring(10, 13)); +// if (code == 136 || code == 137 || code == 138) { +// deviceChannel.setHasAudio(true); +// }else { +// deviceChannel.setHasAudio(false); +// } +// // 设备厂商 +// String manufacturer = getText(itemDevice, "Manufacturer"); +// // 设备型号 +// String model = getText(itemDevice, "Model"); +// // 设备归属 +// String owner = getText(itemDevice, "Owner"); +// // 行政区域 +// String civilCode = getText(itemDevice, "CivilCode"); +// // 虚拟组织所属的业务分组ID,业务分组根据特定的业务需求制定,一个业务分组包含一组特定的虚拟组织 +// String businessGroupID = getText(itemDevice, "BusinessGroupID"); +// // 父设备/区域/系统ID +// String parentID = getText(itemDevice, "ParentID"); +// if (parentID != null && parentID.equalsIgnoreCase("null")) { +// parentID = null; +// } +// // 注册方式(必选)缺省为1;1:符合IETFRFC3261标准的认证注册模式;2:基于口令的双向认证注册模式;3:基于数字证书的双向认证注册模式 +// String registerWay = getText(itemDevice, "RegisterWay"); +// // 保密属性(必选)缺省为0;0:不涉密,1:涉密 +// String secrecy = getText(itemDevice, "Secrecy"); +// // 安装地址 +// String address = getText(itemDevice, "Address"); +// +// switch (code){ +// case 200: +// // 系统目录 +// if (!ObjectUtils.isEmpty(manufacturer)) { +// deviceChannel.setManufacture(manufacturer); +// } +// if (!ObjectUtils.isEmpty(model)) { +// deviceChannel.setModel(model); +// } +// if (!ObjectUtils.isEmpty(owner)) { +// deviceChannel.setOwner(owner); +// } +// if (!ObjectUtils.isEmpty(civilCode)) { +// deviceChannel.setCivilCode(civilCode); +// deviceChannel.setParentId(civilCode); +// }else { +// if (!ObjectUtils.isEmpty(parentID)) { +// deviceChannel.setParentId(parentID); +// } +// } +// if (!ObjectUtils.isEmpty(address)) { +// deviceChannel.setAddress(address); +// } +// deviceChannel.setStatus(true); +// if (!ObjectUtils.isEmpty(registerWay)) { +// try { +// deviceChannel.setRegisterWay(Integer.parseInt(registerWay)); +// }catch (NumberFormatException exception) { +// logger.warn("[xml解析] 从通道数据获取registerWay失败: {}", registerWay); +// } +// } +// if (!ObjectUtils.isEmpty(secrecy)) { +// deviceChannel.setSecrecy(secrecy); +// } +// return deviceChannel; +// case 215: +// // 业务分组 +// deviceChannel.setStatus(true); +// if (!ObjectUtils.isEmpty(parentID)) { +// if (!parentID.trim().equalsIgnoreCase(device.getDeviceId())) { +// deviceChannel.setParentId(parentID); +// } +// }else { +// logger.warn("[xml解析] 业务分组数据中缺少关键信息->ParentId"); +// if (!ObjectUtils.isEmpty(civilCode)) { +// deviceChannel.setCivilCode(civilCode); +// } +// } +// break; +// case 216: +// // 虚拟组织 +// deviceChannel.setStatus(true); +// if (!ObjectUtils.isEmpty(businessGroupID)) { +// deviceChannel.setBusinessGroupId(businessGroupID); +// } +// +// if (!ObjectUtils.isEmpty(parentID)) { +// if (parentID.contains("/")) { +// String[] parentIdArray = parentID.split("/"); +// parentID = parentIdArray[parentIdArray.length - 1]; +// } +// deviceChannel.setParentId(parentID); +// }else { +// if (!ObjectUtils.isEmpty(businessGroupID)) { +// deviceChannel.setParentId(businessGroupID); +// } +// } +// break; +// default: +// // 设备目录 +// if (!ObjectUtils.isEmpty(manufacturer)) { +// deviceChannel.setManufacture(manufacturer); +// } +// if (!ObjectUtils.isEmpty(model)) { +// deviceChannel.setModel(model); +// } +// if (!ObjectUtils.isEmpty(owner)) { +// deviceChannel.setOwner(owner); +// } +// if (!ObjectUtils.isEmpty(civilCode) +// && civilCode.length() <= 8 +// && NumberUtils.isParsable(civilCode) +// && civilCode.length()%2 == 0 +// ) { +// deviceChannel.setCivilCode(civilCode); +// } +// if (!ObjectUtils.isEmpty(businessGroupID)) { +// deviceChannel.setBusinessGroupId(businessGroupID); +// } +// +// // 警区 +// String block = getText(itemDevice, "Block"); +// if (!ObjectUtils.isEmpty(block)) { +// deviceChannel.setBlock(block); +// } +// if (!ObjectUtils.isEmpty(address)) { +// deviceChannel.setAddress(address); +// } +// +// if (!ObjectUtils.isEmpty(secrecy)) { +// deviceChannel.setSecrecy(secrecy); +// } +// +// // 当为设备时,是否有子设备(必选)1有,0没有 +// String parental = getText(itemDevice, "Parental"); +// if (!ObjectUtils.isEmpty(parental)) { +// try { +// // 由于海康会错误的发送65535作为这里的取值,所以这里除非是0否则认为是1 +// if (!ObjectUtils.isEmpty(parental) && parental.length() == 1 && Integer.parseInt(parental) == 0) { +// deviceChannel.setParental(0); +// }else { +// deviceChannel.setParental(1); +// } +// }catch (NumberFormatException e) { +// logger.warn("[xml解析] 从通道数据获取 parental失败: {}", parental); +// } +// } +// // 父设备/区域/系统ID +// +// if (!ObjectUtils.isEmpty(parentID) ) { +// if (parentID.contains("/")) { +// String[] parentIdArray = parentID.split("/"); +// deviceChannel.setParentId(parentIdArray[parentIdArray.length - 1]); +// }else { +// if (parentID.length()%2 == 0) { +// deviceChannel.setParentId(parentID); +// }else { +// logger.warn("[xml解析] 不规范的parentID:{}, 已舍弃", parentID); +// } +// } +// }else { +// if (!ObjectUtils.isEmpty(businessGroupID)) { +// deviceChannel.setParentId(businessGroupID); +// }else { +// if (!ObjectUtils.isEmpty(deviceChannel.getCivilCode())) { +// deviceChannel.setParentId(deviceChannel.getCivilCode()); +// } +// } +// } +// // 注册方式 +// if (!ObjectUtils.isEmpty(registerWay)) { +// try { +// int registerWayInt = Integer.parseInt(registerWay); +// deviceChannel.setRegisterWay(registerWayInt); +// }catch (NumberFormatException exception) { +// logger.warn("[xml解析] 从通道数据获取registerWay失败: {}", registerWay); +// deviceChannel.setRegisterWay(1); +// } +// }else { +// deviceChannel.setRegisterWay(1); +// } +// +// // 信令安全模式(可选)缺省为0; 0:不采用;2:S/MIME 签名方式;3:S/MIME加密签名同时采用方式;4:数字摘要方式 +// String safetyWay = getText(itemDevice, "SafetyWay"); +// if (!ObjectUtils.isEmpty(safetyWay)) { +// try { +// deviceChannel.setSafetyWay(Integer.parseInt(safetyWay)); +// }catch (NumberFormatException e) { +// logger.warn("[xml解析] 从通道数据获取 safetyWay失败: {}", safetyWay); +// } +// } +// +// // 证书序列号(有证书的设备必选) +// String certNum = getText(itemDevice, "CertNum"); +// if (!ObjectUtils.isEmpty(certNum)) { +// deviceChannel.setCertNum(certNum); +// } +// +// // 证书有效标识(有证书的设备必选)缺省为0;证书有效标识:0:无效 1:有效 +// String certifiable = getText(itemDevice, "Certifiable"); +// if (!ObjectUtils.isEmpty(certifiable)) { +// try { +// deviceChannel.setCertifiable(Integer.parseInt(certifiable)); +// }catch (NumberFormatException e) { +// logger.warn("[xml解析] 从通道数据获取 Certifiable失败: {}", certifiable); +// } +// } +// +// // 无效原因码(有证书且证书无效的设备必选) +// String errCode = getText(itemDevice, "ErrCode"); +// if (!ObjectUtils.isEmpty(errCode)) { +// try { +// deviceChannel.setErrCode(Integer.parseInt(errCode)); +// }catch (NumberFormatException e) { +// logger.warn("[xml解析] 从通道数据获取 ErrCode失败: {}", errCode); +// } +// } +// +// // 证书终止有效期(有证书的设备必选) +// String endTime = getText(itemDevice, "EndTime"); +// if (!ObjectUtils.isEmpty(endTime)) { +// deviceChannel.setEndTime(endTime); +// } +// +// +// // 设备/区域/系统IP地址 +// String ipAddress = getText(itemDevice, "IPAddress"); +// if (!ObjectUtils.isEmpty(ipAddress)) { +// deviceChannel.setIpAddress(ipAddress); +// } +// +// // 设备/区域/系统端口 +// String port = getText(itemDevice, "Port"); +// if (!ObjectUtils.isEmpty(port)) { +// try { +// deviceChannel.setPort(Integer.parseInt(port)); +// }catch (NumberFormatException e) { +// logger.warn("[xml解析] 从通道数据获取 Port失败: {}", port); +// } +// } +// +// // 设备口令 +// String password = getText(itemDevice, "Password"); +// if (!ObjectUtils.isEmpty(password)) { +// deviceChannel.setPassword(password); +// } +// +// +// // 设备状态 +// String status = getText(itemDevice, "Status"); +// if (status != null) { +// // ONLINE OFFLINE HIKVISION DS-7716N-E4 NVR的兼容性处理 +// if (status.equalsIgnoreCase("ON") || status.equalsIgnoreCase("On") || status.equalsIgnoreCase("ONLINE") || status.equalsIgnoreCase("OK")) { +// deviceChannel.setStatus(true); +// } +// if (status.equalsIgnoreCase("OFF") || status.equalsIgnoreCase("Off") || status.equalsIgnoreCase("OFFLINE")) { +// deviceChannel.setStatus(false); +// } +// }else { +// deviceChannel.setStatus(true); +// } +//// logger.info("状态字符串: {}", status); +//// logger.info("状态结果: {}", deviceChannel.isStatus()); +// // 经度 +// String longitude = getText(itemDevice, "Longitude"); +// if (NumericUtil.isDouble(longitude)) { +// deviceChannel.setLongitude(Double.parseDouble(longitude)); +// } else { +// deviceChannel.setLongitude(0.00); +// } +// +// // 纬度 +// String latitude = getText(itemDevice, "Latitude"); +// if (NumericUtil.isDouble(latitude)) { +// deviceChannel.setLatitude(Double.parseDouble(latitude)); +// } else { +// deviceChannel.setLatitude(0.00); +// } +// +// deviceChannel.setGpsTime(DateUtil.getNow()); +// +// // -摄像机类型扩展,标识摄像机类型:1-球机;2-半球;3-固定枪机;4-遥控枪机。当目录项为摄像机时可选 +// String ptzType = getText(itemDevice, "PTZType"); +// if (ObjectUtils.isEmpty(ptzType)) { +// //兼容INFO中的信息 +// Element info = itemDevice.element("Info"); +// String ptzTypeFromInfo = XmlUtil.getText(info, "PTZType"); +// if(!ObjectUtils.isEmpty(ptzTypeFromInfo)){ +// try { +// deviceChannel.setPtzType(Integer.parseInt(ptzTypeFromInfo)); +// }catch (NumberFormatException e){ +// logger.warn("[xml解析] 从通道数据info中获取PTZType失败: {}", ptzTypeFromInfo); +// } +// } +// } else { +// try { +// deviceChannel.setPtzType(Integer.parseInt(ptzType)); +// }catch (NumberFormatException e){ +// logger.warn("[xml解析] 从通道数据中获取PTZType失败: {}", ptzType); +// } +// } +// +// // TODO 摄像机位置类型扩展。 +// // 1-省际检查站、 +// // 2-党政机关、 +// // 3-车站码头、 +// // 4-中心广场、 +// // 5-体育场馆、 +// // 6-商业中心、 +// // 7-宗教场所、 +// // 8-校园周边、 +// // 9-治安复杂区域、 +// // 10-交通干线。 +// // String positionType = getText(itemDevice, "PositionType"); +// +// // TODO 摄像机安装位置室外、室内属性。1-室外、2-室内。 +// // String roomType = getText(itemDevice, "RoomType"); +// // TODO 摄像机用途属性 +// // String useType = getText(itemDevice, "UseType"); +// // TODO 摄像机补光属性。1-无补光、2-红外补光、3-白光补光 +// // String supplyLightType = getText(itemDevice, "SupplyLightType"); +// // TODO 摄像机监视方位属性。1-东、2-西、3-南、4-北、5-东南、6-东北、7-西南、8-西北。 +// // String directionType = getText(itemDevice, "DirectionType"); +// // TODO 摄像机支持的分辨率,可有多个分辨率值,各个取值间以“/”分隔。分辨率取值参见附录 F中SDPf字段规定 +// // String resolution = getText(itemDevice, "Resolution"); +// +// // TODO 下载倍速范围(可选),各可选参数以“/”分隔,如设备支持1,2,4倍速下载则应写为“1/2/4 +// // String downloadSpeed = getText(itemDevice, "DownloadSpeed"); +// // TODO 空域编码能力,取值0:不支持;1:1级增强(1个增强层);2:2级增强(2个增强层);3:3级增强(3个增强层) +// // String svcSpaceSupportMode = getText(itemDevice, "SVCSpaceSupportMode"); +// // TODO 时域编码能力,取值0:不支持;1:1级增强;2:2级增强;3:3级增强 +// // String svcTimeSupportMode = getText(itemDevice, "SVCTimeSupportMode"); +// +// +// deviceChannel.setSecrecy(secrecy); +// break; +// } +// } +// +// return deviceChannel; +// } + + /** + * 新增方法支持内部嵌套 + * + * @param element xmlElement + * @param clazz 结果类 + * @param 泛型 + * @return 结果对象 + * @throws NoSuchMethodException + * @throws InvocationTargetException + * @throws InstantiationException + * @throws IllegalAccessException + */ + public static T loadElement(Element element, Class clazz) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException { + Field[] fields = clazz.getDeclaredFields(); + T t = clazz.getDeclaredConstructor().newInstance(); + for (Field field : fields) { + ReflectionUtils.makeAccessible(field); + MessageElement annotation = field.getAnnotation(MessageElement.class); + if (annotation == null) { + continue; + } + String value = annotation.value(); + String subVal = annotation.subVal(); + Element element1 = element.element(value); + if (element1 == null) { + continue; + } + if ("".equals(subVal)) { + // 无下级数据 + Object fieldVal = element1.isTextOnly() ? element1.getText() : loadElement(element1, field.getType()); + Object o = simpleTypeDeal(field.getType(), fieldVal); + ReflectionUtils.setField(field, t, o); + } else { + // 存在下级数据 + ArrayList list = new ArrayList<>(); + Type genericType = field.getGenericType(); + if (!(genericType instanceof ParameterizedType)) { + continue; + } + Class aClass = (Class) ((ParameterizedType) genericType).getActualTypeArguments()[0]; + for (Element element2 : element1.elements(subVal)) { + list.add(loadElement(element2, aClass)); + } + ReflectionUtils.setField(field, t, list); + } + } + return t; + } + + public static T elementDecode(Element element, Class clazz) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException { + Field[] fields = clazz.getDeclaredFields(); + T t = clazz.getDeclaredConstructor().newInstance(); + for (Field field : fields) { + ReflectionUtils.makeAccessible(field); + MessageElementForCatalog annotation = field.getAnnotation(MessageElementForCatalog.class); + if (annotation == null) { + continue; + } + String[] values = annotation.value(); + for (String value : values) { + boolean subVal = value.contains("."); + if (!subVal) { + Element element1 = element.element(value); + if (element1 == null) { + continue; + } + // 无下级数据 + Object fieldVal = element1.isTextOnly() ? element1.getText() : loadElement(element1, field.getType()); + Object o = simpleTypeDeal(field.getType(), fieldVal); + ReflectionUtils.setField(field, t, o); + break; + } else { + String[] pathArray = value.split("\\."); + Element subElement = element; + for (String path : pathArray) { + subElement = subElement.element(path); + if (subElement == null) { + break; + } + } + if (subElement == null) { + continue; + } + Object fieldVal = subElement.isTextOnly() ? subElement.getText() : loadElement(subElement, field.getType()); + Object o = simpleTypeDeal(field.getType(), fieldVal); + ReflectionUtils.setField(field, t, o); + } + } + + } + return t; + } + + /** + * 简单类型处理 + * + * @param tClass + * @param val + * @return + */ + private static Object simpleTypeDeal(Class tClass, Object val) { + try { + if (val == null || val.toString().equalsIgnoreCase("null")) { + return null; + } + if (tClass.equals(String.class)) { + return val.toString(); + } + if (tClass.equals(Integer.class)) { + return Integer.valueOf(val.toString()); + } + if (tClass.equals(Double.class)) { + return Double.valueOf(val.toString()); + + } + if (tClass.equals(Long.class)) { + return Long.valueOf(val.toString()); + } + return val; + }catch (Exception e) { + return null; + } + } +} \ No newline at end of file diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/annotation/MsgId.java b/src/main/java/com/genersoft/iot/vmp/jt1078/annotation/MsgId.java new file mode 100644 index 0000000..d5c2de4 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/annotation/MsgId.java @@ -0,0 +1,15 @@ +package com.genersoft.iot.vmp.jt1078.annotation; + +import java.lang.annotation.*; + +/** + * @author QingtaiJiang + * @date 2023/4/27 18:31 + * @email qingtaij@163.com + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface MsgId { + String id(); +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/bean/JTAlarmSign.java b/src/main/java/com/genersoft/iot/vmp/jt1078/bean/JTAlarmSign.java new file mode 100644 index 0000000..82bb06e --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/bean/JTAlarmSign.java @@ -0,0 +1,264 @@ +package com.genersoft.iot.vmp.jt1078.bean; + +import com.genersoft.iot.vmp.jt1078.bean.config.JTDeviceSubConfig; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +/** + * 报警标志 + */ +@Data +@Schema(description = "报警标志") +public class JTAlarmSign implements JTDeviceSubConfig { + + @Schema(description = "紧急报警,触动报警开关后触发") + private boolean urgent; + @Schema(description = "超速报警") + private boolean alarmSpeeding; + @Schema(description = "疲劳驾驶报警") + private boolean alarmTired; + @Schema(description = "危险驾驶行为报警") + private boolean alarmDangerous; + @Schema(description = "GNSS 模块发生故障报警") + private boolean alarmGnssFault; + @Schema(description = "GNSS 天线未接或被剪断报警") + private boolean alarmGnssBreak; + @Schema(description = "GNSS 天线短路报警") + private boolean alarmGnssShortCircuited; + @Schema(description = "终端主电源欠压报警") + private boolean alarmUnderVoltage; + @Schema(description = "终端主电源掉电报警") + private boolean alarmPowerOff; + @Schema(description = "终端 LCD或显示器故障报警") + private boolean alarmLCD; + @Schema(description = "TTS 模块故障报警") + private boolean alarmTtsFault; + @Schema(description = "摄像头故障报警") + private boolean alarmCameraFault; + @Schema(description = "道路运输证 IC卡模块故障报警") + private boolean alarmIcFault; + @Schema(description = "超速预警") + private boolean warningSpeeding; + @Schema(description = "疲劳驾驶预警") + private boolean warningTired; + @Schema(description = "违规行驶报警") + private boolean alarmWrong; + @Schema(description = "胎压预警") + private boolean warningTirePressure; + @Schema(description = "右转盲区异常报警") + private boolean alarmBlindZone; + @Schema(description = "当天累计驾驶超时报警") + private boolean alarmDrivingTimeout; + @Schema(description = "超时停车报警") + private boolean alarmParkingTimeout; + @Schema(description = "进出区域报警") + private boolean alarmRegion; + @Schema(description = "进出路线报警") + private boolean alarmRoute; + @Schema(description = "路段行驶时间不足/过长报警") + private boolean alarmTravelTime; + @Schema(description = "路线偏离报警") + private boolean alarmRouteDeviation; + @Schema(description = "车辆 VSS 故障") + private boolean alarmVSS; + @Schema(description = "车辆油量异常报警") + private boolean alarmOil; + @Schema(description = "车辆被盗报警(通过车辆防盗器)") + private boolean alarmStolen; + @Schema(description = "车辆非法点火报警") + private boolean alarmIllegalIgnition; + @Schema(description = "车辆非法位移报警") + private boolean alarmIllegalDisplacement; + @Schema(description = "碰撞侧翻报警") + private boolean alarmRollover; + @Schema(description = "侧翻预警") + private boolean warningRollover; + + public JTAlarmSign() { + } + + public JTAlarmSign(long alarmSignInt) { + if (alarmSignInt == 0) { + return; + } + // 解析alarm参数 + this.urgent = (alarmSignInt & 1) == 1; + this.alarmSpeeding = (alarmSignInt >>> 1 & 1) == 1; + this.alarmTired = (alarmSignInt >>> 2 & 1) == 1; + this.alarmDangerous = (alarmSignInt >>> 3 & 1) == 1; + this.alarmGnssFault = (alarmSignInt >>> 4 & 1) == 1; + this.alarmGnssBreak = (alarmSignInt >>> 5 & 1) == 1; + this.alarmGnssShortCircuited = (alarmSignInt >>> 6 & 1) == 1; + this.alarmUnderVoltage = (alarmSignInt >>> 7 & 1) == 1; + this.alarmPowerOff = (alarmSignInt >>> 8 & 1) == 1; + this.alarmLCD = (alarmSignInt >>> 9 & 1) == 1; + this.alarmTtsFault = (alarmSignInt >>> 10 & 1) == 1; + this.alarmCameraFault = (alarmSignInt >>> 11 & 1) == 1; + this.alarmIcFault = (alarmSignInt >>> 12 & 1) == 1; + this.warningSpeeding = (alarmSignInt >>> 13 & 1) == 1; + this.warningTired = (alarmSignInt >>> 14 & 1) == 1; + this.alarmWrong = (alarmSignInt >>> 15 & 1) == 1; + this.warningTirePressure = (alarmSignInt >>> 16 & 1) == 1; + this.alarmBlindZone = (alarmSignInt >>> 17 & 1) == 1; + this.alarmDrivingTimeout = (alarmSignInt >>> 18 & 1) == 1; + this.alarmParkingTimeout = (alarmSignInt >>> 19 & 1) == 1; + this.alarmRegion = (alarmSignInt >>> 20 & 1) == 1; + this.alarmRoute = (alarmSignInt >>> 21 & 1) == 1; + this.alarmTravelTime = (alarmSignInt >>> 22 & 1) == 1; + this.alarmRouteDeviation = (alarmSignInt >>> 23 & 1) == 1; + this.alarmVSS = (alarmSignInt >>> 24 & 1) == 1; + this.alarmOil = (alarmSignInt >>> 25 & 1) == 1; + this.alarmStolen = (alarmSignInt >>> 26 & 1) == 1; + this.alarmIllegalIgnition = (alarmSignInt >>> 27 & 1) == 1; + this.alarmIllegalDisplacement = (alarmSignInt >>> 28 & 1) == 1; + this.alarmRollover = (alarmSignInt >>> 29 & 1) == 1; + this.warningRollover = (alarmSignInt >>> 30 & 1) == 1; + } + + public static JTAlarmSign decode(ByteBuf byteBuf) { + long alarmSignByte = byteBuf.readUnsignedInt(); + return new JTAlarmSign(alarmSignByte); + } + + @Override + public ByteBuf encode() { + // 限定容量 避免影响后续占位 + ByteBuf byteBuf = Unpooled.buffer(); + int alarmSignValue = 0; + if (urgent) { + alarmSignValue = alarmSignValue | 1; + } + if (alarmSpeeding) { + alarmSignValue = alarmSignValue | 1 << 1; + } + if (alarmTired) { + alarmSignValue = alarmSignValue | 1 << 2; + } + if (alarmDangerous) { + alarmSignValue = alarmSignValue | 1 << 3; + } + if (alarmGnssFault) { + alarmSignValue = alarmSignValue | 1 << 4; + } + if (alarmGnssBreak) { + alarmSignValue = alarmSignValue | 1 << 5; + } + if (alarmGnssShortCircuited) { + alarmSignValue = alarmSignValue | 1 << 6; + } + if (alarmUnderVoltage) { + alarmSignValue = alarmSignValue | 1 << 7; + } + if (alarmPowerOff) { + alarmSignValue = alarmSignValue | 1 << 8; + } + if (alarmLCD) { + alarmSignValue = alarmSignValue | 1 << 9; + } + if (alarmTtsFault) { + alarmSignValue = alarmSignValue | 1 << 10; + } + if (alarmCameraFault) { + alarmSignValue = alarmSignValue | 1 << 11; + } + if (alarmIcFault) { + alarmSignValue = alarmSignValue | 1 << 12; + } + if (warningSpeeding) { + alarmSignValue = alarmSignValue | 1 << 13; + } + if (warningTired) { + alarmSignValue = alarmSignValue | 1 << 14; + } + if (alarmWrong) { + alarmSignValue = alarmSignValue | 1 << 15; + } + if (warningTirePressure) { + alarmSignValue = alarmSignValue | 1 << 16; + } + if (alarmBlindZone) { + alarmSignValue = alarmSignValue | 1 << 17; + } + if (alarmDrivingTimeout) { + alarmSignValue = alarmSignValue | 1 << 18; + } + if (alarmParkingTimeout) { + alarmSignValue = alarmSignValue | 1 << 19; + } + if (alarmRegion) { + alarmSignValue = alarmSignValue | 1 << 20; + } + if (alarmRoute) { + alarmSignValue = alarmSignValue | 1 << 21; + } + if (alarmTravelTime) { + alarmSignValue = alarmSignValue | 1 << 22; + } + if (alarmRouteDeviation) { + alarmSignValue = alarmSignValue | 1 << 23; + } + if (alarmVSS) { + alarmSignValue = alarmSignValue | 1 << 24; + } + if (alarmOil) { + alarmSignValue = alarmSignValue | 1 << 25; + } + if (alarmStolen) { + alarmSignValue = alarmSignValue | 1 << 26; + } + if (alarmIllegalIgnition) { + alarmSignValue = alarmSignValue | 1 << 27; + } + if (alarmIllegalDisplacement) { + alarmSignValue = alarmSignValue | 1 << 28; + } + if (alarmRollover) { + alarmSignValue = alarmSignValue | 1 << 29; + } + if (warningRollover) { + alarmSignValue = alarmSignValue | 1 << 30; + } + byteBuf.writeInt(alarmSignValue); + return byteBuf; + } + + @Override + public String toString() { + return "状态报警标志位:" + + "\n 紧急报警:" + (urgent?"开":"关") + + "\n 超速报警:" + (alarmSpeeding?"开":"关") + + "\n 疲劳驾驶报警:" + (alarmTired?"开":"关") + + "\n 危险驾驶行为报警:" + (alarmDangerous?"开":"关") + + "\n GNSS 模块发生故障报警:" + (alarmGnssFault?"开":"关") + + "\n GNSS 天线未接或被剪断报警:" + (alarmGnssBreak?"开":"关") + + "\n GNSS 天线短路报警:" + (alarmGnssShortCircuited?"开":"关") + + "\n 终端主电源欠压报警:" + (alarmUnderVoltage?"开":"关") + + "\n 终端主电源掉电报警:" + (alarmPowerOff?"开":"关") + + "\n 终端LCD或显示器故障报警:" + (alarmLCD?"开":"关") + + "\n TTS 模块故障报警:" + (alarmTtsFault?"开":"关") + + "\n 摄像头故障报警:" + (alarmCameraFault?"开":"关") + + "\n 道路运输证IC卡模块故障报警:" + (alarmIcFault?"开":"关") + + "\n 超速预警:" + (warningSpeeding?"开":"关") + + "\n 疲劳驾驶预警:" + (warningTired?"开":"关") + + "\n 违规行驶报警:" + (alarmWrong ?"开":"关") + + "\n 胎压预警:" + (warningTirePressure?"开":"关") + + "\n 右转盲区异常报警:" + (alarmBlindZone?"开":"关") + + "\n 当天累计驾驶超时报警:" + (alarmDrivingTimeout?"开":"关") + + "\n 超时停车报警:" + (alarmParkingTimeout?"开":"关") + + "\n 进出区域报警:" + (alarmRegion?"开":"关") + + "\n 进出路线报警:" + (alarmRoute?"开":"关") + + "\n 路段行驶时间不足/过长报警:" + (alarmTravelTime?"开":"关") + + "\n 路线偏离报警:" + (alarmRouteDeviation?"开":"关") + + "\n 车辆 VSS 故障:" + (alarmVSS?"开":"关") + + "\n 车辆油量异常报警:" + (alarmOil?"开":"关") + + "\n 车辆被盗报警(通过车辆防盗器):" + (alarmStolen?"开":"关") + + "\n 车辆非法点火报警:" + (alarmIllegalIgnition?"开":"关") + + "\n 车辆非法位移报警:" + (alarmIllegalDisplacement?"开":"关") + + "\n 碰撞侧翻报警:" + (alarmRollover?"开":"关") + + "\n 侧翻预警:" + (warningRollover?"开":"关") + + "\n "; + } + +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/bean/JTAreaAttribute.java b/src/main/java/com/genersoft/iot/vmp/jt1078/bean/JTAreaAttribute.java new file mode 100644 index 0000000..b21e2d2 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/bean/JTAreaAttribute.java @@ -0,0 +1,103 @@ +package com.genersoft.iot.vmp.jt1078.bean; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; +import lombok.Setter; + +@Setter +@Getter +@Schema(description = "区域属性") +public class JTAreaAttribute { + + @Schema(description = "是否启用起始时间与结束时间的判断规则 ,false:否;true:是") + private boolean ruleForTimeLimit; + + @Schema(description = "是否启用最高速度、超速持续时间和夜间最高速度的判断规则 ,false:否;true:是") + private boolean ruleForSpeedLimit; + + @Schema(description = "进区域是否报警给驾驶员,false:否;true:是") + private boolean ruleForAlarmToDriverWhenEnter; + + @Schema(description = "进区域是否报警给平台 ,false:否;true:是") + private boolean ruleForAlarmToPlatformWhenEnter; + + @Schema(description = "出区域是否报警给驾驶员,false:否;true:是") + private boolean ruleForAlarmToDriverWhenExit; + + @Schema(description = "出区域是否报警给平台 ,false:否;true:是") + private boolean ruleForAlarmToPlatformWhenExit; + + @Schema(description = "false:北纬;true:南纬") + private boolean southLatitude; + + @Schema(description = "false:东经;true:西经") + private boolean westLongitude; + + @Schema(description = "false:允许开门;true:禁止开门") + private boolean prohibitOpeningDoors; + + @Schema(description = "false:进区域开启通信模块;true:进区域关闭通信模块") + private boolean ruleForTurnOffCommunicationWhenEnter; + + @Schema(description = "false:进区域不采集 GNSS 详细定位数据;true:进区域采集 GNSS 详细定位数据") + private boolean ruleForGnssWhenEnter; + + public ByteBuf encode(){ + ByteBuf byteBuf = Unpooled.buffer(); + short content = 0 ; + if (ruleForTimeLimit) { + content |= 1; + } + if (ruleForSpeedLimit) { + content |= (1 << 1); + } + if (ruleForAlarmToDriverWhenEnter) { + content |= (1 << 2); + } + if (ruleForAlarmToPlatformWhenEnter) { + content |= (1 << 3); + } + if (ruleForAlarmToDriverWhenExit) { + content |= (1 << 4); + } + if (ruleForAlarmToPlatformWhenExit) { + content |= (1 << 5); + } + if (southLatitude) { + content |= (1 << 6); + } + if (westLongitude) { + content |= (byte) (1 << 7); + } + if (prohibitOpeningDoors) { + content |= (1 << (0 + 8)); + } + if (ruleForTurnOffCommunicationWhenEnter) { + content |= (1 << (1 + 8)); + } + if (ruleForGnssWhenEnter) { + content |= (1 << (2 + 8)); + } + byteBuf.writeShort((short)(content & 0xffff)); + return byteBuf; + } + + public static JTAreaAttribute decode(int attributeInt) { + JTAreaAttribute attribute = new JTAreaAttribute(); + attribute.setRuleForTimeLimit((attributeInt & 1) == 1); + attribute.setRuleForSpeedLimit((attributeInt >> 1 & 1) == 1); + attribute.setRuleForAlarmToDriverWhenEnter((attributeInt >> 2 & 1) == 1); + attribute.setRuleForAlarmToPlatformWhenEnter((attributeInt >> 3 & 1) == 1); + attribute.setRuleForAlarmToDriverWhenExit((attributeInt >> 4 & 1) == 1); + attribute.setRuleForAlarmToPlatformWhenExit((attributeInt >> 5 & 1) == 1); + attribute.setSouthLatitude((attributeInt >> 6 & 1) == 1); + attribute.setWestLongitude((attributeInt >> 7 & 1) == 1); + attribute.setProhibitOpeningDoors((attributeInt >> 8 & 1) == 1); + attribute.setRuleForTurnOffCommunicationWhenEnter((attributeInt >> 9 & 1) == 1); + attribute.setRuleForGnssWhenEnter((attributeInt >> 10 & 1) == 1); + return attribute; + } + +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/bean/JTAreaOrRoute.java b/src/main/java/com/genersoft/iot/vmp/jt1078/bean/JTAreaOrRoute.java new file mode 100644 index 0000000..0ad0218 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/bean/JTAreaOrRoute.java @@ -0,0 +1,4 @@ +package com.genersoft.iot.vmp.jt1078.bean; + +public interface JTAreaOrRoute { +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/bean/JTChannel.java b/src/main/java/com/genersoft/iot/vmp/jt1078/bean/JTChannel.java new file mode 100644 index 0000000..2d4e022 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/bean/JTChannel.java @@ -0,0 +1,67 @@ +package com.genersoft.iot.vmp.jt1078.bean; + +import com.genersoft.iot.vmp.common.enums.ChannelDataType; +import com.genersoft.iot.vmp.gb28181.bean.CommonGBChannel; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.springframework.util.ObjectUtils; + +/** + * JT 通道 + */ +@Data +@Schema(description = "jt808通道") +@EqualsAndHashCode(callSuper = true) +public class JTChannel extends CommonGBChannel { + + @Schema(description = "数据库自增ID") + private int id; + + @Schema(description = "名称") + private String name; + + @Schema(description = "设备的数据库ID") + private int terminalDbId; + + @Schema(description = "通道ID") + private Integer channelId; + + @Schema(description = "是否含有音频") + private boolean hasAudio; + + @Schema(description = "创建时间") + private String createTime; + + @Schema(description = "更新时间") + private String updateTime; + + @Schema(description = "流信息") + private String stream; + + private Integer dataType = ChannelDataType.JT_1078; + + @Override + public String toString() { + return "JTChannel{" + + "id=" + id + + ", name='" + name + '\'' + + ", terminalDbId=" + terminalDbId + + ", channelId=" + channelId + + ", createTime='" + createTime + '\'' + + ", updateTime='" + updateTime + '\'' + + ", hasAudio='" + hasAudio + '\'' + + '}'; + } + + public CommonGBChannel buildCommonGBChannel() { + if (ObjectUtils.isEmpty(this.getGbDeviceId())) { + return null; + } + if (ObjectUtils.isEmpty(this.getGbName())) { + this.setGbName(this.getName()); + } + return this; + + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/bean/JTCircleArea.java b/src/main/java/com/genersoft/iot/vmp/jt1078/bean/JTCircleArea.java new file mode 100644 index 0000000..331ce68 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/bean/JTCircleArea.java @@ -0,0 +1,94 @@ +package com.genersoft.iot.vmp.jt1078.bean; + +import com.genersoft.iot.vmp.jt1078.util.BCDUtil; +import com.genersoft.iot.vmp.utils.DateUtil; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; +import lombok.Setter; + +import java.nio.charset.Charset; +import java.util.Date; + +@Setter +@Getter +@Schema(description = "圆形区域") +public class JTCircleArea implements JTAreaOrRoute{ + + @Schema(description = "区域 ID") + private long id; + + @Schema(description = "") + private JTAreaAttribute attribute; + + @Schema(description = "中心点纬度") + private Double latitude; + + @Schema(description = "中心点经度") + private Double longitude; + + @Schema(description = "半径,单位为米(m)") + private long radius; + + @Schema(description = "起始时间, yyyy-MM-dd HH:mm:ss") + private String startTime; + + @Schema(description = "结束时间, yyyy-MM-dd HH:mm:ss") + private String endTime; + + @Schema(description = "最高速度, 单位为千米每小时(km/h)") + private int maxSpeed; + + @Schema(description = "超速持续时间, 单位为秒(s)") + private int overSpeedDuration; + + @Schema(description = "夜间最高速度, 单位为千米每小时(km/h)") + private int nighttimeMaxSpeed; + + @Schema(description = "区域的名称") + private String name; + + public ByteBuf encode(){ + ByteBuf byteBuf = Unpooled.buffer(); + byteBuf.writeInt((int) (id & 0xffffffffL)); + byteBuf.writeBytes(attribute.encode()); + byteBuf.writeInt((int) (Math.round((latitude * 1000000)) & 0xffffffffL)); + byteBuf.writeInt((int) (Math.round((longitude * 1000000)) & 0xffffffffL)); + byteBuf.writeInt((int) (radius & 0xffffffffL)); + byteBuf.writeBytes(BCDUtil.strToBcd(DateUtil.yyyy_MM_dd_HH_mm_ssTo1078(startTime))); + byteBuf.writeBytes(BCDUtil.strToBcd(DateUtil.yyyy_MM_dd_HH_mm_ssTo1078(endTime))); + byteBuf.writeShort((short)(maxSpeed & 0xffff)); + byteBuf.writeByte(overSpeedDuration); + byteBuf.writeShort((short)(nighttimeMaxSpeed & 0xffff)); + byteBuf.writeShort((short)(name.getBytes(Charset.forName("GBK")).length & 0xffff)); + byteBuf.writeCharSequence(name, Charset.forName("GBK")); + return byteBuf; + } + + public static JTCircleArea decode(ByteBuf buf) { + + JTCircleArea area = new JTCircleArea(); + area.setId(buf.readUnsignedInt()); + int attributeInt = buf.readUnsignedShort(); + JTAreaAttribute areaAttribute = JTAreaAttribute.decode(attributeInt); + area.setAttribute(areaAttribute); + + area.setLatitude(buf.readUnsignedInt()/1000000D); + area.setLongitude(buf.readUnsignedInt()/1000000D); + area.setRadius(buf.readUnsignedInt()); + byte[] startTimeBytes = new byte[6]; + buf.readBytes(startTimeBytes); + area.setStartTime(DateUtil.jt1078Toyyyy_MM_dd_HH_mm_ss(BCDUtil.transform(startTimeBytes))); + byte[] endTimeBytes = new byte[6]; + buf.readBytes(endTimeBytes); + area.setEndTime(DateUtil.jt1078Toyyyy_MM_dd_HH_mm_ss(BCDUtil.transform(endTimeBytes))); + area.setMaxSpeed(buf.readUnsignedShort()); + area.setOverSpeedDuration(buf.readUnsignedByte()); + area.setNighttimeMaxSpeed(buf.readUnsignedShort()); + int nameLength = buf.readUnsignedShort(); + area.setName(buf.readCharSequence(nameLength, Charset.forName("GBK")).toString().trim()); + return area; + } + +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/bean/JTCommunicationModuleAttribute.java b/src/main/java/com/genersoft/iot/vmp/jt1078/bean/JTCommunicationModuleAttribute.java new file mode 100644 index 0000000..795da1a --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/bean/JTCommunicationModuleAttribute.java @@ -0,0 +1,57 @@ +package com.genersoft.iot.vmp.jt1078.bean; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; +import lombok.Setter; + +/** + * JT 通信模块属性 + */ +@Setter +@Getter +@Schema(description = "JT通信模块属性") +public class JTCommunicationModuleAttribute { + + private boolean gprs ; + private boolean cdma ; + private boolean tdScdma ; + private boolean wcdma ; + private boolean cdma2000 ; + private boolean tdLte ; + private boolean other ; + + + public static JTCommunicationModuleAttribute getInstance(short content) { + boolean gprs = (content & 1) == 1; + boolean cdma = (content >>> 1 & 1) == 1; + boolean tdScdma = (content >>> 2 & 1) == 1; + boolean wcdma = (content >>> 3 & 1) == 1; + boolean cdma2000 = (content >>> 4 & 1) == 1; + boolean tdLte = (content >>> 5 & 1) == 1; + boolean other = (content >>> 7 & 1) == 1; + return new JTCommunicationModuleAttribute(gprs, cdma, tdScdma, wcdma, cdma2000, tdLte, other); + } + + public JTCommunicationModuleAttribute(boolean gprs, boolean cdma, boolean tdScdma, boolean wcdma, boolean cdma2000, boolean tdLte, boolean other) { + this.gprs = gprs; + this.cdma = cdma; + this.tdScdma = tdScdma; + this.wcdma = wcdma; + this.cdma2000 = cdma2000; + this.tdLte = tdLte; + this.other = other; + } + + @Override + public String toString() { + return "JCommunicationModuleAttribute{" + + "gprs=" + gprs + + ", cdma=" + cdma + + ", tdScdma=" + tdScdma + + ", wcdma=" + wcdma + + ", cdma2000=" + cdma2000 + + ", tdLte=" + tdLte + + ", other=" + other + + '}'; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/bean/JTConfirmationAlarmMessageType.java b/src/main/java/com/genersoft/iot/vmp/jt1078/bean/JTConfirmationAlarmMessageType.java new file mode 100644 index 0000000..f96cc87 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/bean/JTConfirmationAlarmMessageType.java @@ -0,0 +1,65 @@ +package com.genersoft.iot.vmp.jt1078.bean; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; +import lombok.Setter; + +@Setter +@Getter +@Schema(description = "人工确认报警类型") +public class JTConfirmationAlarmMessageType { + @Schema(description = "确认紧急报警") + private boolean urgent; + @Schema(description = "确认危险预警") + private boolean alarmDangerous; + @Schema(description = "确认进出区域报警") + private boolean alarmRegion; + @Schema(description = "确认进出路线报警") + private boolean alarmRoute; + @Schema(description = "确认路段行驶时间不足/过长报警") + private boolean alarmTravelTime; + @Schema(description = "确认车辆非法点火报警") + private boolean alarmIllegalIgnition; + @Schema(description = "确认车辆非法位移报警") + private boolean alarmIllegalDisplacement; + + public long encode(){ + long result = 0L; + if (urgent) { + result |= 0x01; + } + if (alarmDangerous) { + result |= (0x01 << 3); + } + if (alarmRegion) { + result |= (0x01 << 20); + } + if (alarmRoute) { + result |= (0x01 << 21); + } + if (alarmTravelTime) { + result |= (0x01 << 22); + } + if (alarmIllegalIgnition) { + result |= (0x01 << 27); + } + if (alarmIllegalDisplacement) { + result |= (0x01 << 28); + } + return result; + } + + + @Override + public String toString() { + return "JConfirmationAlarmMessageType{" + + "urgent=" + urgent + + ", alarmDangerous=" + alarmDangerous + + ", alarmRegion=" + alarmRegion + + ", alarmRoute=" + alarmRoute + + ", alarmTravelTime=" + alarmTravelTime + + ", alarmIllegalIgnition=" + alarmIllegalIgnition + + ", alarmIllegalDisplacement=" + alarmIllegalDisplacement + + '}'; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/bean/JTDevice.java b/src/main/java/com/genersoft/iot/vmp/jt1078/bean/JTDevice.java new file mode 100644 index 0000000..a64c549 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/bean/JTDevice.java @@ -0,0 +1,89 @@ +package com.genersoft.iot.vmp.jt1078.bean; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +/** + * JT 设备 + */ +@Data +@Schema(description = "jt808设备") +public class JTDevice { + + private int id; + + @Schema(description = "省域ID") + private String provinceId; + + @Schema(description = "省域文字描述") + private String provinceText; + + @Schema(description = "市县域ID") + private String cityId; + + @Schema(description = "市县域文字描述") + private String cityText; + + @Schema(description = "制造商ID") + private String makerId; + + @Schema(description = "终端型号") + private String model; + + @Schema(description = "终端手机号") + private String phoneNumber; + + @Schema(description = "终端ID") + private String terminalId; + + @Schema(description = "车牌颜色") + private int plateColor; + + @Schema(description = "车牌") + private String plateNo; + + @Schema(description = "经度") + private Double longitude; + + @Schema(description = "纬度") + private Double latitude; + + @Schema(description = "注册时间") + private String registerTime; + + @Schema(description = "创建时间") + private String createTime; + + @Schema(description = "更新时间") + private String updateTime; + + @Schema(description = "状态") + private boolean status; + + @Schema(description = "设备使用的媒体id, 默认为null") + private String mediaServerId; + + @Schema(description = "地理坐标系, 目前支持 WGS84,GCJ02") + private String geoCoordSys; + + @Schema(description = "收流IP") + private String sdpIp; + + @Override + public String toString() { + return "JTDevice{" + + " 终端手机号='" + phoneNumber + '\'' + + ", 省域ID='" + provinceId + '\'' + + ", 省域文字描述='" + provinceText + '\'' + + ", 市县域ID='" + cityId + '\'' + + ", 市县域文字描述='" + cityText + '\'' + + ", 制造商ID='" + makerId + '\'' + + ", 终端型号='" + model + '\'' + + ", 设备ID='" + terminalId + '\'' + + ", 车牌颜色=" + plateColor + + ", 车牌='" + plateNo + '\'' + + ", 注册时间='" + registerTime + '\'' + + ", status=" + status + + '}'; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/bean/JTDeviceAttribute.java b/src/main/java/com/genersoft/iot/vmp/jt1078/bean/JTDeviceAttribute.java new file mode 100644 index 0000000..a932f53 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/bean/JTDeviceAttribute.java @@ -0,0 +1,56 @@ +package com.genersoft.iot.vmp.jt1078.bean; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; +import lombok.Setter; + +/** + * JT 终端属性 + */ +@Setter +@Getter +@Schema(description = "JT终端属性") +public class JTDeviceAttribute { + + @Schema(description = "终端类型") + private JTDeviceType type; + + @Schema(description = "制造商 ID") + private String makerId; + + @Schema(description = "终端型号") + private String deviceModel; + + @Schema(description = "终端 ID") + private String terminalId; + + @Schema(description = "终端 SIM卡 ICCID") + private String iccId; + + @Schema(description = "终端硬件版本号") + private String hardwareVersion; + + @Schema(description = "固件版本号") + private String firmwareVersion ; + + @Schema(description = "GNSS 模块属性") + private JTGnssAttribute gnssAttribute ; + + @Schema(description = "通信模块属性") + private JTCommunicationModuleAttribute communicationModuleAttribute ; + + @Override + public String toString() { + return "JTDeviceAttribute{" + + "type=" + type + + ", makerId='" + makerId + '\'' + + ", deviceModel='" + deviceModel + '\'' + + ", terminalId='" + terminalId + '\'' + + ", iccId='" + iccId + '\'' + + ", hardwareVersion='" + hardwareVersion + '\'' + + ", firmwareVersion='" + firmwareVersion + '\'' + + ", gnssAttribute=" + gnssAttribute + + ", communicationModuleAttribute=" + communicationModuleAttribute + + '}'; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/bean/JTDeviceConfig.java b/src/main/java/com/genersoft/iot/vmp/jt1078/bean/JTDeviceConfig.java new file mode 100644 index 0000000..fef0d67 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/bean/JTDeviceConfig.java @@ -0,0 +1,380 @@ +package com.genersoft.iot.vmp.jt1078.bean; + +import com.genersoft.iot.vmp.jt1078.bean.common.ConfigAttribute; +import com.genersoft.iot.vmp.jt1078.bean.config.*; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +/** + * JT 终端参数设置 + */ +@Schema(description = "JT终端参数设置") +@Data +public class JTDeviceConfig { + + @ConfigAttribute(id = 0x1, type="Long", description = "终端心跳发送间隔,单位为秒(s)") + private Long keepaliveInterval; + + @ConfigAttribute(id = 0x2, type="Long", description = "TCP消息应答超时时间,单位为秒(s)") + private Long tcpResponseTimeout; + + @ConfigAttribute(id = 0x3, type="Long", description = "TCP消息重传次数") + private Long tcpRetransmissionCount; + + @ConfigAttribute(id = 0x4, type="Long", description = "UDP消息应答超时时间,单位为秒(s)") + private Long udpResponseTimeout; + + @ConfigAttribute(id = 0x5, type="Long", description = "UDP消息重传次数") + private Long udpRetransmissionCount; + + @ConfigAttribute(id = 0x6, type="Long", description = "SMS 消息应答超时时间,单位为秒(s)") + private Long smsResponseTimeout; + + @ConfigAttribute(id = 0x7, type="Long", description = "SMS 消息重传次数") + private Long smsRetransmissionCount; + + @ConfigAttribute(id = 0x10, type="String", description = "主服务器APN无线通信拨号访问点,若网络制式为 CDMA,则该处 为 PPP拨号号码") + private String apnMaster; + + @ConfigAttribute(id = 0x11, type="String", description = "主服务器无线通信拨号用户名") + private String dialingUsernameMaster; + + @ConfigAttribute(id = 0x12, type="String", description = "主服务器无线通信拨号密码") + private String dialingPasswordMaster; + + @ConfigAttribute(id = 0x13, type="String", description = "主服务器地址IP或域名,以冒号分割主机和端口 多个服务器使用分号分割") + private String addressMaster; + + @ConfigAttribute(id = 0x14, type="String", description = "备份服务器APN") + private String apnBackup; + + @ConfigAttribute(id = 0x15, type="String", description = "备份服务器无线通信拨号用户名") + private String dialingUsernameBackup; + + @ConfigAttribute(id = 0x16, type="String", description = "备份服务器无线通信拨号密码") + private String dialingPasswordBackup; + + @ConfigAttribute(id = 0x17, type="String", description = "备用服务器备份地址IP或域名,以冒号分割主机和端口 多个服务器使用分号分割") + private String addressBackup; + + @ConfigAttribute(id = 0x1a, type="String", description = "道路运输证IC卡认证主服务器IP地址或域名") + private String addressIcMaster; + + @ConfigAttribute(id = 0x1b, type="Long", description = "道路运输证IC卡认证主服务器TCP端口") + private Long tcpPortIcMaster; + + @ConfigAttribute(id = 0x1c, type="Long", description = "道路运输证IC卡认证主服务器UDP端口") + private Long udpPortIcMaster; + + @ConfigAttribute(id = 0x1d, type="String", description = "道路运输证IC卡认证备份服务器IP地址或域名,端口同主服务器") + private String addressIcBackup; + + @ConfigAttribute(id = 0x20, type="Long", description = "位置汇报策略, 0定时汇报 1定距汇报 2定时和定距汇报") + private Long locationReportingStrategy; + + @ConfigAttribute(id = 0x21, type="Long", description = "位置汇报方案,0根据ACC状态 1根据登录状态和ACC状态,先判断登录状态,若登录再根据ACC状态") + private Long locationReportingPlan; + + @ConfigAttribute(id = 0x22, type="Long", description = "驾驶员未登录汇报时间间隔,单位为秒,值大于零") + private Long reportingIntervalOffline; + + @ConfigAttribute(id = 0x23, type="String", description = "从服务器 APN# 该值为空时 !终端应使用主服务器相同配置") + private String apnSlave; + + @ConfigAttribute(id = 0x24, type="String", description = "从服务器无线通信拨号用户名 # 该值为空时 !终端应使用主服务器 相同配置") + private String dialingUsernameSlave; + + @ConfigAttribute(id = 0x25, type="String", description = "从服务器无线通信拨号密码 # 该值为空时 !终端应使用主服务器相 同配置") + private String dialingPasswordSlave; + + @ConfigAttribute(id = 0x26, type="String", description = "从服务器备份地址 IP或域名 !主机和端口用冒号分割 !多个服务器 使用分号分割") + private String addressSlave; + + @ConfigAttribute(id = 0x27, type="Long", description = "休眠时汇报时间间隔 单位为秒 值大于0") + private Long reportingIntervalDormancy; + + @ConfigAttribute(id = 0x28, type="Long", description = "紧急报警时汇报时间间隔 单位为秒 值大于0") + private Long reportingIntervalEmergencyAlarm; + + @ConfigAttribute(id = 0x29, type="Long", description = "缺省时间汇报间隔 单位为秒 值大于0") + private Long reportingIntervalDefault; + + @ConfigAttribute(id = 0x2c, type="Long", description = "缺省距离汇报间隔 单位为米 值大于0") + private Long reportingDistanceDefault; + + @ConfigAttribute(id = 0x2d, type="Long", description = "驾驶员未登录汇报距离间隔 单位为米 值大于0") + private Long reportingDistanceOffline; + + @ConfigAttribute(id = 0x2e, type="Long", description = "休眠时汇报距离间隔 单位为米 值大于0") + private Long reportingDistanceDormancy; + + @ConfigAttribute(id = 0x2f, type="Long", description = "紧急报警时汇报距离间隔 单位为米 值大于0") + private Long reportingDistanceEmergencyAlarm; + + @ConfigAttribute(id = 0x30, type="Long", description = "拐点补传角度 ,值小于180") + private Long inflectionPointAngle; + + @ConfigAttribute(id = 0x31, type="Integer", description = "电子围栏半径(非法位移國值) ,单位为米(m)") + private Integer fenceRadius; + + @ConfigAttribute(id = 0x32, type="IllegalDrivingPeriods", description = "违规行驶时段范围 ,精确到分") + private JTIllegalDrivingPeriods illegalDrivingPeriods; + + @ConfigAttribute(id = 0x40, type="String", description = "监控平台电话号码") + private String platformPhoneNumber; + + @ConfigAttribute(id = 0x41, type="String", description = "复位电话号码 ,可采用此电话号码拨打终端电话让终端复位") + private String phoneNumberForReset; + + @ConfigAttribute(id = 0x42, type="String", description = "恢复出厂设置电话号码 ,可采用此电话号码拨打终端电话让终端恢 复出厂设置") + private String phoneNumberForFactoryReset; + + @ConfigAttribute(id = 0x42, type="String", description = "监控平台 SMS 电话号码") + private String phoneNumberForSms; + + @ConfigAttribute(id = 0x44, type="String", description = "接收终端 SMS 文本报警号码") + private String phoneNumberForReceiveTextAlarm; + + @ConfigAttribute(id = 0x45, type="Long", description = "终端电话接听策略 。0:自动接听;1:ACC ON时自动接听 ,OFF时手动接听") + private Long phoneAnsweringPolicy; + + @ConfigAttribute(id = 0x46, type="Long", description = "每次最长通话时间 ,单位为秒(s) ,0 为不允许通话 ,0xFFFFFFFF为不限制") + private Long longestCallTimeForPerSession; + + @ConfigAttribute(id = 0x47, type="Long", description = "当月最长通话时间 ,单位为秒(s) ,0 为不允许通话 ,0xFFFFFFFF为 不限制") + private Long longestCallTimeInMonth; + + @ConfigAttribute(id = 0x48, type="String", description = "监听电话号码") + private String phoneNumbersForListen; + + @ConfigAttribute(id = 0x49, type="String", description = "监管平台特权短信号码") + private String privilegedSMSNumber; + + @ConfigAttribute(id = 0x50, type="AlarmSign", description = "报警屏蔽字 ,与位置信息汇报消息中的报警标志相对应 ,相应位为 1 则相应报警被屏蔽") + private JTAlarmSign alarmMaskingWord; + + @ConfigAttribute(id = 0x51, type="AlarmSign", description = "报警发送文本 SMS 开关 , 与位置信息汇报消息中的报警标志相对 应 ,相应位为1 则相应报警时发送文本 SMS") + private JTAlarmSign alarmSendsTextSmsSwitch; + + @ConfigAttribute(id = 0x52, type="AlarmSign", description = "报警拍摄开关 ,与位置信息汇报消息中的报警标志相对应 ,相应位为 1 则相应报警时摄像头拍摄") + private JTAlarmSign alarmShootingSwitch; + + @ConfigAttribute(id = 0x53, type="AlarmSign", description = "报警拍摄存储标志 ,与位置信息汇报消息中的报警标志相对应 ,相应 位为1 则对相应报警时拍的照片进行存储 ,否则实时上传") + private JTAlarmSign alarmShootingStorageFlags; + + @ConfigAttribute(id = 0x54, type="AlarmSign", description = "关键标志 ,与位置信息汇报消息中的报警标志相对应 ,相应位为 1 则 对相应报警为关键报警") + private JTAlarmSign KeySign; + + @ConfigAttribute(id = 0x55, type="Long", description = "最高速度 ,单位为千米每小时(km/h)") + private Long maxSpeed; + + @ConfigAttribute(id = 0x56, type="Long", description = "超速持续时间 ,单位为秒(s)") + private Long overSpeedDuration; + + @ConfigAttribute(id = 0x57, type="Long", description = "连续驾驶时间门限 单位为秒(s)") + private Long continuousDrivingTimeThreshold; + + @ConfigAttribute(id = 0x58, type="Long", description = "当天累计驾驶时间门限 单位为秒(s)") + private Long cumulativeDrivingTimeThresholdForTheDay; + + @ConfigAttribute(id = 0x59, type="Long", description = "最小休息时间 单位为秒(s)") + private Long minimumBreakTime; + + @ConfigAttribute(id = 0x5a, type="Long", description = "最长停车时间 单位为秒(s)") + private Long maximumParkingTime; + + @ConfigAttribute(id = 0x5b, type="Integer", description = "超速预警差值 单位为1/10 千米每小时(1/10km/h)") + private Integer overSpeedWarningDifference; + + @ConfigAttribute(id = 0x5c, type="Integer", description = "疲劳驾驶预警差值 单位为秒 值大于零") + private Integer drowsyDrivingWarningDifference; + + @ConfigAttribute(id = 0x5d, type="CollisionAlarmParams", description = "碰撞报警参数设置") + private JTCollisionAlarmParams collisionAlarmParams; + + @ConfigAttribute(id = 0x5e, type="Integer", description = "侧翻报警参数设置:侧翻角度,单位为度,默认为30") + private Integer rolloverAlarm; + + @ConfigAttribute(id = 0x64, type="CameraTimer", description = "定时拍照控制") + private JTCameraTimer cameraTimer; + + @ConfigAttribute(id = 0x70, type="Long", description = "图像/视频质量 设置范围为1~10 1表示最优质量") + private Long qualityForVideo; + + @ConfigAttribute(id = 0x71, type="Long", description = "亮度,设置范围为0 ~ 255") + private Long brightness; + + @ConfigAttribute(id = 0x72, type="Long", description = "对比度,设置范围为0 ~ 127") + private Long contrastRatio; + + @ConfigAttribute(id = 0x73, type="Long", description = "饱和度,设置范围为0 ~ 127") + private Long saturation; + + @ConfigAttribute(id = 0x74, type="Long", description = "色度,设置范围为0 ~ 255") + private Long chroma; + + @ConfigAttribute(id = 0x75, type="VideoParam", description = "音视频参数设置") + private JTVideoParam videoParam; + + @ConfigAttribute(id = 0x76, type="ChannelListParam", description = "音视频通道列表设置") + private JTChannelListParam channelListParam; + + @ConfigAttribute(id = 0x77, type="ChannelParam", description = "单独视频通道参数设置") + private JTChannelParam channelParam; + + @ConfigAttribute(id = 0x79, type="AlarmRecordingParam", description = "特殊报警录像参数设置") + private JTAlarmRecordingParam alarmRecordingParam; + + @ConfigAttribute(id = 0x7a, type="VideoAlarmBit", description = "视频相关报警屏蔽字") + private JTVideoAlarmBit videoAlarmBit; + + @ConfigAttribute(id = 0x7b, type="AnalyzeAlarmParam", description = "图像分析报警参数设置") + private JTAnalyzeAlarmParam analyzeAlarmParam; + + @ConfigAttribute(id = 0x7c, type="AwakenParam", description = "终端休眠唤醒模式设置") + private JTAwakenParam awakenParam; + + @ConfigAttribute(id = 0x80, type="Long", description = "车辆里程表读数,单位'1/10km") + private Long mileage; + + @ConfigAttribute(id = 0x81, type="Integer", description = "车辆所在的省域ID") + private Integer provincialId; + + @ConfigAttribute(id = 0x82, type="Integer", description = "车辆所在的市域ID") + private Integer cityId; + + @ConfigAttribute(id = 0x83, type="String", description = "公安交通管理部门颁发的机动车号牌") + private String licensePlate; + + @ConfigAttribute(id = 0x84, type="Short", description = "车牌颜色,值按照JT/T697-7.2014中的规定,未上牌车辆填0") + private Short licensePlateColor; + + @ConfigAttribute(id = 0x90, type="GnssPositioningMode", description = "GNSS定位模式") + private JTGnssPositioningMode gnssPositioningMode; + + @ConfigAttribute(id = 0x91, type="Short", description = "GNSS 波特率,定义如下: 0: 4800, 1:9600, 2:19200, 3:38400, 4:57600, 5:115200") + private Short gnssBaudRate; + + @ConfigAttribute(id = 0x92, type="Short", description = "GNSS 模块详细定位数据输出频率,定义如下: 0: 500ms, 1:1000ms(默认值), 2:2000ms, 3:3000ms, 4:4000ms") + private Short gnssOutputFrequency; + + @ConfigAttribute(id = 0x93, type="Long", description = "GNSS 模块详细定位数据采集频率 ,单位为秒(s) ,默认为1") + private Long gnssCollectionFrequency; + + @ConfigAttribute(id = 0x94, type="Short", description = "GNSS 模块详细定位数据上传方式:,定义如下: " + + "0: 本地存储 ,不上传(默认值) , " + + "1:按时间间隔上传, " + + "2:按距离间隔上传, " + + "11:按累计时间上传 ,达到传输时间后自动停止上传, " + + "12:按累计距离上传 ,达到距离后自动停止上传, " + + "13:按累计条数上传 ,达到上传条数后自动停止上传") + private Short gnssDataUploadMethod; + + @ConfigAttribute(id = 0x95, type="Long", description = "GNSS 模块详细定位数据上传设置:,定义如下: " + + "1:单位为秒(s), " + + "2:单位为米(m) , " + + "11:单位为 秒(s), " + + "12:单位为米(m), " + + "13:单位 为条") + private Long gnssDataUploadMethodUnit; + + @ConfigAttribute(id = 0x100, type="Long", description = "CAN总线通道1 采集时间间隔 ,单位为毫秒(ms) ,0 表示不采集") + private Long canCollectionTimeForChannel1; + + @ConfigAttribute(id = 0x101, type="Integer", description = "CAN总线通道1 上传时间间隔 ,单位为秒(s) ,0 表示不上传") + private Integer canUploadIntervalForChannel1; + + @ConfigAttribute(id = 0x102, type="Long", description = "CAN总线通道2 采集时间间隔 ,单位为毫秒(ms) ,0 表示不采集") + private Long canCollectionTimeForChannel2; + + @ConfigAttribute(id = 0x103, type="Integer", description = "CAN总线通道2 上传时间间隔 ,单位为秒(s) ,0 表示不上传") + private Integer canUploadIntervalForChannel2; + + @Override + public String toString() { + return "JTDeviceConfig{" + + "终端心跳发送间隔: " + keepaliveInterval + "秒" + + ", TCP消息应答超时时间:" + tcpResponseTimeout + "秒" + + ", TCP消息重传次数: " + tcpRetransmissionCount + "秒" + + ", UDP消息应答超时时间: " + udpResponseTimeout + + ", UDP消息重传次数: " + udpRetransmissionCount + + ", SMS 消息应答超时时间: " + smsResponseTimeout + "秒" + + ", SMS 消息重传次数: " + smsRetransmissionCount + + ", 主服务器APN无线通信拨号访问点: " + apnMaster + '\'' + + ", 主服务器无线通信拨号用户名: " + dialingUsernameMaster + + ", 主服务器无线通信拨号密码: " + dialingPasswordMaster + + ", 主服务器地址IP或域名: " + addressMaster + + ", 备份服务器APN: " + apnBackup + + ", 备份服务器无线通信拨号用户名: " + dialingUsernameBackup + + ", 备份服务器无线通信拨号密码: " + dialingPasswordBackup + + ", 备用服务器备份地址IP或域名: " + addressBackup + + ", 道路运输证IC卡认证主服务器IP地址或域名: " + addressIcMaster + + ", 道路运输证IC卡认证主服务器TCP端口: " + tcpPortIcMaster + + ", 道路运输证IC卡认证主服务器UDP端口: " + udpPortIcMaster + + ", 道路运输证IC卡认证备份服务器IP地址或域名: " + addressIcBackup + + ", 位置汇报策略: " + locationReportingStrategy + + ", 位置汇报方案: " + locationReportingPlan + + ", 驾驶员未登录汇报时间间隔: " + reportingIntervalOffline + "秒" + + ", 从服务器 APN: " + apnSlave + + ", 从服务器无线通信拨号密码: " + dialingUsernameSlave + + ", 从服务器备份地址 IP或域名: " + dialingPasswordSlave + + ", 从服务器备份地址 IP或域名: " + addressSlave + + ", reportingIntervalDormancy: " + reportingIntervalDormancy + + ", reportingIntervalEmergencyAlarm: " + reportingIntervalEmergencyAlarm + + ", reportingIntervalDefault: " + reportingIntervalDefault + + ", reportingDistanceDefault: " + reportingDistanceDefault + + ", reportingDistanceOffline: " + reportingDistanceOffline + + ", reportingDistanceDormancy: " + reportingDistanceDormancy + + ", reportingDistanceEmergencyAlarm: " + reportingDistanceEmergencyAlarm + + ", inflectionPointAngle: " + inflectionPointAngle + + ", fenceRadius: " + fenceRadius + + ", illegalDrivingPeriods: " + illegalDrivingPeriods + + ", platformPhoneNumber: " + platformPhoneNumber + + ", phoneNumberForReset: " + phoneNumberForReset + + ", phoneNumberForFactoryReset: " + phoneNumberForFactoryReset + + ", phoneNumberForSms: " + phoneNumberForSms + + ", phoneNumberForReceiveTextAlarm: " + phoneNumberForReceiveTextAlarm + + ", phoneAnsweringPolicy: " + phoneAnsweringPolicy + + ", longestCallTimeForPerSession: " + longestCallTimeForPerSession + + ", longestCallTimeInMonth: " + longestCallTimeInMonth + + ", phoneNumbersForListen: " + phoneNumbersForListen + + ", privilegedSMSNumber: " + privilegedSMSNumber + + ", alarmMaskingWord: " + alarmMaskingWord + + ", alarmSendsTextSmsSwitch: " + alarmSendsTextSmsSwitch + + ", alarmShootingSwitch: " + alarmShootingSwitch + + ", alarmShootingStorageFlags: " + alarmShootingStorageFlags + + ", KeySign: " + KeySign + + ", topSpeed: " + maxSpeed + + ", overSpeedDuration: " + overSpeedDuration + + ", continuousDrivingTimeThreshold: " + continuousDrivingTimeThreshold + + ", cumulativeDrivingTimeThresholdForTheDay: " + cumulativeDrivingTimeThresholdForTheDay + + ", minimumBreakTime: " + minimumBreakTime + + ", maximumParkingTime: " + maximumParkingTime + + ", overSpeedWarningDifference: " + overSpeedWarningDifference + + ", drowsyDrivingWarningDifference: " + drowsyDrivingWarningDifference + + ", collisionAlarmParams: " + collisionAlarmParams + + ", rolloverAlarm: " + rolloverAlarm + + ", cameraTimer: " + cameraTimer + + ", qualityForVideo: " + qualityForVideo + + ", brightness: " + brightness + + ", contrastRatio: " + contrastRatio + + ", saturation: " + saturation + + ", chroma: " + chroma + + ", mileage: " + mileage + + ", provincialId: " + provincialId + + ", cityId: " + cityId + + ", licensePlate: " + licensePlate + + ", licensePlateColor: " + licensePlateColor + + ", gnssPositioningMode: " + gnssPositioningMode + + ", gnssBaudRate: " + gnssBaudRate + + ", gnssOutputFrequency: " + gnssOutputFrequency + + ", gnssCollectionFrequency: " + gnssCollectionFrequency + + ", gnssDataUploadMethod: " + gnssDataUploadMethod + + ", gnssDataUploadMethodUnit: " + gnssDataUploadMethodUnit + + ", canCollectionTimeForChannel1: " + canCollectionTimeForChannel1 + + ", canUploadIntervalForChannel1: " + canUploadIntervalForChannel1 + + ", canCollectionTimeForChannel2: " + canCollectionTimeForChannel2 + + ", canUploadIntervalForChannel2: " + canUploadIntervalForChannel2 + + '}'; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/bean/JTDeviceConnectionControl.java b/src/main/java/com/genersoft/iot/vmp/jt1078/bean/JTDeviceConnectionControl.java new file mode 100644 index 0000000..af4cf27 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/bean/JTDeviceConnectionControl.java @@ -0,0 +1,71 @@ +package com.genersoft.iot.vmp.jt1078.bean; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +/** + * JT 终端控制 + */ +@Data +@Schema(description = "终端控制") +public class JTDeviceConnectionControl { + + /** + * false 表示切换到指定监管平台服务器 ,true 表示切换回原 缺省监控平台服务器 + */ + private Boolean switchOn; + /** + * 监管平台鉴权码 + */ + private String authentication; + + /** + * 拨号点名称 + */ + private String name; + + /** + * 拨号用户名 + */ + private String username; + + /** + * 拨号密码 + */ + private String password; + + /** + * 地址 + */ + private String address; + + /** + * TCP端口 + */ + private Integer tcpPort; + + /** + * UDP端口 + */ + private Integer udpPort; + + /** + * 连接到指定服务器时限 + */ + private Long timeLimit; + + @Override + public String toString() { + return "JTDeviceConnectionControl{" + + "switchOn=" + switchOn + + ", authentication='" + authentication + '\'' + + ", name='" + name + '\'' + + ", username='" + username + '\'' + + ", password='" + password + '\'' + + ", address='" + address + '\'' + + ", tcpPort=" + tcpPort + + ", udpPort=" + udpPort + + ", timeLimit=" + timeLimit + + '}'; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/bean/JTDeviceType.java b/src/main/java/com/genersoft/iot/vmp/jt1078/bean/JTDeviceType.java new file mode 100644 index 0000000..caf49a2 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/bean/JTDeviceType.java @@ -0,0 +1,83 @@ +package com.genersoft.iot.vmp.jt1078.bean; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; +import lombok.Setter; + +/** + * JT 终端类型 + */ +@Setter +@Getter +@Schema(description = "JT终端参数设置") +public class JTDeviceType { + + /** + * 适用客运车辆 + */ + private boolean passengerVehicles; + + /** + * 适用危险品车辆 + */ + private boolean dangerousGoodsVehicles; + + /** + * 普通货运车辆 + */ + private boolean freightVehicles; + + /** + * 出租车辆 + */ + private boolean rentalVehicles; + + /** + * 支持硬盘录像 + */ + private boolean hardDiskRecording; + + /** + * false:一体机 ,true:分体机 + */ + private boolean splittingMachine; + + /** + * 适用挂车 + */ + private boolean trailer; + + public static JTDeviceType getInstance(int content) { + boolean passengerVehicles = (content & 1) == 1; + boolean dangerousGoodsVehicles = (content >>> 1 & 1) == 1; + boolean freightVehicles = (content >>> 2 & 1) == 1; + boolean rentalVehicles = (content >>> 3 & 1) == 1; + boolean hardDiskRecording = (content >>> 6 & 1) == 1; + boolean splittingMachine = (content >>> 7 & 1) == 1; + boolean trailer = (content >>> 8 & 1) == 1; + return new JTDeviceType(passengerVehicles, dangerousGoodsVehicles, freightVehicles, rentalVehicles, hardDiskRecording, splittingMachine, trailer); + } + + public JTDeviceType(boolean passengerVehicles, boolean dangerousGoodsVehicles, boolean freightVehicles, boolean rentalVehicles, boolean hardDiskRecording, boolean splittingMachine, boolean trailer) { + this.passengerVehicles = passengerVehicles; + this.dangerousGoodsVehicles = dangerousGoodsVehicles; + this.freightVehicles = freightVehicles; + this.rentalVehicles = rentalVehicles; + this.hardDiskRecording = hardDiskRecording; + this.splittingMachine = splittingMachine; + this.trailer = trailer; + } + + @Override + public String toString() { + return "JTDeviceType{" + + "passengerVehicles=" + passengerVehicles + + ", dangerousGoodsVehicles=" + dangerousGoodsVehicles + + ", freightVehicles=" + freightVehicles + + ", rentalVehicles=" + rentalVehicles + + ", hardDiskRecording=" + hardDiskRecording + + ", splittingMachine=" + splittingMachine + + ", trailer=" + trailer + + '}'; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/bean/JTDriverInformation.java b/src/main/java/com/genersoft/iot/vmp/jt1078/bean/JTDriverInformation.java new file mode 100644 index 0000000..3309e40 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/bean/JTDriverInformation.java @@ -0,0 +1,99 @@ +package com.genersoft.iot.vmp.jt1078.bean; + +import com.genersoft.iot.vmp.jt1078.util.BCDUtil; +import com.genersoft.iot.vmp.utils.DateUtil; +import io.netty.buffer.ByteBuf; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.extern.slf4j.Slf4j; + +import java.nio.charset.Charset; + +@Data +@Slf4j +@Schema(description = "驾驶员身份信息") +public class JTDriverInformation { + + @Schema(description = "0x01:从业资格证 IC卡插入( 驾驶员上班);0x02:从 业资格证 IC卡拔出(驾驶员下班)") + private int status; + + @Schema(description = "插卡/拔卡时间 ,以下字段在状 态为0x01 时才有效并做填充") + private String time; + + @Schema(description = "IC卡读取结果:" + + "0x00:IC卡读卡成功;" + + "0x01:读卡失败 ,原因为卡片密钥认证未通过;" + + "0x02:读卡失败 ,原因为卡片已被锁定;" + + "0x03:读卡失败 ,原因为卡片被拔出;" + + "0x04:读卡失败 ,原因为数据校验错误。" + + "以下字段在 IC卡读取结果等于0x00 时才有效") + private Integer result; + + @Schema(description = "驾驶员姓名") + private String name; + + @Schema(description = "从业资格证编码") + private String certificateCode; + + @Schema(description = "发证机构名称") + private String certificateIssuanceMechanismName; + + @Schema(description = "证件有效期") + private String expire; + + @Schema(description = "驾驶员身份证号") + private String driverIdNumber; + + public static JTDriverInformation decode(ByteBuf buf, boolean is2019) { + JTDriverInformation jtDriverInformation = new JTDriverInformation(); + jtDriverInformation.setStatus(buf.readUnsignedByte()); + byte[] bytes = new byte[6]; + buf.readBytes(bytes); + String timeStr = BCDUtil.transform(bytes); + try { + jtDriverInformation.setTime(DateUtil.jt1078Toyyyy_MM_dd_HH_mm_ss(timeStr)); + }catch (Exception e) { + log.error("[JT-驾驶员身份信息] 解码时无法格式化时间: {}", timeStr); + } + + if (jtDriverInformation.getStatus() == 1) { + int result = (int)buf.readUnsignedByte(); + jtDriverInformation.setResult(result); + if (result == 0) { + // IC卡读卡成功 + int nameLength = buf.readUnsignedByte(); + jtDriverInformation.setName(buf.readCharSequence(nameLength, Charset.forName("GBK")).toString().trim()); + jtDriverInformation.setCertificateCode(buf.readCharSequence(20, Charset.forName("GBK")).toString().trim()); + int certificateIssuanceMechanismNameLength = buf.readUnsignedByte(); + jtDriverInformation.setCertificateIssuanceMechanismName(buf.readCharSequence( + certificateIssuanceMechanismNameLength, Charset.forName("GBK")).toString().trim()); + byte[] bytesForExpire = new byte[4]; + buf.readBytes(bytesForExpire); + String bytesForExpireStr = BCDUtil.transform(bytesForExpire); + try { + jtDriverInformation.setExpire(DateUtil.jt1078dateToyyyy_MM_dd(bytesForExpireStr)); + }catch (Exception e) { + log.error("[JT-驾驶员身份信息] 解码时无法格式化时间: {}", bytesForExpireStr); + } + if (is2019) { + jtDriverInformation.setDriverIdNumber(buf.readCharSequence(20, Charset.forName("GBK")).toString().trim()); + } + } + } + return jtDriverInformation; + } + + @Override + public String toString() { + return "JTDriverInformation{" + + "status=" + status + + ", time='" + time + '\'' + + ", result=" + result + + ", name='" + name + '\'' + + ", certificateCode='" + certificateCode + '\'' + + ", certificateIssuanceMechanismName='" + certificateIssuanceMechanismName + '\'' + + ", expire='" + expire + '\'' + + ", driverIdNumber='" + driverIdNumber + '\'' + + '}'; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/bean/JTGnssAttribute.java b/src/main/java/com/genersoft/iot/vmp/jt1078/bean/JTGnssAttribute.java new file mode 100644 index 0000000..37cd6f4 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/bean/JTGnssAttribute.java @@ -0,0 +1,47 @@ +package com.genersoft.iot.vmp.jt1078.bean; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; +import lombok.Setter; + +/** + * JT GNSS 模块属性 + */ +@Setter +@Getter +@Schema(description = "JTGNSS 模块属性") +public class JTGnssAttribute { + + private boolean gps; + + private boolean beidou; + + private boolean glonass ; + + private boolean gaLiLeo; + + public static JTGnssAttribute getInstance(short content) { + boolean gps = (content & 1) == 1; + boolean beidou = (content >>> 1 & 1) == 1; + boolean glonass = (content >>> 2 & 1) == 1; + boolean gaLiLeo = (content >>> 3 & 1) == 1; + return new JTGnssAttribute(gps, beidou, glonass, gaLiLeo); + } + + public JTGnssAttribute(boolean gps, boolean beidou, boolean glonass, boolean gaLiLeo) { + this.gps = gps; + this.beidou = beidou; + this.glonass = glonass; + this.gaLiLeo = gaLiLeo; + } + + @Override + public String toString() { + return "JGnssAttribute{" + + "gps=" + gps + + ", beidou=" + beidou + + ", glonass=" + glonass + + ", gaLiLeo=" + gaLiLeo + + '}'; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/bean/JTMediaAttribute.java b/src/main/java/com/genersoft/iot/vmp/jt1078/bean/JTMediaAttribute.java new file mode 100644 index 0000000..f86c0ed --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/bean/JTMediaAttribute.java @@ -0,0 +1,129 @@ +package com.genersoft.iot.vmp.jt1078.bean; + +import com.genersoft.iot.vmp.jt1078.bean.config.JTDeviceSubConfig; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import lombok.Getter; +import lombok.Setter; + +/** + * 终端上传音视频属性 + */ +@Setter +@Getter +public class JTMediaAttribute implements JTDeviceSubConfig { + + /** + * 输入音频编码方式: + * 1 G. 721 + * 2 G. 722 + * 3 G. 723 + * 4 G. 728 + * 5 G. 729 + * 6 G. 711A + * 7 G. 711U + * 8 G. 726 + * 9 G. 729A + * 10 DVI4_3 + * 11 DVI4_4 + * 12 DVI4_8K + * 13 DVI4_16K + * 14 LPC + * 15 S16BE_STEREO + * 16 S16BE_MONO + * 17 MPEGAUDIO + * 18 LPCM + * 19 AAC + * 20 WMA9STD + * 21 HEAAC + * 22 PCM_VOICE + * 23 PCM_AUDIO + * 24 AACLC + * 25 MP3 + * 26 ADPCMA + * 27 MP4AUDIO + * 28 AMR + */ + private int audioEncoder; + + /** + * 输入音频声道数 + */ + private int audioChannels; + + /** + * 输入音频采样率: + * 0:8 kHz; + * 1:22. 05 kHz; + * 2:44. 1 kHz; + * 3:48 kHz + */ + private int audioSamplingRate; + + /** + * 输入音频采样位数: + * 0:8 位; + * 1:16 位; + * 2:32 位 + */ + private int audioSamplingBits; + + /** + * 音频帧长度: 范围 1 ~ 4 294 967 295 + */ + private int audioFrameLength; + + /** + * 是否支持音频输出: + * 0:不支持;1:支持 + */ + private int audioOutputEnable; + + /** + * 视频编码方式: + * 98 H. 264 + * 99 H. 265 + * 100 AVS + * 101 SVAC + */ + private int videoEncoder; + + /** + * 终端支持的最大音频物理通道数量: + */ + private int audioChannelMax; + + /** + * 终端支持的最大视频物理通道数量: + */ + private int videoChannelMax; + + @Override + public ByteBuf encode() { + ByteBuf byteBuf = Unpooled.buffer(); + byteBuf.writeByte(audioEncoder); + byteBuf.writeByte(audioChannels); + byteBuf.writeByte(audioSamplingRate); + byteBuf.writeByte(audioSamplingBits); + byteBuf.writeShort(audioFrameLength); + byteBuf.writeByte(audioOutputEnable); + byteBuf.writeByte(videoEncoder); + byteBuf.writeByte(audioChannelMax); + byteBuf.writeByte(videoChannelMax); + return byteBuf; + } + + public static JTMediaAttribute decode(ByteBuf byteBuf) { + JTMediaAttribute jtMediaAttribute = new JTMediaAttribute(); + jtMediaAttribute.setAudioEncoder(byteBuf.readUnsignedByte()); + jtMediaAttribute.setAudioChannels(byteBuf.readUnsignedByte()); + jtMediaAttribute.setAudioSamplingRate(byteBuf.readUnsignedByte()); + jtMediaAttribute.setAudioSamplingBits(byteBuf.readUnsignedByte()); + jtMediaAttribute.setAudioFrameLength(byteBuf.readUnsignedShort()); + jtMediaAttribute.setAudioOutputEnable(byteBuf.readUnsignedByte()); + jtMediaAttribute.setVideoEncoder(byteBuf.readUnsignedByte()); + jtMediaAttribute.setAudioChannelMax(byteBuf.readUnsignedByte()); + jtMediaAttribute.setVideoChannelMax(byteBuf.readUnsignedByte()); + return jtMediaAttribute; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/bean/JTMediaDataInfo.java b/src/main/java/com/genersoft/iot/vmp/jt1078/bean/JTMediaDataInfo.java new file mode 100644 index 0000000..0c67e83 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/bean/JTMediaDataInfo.java @@ -0,0 +1,48 @@ +package com.genersoft.iot.vmp.jt1078.bean; + +import io.netty.buffer.ByteBuf; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; +import lombok.Setter; + +@Setter +@Getter +@Schema(description = "多媒体检索项数据") +public class JTMediaDataInfo { + + @Schema(description = "多媒体数据 ID") + private long id; + + @Schema(description = "多媒体类型, 0:图像;1:音频;2:视频") + private int type; + + @Schema(description = "事件项编码: 0:平台下发指令;1:定时动作;2:抢劫报警触发;3:碰 撞侧翻报警触发;4:门开拍照;5:门关拍照;6:车门由开 变关 ,车速从小于20km到超过20km;7:定距拍照") + private int eventCode; + + @Schema(description = "通道 ID") + private int channelId; + + @Schema(description = "表示拍摄或录制的起始时刻的汇报消息") + private JTPositionBaseInfo positionBaseInfo; + + public static JTMediaDataInfo decode(ByteBuf buf) { + JTMediaDataInfo jtMediaEventInfo = new JTMediaDataInfo(); + jtMediaEventInfo.setId(buf.readUnsignedInt()); + jtMediaEventInfo.setType(buf.readUnsignedByte()); + jtMediaEventInfo.setChannelId(buf.readUnsignedByte()); + jtMediaEventInfo.setEventCode(buf.readUnsignedByte()); + jtMediaEventInfo.setPositionBaseInfo(JTPositionBaseInfo.decode(buf)); + return jtMediaEventInfo; + } + + @Override + public String toString() { + return "JTMediaDataInfo{" + + "id=" + id + + ", type=" + type + + ", eventCode=" + eventCode + + ", channelId=" + channelId + + ", positionBaseInfo=" + positionBaseInfo + + '}'; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/bean/JTMediaEventInfo.java b/src/main/java/com/genersoft/iot/vmp/jt1078/bean/JTMediaEventInfo.java new file mode 100644 index 0000000..fc3e062 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/bean/JTMediaEventInfo.java @@ -0,0 +1,62 @@ +package com.genersoft.iot.vmp.jt1078.bean; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufUtil; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Data +@Schema(description = "多媒体事件信息") +public class JTMediaEventInfo { + + @Schema(description = "多媒体数据 ID") + private long id; + + @Schema(description = "多媒体类型, 0:图像;1:音频;2:视频") + private int type; + + @Schema(description = "多媒体格式编码, 0:JPEG;1:TIF;2:MP3;3:WAV;4:WMV;其他保留") + private int code; + + @Schema(description = "事件项编码: 0:平台下发指令;1:定时动作;2:抢劫报警触发;3:碰 撞侧翻报警触发;4:门开拍照;5:门关拍照;6:车门由开 变关 ,车速从小于20km到超过20km;7:定距拍照") + private int eventCode; + + @Schema(description = "通道 ID") + private int channelId; + + @Schema(description = "媒体数据") + private byte[] mediaData; + + @Schema(description = "位置信息汇报") + private JTPositionBaseInfo positionBaseInfo; + + + public static JTMediaEventInfo decode(ByteBuf buf) { + JTMediaEventInfo jtMediaEventInfo = new JTMediaEventInfo(); + jtMediaEventInfo.setId(buf.readUnsignedInt()); + jtMediaEventInfo.setType(buf.readUnsignedByte()); + jtMediaEventInfo.setCode(buf.readUnsignedByte()); + jtMediaEventInfo.setEventCode(buf.readUnsignedByte()); + jtMediaEventInfo.setChannelId(buf.readUnsignedByte()); + if (buf.readableBytes() > 28) { + ByteBuf byteBuf = buf.readSlice(28); + jtMediaEventInfo.setPositionBaseInfo(JTPositionBaseInfo.decode(byteBuf)); + byte[] bytes = new byte[buf.readableBytes()]; + buf.readBytes(bytes); + jtMediaEventInfo.setMediaData(bytes); + } + return jtMediaEventInfo; + } + + @Override + public String toString() { + return "JTMediaEventInfo{" + + "id=" + id + + ", type=" + type + + ", code=" + code + + ", eventCode=" + eventCode + + ", channelId=" + channelId + + ", fileSize=" + (mediaData == null ? 0 : mediaData.length) + + '}'; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/bean/JTMediaStreamType.java b/src/main/java/com/genersoft/iot/vmp/jt1078/bean/JTMediaStreamType.java new file mode 100644 index 0000000..a5833ee --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/bean/JTMediaStreamType.java @@ -0,0 +1,5 @@ +package com.genersoft.iot.vmp.jt1078.bean; + +public enum JTMediaStreamType { + PLAY,PLAYBACK,TALK +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/bean/JTPassengerNum.java b/src/main/java/com/genersoft/iot/vmp/jt1078/bean/JTPassengerNum.java new file mode 100644 index 0000000..34c8b87 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/bean/JTPassengerNum.java @@ -0,0 +1,63 @@ +package com.genersoft.iot.vmp.jt1078.bean; + +import com.genersoft.iot.vmp.jt1078.bean.config.JTDeviceSubConfig; +import com.genersoft.iot.vmp.jt1078.util.BCDUtil; +import com.genersoft.iot.vmp.utils.DateUtil; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import lombok.Getter; +import lombok.Setter; + +/** + * 终端上传乘客流量 + */ +@Setter +@Getter +public class JTPassengerNum implements JTDeviceSubConfig { + + /** + * 起始时间, YY-MM-DD-HH-MM-SS( GMT + 8 时间,本标准中之后涉及的时间均采用此时区) + */ + private String startTime; + + /** + * 结束时间, YY-MM-DD-HH-MM-SS( GMT + 8 时间,本标准中之后涉及的时间均采用此时区) + */ + private String endTime; + + /** + * 上车人数 + */ + private int getIn; + + /** + * 下车人数 + */ + private int getOut; + + @Override + public ByteBuf encode() { + return null; + } + + public static JTPassengerNum decode(ByteBuf buf) { + JTPassengerNum jtPassengerNum = new JTPassengerNum(); + byte[] bytes = new byte[6]; + buf.readBytes(bytes); + jtPassengerNum.setStartTime(DateUtil.jt1078Toyyyy_MM_dd_HH_mm_ss(BCDUtil.transform(bytes))); + buf.readBytes(bytes); + jtPassengerNum.setEndTime(DateUtil.jt1078Toyyyy_MM_dd_HH_mm_ss(BCDUtil.transform(bytes))); + jtPassengerNum.setGetIn(buf.readUnsignedShort()); + jtPassengerNum.setGetOut(buf.readUnsignedShort()); + return jtPassengerNum; + } + + @Override + public String toString() { + return "终端上传乘客流量:" + + " 时间: " + startTime + " 到 " + endTime + + ", 上车:" + getIn + + ", 下车:" + getOut + ; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/bean/JTPhoneBookContact.java b/src/main/java/com/genersoft/iot/vmp/jt1078/bean/JTPhoneBookContact.java new file mode 100644 index 0000000..b9a9b8c --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/bean/JTPhoneBookContact.java @@ -0,0 +1,43 @@ +package com.genersoft.iot.vmp.jt1078.bean; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; +import lombok.Setter; + +import java.nio.charset.Charset; + +@Setter +@Getter +@Schema(description = "电话本联系人") +public class JTPhoneBookContact { + + @Schema(description = "1:呼入,2:呼出,3:呼入/呼出") + private int sign; + + @Schema(description = "电话号码") + private String phoneNumber; + + @Schema(description = "联系人") + private String contactName; + + public ByteBuf encode(){ + ByteBuf buffer = Unpooled.buffer(); + buffer.writeByte(sign); + buffer.writeByte(phoneNumber.getBytes(Charset.forName("GBK")).length); + buffer.writeCharSequence(phoneNumber, Charset.forName("GBK")); + buffer.writeByte(contactName.getBytes(Charset.forName("GBK")).length); + buffer.writeCharSequence(contactName, Charset.forName("GBK")); + return buffer; + } + + @Override + public String toString() { + return "JTPhoneBookContact{" + + "sign=" + sign + + ", phoneNumber='" + phoneNumber + '\'' + + ", contactName='" + contactName + '\'' + + '}'; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/bean/JTPolygonArea.java b/src/main/java/com/genersoft/iot/vmp/jt1078/bean/JTPolygonArea.java new file mode 100644 index 0000000..e7d3a0f --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/bean/JTPolygonArea.java @@ -0,0 +1,97 @@ +package com.genersoft.iot.vmp.jt1078.bean; + +import com.genersoft.iot.vmp.jt1078.util.BCDUtil; +import com.genersoft.iot.vmp.utils.DateUtil; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; +import lombok.Setter; + +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.List; + +@Setter +@Getter +@Schema(description = "多边形区域") +public class JTPolygonArea implements JTAreaOrRoute{ + + @Schema(description = "区域 ID") + private long id; + + @Schema(description = "") + private JTAreaAttribute attribute; + + @Schema(description = "起始时间, yyyy-MM-dd HH:mm:ss") + private String startTime; + + @Schema(description = "结束时间, yyyy-MM-dd HH:mm:ss") + private String endTime; + + @Schema(description = "最高速度, 单位为千米每小时(km/h)") + private int maxSpeed; + + @Schema(description = "超速持续时间, 单位为秒(s)") + private int overSpeedDuration; + + @Schema(description = "区域顶点") + private List polygonPoints; + + @Schema(description = "夜间最高速度, 单位为千米每小时(km/h)") + private int nighttimeMaxSpeed; + + @Schema(description = "区域的名称") + private String name; + + public ByteBuf encode(){ + ByteBuf byteBuf = Unpooled.buffer(); + byteBuf.writeInt((int) (id & 0xffffffffL)); + byteBuf.writeBytes(attribute.encode()); + byteBuf.writeBytes(BCDUtil.strToBcd(DateUtil.yyyy_MM_dd_HH_mm_ssTo1078(startTime))); + byteBuf.writeBytes(BCDUtil.strToBcd(DateUtil.yyyy_MM_dd_HH_mm_ssTo1078(endTime))); + byteBuf.writeShort((short)(maxSpeed & 0xffff)); + byteBuf.writeByte(overSpeedDuration); + + byteBuf.writeShort((short)(polygonPoints.size() & 0xffff)); + if (!polygonPoints.isEmpty()) { + for (JTPolygonPoint polygonPoint : polygonPoints) { + byteBuf.writeBytes(polygonPoint.encode()); + } + } + byteBuf.writeShort((short)(nighttimeMaxSpeed & 0xffff)); + byteBuf.writeShort((short)(name.getBytes(Charset.forName("GBK")).length & 0xffff)); + byteBuf.writeCharSequence(name, Charset.forName("GBK")); + return byteBuf; + } + + public static JTPolygonArea decode(ByteBuf buf) { + JTPolygonArea area = new JTPolygonArea(); + area.setId(buf.readUnsignedInt()); + int attributeInt = buf.readUnsignedShort(); + JTAreaAttribute areaAttribute = JTAreaAttribute.decode(attributeInt); + area.setAttribute(areaAttribute); + byte[] startTimeBytes = new byte[6]; + buf.readBytes(startTimeBytes); + area.setStartTime(DateUtil.jt1078Toyyyy_MM_dd_HH_mm_ss(BCDUtil.transform(startTimeBytes))); + byte[] endTimeBytes = new byte[6]; + buf.readBytes(endTimeBytes); + area.setEndTime(DateUtil.jt1078Toyyyy_MM_dd_HH_mm_ss(BCDUtil.transform(endTimeBytes))); + area.setMaxSpeed(buf.readUnsignedShort()); + area.setOverSpeedDuration(buf.readUnsignedByte()); + int polygonPointsSize = buf.readUnsignedShort(); + List polygonPointList = new ArrayList<>(polygonPointsSize); + for (int i = 0; i < polygonPointsSize; i++) { + JTPolygonPoint polygonPoint = new JTPolygonPoint(); + polygonPoint.setLatitude(buf.readUnsignedInt()/1000000D); + polygonPoint.setLongitude(buf.readUnsignedInt()/1000000D); + polygonPointList.add(polygonPoint); + } + area.setPolygonPoints(polygonPointList); + area.setNighttimeMaxSpeed(buf.readUnsignedShort()); + int nameLength = buf.readUnsignedShort(); + area.setName(buf.readCharSequence(nameLength, Charset.forName("GBK")).toString().trim()); + return area; + } + +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/bean/JTPolygonPoint.java b/src/main/java/com/genersoft/iot/vmp/jt1078/bean/JTPolygonPoint.java new file mode 100644 index 0000000..6d81691 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/bean/JTPolygonPoint.java @@ -0,0 +1,27 @@ +package com.genersoft.iot.vmp.jt1078.bean; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; +import lombok.Setter; + +@Setter +@Getter +@Schema(description = "多边形区域的顶点") +public class JTPolygonPoint { + + @Schema(description = "顶点纬度") + private Double latitude; + + @Schema(description = "顶点经度") + private Double longitude; + + public ByteBuf encode(){ + ByteBuf byteBuf = Unpooled.buffer(); + byteBuf.writeInt((int) (Math.round((latitude * 1000000)) & 0xffffffffL)); + byteBuf.writeInt((int) (Math.round((longitude * 1000000)) & 0xffffffffL)); + return byteBuf; + } + +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/bean/JTPositionAdditionalInfo.java b/src/main/java/com/genersoft/iot/vmp/jt1078/bean/JTPositionAdditionalInfo.java new file mode 100644 index 0000000..277d02f --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/bean/JTPositionAdditionalInfo.java @@ -0,0 +1,29 @@ +package com.genersoft.iot.vmp.jt1078.bean; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +@Schema(description = "位置附加信息") +public class JTPositionAdditionalInfo { + + @Schema(description = "里程, 单位为1/10km, 对应车上里程表读数") + private int mileage; + + @Schema(description = "油量, 单位为1/10L, 对应车上油量表读数") + private int oil; + + @Schema(description = "行驶记录功能获取的速度,单位为1/10km/h") + private int speed; + + @Schema(description = "报警事件的 ID") + private int alarmId; + // TODO 暂不支持胎压 + + @Schema(description = "车厢温度 ,单位为摄氏度") + private int carriageTemperature; + // TODO 暂不支持胎压 + +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/bean/JTPositionBaseInfo.java b/src/main/java/com/genersoft/iot/vmp/jt1078/bean/JTPositionBaseInfo.java new file mode 100644 index 0000000..39b185b --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/bean/JTPositionBaseInfo.java @@ -0,0 +1,118 @@ +package com.genersoft.iot.vmp.jt1078.bean; + +import com.genersoft.iot.vmp.jt1078.util.BCDUtil; +import io.netty.buffer.ByteBuf; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; + +import java.nio.ByteBuffer; + +@Setter +@Getter +@Slf4j +@Schema(description = "位置基本信息") +public class JTPositionBaseInfo { + + /** + * 报警标志 + */ + @Schema(description = "报警标志") + private JTAlarmSign alarmSign; + + /** + * 状态 + */ + @Schema(description = "状态") + private JTStatus status; + + /** + * 经度 + */ + @Schema(description = "经度") + private Double longitude; + + /** + * 纬度 + */ + @Schema(description = "纬度") + private Double latitude; + + /** + * 高程 + */ + @Schema(description = "高程") + private Integer altitude; + + /** + * 速度 + */ + @Schema(description = "速度") + private Integer speed; + + /** + * 方向 + */ + @Schema(description = "方向") + private Integer direction; + + /** + * 时间 + */ + @Schema(description = "时间") + private String time; + + /** + * 视频报警 + */ + @Schema(description = "视频报警") + private JTVideoAlarm videoAlarm; + + public static JTPositionBaseInfo decode(ByteBuf buf) { + JTPositionBaseInfo positionInfo = new JTPositionBaseInfo(); + if (buf.readableBytes() < 17) { + log.error("[位置基本信息] 解码失败,长度不足: {}", buf.readableBytes()); + return positionInfo; + } + positionInfo.setAlarmSign(new JTAlarmSign(buf.readUnsignedInt())); + + positionInfo.setStatus(new JTStatus(buf.readUnsignedInt())); + + positionInfo.setLatitude(buf.readInt() * 0.000001D); + positionInfo.setLongitude(buf.readInt() * 0.000001D); + positionInfo.setAltitude(buf.readUnsignedShort()); + positionInfo.setSpeed(buf.readUnsignedShort()); + positionInfo.setDirection(buf.readUnsignedShort()); + byte[] timeBytes = new byte[6]; + buf.readBytes(timeBytes); + positionInfo.setTime(BCDUtil.transform(timeBytes)); + return positionInfo; + } + + + public String toSimpleString() { + return "简略位置汇报信息: " + + " \n 经度:" + longitude + + " \n 纬度:" + latitude + + " \n 高程: " + altitude + + " \n 速度: " + speed + + " \n 方向: " + direction + + " \n 时间: " + time + + " \n"; + } + + @Override + public String toString() { + return "位置汇报信息: " + + " \n 报警标志:" + alarmSign.toString() + + " \n 状态:" + status.toString() + + " \n 经度:" + longitude + + " \n 纬度:" + latitude + + " \n 高程: " + altitude + + " \n 速度: " + speed + + " \n 方向: " + direction + + " \n 时间: " + time + + " \n"; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/bean/JTPositionInfo.java b/src/main/java/com/genersoft/iot/vmp/jt1078/bean/JTPositionInfo.java new file mode 100644 index 0000000..c06db2d --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/bean/JTPositionInfo.java @@ -0,0 +1,29 @@ +package com.genersoft.iot.vmp.jt1078.bean; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; + +@Getter +@Schema(description = "位置信息") +public class JTPositionInfo { + + /** + * 位置基本信息 + */ + @Schema(description = "位置基本信息") + private JTPositionBaseInfo base; + + /** + * 位置基本信息 + */ + @Schema(description = "位置附加信息") + private JTPositionAdditionalInfo additional; + + public void setBase(JTPositionBaseInfo base) { + this.base = base; + } + + public void setAdditional(JTPositionAdditionalInfo additional) { + this.additional = additional; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/bean/JTQueryMediaDataCommand.java b/src/main/java/com/genersoft/iot/vmp/jt1078/bean/JTQueryMediaDataCommand.java new file mode 100644 index 0000000..4ffbf36 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/bean/JTQueryMediaDataCommand.java @@ -0,0 +1,67 @@ +package com.genersoft.iot.vmp.jt1078.bean; + +import com.genersoft.iot.vmp.jt1078.util.BCDUtil; +import com.genersoft.iot.vmp.utils.DateUtil; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; +import lombok.Setter; + +@Setter +@Getter +@Schema(description = "存储多媒体数据") +public class JTQueryMediaDataCommand { + + @Schema(description = "多媒体类型: 0:图像;1:音频;2:视频") + private int type; + + @Schema(description = "通道 ID, 0 表示检索该媒体类型的所有通道") + private int chanelId; + + @Schema(description = "事件项编码: 0:平台下发指令;1:定时动作;2:抢劫报警触发;3:碰 撞侧翻报警触发;其他保留") + private int event; + + @Schema(description = "开始时间") + private String startTime; + + @Schema(description = "结束时间") + private String endTime; + + @Schema(description = "删除标志, 0:保留;1:删除, 存储多媒体数据上传命令中使用") + private Integer delete; + + + public ByteBuf decode() { + ByteBuf byteBuf = Unpooled.buffer(); + byteBuf.writeByte(type); + byteBuf.writeByte(chanelId); + byteBuf.writeByte(event); + if (startTime == null) { + byteBuf.writeBytes(BCDUtil.strToBcd("000000000000")); + }else { + byteBuf.writeBytes(BCDUtil.strToBcd(DateUtil.yyyy_MM_dd_HH_mm_ssTo1078(startTime))); + } + if (endTime == null) { + byteBuf.writeBytes(BCDUtil.strToBcd("000000000000")); + }else { + byteBuf.writeBytes(BCDUtil.strToBcd(DateUtil.yyyy_MM_dd_HH_mm_ssTo1078(endTime))); + } + if (delete != null) { + byteBuf.writeByte(delete); + } + return byteBuf; + } + + @Override + public String toString() { + return "JTQueryMediaDataCommand{" + + "type=" + type + + ", chanelId=" + chanelId + + ", event=" + event + + ", startTime='" + startTime + '\'' + + ", endTime='" + endTime + '\'' + + ", delete='" + delete + '\'' + + '}'; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/bean/JTRecordDownloadCatch.java b/src/main/java/com/genersoft/iot/vmp/jt1078/bean/JTRecordDownloadCatch.java new file mode 100644 index 0000000..f81de53 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/bean/JTRecordDownloadCatch.java @@ -0,0 +1,41 @@ +package com.genersoft.iot.vmp.jt1078.bean; + +import com.genersoft.iot.vmp.jt1078.proc.response.J9206; +import lombok.Getter; +import lombok.Setter; +import org.jetbrains.annotations.NotNull; + +import java.util.concurrent.Delayed; +import java.util.concurrent.TimeUnit; + +public class JTRecordDownloadCatch implements Delayed { + + @Getter + @Setter + private String phoneNumber; + + @Getter + @Setter + private String path; + + @Getter + @Setter + private J9206 j9206; + + /** + * 超时时间(单位: 毫秒) + */ + @Getter + @Setter + private long delayTime; + + @Override + public long getDelay(@NotNull TimeUnit unit) { + return unit.convert(delayTime - System.currentTimeMillis(), TimeUnit.MILLISECONDS); + } + + @Override + public int compareTo(@NotNull Delayed o) { + return (int) (this.getDelay(TimeUnit.MILLISECONDS) - o.getDelay(TimeUnit.MILLISECONDS)); + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/bean/JTRectangleArea.java b/src/main/java/com/genersoft/iot/vmp/jt1078/bean/JTRectangleArea.java new file mode 100644 index 0000000..bda58a3 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/bean/JTRectangleArea.java @@ -0,0 +1,97 @@ +package com.genersoft.iot.vmp.jt1078.bean; + +import com.genersoft.iot.vmp.jt1078.util.BCDUtil; +import com.genersoft.iot.vmp.utils.DateUtil; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; +import lombok.Setter; + +import java.nio.charset.Charset; + +@Setter +@Getter +@Schema(description = "矩形区域") +public class JTRectangleArea implements JTAreaOrRoute{ + + @Schema(description = "区域 ID") + private long id; + + @Schema(description = "") + private JTAreaAttribute attribute; + + @Schema(description = "左上点纬度") + private Double latitudeForUpperLeft; + + @Schema(description = "左上点经度") + private Double longitudeForUpperLeft; + + @Schema(description = "右下点纬度") + private Double latitudeForLowerRight; + + @Schema(description = "右下点经度") + private Double longitudeForLowerRight; + + @Schema(description = "起始时间, yyyy-MM-dd HH:mm:ss") + private String startTime; + + @Schema(description = "结束时间, yyyy-MM-dd HH:mm:ss") + private String endTime; + + @Schema(description = "最高速度, 单位为千米每小时(km/h)") + private int maxSpeed; + + @Schema(description = "超速持续时间, 单位为秒(s)") + private int overSpeedDuration; + + @Schema(description = "夜间最高速度, 单位为千米每小时(km/h)") + private int nighttimeMaxSpeed; + + @Schema(description = "区域的名称") + private String name; + + + public ByteBuf encode(){ + ByteBuf byteBuf = Unpooled.buffer(); + byteBuf.writeInt((int) (id & 0xffffffffL)); + byteBuf.writeBytes(attribute.encode()); + byteBuf.writeInt((int) (Math.round((latitudeForUpperLeft * 1000000)) & 0xffffffffL)); + byteBuf.writeInt((int) (Math.round((longitudeForUpperLeft * 1000000)) & 0xffffffffL)); + byteBuf.writeInt((int) (Math.round((latitudeForLowerRight * 1000000)) & 0xffffffffL)); + byteBuf.writeInt((int) (Math.round((longitudeForLowerRight * 1000000)) & 0xffffffffL)); + byteBuf.writeBytes(BCDUtil.strToBcd(DateUtil.yyyy_MM_dd_HH_mm_ssTo1078(startTime))); + byteBuf.writeBytes(BCDUtil.strToBcd(DateUtil.yyyy_MM_dd_HH_mm_ssTo1078(endTime))); + byteBuf.writeShort((short)(maxSpeed & 0xffff)); + byteBuf.writeByte(overSpeedDuration); + byteBuf.writeShort((short)(nighttimeMaxSpeed & 0xffff)); + byteBuf.writeShort((short)(name.getBytes(Charset.forName("GBK")).length & 0xffff)); + byteBuf.writeCharSequence(name, Charset.forName("GBK")); + return byteBuf; + } + + public static JTRectangleArea decode(ByteBuf buf) { + JTRectangleArea area = new JTRectangleArea(); + area.setId(buf.readUnsignedInt()); + int attributeInt = buf.readUnsignedShort(); + JTAreaAttribute areaAttribute = JTAreaAttribute.decode(attributeInt); + area.setAttribute(areaAttribute); + area.setLatitudeForUpperLeft(buf.readUnsignedInt()/1000000D); + area.setLongitudeForUpperLeft(buf.readUnsignedInt()/1000000D); + area.setLatitudeForLowerRight(buf.readUnsignedInt()/1000000D); + area.setLongitudeForLowerRight(buf.readUnsignedInt()/1000000D); + byte[] startTimeBytes = new byte[6]; + buf.readBytes(startTimeBytes); + area.setStartTime(DateUtil.jt1078Toyyyy_MM_dd_HH_mm_ss(BCDUtil.transform(startTimeBytes))); + byte[] endTimeBytes = new byte[6]; + buf.readBytes(endTimeBytes); + area.setEndTime(DateUtil.jt1078Toyyyy_MM_dd_HH_mm_ss(BCDUtil.transform(endTimeBytes))); + area.setMaxSpeed(buf.readUnsignedShort()); + area.setOverSpeedDuration(buf.readUnsignedByte()); + area.setNighttimeMaxSpeed(buf.readUnsignedShort()); + int nameLength = buf.readUnsignedShort(); + area.setName(buf.readCharSequence(nameLength, Charset.forName("GBK")).toString().trim()); + return area; + } + +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/bean/JTRoute.java b/src/main/java/com/genersoft/iot/vmp/jt1078/bean/JTRoute.java new file mode 100644 index 0000000..ffc7802 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/bean/JTRoute.java @@ -0,0 +1,90 @@ +package com.genersoft.iot.vmp.jt1078.bean; + +import com.genersoft.iot.vmp.jt1078.util.BCDUtil; +import com.genersoft.iot.vmp.utils.DateUtil; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; +import lombok.Setter; + +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.List; + +@Setter +@Getter +@Schema(description = "路线") +public class JTRoute implements JTAreaOrRoute{ + + @Schema(description = "路线 ID") + private long id; + + @Schema(description = "路线属性") + private JTRouteAttribute attribute; + + @Schema(description = "起始时间, yyyy-MM-dd HH:mm:ss") + private String startTime; + + @Schema(description = "结束时间, yyyy-MM-dd HH:mm:ss") + private String endTime; + + @Schema(description = "路线拐点") + private List routePointList; + + @Schema(description = "区域的名称") + private String name; + + public ByteBuf encode(){ + ByteBuf byteBuf = Unpooled.buffer(); + byteBuf.writeInt((int) (id & 0xffffffffL)); + byteBuf.writeBytes(attribute.encode()); + byteBuf.writeBytes(BCDUtil.strToBcd(DateUtil.yyyy_MM_dd_HH_mm_ssTo1078(startTime))); + byteBuf.writeBytes(BCDUtil.strToBcd(DateUtil.yyyy_MM_dd_HH_mm_ssTo1078(endTime))); + byteBuf.writeShort((short)(routePointList.size() & 0xffff)); + if (!routePointList.isEmpty()){ + for (JTRoutePoint jtRoutePoint : routePointList) { + byteBuf.writeBytes(jtRoutePoint.encode()); + } + } + byteBuf.writeShort((short)(name.getBytes(Charset.forName("GBK")).length & 0xffff)); + byteBuf.writeCharSequence(name, Charset.forName("GBK")); + return byteBuf; + } + + public static JTRoute decode(ByteBuf buf) { + JTRoute route = new JTRoute(); + route.setId(buf.readUnsignedInt()); + int attributeInt = buf.readUnsignedShort(); + JTRouteAttribute routeAttribute = JTRouteAttribute.decode(attributeInt); + route.setAttribute(routeAttribute); + byte[] startTimeBytes = new byte[6]; + buf.readBytes(startTimeBytes); + route.setStartTime(DateUtil.jt1078Toyyyy_MM_dd_HH_mm_ss(BCDUtil.transform(startTimeBytes))); + byte[] endTimeBytes = new byte[6]; + buf.readBytes(endTimeBytes); + route.setEndTime(DateUtil.jt1078Toyyyy_MM_dd_HH_mm_ss(BCDUtil.transform(endTimeBytes))); + + int routePointsSize = buf.readUnsignedShort(); + List jtRoutePoints = new ArrayList<>(routePointsSize); + for (int i = 0; i < routePointsSize; i++) { + jtRoutePoints.add(JTRoutePoint.decode(buf)); + } + route.setRoutePointList(jtRoutePoints); + int nameLength = buf.readUnsignedShort(); + route.setName(buf.readCharSequence(nameLength, Charset.forName("GBK")).toString().trim()); + return route; + } + + @Override + public String toString() { + return "JTRoute{" + + "id=" + id + + ", attribute=" + attribute + + ", startTime='" + startTime + '\'' + + ", endTime='" + endTime + '\'' + + ", routePointList=" + routePointList + + ", name='" + name + '\'' + + '}'; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/bean/JTRouteAttribute.java b/src/main/java/com/genersoft/iot/vmp/jt1078/bean/JTRouteAttribute.java new file mode 100644 index 0000000..f1aaecf --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/bean/JTRouteAttribute.java @@ -0,0 +1,71 @@ +package com.genersoft.iot.vmp.jt1078.bean; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; +import lombok.Setter; + +@Setter +@Getter +@Schema(description = "路线属性") +public class JTRouteAttribute { + + @Schema(description = "是否启用起始时间与结束时间的判断规则 ,false:否;true:是") + private boolean ruleForTimeLimit; + + @Schema(description = "进区域是否报警给驾驶员,false:否;true:是") + private boolean ruleForAlarmToDriverWhenEnter; + + @Schema(description = "进区域是否报警给平台 ,false:否;true:是") + private boolean ruleForAlarmToPlatformWhenEnter; + + @Schema(description = "出区域是否报警给驾驶员,false:否;true:是") + private boolean ruleForAlarmToDriverWhenExit; + + @Schema(description = "出区域是否报警给平台 ,false:否;true:是") + private boolean ruleForAlarmToPlatformWhenExit; + + public ByteBuf encode(){ + ByteBuf byteBuf = Unpooled.buffer(); + short content = 0; + if (ruleForTimeLimit) { + content |= 1; + } + if (ruleForAlarmToDriverWhenEnter) { + content |= (1 << 2); + } + if (ruleForAlarmToPlatformWhenEnter) { + content |= (1 << 3); + } + if (ruleForAlarmToDriverWhenExit) { + content |= (1 << 4); + } + if (ruleForAlarmToPlatformWhenExit) { + content |= (1 << 5); + } + byteBuf.writeShort((short)(content & 0xffff)); + return byteBuf; + } + + public static JTRouteAttribute decode(int attributeInt) { + JTRouteAttribute attribute = new JTRouteAttribute(); + attribute.setRuleForTimeLimit((attributeInt & 1) == 1); + attribute.setRuleForAlarmToDriverWhenEnter((attributeInt >> 2 & 1) == 1); + attribute.setRuleForAlarmToPlatformWhenEnter((attributeInt >> 3 & 1) == 1); + attribute.setRuleForAlarmToDriverWhenExit((attributeInt >> 4 & 1) == 1); + attribute.setRuleForAlarmToPlatformWhenExit((attributeInt >> 5 & 1) == 1); + return attribute; + } + + @Override + public String toString() { + return "JTRouteAttribute{" + + "ruleForTimeLimit=" + ruleForTimeLimit + + ", ruleForAlarmToDriverWhenEnter=" + ruleForAlarmToDriverWhenEnter + + ", ruleForAlarmToPlatformWhenEnter=" + ruleForAlarmToPlatformWhenEnter + + ", ruleForAlarmToDriverWhenExit=" + ruleForAlarmToDriverWhenExit + + ", ruleForAlarmToPlatformWhenExit=" + ruleForAlarmToPlatformWhenExit + + '}'; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/bean/JTRoutePoint.java b/src/main/java/com/genersoft/iot/vmp/jt1078/bean/JTRoutePoint.java new file mode 100644 index 0000000..c494028 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/bean/JTRoutePoint.java @@ -0,0 +1,98 @@ +package com.genersoft.iot.vmp.jt1078.bean; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; +import lombok.Setter; + +@Setter +@Getter +@Schema(description = "路线拐点") +public class JTRoutePoint { + + @Schema(description = "拐点 ID") + private long id; + + @Schema(description = "路段 ID") + private long routeSectionId; + + @Schema(description = "拐点纬度") + private Double latitude; + + @Schema(description = "拐点经度") + private Double longitude; + + @Schema(description = "路段宽度") + private int routeSectionAttributeWidth; + + @Schema(description = "路段属性") + private JTRouteSectionAttribute routeSectionAttribute; + + @Schema(description = "路段行驶过长國值") + private int routeSectionMaxLength; + + @Schema(description = "路段行驶不足國值") + private int routeSectionMinLength; + + @Schema(description = "路段最高速度") + private int routeSectionMaxSpeed; + + @Schema(description = "路段超速持续时间") + private int routeSectionOverSpeedDuration; + + @Schema(description = "路段夜间最高速度") + private int routeSectionNighttimeMaxSpeed; + + public ByteBuf encode(){ + ByteBuf byteBuf = Unpooled.buffer(); + byteBuf.writeInt((int) (id & 0xffffffffL)); + byteBuf.writeInt((int) (routeSectionId & 0xffffffffL)); + byteBuf.writeInt((int) (Math.round((latitude * 1000000)) & 0xffffffffL)); + byteBuf.writeInt((int) (Math.round((longitude * 1000000)) & 0xffffffffL)); + byteBuf.writeByte(routeSectionAttributeWidth); + byteBuf.writeByte(routeSectionAttribute.encode()); + byteBuf.writeShort((short)(routeSectionMaxLength & 0xffff)); + byteBuf.writeShort((short)(routeSectionMinLength & 0xffff)); + byteBuf.writeShort((short)(routeSectionMaxSpeed & 0xffff)); + byteBuf.writeByte(routeSectionOverSpeedDuration); + byteBuf.writeShort((short)(routeSectionNighttimeMaxSpeed & 0xffff)); + return byteBuf; + } + + public static JTRoutePoint decode(ByteBuf buf) { + JTRoutePoint point = new JTRoutePoint(); + point.setId(buf.readUnsignedInt()); + point.setRouteSectionId(buf.readUnsignedInt()); + point.setLatitude(buf.readUnsignedInt()/1000000D); + point.setLongitude(buf.readUnsignedInt()/1000000D); + point.setRouteSectionAttributeWidth(buf.readUnsignedByte()); + + JTRouteSectionAttribute areaAttribute = JTRouteSectionAttribute.decode(buf.readUnsignedByte()); + point.setRouteSectionAttribute(areaAttribute); + + point.setRouteSectionMaxLength(buf.readUnsignedShort()); + point.setRouteSectionMinLength(buf.readUnsignedShort()); + point.setRouteSectionMaxSpeed(buf.readUnsignedShort()); + point.setRouteSectionOverSpeedDuration(buf.readUnsignedByte()); + point.setRouteSectionNighttimeMaxSpeed(buf.readUnsignedShort()); + return point; + } + + @Override + public String toString() { + return "JTRoutePoint{" + + "id=" + id + + ", routeSectionId=" + routeSectionId + + ", latitude=" + latitude + + ", longitude=" + longitude + + ", routeSectionAttributeWidth=" + routeSectionAttributeWidth + + ", routeSectionAttribute=" + routeSectionAttribute + + ", routeSectionMaxLength=" + routeSectionMaxLength + + ", routeSectionMinLength=" + routeSectionMinLength + + ", routeSectionMaxSpeed=" + routeSectionMaxSpeed + + ", routeSectionOverSpeedDuration=" + routeSectionOverSpeedDuration + + ", routeSectionNighttimeMaxSpeed=" + routeSectionNighttimeMaxSpeed + + '}'; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/bean/JTRouteSectionAttribute.java b/src/main/java/com/genersoft/iot/vmp/jt1078/bean/JTRouteSectionAttribute.java new file mode 100644 index 0000000..3113f4e --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/bean/JTRouteSectionAttribute.java @@ -0,0 +1,52 @@ +package com.genersoft.iot.vmp.jt1078.bean; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; +import lombok.Setter; + +@Setter +@Getter +@Schema(description = "路段属性") +public class JTRouteSectionAttribute { + + @Schema(description = "行驶时间 ,false:否;true:是") + private boolean ruleForTimeLimit; + + @Schema(description = "限速 ,false:否;true:是") + private boolean ruleForSpeedLimit; + + @Schema(description = "false:北纬;true:南纬") + private boolean southLatitude; + + @Schema(description = "false:东经;true:西经") + private boolean westLongitude; + + public byte encode(){ + byte attributeByte = 0; + if (ruleForTimeLimit) { + attributeByte |= 1; + } + if (ruleForSpeedLimit) { + attributeByte |= (1 << 1); + } + if (southLatitude) { + attributeByte |= (1 << 2); + } + if (westLongitude) { + attributeByte |= (1 << 3); + } + return attributeByte; + } + + public static JTRouteSectionAttribute decode(short attributeShort) { + JTRouteSectionAttribute attribute = new JTRouteSectionAttribute(); + attribute.setRuleForTimeLimit((attributeShort & 1) == 1); + attribute.setRuleForSpeedLimit((attributeShort >> 1 & 1) == 1); + attribute.setSouthLatitude((attributeShort >> 2 & 1) == 1); + attribute.setWestLongitude((attributeShort >> 3 & 1) == 1); + return attribute; + } + +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/bean/JTShootingCommand.java b/src/main/java/com/genersoft/iot/vmp/jt1078/bean/JTShootingCommand.java new file mode 100644 index 0000000..9794f3b --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/bean/JTShootingCommand.java @@ -0,0 +1,85 @@ +package com.genersoft.iot.vmp.jt1078.bean; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +@Schema(description = "拍摄命令参数") +public class JTShootingCommand { + + @Schema(description = "通道 ID") + private int chanelId; + + @Schema(description = "0:停止拍摄;0xFFFF:录像;其他:拍照张数") + private int command; + + @Schema(description = "拍照间隔/录像时间, 单位为秒(s) ,0 表示按最小间隔拍照或一直录像") + private int time; + + @Schema(description = "1:保存; 0:实时上传") + private int save; + + @Schema(description = "分辨率: " + + "0x00:最低分辨率" + + "0x01:320 x240;" + + "0x02:640 x480;" + + "0x03:800 x600;" + + "0x04:1024 x768;" + + "0x05:176 x144;" + + "0x06:352 x288;" + + "0x07:704 x288;" + + "0x08:704 x576;" + + "0xff:最高分辨率") + private int resolvingPower; + + @Schema(description = "图像/视频质量: 取值范围为 1 ~ 10 ,1 代表质量损失最小 ,10 表示压缩 比最大") + private int quality; + + @Schema(description = "亮度, 0 ~ 255") + private int brightness; + + @Schema(description = "对比度,0 ~ 127") + private int contrastRatio; + + @Schema(description = "饱和度,0 ~ 127") + private int saturation; + + @Schema(description = "色度,0 ~ 255") + private int chroma; + + public ByteBuf decode() { + ByteBuf byteBuf = Unpooled.buffer(); + byteBuf.writeByte(chanelId); + byteBuf.writeShort((short)(command & 0xffff)); + byteBuf.writeShort((short)(time & 0xffff)); + byteBuf.writeByte(save); + byteBuf.writeByte(resolvingPower); + byteBuf.writeByte(quality); + byteBuf.writeByte(brightness); + byteBuf.writeByte(contrastRatio); + byteBuf.writeByte(saturation); + byteBuf.writeByte(chroma); + return byteBuf; + } + + @Override + public String toString() { + return "JTShootingCommand{" + + "chanelId=" + chanelId + + ", command=" + command + + ", time=" + time + + ", save=" + save + + ", resolvingPower=" + resolvingPower + + ", quality=" + quality + + ", brightness=" + brightness + + ", contrastRatio=" + contrastRatio + + ", saturation=" + saturation + + ", chroma=" + chroma + + '}'; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/bean/JTStatus.java b/src/main/java/com/genersoft/iot/vmp/jt1078/bean/JTStatus.java new file mode 100644 index 0000000..81174dc --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/bean/JTStatus.java @@ -0,0 +1,136 @@ +package com.genersoft.iot.vmp.jt1078.bean; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Data +@Schema(description = "状态信息") +public class JTStatus { + + @Schema(description = "false:ACC关;true: ACC开") + private boolean acc; + + @Schema(description = "false:未定位;true: 定位") + private boolean positioning; + + @Schema(description = "false:北纬;true: 南纬") + private boolean southLatitude; + + @Schema(description = "false:东经;true: 西经") + private boolean wesLongitude; + + @Schema(description = "false:运营状态;true: 停运状态") + private boolean outage; + + @Schema(description = "false:经纬度未经保密插件加密;true: 经纬度已经保密插件加密") + private boolean positionEncryption; + + + @Schema(description = "true: 紧急刹车系统采集的前撞预警") + private boolean warningFrontCrash; + + @Schema(description = "true: 车道偏移预警") + private boolean warningShifting; + + @Schema(description = "00:空车;01:半载;10:保留;11:满载。可表示客车的空载状态 ,重车及货车的空载、满载状态 ,该状态可由人工输入或传感器获取") + private int load; + + @Schema(description = "false:车辆油路正常;true: 车辆油路断开") + private boolean oilWayBreak; + + @Schema(description = "false:车辆电路正常;true: 车辆电路断开") + private boolean circuitBreak; + + @Schema(description = "false:车门解锁;true: 车门加锁") + private boolean doorLocking; + + @Schema(description = "false:门1 关;true: 门1 开(前门)") + private boolean door1Open; + + @Schema(description = "false:门2 关;true: 门2 开(中门)") + private boolean door2Open; + + @Schema(description = "false:门3 关;true: 门3 开(后门)") + private boolean door3Open; + + @Schema(description = "false:门4 关;true: 门4 开(驾驶席门)") + private boolean door4Open; + + @Schema(description = "false:门5 关;true: 门5 开(自定义)") + private boolean door5Open; + + @Schema(description = "false:未使用 GPS 卫星进行定位;true:使用 GPS 卫星进行定位") + private boolean gps; + + @Schema(description = "false:未使用北斗卫星进行定位;true:使用北斗卫星进行定位") + private boolean beidou; + + @Schema(description = "false:未使用GLONASS 卫星进行定位;true:使用GLONASS 卫星进行定位") + private boolean glonass; + + @Schema(description = "false:未使用GaLiLeo 卫星进行定位;true:使用GaLiLeo 卫星进行定位") + private boolean gaLiLeo; + + @Schema(description = "false:车辆处于停止状态;true:车辆处于行驶状态") + private boolean driving; + + public JTStatus() { + } + + public JTStatus(long statusInt) { + if (statusInt == 0) { + return; + } + this.acc = (statusInt & 1) == 1; + this.positioning = (statusInt >>> 1 & 1) == 1; + this.southLatitude = (statusInt >>> 2 & 1) == 1; + this.wesLongitude = (statusInt >>> 3 & 1) == 1; + this.outage = (statusInt >>> 4 & 1) == 1; + this.positionEncryption = (statusInt >>> 5 & 1) == 1; + this.warningFrontCrash = (statusInt >>> 6 & 1) == 1; + this.warningShifting = (statusInt >>> 7 & 1) == 1; + this.load = (int)(statusInt >>> 8 & 3); + this.oilWayBreak = (statusInt >>> 10 & 1) == 1; + this.circuitBreak = (statusInt >>> 11 & 1) == 1; + this.doorLocking = (statusInt >>> 12 & 1) == 1; + this.door1Open = (statusInt >>> 13 & 1) == 1; + this.door2Open = (statusInt >>> 14 & 1) == 1; + this.door3Open = (statusInt >>> 15 & 1) == 1; + this.door4Open = (statusInt >>> 16 & 1) == 1; + this.door5Open = (statusInt >>> 17 & 1) == 1; + this.gps = (statusInt >>> 18 & 1) == 1; + this.beidou = (statusInt >>> 19 & 1) == 1; + this.glonass = (statusInt >>> 20 & 1) == 1; + this.gaLiLeo = (statusInt >>> 21 & 1) == 1; + this.driving = (statusInt >>> 22 & 1) == 1; + } + + @Override + public String toString() { + return "状态位:" + + "\n acc状态:" + (acc?"开":"关") + + "\n 定位状态:" + (positioning?"定位":"未定位") + + "\n 南北纬:" + (southLatitude?"南纬":"北纬") + + "\n 东西经:" + (wesLongitude?"西经":"东经") + + "\n 运营状态:" + (outage?"停运":"运营") + + "\n 经纬度保密:" + (positionEncryption?"加密":"未加密") + + "\n 前撞预警:" + (warningFrontCrash?"紧急刹车系统采集的前撞预警":"无") + + "\n 车道偏移预警:" + (warningShifting?"车道偏移预警":"无") + + "\n 空/半/满载状态:" + (load == 0?"空车":(load == 1?"半载":(load == 3?"满载":"未定义状态"))) + + "\n 车辆油路状态:" + (oilWayBreak?"车辆油路断开":"车辆油路正常") + + "\n 车辆电路状态:" + (circuitBreak?"车辆电路断开":"车辆电路正常") + + "\n 门锁状态:" + (doorLocking?"车门加锁":"车门解锁") + + "\n 门1(前门)状态:" + (door1Open?"开":"关") + + "\n 门2(中门)状态:" + (door2Open?"开":"关") + + "\n 门3(后门)状态:" + (door3Open?"开":"关") + + "\n 门4(驾驶席门)状态:" + (door4Open?"开":"关") + + "\n 门5(自定义)状态:" + (door5Open?"开":"关") + + "\n GPS卫星定位状态: " + (gps?"使用":"未使用") + + "\n 北斗卫星定位状态: " + (beidou?"使用":"未使用") + + "\n GLONASS卫星定位状态: " + (glonass?"使用":"未使用") + + "\n GaLiLeo卫星定位状态: " + (gaLiLeo?"使用":"未使用") + + "\n GaLiLeo卫星定位状态: " + (gaLiLeo?"使用":"未使用") + + "\n 车辆行驶状态: " + (driving?"车辆行驶":"车辆停止") + + "\n "; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/bean/JTTextSign.java b/src/main/java/com/genersoft/iot/vmp/jt1078/bean/JTTextSign.java new file mode 100644 index 0000000..4bbcaf4 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/bean/JTTextSign.java @@ -0,0 +1,45 @@ +package com.genersoft.iot.vmp.jt1078.bean; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +/** + * 文本信息标志 + */ +@Data +@Schema(description = "文本信息标志") +public class JTTextSign { + + @Schema(description = "1紧急,2服务,3通知") + private int type; + + @Schema(description = "1终端显示器显示") + private boolean terminalDisplay; + + @Schema(description = "1广告屏显示") + private boolean adScreen; + + @Schema(description = "1终端 TTS 播读") + private boolean tts; + + @Schema(description = "false: 中心导航信息 true CAN故障码信息") + private boolean source; + + public byte encode(){ + byte byteSign = 0; + byteSign |= (byte) type; + if (terminalDisplay) { + byteSign |= (0x1 << 2); + } + if (tts) { + byteSign |= (0x1 << 3); + } + if (adScreen) { + byteSign |= (0x1 << 4); + } + if (source) { + byteSign |= (0x1 << 5); + } + return byteSign; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/bean/JTVehicleControl.java b/src/main/java/com/genersoft/iot/vmp/jt1078/bean/JTVehicleControl.java new file mode 100644 index 0000000..41fd574 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/bean/JTVehicleControl.java @@ -0,0 +1,33 @@ +package com.genersoft.iot.vmp.jt1078.bean; + +import com.genersoft.iot.vmp.jt1078.bean.common.ConfigAttribute; +import lombok.Getter; +import lombok.Setter; + +import java.util.Objects; + +/** + * 车辆控制类型 + */ +@Setter +@Getter +public class JTVehicleControl { + + private int length; + + private void setLength(Object value) { + if (Objects.isNull(value)) { + length--; + }else { + length ++; + } + } + + @ConfigAttribute(id = 0X0001, type="Byte", description = "车门, 0:车门锁闭 1:车门开启") + private Integer controlCarDoor; + + public void setControlCarDoor(Integer controlCarDoor) { + this.controlCarDoor = controlCarDoor; + setLength(controlCarDoor); + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/bean/JTVideoAlarm.java b/src/main/java/com/genersoft/iot/vmp/jt1078/bean/JTVideoAlarm.java new file mode 100644 index 0000000..e55f5f6 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/bean/JTVideoAlarm.java @@ -0,0 +1,92 @@ +package com.genersoft.iot.vmp.jt1078.bean; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; +import lombok.Setter; + +import java.util.ArrayList; +import java.util.List; + +@Setter +@Getter +@Schema(description = "视频报警上报") +public class JTVideoAlarm { + + @Schema(description = "视频信号丢失报警的通道") + private List videoLossChannels; + + @Schema(description = "视频信号遮挡报警的通道") + private List videoOcclusionChannels; + + @Schema(description = "存储器故障报警状态,第 1-12 个主存储器,12-15 分别表示第 1-4 个灾备存储装置") + private List storageFaultAlarm; + + @Schema(description = "异常驾驶行为-疲劳") + private boolean drivingForFatigue; + + @Schema(description = "异常驾驶行为-打电话") + private boolean drivingForCall; + + @Schema(description = "异常驾驶行为-抽烟") + private boolean drivingSmoking; + + @Schema(description = "其他视频设备故障") + private boolean otherDeviceFailure; + + @Schema(description = "客车超员报警") + private boolean overcrowding; + + @Schema(description = "特殊报警录像达到存储阈值报警") + private boolean specialRecordFull; + + public JTVideoAlarm() { + } + + public static JTVideoAlarm getInstance(int alarm, int loss, int occlusion, short storageFault, short driving) { + JTVideoAlarm jtVideoAlarm = new JTVideoAlarm(); + if (alarm == 0) { + return jtVideoAlarm; + } + boolean lossAlarm = (alarm & 1) == 1; + boolean occlusionAlarm = (alarm >>> 1 & 1) == 1; + boolean storageFaultAlarm = (alarm >>> 2 & 1) == 1; + jtVideoAlarm.setOtherDeviceFailure((alarm >>> 3 & 1) == 1); + jtVideoAlarm.setOvercrowding((alarm >>> 4 & 1) == 1); + boolean drivingAlarm = (alarm >>> 5 & 1) == 1; + jtVideoAlarm.setSpecialRecordFull((alarm >>> 6 & 1) == 1); + if (lossAlarm && loss > 0) { + List videoLossChannels = new ArrayList<>(); + for (int i = 0; i < 32; i++) { + if ((loss >>> i & 1) == 1 ) { + videoLossChannels.add(i); + } + } + jtVideoAlarm.setVideoLossChannels(videoLossChannels); + } + if (occlusionAlarm && occlusion > 0) { + List videoOcclusionChannels = new ArrayList<>(); + for (int i = 0; i < 32; i++) { + if ((occlusion >>> i & 1) == 1) { + videoOcclusionChannels.add(i); + } + } + jtVideoAlarm.setVideoOcclusionChannels(videoOcclusionChannels); + } + if (storageFaultAlarm && storageFault > 0) { + List storageFaultAlarmContent = new ArrayList<>(); + for (int i = 0; i < 16; i++) { + if ((storageFault >>> i & 1) == 1) { + storageFaultAlarmContent.add(i); + } + } + jtVideoAlarm.setStorageFaultAlarm(storageFaultAlarmContent); + } + if (drivingAlarm && driving > 0) { + jtVideoAlarm.setDrivingForFatigue((driving & 1) == 1 ); + jtVideoAlarm.setDrivingForCall((driving >>> 1 & 1) == 1 ); + jtVideoAlarm.setDrivingSmoking((driving >>> 2 & 1) == 1 ); + } + return jtVideoAlarm; + } + +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/bean/common/ConfigAttribute.java b/src/main/java/com/genersoft/iot/vmp/jt1078/bean/common/ConfigAttribute.java new file mode 100644 index 0000000..c6dde81 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/bean/common/ConfigAttribute.java @@ -0,0 +1,14 @@ +package com.genersoft.iot.vmp.jt1078.bean.common; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +@Retention(RetentionPolicy.RUNTIME) +public @interface ConfigAttribute { + + long id(); + + String type(); + + String description(); +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/bean/config/JTAlarmRecordingParam.java b/src/main/java/com/genersoft/iot/vmp/jt1078/bean/config/JTAlarmRecordingParam.java new file mode 100644 index 0000000..77d79ca --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/bean/config/JTAlarmRecordingParam.java @@ -0,0 +1,46 @@ +package com.genersoft.iot.vmp.jt1078.bean.config; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import lombok.Getter; +import lombok.Setter; + +/** + * 特殊报警录像参数 + */ +@Setter +@Getter +public class JTAlarmRecordingParam implements JTDeviceSubConfig{ + + /** + * 特殊报警录像存储阈值, 百分比,取值 特殊报警录像占用主存储器存储阈值百 1 ~ 99,默认值为 20 + */ + private int storageLimit; + + /** + * 特殊报警录像持续时间,特殊报警录像的最长持续时间,单位为分钟(min) ,默认值为 5 + */ + private int duration; + + /** + * 特殊报警标识起始时间, 特殊报警发生前进行标记的录像时间, 单位为分钟( min) ,默认值为 1 + */ + private int startTime; + + @Override + public ByteBuf encode() { + ByteBuf byteBuf = Unpooled.buffer(); + byteBuf.writeByte(storageLimit); + byteBuf.writeByte(duration); + byteBuf.writeByte(startTime); + return byteBuf; + } + + public static JTAlarmRecordingParam decode(ByteBuf byteBuf) { + JTAlarmRecordingParam alarmRecordingParam = new JTAlarmRecordingParam(); + alarmRecordingParam.setStorageLimit(byteBuf.readUnsignedByte()); + alarmRecordingParam.setDuration(byteBuf.readUnsignedByte()); + alarmRecordingParam.setStartTime(byteBuf.readUnsignedByte()); + return alarmRecordingParam; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/bean/config/JTAloneChanel.java b/src/main/java/com/genersoft/iot/vmp/jt1078/bean/config/JTAloneChanel.java new file mode 100644 index 0000000..22ceff3 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/bean/config/JTAloneChanel.java @@ -0,0 +1,136 @@ +package com.genersoft.iot.vmp.jt1078.bean.config; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import lombok.Getter; +import lombok.Setter; + +/** + * 单独通道视频 + */ +@Setter +@Getter +public class JTAloneChanel implements JTDeviceSubConfig{ + + /** + * 逻辑通道号 + */ + private int logicChannelId; + + /** + * 实时流编码模式 + * 0:CBR( 固定码率) ; + * 1:VBR( 可变码率) ; + * 2:ABR( 平均码率) ; + * 100 ~ 127:自定义 + */ + private int liveStreamCodeRateType; + + /** + * 实时流分辨率 + * 0:QCIF; + * 1:CIF; + * 2:WCIF; + * 3:D1; + * 4:WD1; + * 5:720P; + * 6:1 080P; + * 100 ~ 127:自定义 + */ + private int liveStreamResolving; + + /** + * 实时流关键帧间隔, 范围(1 ~ 1 000) 帧 + */ + private int liveStreamIInterval; + + /** + * 实时流目标帧率,范围(1 ~ 120) 帧 / s + */ + private int liveStreamFrameRate; + + /** + * 实时流目标码率,单位为千位每秒( kbps) + */ + private long liveStreamCodeRate; + + + /** + * 存储流编码模式 + * 0:CBR( 固定码率) ; + * 1:VBR( 可变码率) ; + * 2:ABR( 平均码率) ; + * 100 ~ 127:自定义 + */ + private int storageStreamCodeRateType; + + /** + * 存储流分辨率 + * 0:QCIF; + * 1:CIF; + * 2:WCIF; + * 3:D1; + * 4:WD1; + * 5:720P; + * 6:1 080P; + * 100 ~ 127:自定义 + */ + private int storageStreamResolving; + + /** + * 存储流关键帧间隔, 范围(1 ~ 1 000) 帧 + */ + private int storageStreamIInterval; + + /** + * 存储流目标帧率,范围(1 ~ 120) 帧 / s + */ + private int storageStreamFrameRate; + + /** + * 存储流目标码率,单位为千位每秒( kbps) + */ + private long storageStreamCodeRate; + + /** + * 字幕叠加设置 + */ + private JTOSDConfig osd; + + @Override + public ByteBuf encode() { + ByteBuf byteBuf = Unpooled.buffer(); + byteBuf.writeByte(logicChannelId); + byteBuf.writeByte(liveStreamCodeRateType); + byteBuf.writeByte(liveStreamResolving); + byteBuf.writeShort((short)(liveStreamIInterval & 0xffff)); + byteBuf.writeByte(liveStreamFrameRate); + byteBuf.writeInt((int) (liveStreamCodeRate & 0xffffffffL)); + + byteBuf.writeByte(storageStreamCodeRateType); + byteBuf.writeByte(storageStreamResolving); + byteBuf.writeShort((short)(storageStreamIInterval & 0xffff)); + byteBuf.writeByte(storageStreamFrameRate); + byteBuf.writeInt((int) (storageStreamCodeRate & 0xffffffffL)); + byteBuf.writeBytes(osd.encode()); + return byteBuf; + } + + public static JTAloneChanel decode(ByteBuf buf) { + JTAloneChanel jtAloneChanel = new JTAloneChanel(); + jtAloneChanel.setLogicChannelId(buf.readByte()); + jtAloneChanel.setLiveStreamCodeRateType(buf.readByte()); + jtAloneChanel.setLiveStreamResolving(buf.readByte()); + jtAloneChanel.setLiveStreamIInterval(buf.readUnsignedShort()); + jtAloneChanel.setLiveStreamFrameRate(buf.readByte()); + jtAloneChanel.setLiveStreamCodeRate(buf.readUnsignedInt()); + + jtAloneChanel.setStorageStreamCodeRateType(buf.readByte()); + jtAloneChanel.setStorageStreamResolving(buf.readByte()); + jtAloneChanel.setStorageStreamIInterval(buf.readUnsignedShort()); + jtAloneChanel.setStorageStreamFrameRate(buf.readByte()); + jtAloneChanel.setStorageStreamCodeRate(buf.readUnsignedInt()); + jtAloneChanel.setOsd(JTOSDConfig.decode(buf)); + return null; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/bean/config/JTAnalyzeAlarmParam.java b/src/main/java/com/genersoft/iot/vmp/jt1078/bean/config/JTAnalyzeAlarmParam.java new file mode 100644 index 0000000..6e97f1f --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/bean/config/JTAnalyzeAlarmParam.java @@ -0,0 +1,41 @@ +package com.genersoft.iot.vmp.jt1078.bean.config; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import lombok.Getter; +import lombok.Setter; + +/** + * 视频分析报警参数 + */ +@Setter +@Getter +public class JTAnalyzeAlarmParam implements JTDeviceSubConfig{ + + /** + * 车辆核载人数 + */ + private int numberForPeople; + + + /** + * 疲劳程度阈值 + */ + private int fatigueThreshold; + + + @Override + public ByteBuf encode() { + ByteBuf byteBuf = Unpooled.buffer(); + byteBuf.writeByte(numberForPeople); + byteBuf.writeByte(fatigueThreshold); + return byteBuf; + } + + public static JTAnalyzeAlarmParam decode(ByteBuf byteBuf) { + JTAnalyzeAlarmParam analyzeAlarmParam = new JTAnalyzeAlarmParam(); + analyzeAlarmParam.setNumberForPeople(byteBuf.readUnsignedByte()); + analyzeAlarmParam.setFatigueThreshold(byteBuf.readUnsignedByte()); + return analyzeAlarmParam; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/bean/config/JTAwakenParam.java b/src/main/java/com/genersoft/iot/vmp/jt1078/bean/config/JTAwakenParam.java new file mode 100644 index 0000000..53850b9 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/bean/config/JTAwakenParam.java @@ -0,0 +1,270 @@ +package com.genersoft.iot.vmp.jt1078.bean.config; + +import com.genersoft.iot.vmp.jt1078.util.BCDUtil; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import lombok.Getter; +import lombok.Setter; + +/** + * 终端休眠唤醒模式设置 + */ +@Setter +@Getter +public class JTAwakenParam implements JTDeviceSubConfig{ + + /** + * 休眠唤醒模式-条件唤醒 + */ + private boolean wakeUpModeByCondition; + + /** + * 休眠唤醒模式-定时唤醒 + */ + private boolean wakeUpModeByTime; + + /** + * 休眠唤醒模式-手动唤醒 + */ + private boolean wakeUpModeByManual; + + /** + * 唤醒条件类型-紧急报警 + */ + private boolean wakeUpConditionsByAlarm; + + /** + * 唤醒条件类型-碰撞侧翻报警 + */ + private boolean wakeUpConditionsByRollover; + + /** + * 唤醒条件类型-车辆开门 + */ + private boolean wakeUpConditionsByOpenTheDoor; + + /** + * 定时唤醒日设置-周一 + */ + private boolean awakeningDayForMonday; + + /** + * 定时唤醒日设置-周二 + */ + private boolean awakeningDayForTuesday; + + /** + * 定时唤醒日设置-周三 + */ + private boolean awakeningDayForWednesday; + + /** + * 定时唤醒日设置-周四 + */ + private boolean awakeningDayForThursday; + + /** + * 定时唤醒日设置-周五 + */ + private boolean awakeningDayForFriday; + + /** + * 定时唤醒日设置-周六 + */ + private boolean awakeningDayForSaturday; + + /** + * 定时唤醒日设置-周日 + */ + private boolean awakeningDayForSunday; + + /** + * 日定时唤醒-启用时间段1 + */ + private boolean time1Enable; + + /** + * 日定时唤醒-时间段1开始时间 + */ + private String time1StartTime; + + /** + * 日定时唤醒-时间段1结束时间 + */ + private String time1EndTime; + + /** + * 日定时唤醒-启用时间段2 + */ + private boolean time2Enable; + + /** + * 日定时唤醒-时间段2开始时间 + */ + private String time2StartTime; + + /** + * 日定时唤醒-时间段2结束时间 + */ + private String time2EndTime; + + /** + * 日定时唤醒-启用时间段3 + */ + private boolean time3Enable; + + /** + * 日定时唤醒-时间段3开始时间 + */ + private String time3StartTime; + + /** + * 日定时唤醒-时间段3结束时间 + */ + private String time3EndTime; + + /** + * 日定时唤醒-启用时间段4 + */ + private boolean time4Enable; + + /** + * 日定时唤醒-时间段4开始时间 + */ + private String time4StartTime; + + /** + * 日定时唤醒-时间段4结束时间 + */ + private String time4EndTime; + + @Override + public ByteBuf encode() { + ByteBuf byteBuf = Unpooled.buffer(); + byte wakeUpTypeByte = 0; + byte wakeUpConditionsByte = 0; + byte wakeDayByte = 0; + if (wakeUpModeByCondition) { + wakeUpTypeByte = (byte)(wakeUpTypeByte | 1); + } + if (wakeUpModeByTime) { + wakeUpTypeByte = (byte)(wakeUpTypeByte | (1 << 1)); + } + if (wakeUpModeByManual) { + wakeUpTypeByte = (byte)(wakeUpTypeByte | (1 << 2)); + } + byteBuf.writeByte(wakeUpTypeByte); + if (wakeUpConditionsByAlarm) { + wakeUpConditionsByte = (byte)(wakeUpConditionsByte | 1); + } + if (wakeUpConditionsByRollover) { + wakeUpConditionsByte = (byte)(wakeUpConditionsByte | (1 << 1)); + } + if (wakeUpConditionsByOpenTheDoor) { + wakeUpConditionsByte = (byte)(wakeUpConditionsByte | (1 << 2)); + } + byteBuf.writeByte(wakeUpConditionsByte); + if (awakeningDayForMonday) { + wakeDayByte = (byte)(wakeDayByte | 1); + } + if (awakeningDayForTuesday) { + wakeDayByte = (byte)(wakeDayByte | (1 << 1)); + } + if (awakeningDayForWednesday) { + wakeDayByte = (byte)(wakeDayByte | (1 << 2)); + } + if (awakeningDayForThursday) { + wakeDayByte = (byte)(wakeDayByte | (1 << 3)); + } + if (awakeningDayForFriday) { + wakeDayByte = (byte)(wakeDayByte | (1 << 4)); + } + if (awakeningDayForSaturday) { + wakeDayByte = (byte)(wakeDayByte | (1 << 5)); + } + if (awakeningDayForSunday) { + wakeDayByte = (byte)(wakeDayByte | (1 << 6)); + } + byteBuf.writeByte(wakeDayByte); + byte enableByte = 0; + if (time1Enable) { + enableByte = (byte)(enableByte | 1); + } + if (time2Enable) { + enableByte = (byte)(enableByte | (1 << 1)); + } + if (time3Enable) { + enableByte = (byte)(enableByte | (1 << 2)); + } + if (time4Enable) { + enableByte = (byte)(enableByte | (1 << 3)); + } + byteBuf.writeByte(enableByte); + byteBuf.writeBytes(transportTime(time1StartTime)); + byteBuf.writeBytes(transportTime(time1EndTime)); + byteBuf.writeBytes(transportTime(time2StartTime)); + byteBuf.writeBytes(transportTime(time2EndTime)); + byteBuf.writeBytes(transportTime(time3StartTime)); + byteBuf.writeBytes(transportTime(time3EndTime)); + byteBuf.writeBytes(transportTime(time4StartTime)); + byteBuf.writeBytes(transportTime(time4EndTime)); + return byteBuf; + } + + private byte[] transportTime(String time) { + return BCDUtil.strToBcd(time.replace(":", "")); + } + + public static JTAwakenParam decode(ByteBuf byteBuf) { + JTAwakenParam awakenParam = new JTAwakenParam(); + short wakeUpTypeByte = byteBuf.readUnsignedByte(); + awakenParam.wakeUpModeByCondition = ((wakeUpTypeByte & 1) == 1); + awakenParam.wakeUpModeByTime = ((wakeUpTypeByte >>> 1 & 1) == 1); + awakenParam.wakeUpModeByManual = ((wakeUpTypeByte >>> 2 & 1) == 1); + + short wakeUpConditionsByte = byteBuf.readUnsignedByte(); + awakenParam.wakeUpConditionsByAlarm = ((wakeUpConditionsByte & 1) == 1); + awakenParam.wakeUpConditionsByRollover = ((wakeUpConditionsByte >>> 1 & 1) == 1); + awakenParam.wakeUpConditionsByOpenTheDoor = ((wakeUpConditionsByte >>> 2 & 1) == 1); + + short wakeDayByte = byteBuf.readUnsignedByte(); + awakenParam.awakeningDayForMonday = ((wakeDayByte & 1) == 1); + awakenParam.awakeningDayForTuesday = ((wakeDayByte >>> 1 & 1) == 1); + awakenParam.awakeningDayForWednesday = ((wakeDayByte >>> 2 & 1) == 1); + awakenParam.awakeningDayForThursday = ((wakeDayByte >>> 3 & 1) == 1); + awakenParam.awakeningDayForFriday = ((wakeDayByte >>> 4 & 1) == 1); + awakenParam.awakeningDayForSaturday = ((wakeDayByte >>> 5 & 1) == 1); + awakenParam.awakeningDayForSunday = ((wakeDayByte >>> 6 & 1) == 1); + short enableByte = byteBuf.readUnsignedByte(); + awakenParam.time1Enable = ((enableByte & 1) == 1); + awakenParam.time2Enable = ((enableByte >>> 1 & 1) == 1); + awakenParam.time3Enable = ((enableByte >>> 2 & 1) == 1); + awakenParam.time4Enable = ((enableByte >>> 3 & 1) == 1); + byte[] timeBytes = new byte[2]; + byteBuf.readBytes(timeBytes); + awakenParam.time1StartTime = transportTime(timeBytes); + byteBuf.readBytes(timeBytes); + awakenParam.time1EndTime = transportTime(timeBytes); + + byteBuf.readBytes(timeBytes); + awakenParam.time2StartTime = transportTime(timeBytes); + byteBuf.readBytes(timeBytes); + awakenParam.time2EndTime = transportTime(timeBytes); + + byteBuf.readBytes(timeBytes); + awakenParam.time3StartTime = transportTime(timeBytes); + byteBuf.readBytes(timeBytes); + awakenParam.time3EndTime = transportTime(timeBytes); + + byteBuf.readBytes(timeBytes); + awakenParam.time4StartTime = transportTime(timeBytes); + byteBuf.readBytes(timeBytes); + awakenParam.time4EndTime = transportTime(timeBytes); + return awakenParam; + } + + private static String transportTime(byte[] timeBytes) { + String time1Str = BCDUtil.transform(timeBytes); + return time1Str.replace(time1Str.substring(0, 2), time1Str.substring(0, 2) + ":"); + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/bean/config/JTCameraTimer.java b/src/main/java/com/genersoft/iot/vmp/jt1078/bean/config/JTCameraTimer.java new file mode 100644 index 0000000..cfe4301 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/bean/config/JTCameraTimer.java @@ -0,0 +1,113 @@ +package com.genersoft.iot.vmp.jt1078.bean.config; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import lombok.Getter; +import lombok.Setter; + +/** + * 定时拍照控制 + */ +@Setter +@Getter +public class JTCameraTimer implements JTDeviceSubConfig{ + /** + * 摄像通道1 定时拍照开关标志 + */ + private boolean switchForChannel1; + /** + * 摄像通道2 定时拍照开关标志 + */ + private boolean switchForChannel2; + /** + * 摄像通道3 定时拍照开关标志 + */ + private boolean switchForChannel3; + /** + * 摄像通道4 定时拍照开关标志 + */ + private boolean switchForChannel4; + /** + * 摄像通道5 定时拍照开关标志 + */ + private boolean switchForChannel5; + + /** + * 摄像通道1 定时拍照存储标志, true: 上传, false: 存储 + */ + private boolean storageFlagsForChannel1; + + /** + * 摄像通道2 定时拍照存储标志 true: 上传, false: 存储 + */ + private boolean storageFlagsForChannel2; + + /** + * 摄像通道3 定时拍照存储标志 true: 上传, false: 存储 + */ + private boolean storageFlagsForChannel3; + + /** + * 摄像通道4 定时拍照存储标志 true: 上传, false: 存储 + */ + private boolean storageFlagsForChannel4; + + /** + * 摄像通道5 定时拍照存储标志 true: 上传, false: 存储 + */ + private boolean storageFlagsForChannel5; + + /** + * 定时时间单位,true: 分, false: 秒,当数值小于5s时,终端按5s处理 + */ + private boolean timeUnit; + + /** + * 定时时间间隔 + */ + private Integer timeInterval; + + @Override + public ByteBuf encode() { + ByteBuf byteBuf = Unpooled.buffer(); + byte[] bytes = new byte[4]; + bytes[0] = 0; + if (switchForChannel1) { + bytes[0] = (byte)(bytes[0] | 1); + } + if (switchForChannel2) { + bytes[0] = (byte)(bytes[0] | 2); + } + if (switchForChannel3) { + bytes[0] = (byte)(bytes[0] | 4); + } + if (switchForChannel4) { + bytes[0] = (byte)(bytes[0] | 8); + } + if (switchForChannel5) { + bytes[0] = (byte)(bytes[0] | 16); + } + bytes[1] = 0; + if (storageFlagsForChannel1) { + bytes[1] = (byte)(bytes[1] | 1); + } + if (storageFlagsForChannel2) { + bytes[1] = (byte)(bytes[1] | 2); + } + if (storageFlagsForChannel3) { + bytes[1] = (byte)(bytes[1] | 4); + } + if (storageFlagsForChannel4) { + bytes[1] = (byte)(bytes[1] | 8); + } + if (storageFlagsForChannel5) { + bytes[1] = (byte)(bytes[1] | 16); + } + bytes[3] = (byte)(timeInterval & 0xfe); + if (timeUnit) { + bytes[3] = (byte)(bytes[3] | 1); + } + byteBuf.writeBytes(bytes); + return byteBuf; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/bean/config/JTChanelConfig.java b/src/main/java/com/genersoft/iot/vmp/jt1078/bean/config/JTChanelConfig.java new file mode 100644 index 0000000..860dc82 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/bean/config/JTChanelConfig.java @@ -0,0 +1,56 @@ +package com.genersoft.iot.vmp.jt1078.bean.config; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import lombok.Getter; +import lombok.Setter; + +/** + * 音视频通道 + */ +@Setter +@Getter +public class JTChanelConfig implements JTDeviceSubConfig{ + + /** + * 物理通道号 单独 + */ + private int physicalChannelId; + + /** + * 逻辑通道号 + */ + private int logicChannelId; + + /** + * 通道类型: + * 0:音视频; + * 1:音频 + * 2:视频 + */ + private int channelType; + /** + * 是否连接云台: 通道类型为 0 和 2 时,此字段有效 + * 0:未连接;1:连接 + */ + private int ptzEnable; + + @Override + public ByteBuf encode() { + ByteBuf byteBuf = Unpooled.buffer(); + byteBuf.writeByte(physicalChannelId); + byteBuf.writeByte(logicChannelId); + byteBuf.writeByte(channelType); + byteBuf.writeByte(ptzEnable); + return byteBuf; + } + + public static JTChanelConfig decode(ByteBuf byteBuf) { + JTChanelConfig jtChanel = new JTChanelConfig(); + jtChanel.setPhysicalChannelId(byteBuf.readUnsignedByte()); + jtChanel.setLogicChannelId(byteBuf.readUnsignedByte()); + jtChanel.setChannelType(byteBuf.readUnsignedByte()); + jtChanel.setPtzEnable(byteBuf.readUnsignedByte()); + return jtChanel; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/bean/config/JTChannelListParam.java b/src/main/java/com/genersoft/iot/vmp/jt1078/bean/config/JTChannelListParam.java new file mode 100644 index 0000000..ff03faa --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/bean/config/JTChannelListParam.java @@ -0,0 +1,61 @@ +package com.genersoft.iot.vmp.jt1078.bean.config; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import lombok.Getter; +import lombok.Setter; + +import java.util.ArrayList; +import java.util.List; + +/** + * 音视频通道列表设置 + */ +@Setter +@Getter +public class JTChannelListParam implements JTDeviceSubConfig{ + + /** + * 音视频通道总数 + */ + private int videoAndAudioCount; + + /** + * 音频通道总数 + */ + private int audioCount; + + /** + * 视频通道总数 + */ + private int videoCount; + + private List chanelList; + + + @Override + public ByteBuf encode() { + ByteBuf byteBuf = Unpooled.buffer(); + byteBuf.writeByte(videoAndAudioCount); + byteBuf.writeByte(audioCount); + byteBuf.writeByte(videoCount); + for (JTChanelConfig jtChanel : chanelList) { + byteBuf.writeBytes(jtChanel.encode()); + } + return byteBuf; + } + + public static JTChannelListParam decode(ByteBuf byteBuf) { + JTChannelListParam channelListParam = new JTChannelListParam(); + channelListParam.setVideoAndAudioCount(byteBuf.readUnsignedByte()); + channelListParam.setAudioCount(byteBuf.readUnsignedByte()); + channelListParam.setVideoCount(byteBuf.readUnsignedByte()); + int total = channelListParam.getVideoAndAudioCount() + channelListParam.getVideoCount() + channelListParam.getAudioCount(); + List chanelList = new ArrayList<>(total); + for (int i = 0; i < total; i++) { + chanelList.add(JTChanelConfig.decode(byteBuf)); + } + channelListParam.setChanelList(chanelList); + return channelListParam; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/bean/config/JTChannelParam.java b/src/main/java/com/genersoft/iot/vmp/jt1078/bean/config/JTChannelParam.java new file mode 100644 index 0000000..7efc710 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/bean/config/JTChannelParam.java @@ -0,0 +1,46 @@ +package com.genersoft.iot.vmp.jt1078.bean.config; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import lombok.Getter; +import lombok.Setter; + +import java.util.ArrayList; +import java.util.List; + +/** + * 单独视频通道参数设置 + */ +@Setter +@Getter +public class JTChannelParam implements JTDeviceSubConfig { + + /** + * 单独通道视频参数设置列表 + */ + private List jtAloneChanelList; + + @Override + public ByteBuf encode() { + ByteBuf byteBuf = Unpooled.buffer(); + byteBuf.writeByte(jtAloneChanelList.size()); + for (JTAloneChanel jtAloneChanel : jtAloneChanelList) { + if (jtAloneChanel == null) { + continue; + } + byteBuf.writeBytes(jtAloneChanel.encode()); + } + return byteBuf; + } + + public static JTChannelParam decode(ByteBuf byteBuf) { + JTChannelParam channelParam = new JTChannelParam(); + int length = byteBuf.readUnsignedByte(); + List jtAloneChanelList = new ArrayList<>(length); + for (int i = 0; i < length; i++) { + jtAloneChanelList.add(JTAloneChanel.decode(byteBuf)); + } + channelParam.setJtAloneChanelList(jtAloneChanelList); + return channelParam; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/bean/config/JTCollisionAlarmParams.java b/src/main/java/com/genersoft/iot/vmp/jt1078/bean/config/JTCollisionAlarmParams.java new file mode 100644 index 0000000..6d8c609 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/bean/config/JTCollisionAlarmParams.java @@ -0,0 +1,34 @@ +package com.genersoft.iot.vmp.jt1078.bean.config; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import lombok.Getter; +import lombok.Setter; + +/** + * 碰撞报警参数设置 + */ +@Setter +@Getter +public class JTCollisionAlarmParams implements JTDeviceSubConfig{ + + /** + * 碰撞时间 单位为毫秒(ms) + */ + private int collisionAlarmTime; + + /** + * 碰撞加速度 单位为0.1g,设置范围为0~79,默认为10 + */ + private int collisionAcceleration; + + @Override + public ByteBuf encode() { + ByteBuf byteBuf = Unpooled.buffer(); + byte[] bytes = new byte[2]; + bytes[0] = (byte) (collisionAlarmTime & 0xff); + bytes[1] = (byte) (collisionAcceleration & 0xff); + byteBuf.writeBytes(bytes); + return byteBuf; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/bean/config/JTDeviceSubConfig.java b/src/main/java/com/genersoft/iot/vmp/jt1078/bean/config/JTDeviceSubConfig.java new file mode 100644 index 0000000..96d3015 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/bean/config/JTDeviceSubConfig.java @@ -0,0 +1,7 @@ +package com.genersoft.iot.vmp.jt1078.bean.config; + +import io.netty.buffer.ByteBuf; + +public interface JTDeviceSubConfig { + ByteBuf encode(); +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/bean/config/JTGnssPositioningMode.java b/src/main/java/com/genersoft/iot/vmp/jt1078/bean/config/JTGnssPositioningMode.java new file mode 100644 index 0000000..79b9e70 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/bean/config/JTGnssPositioningMode.java @@ -0,0 +1,52 @@ +package com.genersoft.iot.vmp.jt1078.bean.config; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import lombok.Getter; +import lombok.Setter; + +/** + * GNSS 定位模式 + */ +@Setter +@Getter +public class JTGnssPositioningMode implements JTDeviceSubConfig{ + + /** + * GPS 定位 true: 开启, false: 关闭 + */ + private boolean gps; + /** + * 北斗定位 true: 开启, false: 关闭 + */ + private boolean beidou; + /** + * GLONASS定位 true: 开启, false: 关闭 + */ + private boolean glonass; + /** + * GaLiLeo定位 true: 开启, false: 关闭 + */ + private boolean gaLiLeo; + + @Override + public ByteBuf encode() { + ByteBuf byteBuf = Unpooled.buffer(); + byte[] bytes = new byte[1]; + bytes[0] = 0; + if (gps) { + bytes[0] = (byte)(bytes[0] | 1); + } + if (beidou) { + bytes[0] = (byte)(bytes[0] | 2); + } + if (glonass) { + bytes[0] = (byte)(bytes[0] | 4); + } + if (gaLiLeo) { + bytes[0] = (byte)(bytes[0] | 8); + } + byteBuf.writeBytes(bytes); + return byteBuf; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/bean/config/JTIllegalDrivingPeriods.java b/src/main/java/com/genersoft/iot/vmp/jt1078/bean/config/JTIllegalDrivingPeriods.java new file mode 100644 index 0000000..297a488 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/bean/config/JTIllegalDrivingPeriods.java @@ -0,0 +1,37 @@ +package com.genersoft.iot.vmp.jt1078.bean.config; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import lombok.Getter; +import lombok.Setter; + +/** + * 违规行驶时段范围 ,精确到分 + */ +@Setter +@Getter +public class JTIllegalDrivingPeriods implements JTDeviceSubConfig{ + /** + * 违规行驶时段-开始时间 HH:mm + */ + private String startTime; + + /** + * 违规行驶时段-结束时间 HH:mm + */ + private String endTime; + + @Override + public ByteBuf encode() { + ByteBuf byteBuf = Unpooled.buffer(); + byte[] bytes = new byte[4]; + String[] startTimeArray = startTime.split(":"); + String[] endTimeArray = endTime.split(":"); + bytes[0] = (byte)Integer.parseInt(startTimeArray[0]); + bytes[1] = (byte)Integer.parseInt(startTimeArray[1]); + bytes[2] = (byte)Integer.parseInt(endTimeArray[0]); + bytes[3] = (byte)Integer.parseInt(endTimeArray[1]); + byteBuf.writeBytes(bytes); + return byteBuf; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/bean/config/JTOSDConfig.java b/src/main/java/com/genersoft/iot/vmp/jt1078/bean/config/JTOSDConfig.java new file mode 100644 index 0000000..2bfde12 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/bean/config/JTOSDConfig.java @@ -0,0 +1,92 @@ +package com.genersoft.iot.vmp.jt1078.bean.config; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import lombok.Getter; +import lombok.Setter; + +/** + * OSD字幕叠加设置 + */ +@Setter +@Getter +public class JTOSDConfig { + + /** + * 日期和时间 + */ + private boolean time; + + /** + * 车牌号码 + */ + private boolean licensePlate; + + /** + * 逻辑通道号 + */ + private boolean channelId; + + /** + * 经纬度 + */ + private boolean position; + + /** + * 行驶记录速度 + */ + private boolean speed; + + /** + * 卫星定位速度 + */ + private boolean speedForGPS; + + /** + * 连续驾驶时间 + */ + private boolean drivingTime; + + public ByteBuf encode(){ + ByteBuf byteBuf = Unpooled.buffer(); + byte content = 0; + if (time) { + content = (byte)(content | 1); + } + if (licensePlate) { + content = (byte)(content | (1 << 1)); + } + if (channelId) { + content = (byte)(content | (1 << 2)); + } + if (position) { + content = (byte)(content | (1 << 3)); + } + if (speed) { + content = (byte)(content | (1 << 4)); + } + if (speedForGPS) { + content = (byte)(content | (1 << 5)); + } + if (drivingTime) { + content = (byte)(content | (1 << 6)); + } + byteBuf.writeByte(content); + byteBuf.writeByte(0); + return byteBuf; + + } + + public static JTOSDConfig decode(ByteBuf buf) { + JTOSDConfig config = new JTOSDConfig(); + int content = buf.readUnsignedShort(); + config.setTime((content & 1) == 1); + config.setLicensePlate((content >>> 1 & 1) == 1); + config.setChannelId((content >>> 2 & 1) == 1); + config.setPosition((content >>> 3 & 1) == 1); + config.setSpeed((content >>> 4 & 1) == 1); + config.setSpeedForGPS((content >>> 5 & 1) == 1); + config.setDrivingTime((content >>> 6 & 1) == 1); + return config; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/bean/config/JTVideoAlarmBit.java b/src/main/java/com/genersoft/iot/vmp/jt1078/bean/config/JTVideoAlarmBit.java new file mode 100644 index 0000000..27a52b3 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/bean/config/JTVideoAlarmBit.java @@ -0,0 +1,143 @@ +package com.genersoft.iot.vmp.jt1078.bean.config; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; + +/** + * 视频报警标志位 + */ +public class JTVideoAlarmBit implements JTDeviceSubConfig{ + + /** + * 视频信号丢失报警 + */ + private boolean lossSignal; + /** + * 视频信号遮挡报警 + */ + private boolean occlusionSignal; + /** + * 存储单元故障报警 + */ + private boolean storageFault; + /** + * 其他视频设备故障报警 + */ + private boolean otherDeviceFailure; + /** + * 客车超员报警 + */ + private boolean overcrowding; + /** + * 异常驾驶行为报警 + */ + private boolean abnormalDriving; + /** + * 特殊报警录像达到存储阈值报警 + */ + private boolean storageLimit; + + public boolean isLossSignal() { + return lossSignal; + } + + public void setLossSignal(boolean lossSignal) { + this.lossSignal = lossSignal; + } + + public boolean isOcclusionSignal() { + return occlusionSignal; + } + + public void setOcclusionSignal(boolean occlusionSignal) { + this.occlusionSignal = occlusionSignal; + } + + public boolean isStorageFault() { + return storageFault; + } + + public void setStorageFault(boolean storageFault) { + this.storageFault = storageFault; + } + + public boolean isOtherDeviceFailure() { + return otherDeviceFailure; + } + + public void setOtherDeviceFailure(boolean otherDeviceFailure) { + this.otherDeviceFailure = otherDeviceFailure; + } + + public boolean isOvercrowding() { + return overcrowding; + } + + public void setOvercrowding(boolean overcrowding) { + this.overcrowding = overcrowding; + } + + public boolean isAbnormalDriving() { + return abnormalDriving; + } + + public void setAbnormalDriving(boolean abnormalDriving) { + this.abnormalDriving = abnormalDriving; + } + + public boolean isStorageLimit() { + return storageLimit; + } + + public void setStorageLimit(boolean storageLimit) { + this.storageLimit = storageLimit; + } + + @Override + public ByteBuf encode() { + ByteBuf byteBuf = Unpooled.buffer(); + byte content = 0; + if (lossSignal) { + content = content |= 1; + } + if (occlusionSignal) { + content = content |= (1 << 1); + } + if (storageFault) { + content = content |= (1 << 2); + } + if (otherDeviceFailure) { + content = content |= (1 << 3); + } + if (overcrowding) { + content = content |= (1 << 4); + } + if (abnormalDriving) { + content = content |= (1 << 5); + } + if (storageLimit) { + content = content |= (1 << 6); + } + byteBuf.writeByte(content); + byteBuf.writeByte(0); + byteBuf.writeByte(0); + byteBuf.writeByte(0); + return byteBuf; + } + + public static JTVideoAlarmBit decode(ByteBuf byteBuf) { + JTVideoAlarmBit videoAlarmBit = new JTVideoAlarmBit(); + byte content = byteBuf.readByte(); + videoAlarmBit.setLossSignal((content & 1) == 1); + videoAlarmBit.setOcclusionSignal((content >>> 1 & 1) == 1); + videoAlarmBit.setStorageFault((content >>> 2 & 1) == 1); + videoAlarmBit.setOtherDeviceFailure((content >>> 3 & 1) == 1); + videoAlarmBit.setOvercrowding((content >>> 4 & 1) == 1); + videoAlarmBit.setAbnormalDriving((content >>> 5 & 1) == 1); + videoAlarmBit.setStorageLimit((content >>> 6 & 1) == 1); + byteBuf.readByte(); + byteBuf.readByte(); + byteBuf.readByte(); + return videoAlarmBit; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/bean/config/JTVideoParam.java b/src/main/java/com/genersoft/iot/vmp/jt1078/bean/config/JTVideoParam.java new file mode 100644 index 0000000..098e7e6 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/bean/config/JTVideoParam.java @@ -0,0 +1,133 @@ +package com.genersoft.iot.vmp.jt1078.bean.config; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import lombok.Data; + +/** + * 违规行驶时段范围 ,精确到分 + */ +@Data +public class JTVideoParam implements JTDeviceSubConfig{ + /** + * 实时流编码模式 + * 0:CBR( 固定码率) ; + * 1:VBR( 可变码率) ; + * 2:ABR( 平均码率) ; + * 100 ~ 127:自定义 + */ + private int liveStreamCodeRateType; + + /** + * 实时流分辨率 + * 0:QCIF; + * 1:CIF; + * 2:WCIF; + * 3:D1; + * 4:WD1; + * 5:720P; + * 6:1 080P; + * 100 ~ 127:自定义 + */ + private int liveStreamResolving; + + /** + * 实时流关键帧间隔, 范围(1 ~ 1 000) 帧 + */ + private int liveStreamIInterval; + + /** + * 实时流目标帧率,范围(1 ~ 120) 帧 / s + */ + private int liveStreamFrameRate; + + /** + * 实时流目标码率,单位为千位每秒( kbps) + */ + private long liveStreamCodeRate; + + + /** + * 存储流编码模式 + * 0:CBR( 固定码率) + * 1:VBR( 可变码率) + * 2:ABR( 平均码率) + * 100 ~ 127:自定义 + */ + private int storageStreamCodeRateType; + + /** + * 存储流分辨率 + * 0:QCIF; + * 1:CIF; + * 2:WCIF; + * 3:D1; + * 4:WD1; + * 5:720P; + * 6:1 080P; + * 100 ~ 127:自定义 + */ + private int storageStreamResolving; + + /** + * 存储流关键帧间隔, 范围(1 ~ 1 000) 帧 + */ + private int storageStreamIInterval; + + /** + * 存储流目标帧率,范围(1 ~ 120) 帧 / s + */ + private int storageStreamFrameRate; + + /** + * 存储流目标码率,单位为千位每秒( kbps) + */ + private long storageStreamCodeRate; + + /** + * 字幕叠加设置 + */ + private JTOSDConfig osd; + + /** + * 是否启用音频输出, 0:不启用;1:启用 + */ + private int audioEnable; + + @Override + public ByteBuf encode() { + ByteBuf byteBuf = Unpooled.buffer(); + byteBuf.writeByte(liveStreamCodeRateType); + byteBuf.writeByte(liveStreamResolving); + byteBuf.writeShort((short)(liveStreamIInterval & 0xffff)); + byteBuf.writeByte(liveStreamFrameRate); + byteBuf.writeInt((int) (liveStreamCodeRate & 0xffffffffL)); + + byteBuf.writeByte(storageStreamCodeRateType); + byteBuf.writeByte(storageStreamResolving); + byteBuf.writeShort((short)(storageStreamIInterval & 0xffff)); + byteBuf.writeByte(storageStreamFrameRate); + byteBuf.writeInt((int) (storageStreamCodeRate & 0xffffffffL)); + byteBuf.writeBytes(osd.encode()); + byteBuf.writeByte(audioEnable); + return byteBuf; + } + + public static JTVideoParam decode(ByteBuf buf) { + JTVideoParam videoParam = new JTVideoParam(); + videoParam.setLiveStreamCodeRateType(buf.readByte()); + videoParam.setLiveStreamResolving(buf.readByte()); + videoParam.setLiveStreamIInterval(buf.readUnsignedShort()); + videoParam.setLiveStreamFrameRate(buf.readByte()); + videoParam.setLiveStreamCodeRate(buf.readUnsignedInt()); + + videoParam.setStorageStreamCodeRateType(buf.readByte()); + videoParam.setStorageStreamResolving(buf.readByte()); + videoParam.setStorageStreamIInterval(buf.readUnsignedShort()); + videoParam.setStorageStreamFrameRate(buf.readByte()); + videoParam.setStorageStreamCodeRate(buf.readUnsignedInt()); + videoParam.setOsd(JTOSDConfig.decode(buf)); + videoParam.setAudioEnable(buf.readByte()); + return null; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/cmd/JT1078Template.java b/src/main/java/com/genersoft/iot/vmp/jt1078/cmd/JT1078Template.java new file mode 100644 index 0000000..306b11e --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/cmd/JT1078Template.java @@ -0,0 +1,693 @@ +package com.genersoft.iot.vmp.jt1078.cmd; + +import com.genersoft.iot.vmp.conf.exception.ControllerException; +import com.genersoft.iot.vmp.jt1078.proc.entity.Cmd; +import com.genersoft.iot.vmp.jt1078.proc.response.*; +import com.genersoft.iot.vmp.jt1078.session.SessionManager; +import com.genersoft.iot.vmp.vmanager.bean.ErrorCode; +import org.springframework.stereotype.Component; + +import java.util.Random; + +/** + * @author QingtaiJiang + * @date 2023/4/27 18:58 + * @email qingtaij@163.com + */ +@Component +public class JT1078Template { + + private final Random random = new Random(); + + private static final String H8103 = "8103"; + private static final String H8104 = "8104"; + private static final String H8105 = "8105"; + private static final String H8106 = "8106"; + private static final String H8107 = "8107"; + private static final String H8201 = "8201"; + private static final String H8202 = "8202"; + private static final String H8203 = "8203"; + private static final String H8204 = "8204"; + private static final String H8300 = "8300"; + private static final String H8400 = "8400"; + private static final String H8401 = "8401"; + private static final String H8500 = "8500"; + private static final String H8600 = "8600"; + private static final String H8601 = "8601"; + private static final String H8602 = "8602"; + private static final String H8603 = "8603"; + private static final String H8604 = "8604"; + private static final String H8605 = "8605"; + private static final String H8606 = "8606"; + private static final String H8607 = "8607"; + private static final String H8608 = "8608"; + private static final String H8702 = "8702"; + private static final String H8801 = "8801"; + private static final String H8802 = "8802"; + private static final String H8803 = "8803"; + private static final String H8804 = "8804"; + private static final String H8805 = "8805"; + private static final String H9003 = "9003"; + private static final String H9101 = "9101"; + private static final String H9102 = "9102"; + private static final String H9201 = "9201"; + private static final String H9202 = "9202"; + private static final String H9205 = "9205"; + private static final String H9206 = "9206"; + private static final String H9207 = "9207"; + private static final String H9301 = "9301"; + private static final String H9302 = "9302"; + private static final String H9303 = "9303"; + private static final String H9304 = "9304"; + private static final String H9305 = "9305"; + private static final String H9306 = "9306"; + + private static final String H0001 = "0001"; + private static final String H0104 = "0104"; + private static final String H0107 = "0107"; + private static final String H0201 = "0201"; + private static final String H0500 = "0500"; + private static final String H0608 = "0608"; + private static final String H0702 = "0702"; + private static final String H0801 = "0801"; + private static final String H0802 = "0802"; + private static final String H0805 = "0805"; + private static final String H1003 = "1003"; + private static final String H1205 = "1205"; + + public void checkTerminalStatus(String devId){ + if (SessionManager.INSTANCE.get(devId) == null) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "终端不在线"); + } + } + + /** + * 开启直播视频 + * + * @param devId 设备号 + * @param j9101 开启视频参数 + */ + public Object startLive(String devId, J9101 j9101, Integer timeOut) { + checkTerminalStatus(devId); + Cmd cmd = new Cmd.Builder() + .setPhoneNumber(devId) + .setPackageNo(randomInt()) + .setMsgId(H9101) + .setRespId(H0001) + .setRs(j9101) + .build(); + return SessionManager.INSTANCE.request(cmd, timeOut); + } + + /** + * 关闭直播视频 + * + * @param devId 设备号 + * @param j9102 关闭视频参数 + */ + public Object stopLive(String devId, J9102 j9102, Integer timeOut) { + checkTerminalStatus(devId); + Cmd cmd = new Cmd.Builder() + .setPhoneNumber(devId) + .setPackageNo(randomInt()) + .setMsgId(H9102) + .setRespId(H0001) + .setRs(j9102) + .build(); + return SessionManager.INSTANCE.request(cmd, timeOut); + } + + /** + * 查询音视频列表 + * + * @param devId 设备号 + * @param j9205 查询音视频列表 + */ + public Object queryBackTime(String devId, J9205 j9205, Integer timeOut) { + checkTerminalStatus(devId); + Cmd cmd = new Cmd.Builder() + .setPhoneNumber(devId) + .setPackageNo(randomInt()) + .setMsgId(H9205) + .setRespId(H1205) + .setRs(j9205) + .build(); + return SessionManager.INSTANCE.request(cmd, timeOut); + } + + /** + * 开启视频回放 + * + * @param devId 设备号 + * @param j9201 视频回放参数 + */ + public Object startBackLive(String devId, J9201 j9201, Integer timeOut) { + checkTerminalStatus(devId); + Cmd cmd = new Cmd.Builder() + .setPhoneNumber(devId) + .setPackageNo(randomInt()) + .setMsgId(H9201) + .setRespId(H1205) + .setRs(j9201) + .build(); + return SessionManager.INSTANCE.request(cmd, timeOut); + } + + /** + * 视频回放控制 + * + * @param devId 设备号 + * @param j9202 控制视频回放参数 + */ + public Object controlBackLive(String devId, J9202 j9202, Integer timeOut) { + checkTerminalStatus(devId); + Cmd cmd = new Cmd.Builder() + .setPhoneNumber(devId) + .setPackageNo(randomInt()) + .setMsgId(H9202) + .setRespId(H0001) + .setRs(j9202) + .build(); + return SessionManager.INSTANCE.request(cmd, timeOut); + } + + /** + * 文件上传 + * + * @param devId 设备号 + * @param j9206 文件上传参数 + */ + public Object fileUpload(String devId, J9206 j9206, Integer timeOut) { + checkTerminalStatus(devId); + Cmd cmd = new Cmd.Builder() + .setPhoneNumber(devId) + .setPackageNo(randomInt()) + .setMsgId(H9206) + .setRespId(H0001) + .setRs(j9206) + .build(); + return SessionManager.INSTANCE.request(cmd, timeOut); + } + + /** + * 文件上传控制 + * + * @param devId 设备号 + * @param j9207 文件上传控制参数 + */ + public Object fileUploadControl(String devId, J9207 j9207, Integer timeOut) { + checkTerminalStatus(devId); + Cmd cmd = new Cmd.Builder() + .setPhoneNumber(devId) + .setPackageNo(randomInt()) + .setMsgId(H9207) + .setRespId(H0001) + .setRs(j9207) + .build(); + return SessionManager.INSTANCE.request(cmd, timeOut); + } + + /** + * 云台控制指令-云台旋转 + * + * @param devId 设备号 + * @param j9301 云台旋转参数 + */ + public Object ptzRotate(String devId, J9301 j9301, Integer timeOut) { + checkTerminalStatus(devId); + Cmd cmd = new Cmd.Builder() + .setPhoneNumber(devId) + .setPackageNo(randomInt()) + .setMsgId(H9301) + .setRespId(H0001) + .setRs(j9301) + .build(); + return SessionManager.INSTANCE.request(cmd, timeOut); + } + + /** + * 云台控制指令-云台调整焦距控制 + * + * @param devId 设备号 + * @param j9302 云台焦距控制参数 + */ + public Object ptzFocal(String devId, J9302 j9302, Integer timeOut) { + checkTerminalStatus(devId); + Cmd cmd = new Cmd.Builder() + .setPhoneNumber(devId) + .setPackageNo(randomInt()) + .setMsgId(H9302) + .setRespId(H0001) + .setRs(j9302) + .build(); + return SessionManager.INSTANCE.request(cmd, timeOut); + } + + /** + * 云台控制指令-云台调整光圈控制 + * + * @param devId 设备号 + * @param j9303 云台光圈控制参数 + */ + public Object ptzIris(String devId, J9303 j9303, Integer timeOut) { + checkTerminalStatus(devId); + Cmd cmd = new Cmd.Builder() + .setPhoneNumber(devId) + .setPackageNo(randomInt()) + .setMsgId(H9303) + .setRespId(H0001) + .setRs(j9303) + .build(); + return SessionManager.INSTANCE.request(cmd, timeOut); + } + + /** + * 云台控制指令-云台雨刷控制 + * + * @param devId 设备号 + * @param j9304 云台雨刷控制参数 + */ + public Object ptzWiper(String devId, J9304 j9304, Integer timeOut) { + checkTerminalStatus(devId); + Cmd cmd = new Cmd.Builder() + .setPhoneNumber(devId) + .setPackageNo(randomInt()) + .setMsgId(H9304) + .setRespId(H0001) + .setRs(j9304) + .build(); + return SessionManager.INSTANCE.request(cmd, timeOut); + } + + /** + * 云台控制指令-红外补光控制 + * + * @param devId 设备号 + * @param j9305 云台红外补光控制参数 + */ + public Object ptzSupplementaryLight(String devId, J9305 j9305, Integer timeOut) { + checkTerminalStatus(devId); + Cmd cmd = new Cmd.Builder() + .setPhoneNumber(devId) + .setPackageNo(randomInt()) + .setMsgId(H9305) + .setRespId(H0001) + .setRs(j9305) + .build(); + return SessionManager.INSTANCE.request(cmd, timeOut); + } + + /** + * 云台控制指令-变倍控制 + * + * @param devId 设备号 + * @param j9306 云台变倍控制参数 + */ + public Object ptzZoom(String devId, J9306 j9306, Integer timeOut) { + checkTerminalStatus(devId); + Cmd cmd = new Cmd.Builder() + .setPhoneNumber(devId) + .setPackageNo(randomInt()) + .setMsgId(H9306) + .setRespId(H0001) + .setRs(j9306) + .build(); + return SessionManager.INSTANCE.request(cmd, timeOut); + } + + /** + * 查询终端参数 + * + * @param devId 设备号 + */ + public Object getDeviceConfig(String devId, J8104 j8104, Integer timeOut) { + checkTerminalStatus(devId); + Cmd cmd = new Cmd.Builder() + .setPhoneNumber(devId) + .setPackageNo(randomInt()) + .setMsgId(H8104) + .setRespId(H0104) + .setRs(j8104) + .build(); + return SessionManager.INSTANCE.request(cmd, timeOut); + } + + /** + * 查询指定终端参数 + * + * @param devId 设备号 + */ + public Object getDeviceSpecifyConfig(String devId, J8106 j8106, Integer timeOut) { + checkTerminalStatus(devId); + Cmd cmd = new Cmd.Builder() + .setPhoneNumber(devId) + .setPackageNo(randomInt()) + .setMsgId(H8106) + .setRespId(H0104) + .setRs(j8106) + .build(); + return SessionManager.INSTANCE.request(cmd, timeOut); + } + + /** + * 设置终端参数 + * + * @param devId 设备号 + */ + public Object setDeviceSpecifyConfig(String devId, J8103 j8103, Integer timeOut) { + checkTerminalStatus(devId); + Cmd cmd = new Cmd.Builder() + .setPhoneNumber(devId) + .setPackageNo(randomInt()) + .setMsgId(H8103) + .setRespId(H0001) + .setRs(j8103) + .build(); + return SessionManager.INSTANCE.request(cmd, timeOut); + } + + private Long randomInt() { + return (long) random.nextInt(1000) + 1; + } + + /** + * 设备控制 + */ + public Object deviceControl(String devId, J8105 j8105, int timeOut) { + checkTerminalStatus(devId); + Cmd cmd = new Cmd.Builder() + .setPhoneNumber(devId) + .setPackageNo(randomInt()) + .setMsgId(H8105) + .setRespId(H0001) + .setRs(j8105) + .build(); + return SessionManager.INSTANCE.request(cmd, timeOut); + } + + /** + * 查询终端属性 + */ + public Object deviceAttribute(String devId, J8107 j8107, int timeOut) { + checkTerminalStatus(devId); + Cmd cmd = new Cmd.Builder() + .setPhoneNumber(devId) + .setPackageNo(randomInt()) + .setMsgId(H8107) + .setRespId(H0107) + .setRs(j8107) + .build(); + return SessionManager.INSTANCE.request(cmd, timeOut); + } + + /** + * 位置信息查询 + */ + public Object queryPositionInfo(String devId, J8201 j8201, int timeOut) { + checkTerminalStatus(devId); + Cmd cmd = new Cmd.Builder() + .setPhoneNumber(devId) + .setPackageNo(randomInt()) + .setMsgId(H8201) + .setRespId(H0201) + .setRs(j8201) + .build(); + return SessionManager.INSTANCE.request(cmd, timeOut); + } + + public Object tempPositionTrackingControl(String devId, J8202 j8202, int timeOut) { + checkTerminalStatus(devId); + Cmd cmd = new Cmd.Builder() + .setPhoneNumber(devId) + .setPackageNo(randomInt()) + .setMsgId(H8202) + .setRespId(H0001) + .setRs(j8202) + .build(); + return SessionManager.INSTANCE.request(cmd, timeOut); + } + + public Object confirmationAlarmMessage(String devId, J8203 j8203, int timeOut) { + checkTerminalStatus(devId); + Cmd cmd = new Cmd.Builder() + .setPhoneNumber(devId) + .setPackageNo(randomInt()) + .setMsgId(H8203) + .setRespId(H0001) + .setRs(j8203) + .build(); + return SessionManager.INSTANCE.request(cmd, timeOut); + } + + public Object linkDetection(String devId, J8204 j8204, int timeOut) { + checkTerminalStatus(devId); + Cmd cmd = new Cmd.Builder() + .setPhoneNumber(devId) + .setPackageNo(randomInt()) + .setMsgId(H8204) + .setRespId(H0001) + .setRs(j8204) + .build(); + return SessionManager.INSTANCE.request(cmd, timeOut); + } + + public Object textMessage(String devId, J8300 j8300, int timeOut) { + checkTerminalStatus(devId); + Cmd cmd = new Cmd.Builder() + .setPhoneNumber(devId) + .setPackageNo(randomInt()) + .setMsgId(H8300) + .setRespId(H0001) + .setRs(j8300) + .build(); + return SessionManager.INSTANCE.request(cmd, timeOut); + } + + public Object telephoneCallback(String devId, J8400 j8400, int timeOut) { + checkTerminalStatus(devId); + Cmd cmd = new Cmd.Builder() + .setPhoneNumber(devId) + .setPackageNo(randomInt()) + .setMsgId(H8400) + .setRespId(H0001) + .setRs(j8400) + .build(); + return SessionManager.INSTANCE.request(cmd, timeOut); + } + + public Object setPhoneBook(String devId, J8401 j8401, int timeOut) { + checkTerminalStatus(devId); + Cmd cmd = new Cmd.Builder() + .setPhoneNumber(devId) + .setPackageNo(randomInt()) + .setMsgId(H8401) + .setRespId(H0001) + .setRs(j8401) + .build(); + return SessionManager.INSTANCE.request(cmd, timeOut); + } + + public Object vehicleControl(String devId, J8500 j8500, int timeOut) { + checkTerminalStatus(devId); + Cmd cmd = new Cmd.Builder() + .setPhoneNumber(devId) + .setPackageNo(randomInt()) + .setMsgId(H8500) + .setRespId(H0500) + .setRs(j8500) + .build(); + return SessionManager.INSTANCE.request(cmd, timeOut); + } + + public Object setAreaForCircle(String devId, J8600 j8600, int timeOut) { + checkTerminalStatus(devId); + Cmd cmd = new Cmd.Builder() + .setPhoneNumber(devId) + .setPackageNo(randomInt()) + .setMsgId(H8600) + .setRespId(H0001) + .setRs(j8600) + .build(); + return SessionManager.INSTANCE.request(cmd, timeOut); + } + + public Object deleteAreaForCircle(String devId, J8601 j8601, int timeOut) { + checkTerminalStatus(devId); + Cmd cmd = new Cmd.Builder() + .setPhoneNumber(devId) + .setPackageNo(randomInt()) + .setMsgId(H8601) + .setRespId(H0001) + .setRs(j8601) + .build(); + return SessionManager.INSTANCE.request(cmd, timeOut); + } + + public Object setAreaForRectangle(String devId, J8602 j8602, int timeOut) { + checkTerminalStatus(devId); + Cmd cmd = new Cmd.Builder() + .setPhoneNumber(devId) + .setPackageNo(randomInt()) + .setMsgId(H8602) + .setRespId(H0001) + .setRs(j8602) + .build(); + return SessionManager.INSTANCE.request(cmd, timeOut); + } + + public Object deleteAreaForRectangle(String devId, J8603 j8603, int timeOut) { + checkTerminalStatus(devId); + Cmd cmd = new Cmd.Builder() + .setPhoneNumber(devId) + .setPackageNo(randomInt()) + .setMsgId(H8603) + .setRespId(H0001) + .setRs(j8603) + .build(); + return SessionManager.INSTANCE.request(cmd, timeOut); + } + + public Object setAreaForPolygon(String devId, J8604 j8604, int timeOut) { + checkTerminalStatus(devId); + Cmd cmd = new Cmd.Builder() + .setPhoneNumber(devId) + .setPackageNo(randomInt()) + .setMsgId(H8604) + .setRespId(H0001) + .setRs(j8604) + .build(); + return SessionManager.INSTANCE.request(cmd, timeOut); + } + + public Object deleteAreaForPolygon(String devId, J8605 j8605, int timeOut) { + checkTerminalStatus(devId); + Cmd cmd = new Cmd.Builder() + .setPhoneNumber(devId) + .setPackageNo(randomInt()) + .setMsgId(H8605) + .setRespId(H0001) + .setRs(j8605) + .build(); + return SessionManager.INSTANCE.request(cmd, timeOut); + } + + public Object setRoute(String devId, J8606 j8606, int timeOut) { + checkTerminalStatus(devId); + Cmd cmd = new Cmd.Builder() + .setPhoneNumber(devId) + .setPackageNo(randomInt()) + .setMsgId(H8606) + .setRespId(H0001) + .setRs(j8606) + .build(); + return SessionManager.INSTANCE.request(cmd, timeOut); + } + + public Object deleteRoute(String devId, J8607 j8607, int timeOut) { + checkTerminalStatus(devId); + Cmd cmd = new Cmd.Builder() + .setPhoneNumber(devId) + .setPackageNo(randomInt()) + .setMsgId(H8607) + .setRespId(H0001) + .setRs(j8607) + .build(); + return SessionManager.INSTANCE.request(cmd, timeOut); + } + + public Object queryAreaOrRoute(String devId, J8608 j8608, int timeOut) { + checkTerminalStatus(devId); + Cmd cmd = new Cmd.Builder() + .setPhoneNumber(devId) + .setPackageNo(randomInt()) + .setMsgId(H8608) + .setRespId(H0608) + .setRs(j8608) + .build(); + return SessionManager.INSTANCE.request(cmd, timeOut); + } + + public Object queryDriverInformation(String devId, J8702 j8702, int timeOut) { + checkTerminalStatus(devId); + Cmd cmd = new Cmd.Builder() + .setPhoneNumber(devId) + .setPackageNo(randomInt()) + .setMsgId(H8702) + .setRespId(H0702) + .setRs(j8702) + .build(); + return SessionManager.INSTANCE.request(cmd, timeOut); + } + + public Object shooting(String devId, J8801 j8801, int timeOut) { + checkTerminalStatus(devId); + Cmd cmd = new Cmd.Builder() + .setPhoneNumber(devId) + .setPackageNo(randomInt()) + .setMsgId(H8801) + .setRespId(H0805) + .setRs(j8801) + .build(); + return SessionManager.INSTANCE.request(cmd, timeOut); + } + + public Object queryMediaData(String devId, J8802 j8802, int timeOut) { + checkTerminalStatus(devId); + Cmd cmd = new Cmd.Builder() + .setPhoneNumber(devId) + .setPackageNo(randomInt()) + .setMsgId(H8802) + .setRespId(H0802) + .setRs(j8802) + .build(); + return SessionManager.INSTANCE.request(cmd, timeOut); + } + + public Object uploadMediaData(String devId, J8803 j8803, int timeOut) { + checkTerminalStatus(devId); + Cmd cmd = new Cmd.Builder() + .setPhoneNumber(devId) + .setPackageNo(randomInt()) + .setMsgId(H8803) + .setRespId(H0801) + .setRs(j8803) + .build(); + return SessionManager.INSTANCE.request(cmd, timeOut); + } + + public Object record(String devId, J8804 j8804, int timeOut) { + checkTerminalStatus(devId); + Cmd cmd = new Cmd.Builder() + .setPhoneNumber(devId) + .setPackageNo(randomInt()) + .setMsgId(H8804) + .setRespId(H0001) + .setRs(j8804) + .build(); + return SessionManager.INSTANCE.request(cmd, timeOut); + } + + public Object uploadMediaDataForSingle(String devId, J8805 j8805, int timeOut) { + checkTerminalStatus(devId); + Cmd cmd = new Cmd.Builder() + .setPhoneNumber(devId) + .setPackageNo(randomInt()) + .setMsgId(H8805) + .setRespId(H0801) + .setRs(j8805) + .build(); + return SessionManager.INSTANCE.request(cmd, timeOut); + } + + public Object queryMediaAttribute(String devId, J9003 j9003, int timeOut) { + checkTerminalStatus(devId); + Cmd cmd = new Cmd.Builder() + .setPhoneNumber(devId) + .setPackageNo(randomInt()) + .setMsgId(H9003) + .setRespId(H1003) + .setRs(j9003) + .build(); + return SessionManager.INSTANCE.request(cmd, timeOut); + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/codec/decode/Jt808Decoder.java b/src/main/java/com/genersoft/iot/vmp/jt1078/codec/decode/Jt808Decoder.java new file mode 100644 index 0000000..3bfcfcf --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/codec/decode/Jt808Decoder.java @@ -0,0 +1,177 @@ +package com.genersoft.iot.vmp.jt1078.codec.decode; + +import com.genersoft.iot.vmp.jt1078.proc.Header; +import com.genersoft.iot.vmp.jt1078.proc.factory.CodecFactory; +import com.genersoft.iot.vmp.jt1078.proc.request.Re; +import com.genersoft.iot.vmp.jt1078.proc.response.Rs; +import com.genersoft.iot.vmp.jt1078.service.Ijt1078Service; +import com.genersoft.iot.vmp.jt1078.session.Session; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufUtil; +import io.netty.buffer.CompositeByteBuf; +import io.netty.buffer.UnpooledByteBufAllocator; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.ByteToMessageDecoder; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.context.ApplicationEvent; +import org.springframework.context.ApplicationEventPublisher; +import lombok.extern.slf4j.Slf4j; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * @author QingtaiJiang + * @date 2023/4/27 18:10 + * @email qingtaij@163.com + */ +@Slf4j +public class Jt808Decoder extends ByteToMessageDecoder { + + private ApplicationEventPublisher applicationEventPublisher = null; + private Ijt1078Service service = null; + + public Jt808Decoder(ApplicationEventPublisher applicationEventPublisher, Ijt1078Service service ) { + this.applicationEventPublisher = applicationEventPublisher; + this.service = service; + } + + @Override + protected void decode(ChannelHandlerContext ctx, ByteBuf in, List out) throws Exception { + Session session = ctx.channel().attr(Session.KEY).get(); + log.info("> {} hex: 7e{}7e", session, ByteBufUtil.hexDump(in)); + try { + // 按照部标定义执行校验和转义 + ByteBuf buf = unEscapeAndCheck(in); + buf.retain(); + Header header = new Header(); + header.setMsgId(ByteBufUtil.hexDump(buf.readSlice(2))); + header.setMsgPro(buf.readUnsignedShort()); + // 从消息属性中读取是否存在分包 + boolean isSubpackage = (header.getMsgPro() >>> 13 & 1) == 1; + if (header.is2019Version()) { + header.setVersion(buf.readUnsignedByte()); + String devId = ByteBufUtil.hexDump(buf.readSlice(10)); + header.setPhoneNumber(devId.replaceFirst("^0*", "")); + } else { + header.setPhoneNumber(ByteBufUtil.hexDump(buf.readSlice(6)).replaceFirst("^0*", "")); + } + header.setSn(buf.readUnsignedShort()); + if (isSubpackage) { + int packageCount = buf.readUnsignedShort(); + int packageNumber = buf.readUnsignedShort(); + log.debug("[分包消息] header: {}, 序号: {}, 总数: {}", header, packageNumber, packageCount); + // 缓存带合并的分包消息 + ByteBuf intactBuf = MultiPacketManager.INSTANCE.add(header, packageCount, buf); + if (intactBuf == null) { + return; + } + buf = intactBuf; + } + Re handler = CodecFactory.getHandler(header.getMsgId()); + if (handler == null) { + log.error("get msgId is null {}", header.getMsgId()); + buf.release(); + return; + } + + Rs decode = handler.decode(buf, header, session, service); + ApplicationEvent applicationEvent = handler.getEvent(); + if (applicationEvent != null) { + applicationEventPublisher.publishEvent(applicationEvent); + } + if (decode != null) { + out.add(decode); + } + } finally { + in.skipBytes(in.readableBytes()); + } + } + + + /** + * 转义与验证校验码 + * + * @param byteBuf 转义Buf + * @return 转义好的数据 + */ + public ByteBuf unEscapeAndCheck(ByteBuf byteBuf) throws Exception { + int low = byteBuf.readerIndex(); + int high = byteBuf.writerIndex(); + byte checkSum = 0; + int calculationCheckSum = 0; + + byte aByte = byteBuf.getByte(high - 2); + byte protocolEscapeFlag7d = 0x7d; + //0x7d转义 + byte protocolEscapeFlag01 = 0x01; + //0x7e转义 + byte protocolEscapeFlag02 = 0x02; + if (aByte == protocolEscapeFlag7d) { + byte b2 = byteBuf.getByte(high - 1); + if (b2 == protocolEscapeFlag01) { + checkSum = protocolEscapeFlag7d; + } else if (b2 == protocolEscapeFlag02) { + checkSum = 0x7e; + } else { + log.error("转义1异常:{}", ByteBufUtil.hexDump(byteBuf)); + throw new Exception("转义错误"); + } + high = high - 2; + } else { + high = high - 1; + checkSum = byteBuf.getByte(high); + } + List bufList = new ArrayList<>(); + int index = low; + while (index < high) { + byte b = byteBuf.getByte(index); + if (b == protocolEscapeFlag7d) { + byte c = byteBuf.getByte(index + 1); + if (c == protocolEscapeFlag01) { + ByteBuf slice = slice0x01(byteBuf, low, index); + bufList.add(slice); + b = protocolEscapeFlag7d; + } else if (c == protocolEscapeFlag02) { + ByteBuf slice = slice0x02(byteBuf, low, index); + bufList.add(slice); + b = 0x7e; + } else { + log.error("转义2异常:{}", ByteBufUtil.hexDump(byteBuf)); + throw new Exception("转义错误"); + } + index += 2; + low = index; + } else { + index += 1; + } + calculationCheckSum = calculationCheckSum ^ b; + } + + if (calculationCheckSum == checkSum) { + if (bufList.isEmpty()) { + return byteBuf.slice(low, high); + } else { + bufList.add(byteBuf.slice(low, high - low)); + return new CompositeByteBuf(UnpooledByteBufAllocator.DEFAULT, false, bufList.size(), bufList); + } + } else { + log.info("{} 解析校验码:{}--计算校验码:{}", ByteBufUtil.hexDump(byteBuf), checkSum, calculationCheckSum); + throw new Exception("校验码错误!"); + } + } + + + private ByteBuf slice0x01(ByteBuf buf, int low, int sign) { + return buf.slice(low, sign - low + 1); + } + + private ByteBuf slice0x02(ByteBuf buf, int low, int sign) { + buf.setByte(sign, 0x7e); + return buf.slice(low, sign - low + 1); + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/codec/decode/MultiPacketManager.java b/src/main/java/com/genersoft/iot/vmp/jt1078/codec/decode/MultiPacketManager.java new file mode 100644 index 0000000..73f64b1 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/codec/decode/MultiPacketManager.java @@ -0,0 +1,59 @@ +package com.genersoft.iot.vmp.jt1078.codec.decode; + +import com.genersoft.iot.vmp.jt1078.proc.Header; +import io.netty.buffer.*; +import lombok.extern.slf4j.Slf4j; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; + +@Slf4j +public enum MultiPacketManager { + INSTANCE; + // 用与消息的缓存 + private final Map packetMap = new ConcurrentHashMap<>(); + private final Map packetTimeMap = new ConcurrentHashMap<>(); + + MultiPacketManager() { + startLister(); + } + + /** + * 增加待合并的分包,如果分包接受完毕会返回完整的数据包 + */ + public ByteBuf add(Header header, Integer count, ByteBuf byteBuf) { + String key = header.getMsgId() + "/" + header.getPhoneNumber(); + CompositeByteBuf compositeBuf = packetMap.computeIfAbsent(key, k -> new CompositeByteBuf(UnpooledByteBufAllocator.DEFAULT, false, count)); +// compositeBuf.addComponent(true, byteBuf.readSlice(byteBuf.readableBytes())); + compositeBuf.addComponent(true, byteBuf); + packetTimeMap.put(key, System.currentTimeMillis()); + if (count == compositeBuf.numComponents()) { + packetMap.remove(key); + packetTimeMap.remove(key); + compositeBuf.retain(); + return compositeBuf; + } + return null; + } + + private void startLister(){ + Timer timer = new Timer(); + timer.schedule(new TimerTask() { + @Override + public void run() { + long expireTime = System.currentTimeMillis() - 20 * 1000; + if (!packetTimeMap.isEmpty()) { + for (String key : packetTimeMap.keySet()) { + if (packetTimeMap.get(key) < expireTime) { + log.info("分包消息超时 key: {}", key); + packetTimeMap.remove(key); + packetMap.remove(key); + } + } + } + } + }, 2000L, 2000L); + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/codec/encode/Jt808Encoder.java b/src/main/java/com/genersoft/iot/vmp/jt1078/codec/encode/Jt808Encoder.java new file mode 100644 index 0000000..c3c6d1c --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/codec/encode/Jt808Encoder.java @@ -0,0 +1,34 @@ +package com.genersoft.iot.vmp.jt1078.codec.encode; + + +import com.genersoft.iot.vmp.jt1078.proc.response.Rs; +import com.genersoft.iot.vmp.jt1078.session.Session; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufUtil; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.MessageToByteEncoder; +import lombok.extern.slf4j.Slf4j; + +import java.util.List; + +/** + * @author QingtaiJiang + * @date 2023/4/27 18:10 + * @email qingtaij@163.com + */ +@Slf4j +public class Jt808Encoder extends MessageToByteEncoder { + + @Override + protected void encode(ChannelHandlerContext ctx, Rs msg, ByteBuf out) throws Exception { + Session session = ctx.channel().attr(Session.KEY).get(); + + List encodeList = Jt808EncoderCmd.encode(msg, session, session.nextSerialNo()); + if(encodeList!=null && !encodeList.isEmpty()){ + for (ByteBuf byteBuf : encodeList) { + log.debug("< {} hex:{}", session, ByteBufUtil.hexDump(byteBuf)); + out.writeBytes(byteBuf); + } + } + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/codec/encode/Jt808EncoderCmd.java b/src/main/java/com/genersoft/iot/vmp/jt1078/codec/encode/Jt808EncoderCmd.java new file mode 100644 index 0000000..3513c38 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/codec/encode/Jt808EncoderCmd.java @@ -0,0 +1,184 @@ +package com.genersoft.iot.vmp.jt1078.codec.encode; + +import com.genersoft.iot.vmp.jt1078.annotation.MsgId; +import com.genersoft.iot.vmp.jt1078.proc.Header; +import com.genersoft.iot.vmp.jt1078.proc.entity.Cmd; +import com.genersoft.iot.vmp.jt1078.proc.response.Rs; +import com.genersoft.iot.vmp.jt1078.session.Session; +import com.genersoft.iot.vmp.jt1078.util.Bin; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufUtil; +import io.netty.buffer.CompositeByteBuf; +import io.netty.buffer.Unpooled; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.MessageToByteEncoder; +import io.netty.util.ByteProcessor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.util.StringUtils; + +import java.util.LinkedList; +import java.util.List; + +/** + * @author QingtaiJiang + * @date 2023/4/27 18:25 + * @email qingtaij@163.com + */ +@Slf4j +public class Jt808EncoderCmd extends MessageToByteEncoder { + + @Override + protected void encode(ChannelHandlerContext ctx, Cmd cmd, ByteBuf out) throws Exception { + Session session = ctx.channel().attr(Session.KEY).get(); + Rs msg = cmd.getRs(); + List encodeList = encode(msg, session, cmd.getPackageNo().intValue()); + if (encodeList != null && !encodeList.isEmpty()) { + for (ByteBuf byteBuf : encodeList) { + log.debug("< {} hex:{}", session, ByteBufUtil.hexDump(byteBuf)); + out.writeBytes(byteBuf); + } + } + } + + + public static List encode(Rs msg, Session session, Integer packageNo) { + String id = msg.getClass().getAnnotation(MsgId.class).id(); + if (!StringUtils.hasLength(id)) { + log.error("Not find msgId"); + return null; + } + ByteBuf encode = msg.encode(); + Header header = msg.getHeader(); + + List byteBufList = new LinkedList<>(); + if (encode.readableBytes() > 1000) { + int index = 1; + int total = encode.readableBytes()%1000 == 0 ? encode.readableBytes()/1000 : (encode.readableBytes()/1000 + 1); + while (encode.isReadable()) { + ByteBuf byteBuf; + if (index == total) { + byteBuf = buildMsgByte(header, id, session, packageNo, encode.readRetainedSlice(encode.readableBytes()), index, total); + }else { + byteBuf = buildMsgByte(header, id, session, packageNo, encode.readRetainedSlice(1000), index, total); + } + + byteBufList.add(byteBuf); + index ++; + } + }else { + byteBufList.add(buildMsgByte(header, id, session, packageNo, encode, 0, 0)); + } + return byteBufList; + } + + // 分包 + private static ByteBuf buildMsgByte(Header header, String id, Session session, Integer packageNo, ByteBuf encode, Integer packetIndex, Integer packetTotal) { + ByteBuf byteBuf = Unpooled.buffer(); + + byteBuf.writeBytes(ByteBufUtil.decodeHexDump(id)); + + if (header == null) { + header = session.getHeader(); + } + + if (header.is2019Version()) { + int msgBody = encode.readableBytes() | 1 << 14; + if (packetIndex > 0) { + msgBody = msgBody | 1 << 13; + } + // 消息体属性 + byteBuf.writeShort(msgBody); + + // 版本号 + byteBuf.writeByte(header.getVersion()); + + // 终端手机号 + byteBuf.writeBytes(ByteBufUtil.decodeHexDump(Bin.strHexPaddingLeft(header.getPhoneNumber(), 20))); + } else { + // 消息体属性 + byteBuf.writeShort(encode.readableBytes()); + + byteBuf.writeBytes(ByteBufUtil.decodeHexDump(Bin.strHexPaddingLeft(header.getPhoneNumber(), 12))); + } + + // 消息体流水号 + byteBuf.writeShort(packageNo); + + if (packetIndex > 0) { + byteBuf.writeShort(packetTotal); + byteBuf.writeShort(packetIndex); + } + + // 写入消息体 + byteBuf.writeBytes(encode); + + // 计算校验码,并反转义 + byteBuf = escapeAndCheck0(byteBuf); + return byteBuf; + } + + + private static final ByteProcessor searcher = value -> !(value == 0x7d || value == 0x7e); + + //转义与校验 + public static ByteBuf escapeAndCheck0(ByteBuf source) { + + sign(source); + + int low = source.readerIndex(); + int high = source.writerIndex(); + + LinkedList bufList = new LinkedList<>(); + int mark, len; + while ((mark = source.forEachByte(low, high - low, searcher)) > 0) { + + len = mark + 1 - low; + ByteBuf[] slice = slice(source, low, len); + bufList.add(slice[0]); + bufList.add(slice[1]); + low += len; + } + + if (bufList.size() > 0) { + bufList.add(source.slice(low, high - low)); + } else { + bufList.add(source); + } + + ByteBuf delimiter = Unpooled.buffer(1, 1).writeByte(0x7e).retain(); + bufList.addFirst(delimiter); + bufList.addLast(delimiter); + + CompositeByteBuf byteBufLs = Unpooled.compositeBuffer(bufList.size()); + byteBufLs.addComponents(true, bufList); + return byteBufLs; + } + + public static void sign(ByteBuf buf) { + byte checkCode = bcc(buf); + buf.writeByte(checkCode); + } + + public static byte bcc(ByteBuf byteBuf) { + byte cs = 0; + while (byteBuf.isReadable()) + cs ^= byteBuf.readByte(); + byteBuf.resetReaderIndex(); + return cs; + } + + protected static ByteBuf[] slice(ByteBuf byteBuf, int index, int length) { + byte first = byteBuf.getByte(index + length - 1); + + ByteBuf[] byteBufList = new ByteBuf[2]; + byteBufList[0] = byteBuf.retainedSlice(index, length); + + if (first == 0x7d) { + byteBufList[1] = Unpooled.buffer(1, 1).writeByte(0x01); + } else { + byteBuf.setByte(index + length - 1, 0x7d); + byteBufList[1] = Unpooled.buffer(1, 1).writeByte(0x02); + } + return byteBufList; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/codec/netty/Jt808Handler.java b/src/main/java/com/genersoft/iot/vmp/jt1078/codec/netty/Jt808Handler.java new file mode 100644 index 0000000..202f81e --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/codec/netty/Jt808Handler.java @@ -0,0 +1,98 @@ +package com.genersoft.iot.vmp.jt1078.codec.netty; + +import com.genersoft.iot.vmp.jt1078.event.ConnectChangeEvent; +import com.genersoft.iot.vmp.jt1078.proc.response.Rs; +import com.genersoft.iot.vmp.jt1078.session.Session; +import com.genersoft.iot.vmp.jt1078.session.SessionManager; +import io.netty.channel.Channel; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandlerAdapter; +import io.netty.handler.timeout.IdleState; +import io.netty.handler.timeout.IdleStateEvent; +import io.netty.util.ReferenceCountUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.context.ApplicationEventPublisher; +import lombok.extern.slf4j.Slf4j; + +/** + * @author QingtaiJiang + * @date 2023/4/27 18:14 + * @email qingtaij@163.com + */ +@Slf4j +public class Jt808Handler extends ChannelInboundHandlerAdapter { + + private ApplicationEventPublisher applicationEventPublisher = null; + + public Jt808Handler(ApplicationEventPublisher applicationEventPublisher) { + this.applicationEventPublisher = applicationEventPublisher; + } + + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { + if (msg instanceof Rs) { + ctx.writeAndFlush(msg); + } else { + ctx.fireChannelRead(msg); + } + // 读取完成后的消息释放 + ReferenceCountUtil.release(msg); + } + + @Override + public void channelActive(ChannelHandlerContext ctx) { + Channel channel = ctx.channel(); + Session session = SessionManager.INSTANCE.newSession(channel); + channel.attr(Session.KEY).set(session); + log.info("> Tcp connect {}", session); + if (session.getPhoneNumber() == null) { + return; + } + ConnectChangeEvent event = new ConnectChangeEvent(this); + event.setConnected(true); + event.setPhoneNumber(session.getPhoneNumber()); + applicationEventPublisher.publishEvent(event); + } + + @Override + public void channelInactive(ChannelHandlerContext ctx) { + Session session = ctx.channel().attr(Session.KEY).get(); + log.info("< Tcp disconnect {}", session); + ctx.close(); + if (session.getPhoneNumber() == null) { + return; + } + ConnectChangeEvent event = new ConnectChangeEvent(this); + event.setConnected(false); + event.setPhoneNumber(session.getPhoneNumber()); + applicationEventPublisher.publishEvent(event); + + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable e) { + Session session = ctx.channel().attr(Session.KEY).get(); + String message = e.getMessage(); + if (message.toLowerCase().contains("Connection reset by peer".toLowerCase())) { + log.info("< exception{} {}", session, e.getMessage()); + } else { + log.info("< exception{} {}", session, e.getMessage(), e); + } + + } + + @Override + public void userEventTriggered(ChannelHandlerContext ctx, Object evt) { + if (evt instanceof IdleStateEvent) { + IdleStateEvent event = (IdleStateEvent) evt; + IdleState state = event.state(); + if (state == IdleState.READER_IDLE || state == IdleState.WRITER_IDLE) { + Session session = ctx.channel().attr(Session.KEY).get(); + log.warn("< Proactively disconnect{}", session); + ctx.close(); + } + } + } + +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/codec/netty/TcpServer.java b/src/main/java/com/genersoft/iot/vmp/jt1078/codec/netty/TcpServer.java new file mode 100644 index 0000000..3996c3f --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/codec/netty/TcpServer.java @@ -0,0 +1,123 @@ +package com.genersoft.iot.vmp.jt1078.codec.netty; + +import com.genersoft.iot.vmp.jt1078.codec.decode.Jt808Decoder; +import com.genersoft.iot.vmp.jt1078.codec.encode.Jt808Encoder; +import com.genersoft.iot.vmp.jt1078.codec.encode.Jt808EncoderCmd; +import com.genersoft.iot.vmp.jt1078.proc.factory.CodecFactory; +import com.genersoft.iot.vmp.jt1078.service.Ijt1078Service; +import io.netty.bootstrap.ServerBootstrap; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelInitializer; +import io.netty.channel.EventLoopGroup; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.nio.NioChannelOption; +import io.netty.channel.socket.nio.NioServerSocketChannel; +import io.netty.channel.socket.nio.NioSocketChannel; +import io.netty.handler.codec.DelimiterBasedFrameDecoder; +import io.netty.handler.timeout.IdleStateHandler; +import io.netty.util.concurrent.Future; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.CommandLineRunner; +import org.springframework.context.ApplicationContextInitializer; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.stereotype.Component; +import lombok.extern.slf4j.Slf4j; + +import java.util.concurrent.TimeUnit; + +/** + * @author QingtaiJiang + * @date 2023/4/27 18:01 + * @email qingtaij@163.com + */ + +@Slf4j +public class TcpServer { + + private final Integer port; + private boolean isRunning = false; + private EventLoopGroup bossGroup = null; + private EventLoopGroup workerGroup = null; + private ApplicationEventPublisher applicationEventPublisher = null; + private Ijt1078Service service = null; + + private final ByteBuf DECODER_JT808 = Unpooled.wrappedBuffer(new byte[]{0x7e}); + + public TcpServer(Integer port, ApplicationEventPublisher applicationEventPublisher, Ijt1078Service service) { + this.port = port; + this.applicationEventPublisher = applicationEventPublisher; + this.service = service; + } + + private void startTcpServer() { + try { + CodecFactory.init(); + this.bossGroup = new NioEventLoopGroup(); + this.workerGroup = new NioEventLoopGroup(); + ServerBootstrap bootstrap = new ServerBootstrap(); + bootstrap.channel(NioServerSocketChannel.class); + bootstrap.group(bossGroup, workerGroup); + + bootstrap.option(NioChannelOption.SO_BACKLOG, 1024) + .option(NioChannelOption.SO_REUSEADDR, true) + .childOption(NioChannelOption.TCP_NODELAY, true) + .childHandler(new ChannelInitializer() { + @Override + public void initChannel(NioSocketChannel channel) { + channel.pipeline() + .addLast(new IdleStateHandler(10, 0, 0, TimeUnit.MINUTES)) + .addLast(new DelimiterBasedFrameDecoder(1024 * 2, DECODER_JT808)) + .addLast(new Jt808Decoder(applicationEventPublisher, service)) + .addLast(new Jt808Encoder()) + .addLast(new Jt808EncoderCmd()) + .addLast(new Jt808Handler(applicationEventPublisher)); + } + }); + ChannelFuture channelFuture = bootstrap.bind(port).sync(); + // 监听设备TCP端口是否启动成功 + channelFuture.addListener(future -> { + if (!future.isSuccess()) { + log.error("Binding port:{} fail! cause: {}", port, future.cause().getCause(), future.cause()); + } + }); + log.info("服务:JT808 Server 启动成功, port:{}", port); + channelFuture.channel().closeFuture().sync(); + } catch (Exception e) { + log.warn("服务:JT808 Server 启动异常, port:{},{}", port, e.getMessage(), e); + } finally { + stop(); + } + } + + /** + * 开启一个新的线程,拉起来Netty + */ + public synchronized void start() { + if (this.isRunning) { + log.warn("服务:JT808 Server 已经启动, port:{}", port); + return; + } + this.isRunning = true; + new Thread(this::startTcpServer).start(); + } + + public synchronized void stop() { + if (!this.isRunning) { + log.warn("服务:JT808 Server 已经停止, port:{}", port); + } + this.isRunning = false; + Future future = this.bossGroup.shutdownGracefully(); + if (!future.isSuccess()) { + log.warn("bossGroup 无法正常停止", future.cause()); + } + future = this.workerGroup.shutdownGracefully(); + if (!future.isSuccess()) { + log.warn("workerGroup 无法正常停止", future.cause()); + } + log.warn("服务:JT808 Server 已经停止, port:{}", port); + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/config/JT1078AutoConfiguration.java b/src/main/java/com/genersoft/iot/vmp/jt1078/config/JT1078AutoConfiguration.java new file mode 100644 index 0000000..507a05a --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/config/JT1078AutoConfiguration.java @@ -0,0 +1,33 @@ +package com.genersoft.iot.vmp.jt1078.config; + +import com.genersoft.iot.vmp.jt1078.codec.netty.TcpServer; +import com.genersoft.iot.vmp.jt1078.service.Ijt1078Service; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.annotation.Order; + +/** + * @author QingtaiJiang + * @date 2023/4/27 19:35 + * @email qingtaij@163.com + */ +@Order(Integer.MIN_VALUE) +@Configuration +@ConditionalOnProperty(value = "jt1078.enable", havingValue = "true") +public class JT1078AutoConfiguration { + + @Autowired + private ApplicationEventPublisher applicationEventPublisher; + + @Autowired + private Ijt1078Service service; + + @Bean(initMethod = "start", destroyMethod = "stop") + public TcpServer jt1078Server(@Value("${jt1078.port}") Integer port) { + return new TcpServer(port, applicationEventPublisher, service); + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/config/JT1078Config.java b/src/main/java/com/genersoft/iot/vmp/jt1078/config/JT1078Config.java new file mode 100644 index 0000000..6a6afee --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/config/JT1078Config.java @@ -0,0 +1,18 @@ +package com.genersoft.iot.vmp.jt1078.config; + +import com.genersoft.iot.vmp.jt1078.bean.common.ConfigAttribute; +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.core.annotation.Order; +import org.springframework.stereotype.Component; + +@Component +@Data +@ConfigurationProperties(prefix = "jt1078", ignoreInvalidFields = true) +@Order(3) +public class JT1078Config { + + private Integer port; + + private String password; +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/controller/JT1078Controller.java b/src/main/java/com/genersoft/iot/vmp/jt1078/controller/JT1078Controller.java new file mode 100644 index 0000000..5310ff0 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/controller/JT1078Controller.java @@ -0,0 +1,1011 @@ +package com.genersoft.iot.vmp.jt1078.controller; + +import com.genersoft.iot.vmp.common.StreamInfo; +import com.genersoft.iot.vmp.conf.UserSetting; +import com.genersoft.iot.vmp.conf.exception.ControllerException; +import com.genersoft.iot.vmp.conf.ftpServer.FtpSetting; +import com.genersoft.iot.vmp.conf.security.JwtUtils; +import com.genersoft.iot.vmp.jt1078.bean.*; +import com.genersoft.iot.vmp.jt1078.controller.bean.*; +import com.genersoft.iot.vmp.jt1078.proc.request.J1205; +import com.genersoft.iot.vmp.jt1078.service.Ijt1078PlayService; +import com.genersoft.iot.vmp.jt1078.service.Ijt1078Service; +import com.genersoft.iot.vmp.service.bean.InviteErrorCode; +import com.genersoft.iot.vmp.vmanager.bean.ErrorCode; +import com.genersoft.iot.vmp.vmanager.bean.StreamContent; +import com.genersoft.iot.vmp.vmanager.bean.WVPResult; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import jakarta.servlet.ServletOutputStream; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.http.MediaType; +import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.context.request.async.DeferredResult; + +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLEncoder; +import java.util.List; + + +@Slf4j +@ConditionalOnProperty(value = "jt1078.enable", havingValue = "true") +@RestController +@Tag(name = "部标设备控制") +@RequestMapping("/api/jt1078") +public class JT1078Controller { + + @Resource + private Ijt1078Service service; + + @Resource + private Ijt1078PlayService jt1078PlayService; + + @Autowired + private UserSetting userSetting; + + @Autowired + private FtpSetting ftpSetting; + + @Operation(summary = "JT-开始点播", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "phoneNumber", description = "设备手机号", required = true) + @Parameter(name = "channelId", description = "通道编号, 一般为从1开始的数字", required = true) + @Parameter(name = "type", description = "类型:0:音视频,1:视频,3:音频", required = true) + @GetMapping("/live/start") + public DeferredResult> startLive(HttpServletRequest request, + @Parameter(required = true) String phoneNumber, + @Parameter(required = true) Integer channelId, + @Parameter(required = false) Integer type) { + if (type == null || (type != 0 && type != 1 && type != 3)) { + type = 0; + } + DeferredResult> result = new DeferredResult<>(userSetting.getPlayTimeout().longValue()); + result.onTimeout(()->{ + log.info("[JT-点播等待超时] phoneNumber:{}, channelId:{}, ", phoneNumber, channelId); + // 释放rtpserver + WVPResult wvpResult = new WVPResult<>(); + wvpResult.setCode(ErrorCode.ERROR100.getCode()); + wvpResult.setMsg("超时"); + result.setResult(wvpResult); + jt1078PlayService.stopPlay(phoneNumber, channelId); + }); + + jt1078PlayService.play(phoneNumber, channelId, type, wvpResult -> { + WVPResult wvpResultForFinish = new WVPResult<>(); + wvpResultForFinish.setCode(wvpResult.getCode()); + wvpResultForFinish.setMsg(wvpResult.getMsg()); + if (wvpResult.getCode() == InviteErrorCode.SUCCESS.getCode()) { + StreamInfo streamInfo = wvpResult.getData(); + + if (streamInfo != null) { + if (userSetting.getUseSourceIpAsStreamIp()) { + streamInfo=streamInfo.clone();//深拷贝 + String host; + try { + URL url=new URL(request.getRequestURL().toString()); + host=url.getHost(); + } catch (MalformedURLException e) { + host=request.getLocalAddr(); + } + streamInfo.changeStreamIp(host); + } + wvpResultForFinish.setData(new StreamContent(streamInfo)); + } + } + result.setResult(wvpResultForFinish); + }); + + return result; + } + + @Operation(summary = "JT-结束点播", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "phoneNumber", description = "设备手机号", required = true) + @Parameter(name = "channelId", description = "通道国标编号, 一般为从1开始的数字", required = true) + @GetMapping("/live/stop") + public void stopLive(HttpServletRequest request, + @Parameter(required = true) String phoneNumber, + @Parameter(required = true) Integer channelId) { + jt1078PlayService.stopPlay(phoneNumber, channelId); + } + + @Operation(summary = "JT-语音对讲", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "phoneNumber", description = "设备手机号", required = true) + @Parameter(name = "channelId", description = "通道国标编号, 一般为从1开始的数字", required = true) + @GetMapping("/talk/start") + public StreamContent startTalk(HttpServletRequest request, + @Parameter(required = true) String phoneNumber, + @Parameter(required = true) Integer channelId) { + + StreamInfo streamInfo = jt1078PlayService.startTalk(phoneNumber, channelId); + if (userSetting.getUseSourceIpAsStreamIp()) { + String host; + try { + URL url=new URL(request.getRequestURL().toString()); + host=url.getHost(); + } catch (MalformedURLException e) { + host=request.getLocalAddr(); + } + streamInfo.changeStreamIp(host); + } + streamInfo.setIp("localhost"); + return new StreamContent(streamInfo); + } + + @Operation(summary = "JT-结束对讲", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "phoneNumber", description = "设备手机号", required = true) + @Parameter(name = "channelId", description = "通道国标编号, 一般为从1开始的数字", required = true) + @GetMapping("/talk/stop") + public void stopTalk(HttpServletRequest request, + @Parameter(required = true) String phoneNumber, + @Parameter(required = true) Integer channelId) { + jt1078PlayService.stopTalk(phoneNumber, channelId); + } + + + @Operation(summary = "JT-暂停点播", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "phoneNumber", description = "设备手机号", required = true) + @Parameter(name = "channelId", description = "通道国标编号, 一般为从1开始的数字", required = true) + @GetMapping("/live/pause") + public void pauseLive(HttpServletRequest request, + @Parameter(required = true) String phoneNumber, + @Parameter(required = true) Integer channelId) { + jt1078PlayService.pausePlay(phoneNumber, channelId); + } + + @Operation(summary = "JT-继续点播", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "phoneNumber", description = "设备手机号", required = true) + @Parameter(name = "channelId", description = "通道国标编号, 一般为从1开始的数字", required = true) + @GetMapping("/live/continue") + public void continueLive(HttpServletRequest request, + @Parameter(required = true) String phoneNumber, + @Parameter(required = true) Integer channelId) { + + jt1078PlayService.continueLivePlay(phoneNumber, channelId); + } + + @Operation(summary = "JT-切换码流类型", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "phoneNumber", description = "设备手机号", required = true) + @Parameter(name = "channelId", description = "通道国标编号, 一般为从1开始的数字", required = true) + @Parameter(name = "streamType", description = "0:主码流; 1:子码流", required = true) + @GetMapping("/live/switch") + public void changeStreamType(HttpServletRequest request, + @Parameter(required = true) String phoneNumber, + @Parameter(required = true) Integer channelId, + @Parameter(required = true) Integer streamType) { + service.changeStreamType(phoneNumber, channelId, streamType); + } + + @Operation(summary = "JT-录像-查询资源列表", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "phoneNumber", description = "设备手机号", required = true) + @Parameter(name = "channelId", description = "通道国标编号, 一般为从1开始的数字", required = true) + @Parameter(name = "startTime", description = "开始时间,格式: yyyy-MM-dd HH:mm:ss", required = true) + @Parameter(name = "endTime", description = "结束时间,格式: yyyy-MM-dd HH:mm:ss", required = true) + @GetMapping("/record/list") + public WVPResult> playbackList(HttpServletRequest request, + @Parameter(required = true) String phoneNumber, + @Parameter(required = true) Integer channelId, + @Parameter(required = true) String startTime, + @Parameter(required = true) String endTime + ) { + List recordList = jt1078PlayService.getRecordList(phoneNumber, channelId, startTime, endTime); + if (recordList == null) { + return WVPResult.fail(ErrorCode.ERROR100); + }else { + return WVPResult.success(recordList); + } + } + @Operation(summary = "JT-录像-开始回放", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "phoneNumber", description = "设备手机号", required = true) + @Parameter(name = "channelId", description = "通道国标编号, 一般为从1开始的数字", required = true) + @Parameter(name = "startTime", description = "开始时间,格式: yyyy-MM-dd HH:mm:ss", required = true) + @Parameter(name = "endTime", description = "结束时间,格式: yyyy-MM-dd HH:mm:ss", required = true) + @Parameter(name = "type", description = "0.音视频 1.音频 2.视频 3.视频或音视频", required = true) + @Parameter(name = "rate", description = "0.所有码流 1.主码流 2.子码流(如果此通道只传输音频,此字段置0)", required = true) + @Parameter(name = "playbackType", description = "0.正常回放 1.快进回放 2.关键帧快退回放 3.关键帧播放 4.单帧上传", required = true) + @Parameter(name = "playbackSpeed", description = "0.无效 1.1倍 2.2倍 3.4倍 4.8倍 5.16倍 (回放控制为1和2时,此字段内容有效,否则置0)", required = true) + @GetMapping("/playback/start") + public DeferredResult> recordLive(HttpServletRequest request, + @Parameter(required = true) String phoneNumber, + @Parameter(required = true) Integer channelId, + @Parameter(required = true) String startTime, + @Parameter(required = true) String endTime, + @Parameter(required = false) Integer type, + @Parameter(required = false) Integer rate, + @Parameter(required = false) Integer playbackType, + @Parameter(required = false) Integer playbackSpeed + + ) { + DeferredResult> result = new DeferredResult<>(userSetting.getPlayTimeout().longValue()); + result.onTimeout(()->{ + log.info("[JT-回放-等待超时] phoneNumber:{}, channelId:{}, ", phoneNumber, channelId); + // 释放rtpserver + WVPResult wvpResult = new WVPResult<>(); + wvpResult.setCode(ErrorCode.ERROR100.getCode()); + wvpResult.setMsg("回放超时"); + result.setResult(wvpResult); + jt1078PlayService.stopPlay(phoneNumber, channelId); + }); + + jt1078PlayService.playback(phoneNumber, channelId, startTime, endTime,type, rate, playbackType, playbackSpeed, wvpResult -> { + WVPResult wvpResultForFinish = new WVPResult<>(); + wvpResultForFinish.setCode(wvpResult.getCode()); + wvpResultForFinish.setMsg(wvpResult.getMsg()); + if (wvpResult.getCode() == InviteErrorCode.SUCCESS.getCode()) { + StreamInfo streamInfo = wvpResult.getData(); + if (streamInfo != null) { + if (userSetting.getUseSourceIpAsStreamIp()) { + String host; + try { + URL url=new URL(request.getRequestURL().toString()); + host=url.getHost(); + } catch (MalformedURLException e) { + host=request.getLocalAddr(); + } + streamInfo.changeStreamIp(host); + } + wvpResultForFinish.setData(new StreamContent(streamInfo)); + } + } + result.setResult(wvpResultForFinish); + }); + + return result; + } + + @Operation(summary = "JT-录像-回放控制", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "phoneNumber", description = "设备手机号", required = true) + @Parameter(name = "channelId", description = "通道国标编号, 一般为从1开始的数字", required = true) + @Parameter(name = "command", description = "0:开始回放; 1:暂停回放; 2:结束回放; 3:快进回放; 4:关键帧快退回放; 5:拖动回放; 6:关键帧播放", required = true) + @Parameter(name = "playbackSpeed", description = "0.无效 1.1倍 2.2倍 3.4倍 4.8倍 5.16倍 (回放控制为3和4时,此字段内容有效,否则置0)", required = false) + @Parameter(name = "time", description = "拖动回放位置(时间)", required = false) + @GetMapping("/playback/control") + public void recordControl(@Parameter(required = true) String phoneNumber, + @Parameter(required = true) Integer channelId, + @Parameter(required = false) Integer command, + @Parameter(required = false) String time, + @Parameter(required = false) Integer playbackSpeed + + ) { + jt1078PlayService.playbackControl(phoneNumber, channelId, command, playbackSpeed, time); + } + + @Operation(summary = "JT-录像-结束回放", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "phoneNumber", description = "设备手机号", required = true) + @Parameter(name = "channelId", description = "通道国标编号, 一般为从1开始的数字", required = true) + @GetMapping("/playback/stop") + public void stopPlayback(HttpServletRequest request, + @Parameter(required = true) String phoneNumber, + @Parameter(required = true) Integer channelId) { + jt1078PlayService.stopPlayback(phoneNumber, channelId); + } + + @Operation(summary = "JT-录像-获取下载地址", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "phoneNumber", description = "设备手机号", required = true) + @Parameter(name = "channelId", description = "通道国标编号, 一般为从1开始的数字", required = true) + @Parameter(name = "startTime", description = "开始时间,格式: yyyy-MM-dd HH:mm:ss", required = true) + @Parameter(name = "endTime", description = "结束时间,格式: yyyy-MM-dd HH:mm:ss", required = true) + @Parameter(name = "alarmSign", description = "报警标志", required = true) + @Parameter(name = "mediaType", description = "音视频资源类型: 0.音视频 1.音频 2.视频 3.视频或音视频", required = true) + @Parameter(name = "streamType", description = "码流类型:0.所有码流 1.主码流 2.子码流(如果此通道只传输音频,此字段置0)", required = true) + @Parameter(name = "storageType", description = "存储器类型", required = true) + @GetMapping("/playback/downloadUrl") + public String getRecordTempUrl(HttpServletRequest request, + @Parameter(required = true) String phoneNumber, + @Parameter(required = true) Integer channelId, + @Parameter(required = true) String startTime, + @Parameter(required = true) String endTime, + @Parameter(required = false) Integer alarmSign, + @Parameter(required = false) Integer mediaType, + @Parameter(required = false) Integer streamType, + @Parameter(required = false) Integer storageType + + ){ + log.info("[JT-录像] 下载,设备:{}, 通道: {}, 开始时间: {}, 结束时间: {},报警标志: {}, 音视频类型: {}, 码流类型: {},存储器类型: {}, ", + phoneNumber, channelId, startTime, endTime, alarmSign, mediaType, streamType, storageType); + if (!ftpSetting.getEnable()) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "未启用ftp服务,无法下载录像"); + } + return service.getRecordTempUrl(phoneNumber, channelId, startTime, endTime, alarmSign, mediaType, streamType, storageType); + } + + @Operation(summary = "JT-录像-下载", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "path", description = "临时下载路径", required = true) + @GetMapping("/playback/download") + public void download(HttpServletRequest request, HttpServletResponse response, @Parameter(required = true) String path) throws IOException { + if (!ftpSetting.getEnable()) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "未启用ftp服务,无法下载录像"); + } + DeferredResult result = new DeferredResult<>(); + ServletOutputStream outputStream = response.getOutputStream(); + response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE); + response.addHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode(path + ".mp4", "UTF-8")); +// response.setContentLength(394983300); + response.setStatus(HttpServletResponse.SC_OK); + service.recordDownload(path, outputStream); + } + + @Operation(summary = "JT-云台控制", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "phoneNumber", description = "设备手机号", required = true) + @Parameter(name = "channelId", description = "通道国标编号, 一般为从1开始的数字", required = true) + @Parameter(name = "command", description = "控制指令,允许值: left, right, up, down, zoomin, zoomout, irisin, irisout, focusnear, focusfar, stop", required = true) + @Parameter(name = "speed", description = "速度(0-255), command,值 left, right, up, down时有效", required = true) + @GetMapping("/ptz") + public void ptz(String phoneNumber, Integer channelId, String command, int speed){ + + log.info("[JT-云台控制] phoneNumber:{}, channelId:{}, command: {}, speed: {}", phoneNumber, channelId, command, speed); + service.ptzControl(phoneNumber, channelId, command, speed); + } + + @Operation(summary = "JT-补光灯开关", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "phoneNumber", description = "设备手机号", required = true) + @Parameter(name = "channelId", description = "通道国标编号, 一般为从1开始的数字", required = true) + @Parameter(name = "command", description = "控制指令,允许值: on off", required = true) + @GetMapping("/fill-light") + public void fillLight(String phoneNumber, Integer channelId, String command){ + + log.info("[JT-补光灯开关] phoneNumber:{}, channelId:{}, command: {}", phoneNumber, channelId, command); + service.supplementaryLight(phoneNumber, channelId, command); + } + + @Operation(summary = "JT-雨刷开关", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "phoneNumber", description = "设备手机号", required = true) + @Parameter(name = "channelId", description = "通道国标编号, 一般为从1开始的数字", required = true) + @Parameter(name = "command", description = "控制指令,允许值: on off", required = true) + @GetMapping("/wiper") + public void wiper(String phoneNumber, Integer channelId, String command){ + + log.info("[JT-雨刷开关] phoneNumber:{}, channelId:{}, command: {}", phoneNumber, channelId, command); + service.wiper(phoneNumber, channelId, command); + } + + @Operation(summary = "JT-查询终端参数", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "phoneNumber", description = "设备手机号", required = true) + @GetMapping("/config/get") + public JTDeviceConfig config(String phoneNumber, String[] params){ + + log.info("[JT-查询终端参数] phoneNumber:{}", phoneNumber); + return service.queryConfig(phoneNumber, params); + } + + @Operation(summary = "JT-设置终端参数", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "phoneNumber", description = "设备编号", required = true) + @Parameter(name = "config", description = "终端参数", required = true) + @PostMapping("/config/set") + public void setConfig(@RequestBody SetConfigParam config){ + + log.info("[JT-设置终端参数] 参数: {}", config.toString()); + service.setConfig(config.getPhoneNumber(), config.getConfig()); + } + + @Operation(summary = "终端控制-连接指定的服务器", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "control", description = "终端控制参数", required = true) + @PostMapping("/control/connection") + public void connectionControl(@RequestBody ConnectionControlParam control){ + + log.info("[JT-终端控制] 参数: {}", control.toString()); + service.connectionControl(control.getPhoneNumber(), control.getControl()); + } + + @Operation(summary = "JT-终端控制-复位", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "phoneNumber", description = "设备编号", required = true) + @PostMapping("/control/reset") + public void resetControl(String phoneNumber){ + + log.info("[JT-复位] phoneNumber: {}", phoneNumber); + service.resetControl(phoneNumber); + } + + @Operation(summary = "JT-终端控制-恢复出厂设置", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "phoneNumber", description = "设备编号", required = true) + @PostMapping("/control/factory-reset") + public void factoryResetControl(String phoneNumber){ + + log.info("[JT-恢复出厂设置] phoneNumber: {}", phoneNumber); + service.factoryResetControl(phoneNumber); + } + + @Operation(summary = "JT-查询终端属性", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "phoneNumber", description = "设备编号", required = true) + @GetMapping("/attribute") + public JTDeviceAttribute attribute(String phoneNumber){ + + log.info("[JT-查询终端属性] phoneNumber: {}", phoneNumber); + return service.attribute(phoneNumber); + } + + @Operation(summary = "JT-查询位置信息", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "phoneNumber", description = "设备编号", required = true) + @GetMapping("/position-info") + public JTPositionBaseInfo queryPositionInfo(String phoneNumber){ + + log.info("[JT-查询位置信息] phoneNumber: {}", phoneNumber); + return service.queryPositionInfo(phoneNumber); + } + + @Operation(summary = "JT-临时位置跟踪控制", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "phoneNumber", description = "设备编号", required = true) + @Parameter(name = "timeInterval", description = "时间间隔,单位为秒,时间间隔为0 时停止跟踪,停止跟踪无需带后继字段", required = true) + @Parameter(name = "validityPeriod", description = "位置跟踪有效期, 单位为秒,终端在接收到位置跟踪控制消息后,在有效期截止时间之前依据消息中的时间间隔发送位置汇报", required = true) + @GetMapping("/control/temp-position-tracking") + public void tempPositionTrackingControl(String phoneNumber, Integer timeInterval, Long validityPeriod){ + + log.info("[JT-临时位置跟踪控制] phoneNumber: {}, 时间间隔 {}秒, 位置跟踪有效期 {}秒", phoneNumber, timeInterval, validityPeriod); + service.tempPositionTrackingControl(phoneNumber, timeInterval, validityPeriod); + } + + @Operation(summary = "JT-人工确认报警消息", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "phoneNumber", description = "设备编号", required = true) + @Parameter(name = "timeInterval", description = "时间间隔,单位为秒,时间间隔为0 时停止跟踪,停止跟踪无需带后继字段", required = true) + @Parameter(name = "validityPeriod", description = "位置跟踪有效期, 单位为秒,终端在接收到位置跟踪控制消息后,在有效期截止时间之前依据消息中的时间间隔发送位置汇报", required = true) + @PostMapping("/confirmation-alarm-message") + public void confirmationAlarmMessage(@RequestBody ConfirmationAlarmMessageParam param){ + + log.info("[JT-人工确认报警消息] 参数: {}", param); + service.confirmationAlarmMessage(param.getPhoneNumber(), param.getAlarmPackageNo(), param.getAlarmMessageType()); + } + + @Operation(summary = "JT-链路检测", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "phoneNumber", description = "设备编号", required = true) + @GetMapping("/link-detection") + public Integer linkDetection(String phoneNumber){ + + log.info("[JT-链路检测] phoneNumber: {}", phoneNumber); + return service.linkDetection(phoneNumber); + } + + @Operation(summary = "JT-文本信息下发", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "textMessageParam", description = "文本信息下发参数", required = true) + @PostMapping("/text-msg") + public WVPResult textMessage(@RequestBody TextMessageParam textMessageParam){ + + log.info("[JT-文本信息下发] textMessageParam: {}", textMessageParam); + int result = service.textMessage(textMessageParam.getPhoneNumber(), textMessageParam.getSign(), textMessageParam.getTextType(), textMessageParam.getContent()); + if (result == 0) { + return WVPResult.success(result); + }else { + WVPResult fail = WVPResult.fail(ErrorCode.ERROR100); + fail.setData(result); + return fail; + } + } + + @Operation(summary = "JT-电话回拨", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "phoneNumber", description = "设备编号", required = true) + @Parameter(name = "sign", description = "标志: 0:普通通话,1:监听", required = true) + @Parameter(name = "destPhoneNumber", description = "回拨电话号码", required = true) + @GetMapping("/telephone-callback") + public WVPResult telephoneCallback(String phoneNumber, Integer sign, String destPhoneNumber){ + + log.info("[JT-电话回拨] phoneNumber: {}, sign: {}, phoneNumber: {},", phoneNumber, sign, phoneNumber); + int result = service.telephoneCallback(phoneNumber, sign, destPhoneNumber); + if (result == 0) { + return WVPResult.success(result); + }else { + WVPResult fail = WVPResult.fail(ErrorCode.ERROR100); + fail.setData(result); + return fail; + } + } + + @Operation(summary = "JT-设置电话本", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "setPhoneBookParam", description = "设置电话本参数", required = true) + @PostMapping("/set-phone-book") + public WVPResult setPhoneBook(@RequestBody SetPhoneBookParam setPhoneBookParam){ + + log.info("[JT-设置电话本] setPhoneBookParam: {}", setPhoneBookParam); + int result = service.setPhoneBook(setPhoneBookParam.getPhoneNumber(), setPhoneBookParam.getType(), setPhoneBookParam.getPhoneBookContactList()); + if (result == 0) { + return WVPResult.success(result); + }else { + WVPResult fail = WVPResult.fail(ErrorCode.ERROR100); + fail.setData(result); + return fail; + } + } + + @Operation(summary = "JT-车门控制", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "phoneNumber", description = "设备编号", required = true) + @Parameter(name = "open", description = "开启车门", required = true) + @GetMapping("/control/door") + public WVPResult controlDoor(String phoneNumber, Boolean open){ + + log.info("[JT-车门控制] phoneNumber: {}, open: {},", phoneNumber, open); + JTPositionBaseInfo positionBaseInfo = service.controlDoor(phoneNumber, open); + if (positionBaseInfo == null || positionBaseInfo.getStatus() == null) { + return WVPResult.fail(ErrorCode.ERROR100.getCode(), "控制失败"); + } + if (open == !positionBaseInfo.getStatus().isDoorLocking()) { + return WVPResult.success(null); + }else { + return WVPResult.fail(ErrorCode.ERROR100.getCode(), "控制失败"); + } + } + + @Operation(summary = "JT-更新圆形区域", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "areaParam", description = "设置区域参数", required = true) + @PostMapping("/area/circle/update") + public WVPResult updateAreaForCircle(@RequestBody SetAreaParam areaParam){ + + log.info("[JT-更新圆形区域] areaParam: {},", areaParam); + int result = service.setAreaForCircle(0, areaParam.getPhoneNumber(), areaParam.getCircleAreaList()); + if (result == 0) { + return WVPResult.success(result); + }else { + WVPResult fail = WVPResult.fail(ErrorCode.ERROR100); + fail.setData(result); + return fail; + } + } + + @Operation(summary = "JT-追加圆形区域", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "areaParam", description = "设置区域参数", required = true) + @PostMapping("/area/circle/add") + public WVPResult addAreaForCircle(@RequestBody SetAreaParam areaParam){ + + log.info("[JT-追加圆形区域] areaParam: {},", areaParam); + int result = service.setAreaForCircle(1, areaParam.getPhoneNumber(), areaParam.getCircleAreaList()); + if (result == 0) { + return WVPResult.success(result); + }else { + WVPResult fail = WVPResult.fail(ErrorCode.ERROR100); + fail.setData(result); + return fail; + } + } + + @Operation(summary = "JT-修改圆形区域", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "areaParam", description = "设置区域参数", required = true) + @PostMapping("/area/circle/edit") + public WVPResult editAreaForCircle(@RequestBody SetAreaParam areaParam){ + + log.info("[JT-修改圆形区域] areaParam: {},", areaParam); + int result = service.setAreaForCircle(2, areaParam.getPhoneNumber(), areaParam.getCircleAreaList()); + if (result == 0) { + return WVPResult.success(result); + }else { + WVPResult fail = WVPResult.fail(ErrorCode.ERROR100); + fail.setData(result); + return fail; + } + } + + @Operation(summary = "JT-删除圆形区域", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "phoneNumber", description = "设备编号", required = true) + @Parameter(name = "ids", description = "待删除圆形区域的id,例如1,2,3", required = true) + @GetMapping("/area/circle/delete") + public WVPResult deleteAreaForCircle(String phoneNumber, @RequestParam(value = "ids", required = false) List ids){ + + log.info("[JT-删除圆形区域] phoneNumber: {}, ids:{}", phoneNumber, ids); + int result = service.deleteAreaForCircle(phoneNumber, ids); + if (result == 0) { + return WVPResult.success(result); + }else { + WVPResult fail = WVPResult.fail(ErrorCode.ERROR100); + fail.setData(result); + return fail; + } + } + + @Operation(summary = "JT-查询圆形区域", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "phoneNumber", description = "设备编号", required = true) + @GetMapping("/area/circle/query") + public WVPResult> queryAreaForCircle(String phoneNumber, @RequestParam(value = "ids", required = false) List ids){ + + log.info("[JT-查询圆形区域] phoneNumber: {}, ids:{}", phoneNumber, ids); + List result = service.queryAreaForCircle(phoneNumber, ids); + if (result != null) { + return WVPResult.success(result); + }else { + return WVPResult.fail(ErrorCode.ERROR100); + } + } + + + @Operation(summary = "JT-更新矩形区域", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "areaParam", description = "设置区域参数", required = true) + @PostMapping("/area/rectangle/update") + public WVPResult updateAreaForRectangle(@RequestBody SetAreaParam areaParam){ + + log.info("[JT-更新矩形区域] areaParam: {},", areaParam); + int result = service.setAreaForRectangle(0, areaParam.getPhoneNumber(), areaParam.getRectangleAreas()); + if (result == 0) { + return WVPResult.success(result); + }else { + WVPResult fail = WVPResult.fail(ErrorCode.ERROR100); + fail.setData(result); + return fail; + } + } + + @Operation(summary = "JT-追加矩形区域", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "areaParam", description = "设置区域参数", required = true) + @PostMapping("/area/rectangle/add") + public WVPResult addAreaForRectangle(@RequestBody SetAreaParam areaParam){ + + log.info("[JT-追加矩形区域] areaParam: {},", areaParam); + int result = service.setAreaForRectangle(1, areaParam.getPhoneNumber(), areaParam.getRectangleAreas()); + if (result == 0) { + return WVPResult.success(result); + }else { + WVPResult fail = WVPResult.fail(ErrorCode.ERROR100); + fail.setData(result); + return fail; + } + } + + @Operation(summary = "JT-修改矩形区域", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "areaParam", description = "设置区域参数", required = true) + @PostMapping("/area/rectangle/edit") + public WVPResult editAreaForRectangle(@RequestBody SetAreaParam areaParam){ + + log.info("[JT-修改矩形区域] areaParam: {},", areaParam); + int result = service.setAreaForRectangle(2, areaParam.getPhoneNumber(), areaParam.getRectangleAreas()); + if (result == 0) { + return WVPResult.success(result); + }else { + WVPResult fail = WVPResult.fail(ErrorCode.ERROR100); + fail.setData(result); + return fail; + } + } + + @Operation(summary = "JT-删除矩形区域", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "phoneNumber", description = "设备编号", required = true) + @Parameter(name = "ids", description = "待删除圆形区域的id,例如1,2,3", required = true) + @GetMapping("/area/rectangle/delete") + public WVPResult deleteAreaForRectangle(String phoneNumber, @RequestParam(value = "ids", required = false) List ids){ + + log.info("[JT-删除矩形区域] phoneNumber: {}, ids:{}", phoneNumber, ids); + int result = service.deleteAreaForRectangle(phoneNumber, ids); + if (result == 0) { + return WVPResult.success(result); + }else { + WVPResult fail = WVPResult.fail(ErrorCode.ERROR100); + fail.setData(result); + return fail; + } + } + + @Operation(summary = "JT-查询矩形区域", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "phoneNumber", description = "设备编号", required = true) + @GetMapping("/area/rectangle/query") + public WVPResult> queryAreaForRectangle(String phoneNumber, @RequestParam(value = "ids", required = false) List ids){ + + log.info("[JT-查询矩形区域] phoneNumber: {}, ids:{}", phoneNumber, ids); + List result = service.queryAreaForRectangle(phoneNumber, ids); + if (result != null) { + return WVPResult.success(result); + }else { + return WVPResult.fail(ErrorCode.ERROR100); + } + } + + @Operation(summary = "JT-设置多边形区域", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "areaParam", description = "设置区域参数", required = true) + @PostMapping("/area/polygon/set") + public WVPResult setAreaForPolygon(@RequestBody SetAreaParam areaParam){ + + log.info("[JT-设置多边形区域] areaParam: {},", areaParam); + int result = service.setAreaForPolygon(areaParam.getPhoneNumber(), areaParam.getPolygonArea()); + if (result == 0) { + return WVPResult.success(result); + }else { + WVPResult fail = WVPResult.fail(ErrorCode.ERROR100); + fail.setData(result); + return fail; + } + } + + @Operation(summary = "JT-删除多边形区域", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "phoneNumber", description = "设备编号", required = true) + @Parameter(name = "ids", description = "待删除圆形区域的id,例如1,2,3", required = true) + @GetMapping("/area/polygon/delete") + public WVPResult deleteAreaForPolygon(String phoneNumber, @RequestParam(value = "ids", required = false) List ids){ + + log.info("[JT-删除多边形区域] phoneNumber: {}, ids:{}", phoneNumber, ids); + int result = service.deleteAreaForPolygon(phoneNumber, ids); + if (result == 0) { + return WVPResult.success(result); + }else { + WVPResult fail = WVPResult.fail(ErrorCode.ERROR100); + fail.setData(result); + return fail; + } + } + + @Operation(summary = "JT-查询多边形区域", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "phoneNumber", description = "设备编号", required = true) + @GetMapping("/area/polygon/query") + public WVPResult> queryAreaForPolygon(String phoneNumber, @RequestParam(value = "ids", required = false) List ids){ + + log.info("[JT-查询多边形区域] phoneNumber: {}, ids:{}", phoneNumber, ids); + List result = service.queryAreaForPolygon(phoneNumber, ids); + if (result != null) { + return WVPResult.success(result); + }else { + return WVPResult.fail(ErrorCode.ERROR100); + } + } + + @Operation(summary = "JT-设置路线", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "areaParam", description = "设置区域参数", required = true) + @PostMapping("/route/set") + public WVPResult setRoute(@RequestBody SetAreaParam areaParam){ + + log.info("[JT-设置路线] areaParam: {},", areaParam); + int result = service.setRoute(areaParam.getPhoneNumber(), areaParam.getRoute()); + if (result == 0) { + return WVPResult.success(result); + }else { + WVPResult fail = WVPResult.fail(ErrorCode.ERROR100); + fail.setData(result); + return fail; + } + } + + @Operation(summary = "JT-删除路线", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "phoneNumber", description = "设备编号", required = true) + @Parameter(name = "ids", description = "待删除圆形区域的id,例如1,2,3", required = true) + @GetMapping("/route/delete") + public WVPResult deleteRoute(String phoneNumber, @RequestParam(value = "ids", required = false) List ids){ + + log.info("[JT-删除路线] phoneNumber: {}, ids:{}", phoneNumber, ids); + int result = service.deleteRoute(phoneNumber, ids); + if (result == 0) { + return WVPResult.success(result); + }else { + WVPResult fail = WVPResult.fail(ErrorCode.ERROR100); + fail.setData(result); + return fail; + } + } + + @Operation(summary = "JT-查询路线", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "phoneNumber", description = "设备编号", required = true) + @GetMapping("/route/query") + public WVPResult> queryRoute(String phoneNumber, @RequestParam(value = "ids", required = false) List ids){ + + log.info("[JT-查询路线] phoneNumber: {}, ids:{}", phoneNumber, ids); + List result = service.queryRoute(phoneNumber, ids); + if (result != null) { + return WVPResult.success(result); + }else { + return WVPResult.fail(ErrorCode.ERROR100); + } + } + + // TODO 待实现 行驶记录数据采集命令 行驶记录数据上传 行驶记录参数下传命令 电子运单上报 CAN总线数据上传 + + @Operation(summary = "JT-上报驾驶员身份信息请求", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "phoneNumber", description = "设备编号", required = true) + @GetMapping("/driver-information") + public WVPResult queryDriverInformation(String phoneNumber){ + + log.info("[JT-上报驾驶员身份信息请求] phoneNumber: {}", phoneNumber); + JTDriverInformation jtDriverInformation = service.queryDriverInformation(phoneNumber); + if (jtDriverInformation != null) { + return WVPResult.success(jtDriverInformation); + }else { + return WVPResult.fail(ErrorCode.ERROR100); + } + } + + @Operation(summary = "JT-摄像头立即拍摄命令", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "phoneNumber", description = "设备编号", required = true) + @PostMapping("/shooting") + public WVPResult> shooting(@RequestBody ShootingParam param){ + + log.info("[JT-摄像头立即拍摄命令] param: {}", param ); + List ids = service.shooting(param.getPhoneNumber(), param.getShootingCommand()); + if (ids != null) { + return WVPResult.success(ids); + }else { + return WVPResult.fail(ErrorCode.ERROR100); + } + } + + @Operation(summary = "JT-抓图", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "phoneNumber", description = "设备编号", required = true) + @Parameter(name = "channelId", description = "通道编号", required = true) + @GetMapping("/snap") + public void snap(HttpServletResponse response, String phoneNumber, Integer channelId){ + + log.info("[JT-抓图] 设备编号: {}, 通道编号: {}", phoneNumber, channelId ); + Assert.notNull(channelId, "缺少通道编号"); + try { + ServletOutputStream outputStream = response.getOutputStream(); + response.setContentType(MediaType.IMAGE_JPEG_VALUE); +// response.addHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode(phoneNumber + "_" + channelId + ".jpg", "UTF-8")); + byte[] data = service.snap(phoneNumber, channelId); + outputStream.write(data); + outputStream.flush(); + }catch (Exception e) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), e.getMessage()); + } + } + + @Operation(summary = "JT-存储多媒体数据检索", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "param", description = "存储多媒体数据参数", required = true) + @PostMapping("/media/list") + public WVPResult> queryMediaData(@RequestBody QueryMediaDataParam param){ + + log.info("[JT-存储多媒体数据检索] param: {}", param ); + List ids = service.queryMediaData(param.getPhoneNumber(), param.getQueryMediaDataCommand()); + if (ids != null) { + return WVPResult.success(ids); + }else { + return WVPResult.fail(ErrorCode.ERROR100); + } + } + + @Operation(summary = "JT-单条存储多媒体数据上传", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "phoneNumber", description = "设备编号", required = true) + @Parameter(name = "mediaId", description = "多媒体ID", required = true) + @GetMapping("/media/upload/one/upload") + public void uploadOneMedia(HttpServletResponse response, String phoneNumber, Long mediaId){ + + log.info("[JT-单条存储多媒体数据上传] 设备编号: {}, 多媒体ID: {}", phoneNumber, mediaId ); + Assert.notNull(mediaId, "缺少通道编号"); + try { + ServletOutputStream outputStream = response.getOutputStream(); + response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE); + service.uploadOneMedia(phoneNumber, mediaId, outputStream, false); + }catch (Exception e) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), e.getMessage()); + } + } + + @Operation(summary = "JT-单条存储多媒体数据删除", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "phoneNumber", description = "设备编号", required = true) + @Parameter(name = "mediaId", description = "多媒体ID", required = true) + @GetMapping("/media/upload/one/delete") + public void deleteOneMedia(HttpServletResponse response, String phoneNumber, Long mediaId){ + + log.info("[JT-单条存储多媒体数据上传] 设备编号: {}, 多媒体ID: {}", phoneNumber, mediaId ); + Assert.notNull(mediaId, "缺少通道编号"); + try { + ServletOutputStream outputStream = response.getOutputStream(); + response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE); + service.uploadOneMedia(phoneNumber, mediaId, outputStream, true); + }catch (Exception e) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), e.getMessage()); + } + } + +// @Operation(summary = "JT-存储多媒体数据上传命令", security = @SecurityRequirement(name = JwtUtils.HEADER)) +// @Parameter(name = "param", description = "存储多媒体数据参数", required = true) +// @PostMapping("/media-data-upload") +// public DeferredResult>> updateMediaData(@RequestBody QueryMediaDataParam param){ +// +// log.info("[JT-存储多媒体数据上传命令] param: {}", param ); +// DeferredResult>> deferredResult = new DeferredResult<>(30000L); +// List resultList = new ArrayList<>(); +// +// deferredResult.onTimeout(()->{ +// log.info("[JT-存储多媒体数据上传命令超时] param: {}", param ); +// WVPResult> fail = WVPResult.fail(ErrorCode.ERROR100); +// fail.setMsg("超时"); +// fail.setData(resultList); +// deferredResult.setResult(fail); +// }); +// List ids; +// if (param.getMediaId() != null) { +// ids = new ArrayList<>(); +// JTMediaDataInfo mediaDataInfo = new JTMediaDataInfo(); +// mediaDataInfo.setId(param.getMediaId()); +// ids.add(mediaDataInfo); +// }else { +// ids = service.queryMediaData(param.getPhoneNumber(), param.getQueryMediaDataCommand()); +// } +// if (ids.isEmpty()) { +// deferredResult.setResult(WVPResult.fail(ErrorCode.ERROR100)); +// return deferredResult; +// } +// Map idMap= new HashMap<>(); +// for (JTMediaDataInfo mediaDataInfo : ids) { +// idMap.put(mediaDataInfo.getId() + ".jpg", mediaDataInfo); +// } +// // 开启文件监听 +// FileAlterationObserver observer = new FileAlterationObserver(new File("mediaEvent")); +// observer.addListener(new FileAlterationListenerAdaptor() { +// @Override +// public void onFileCreate(File file) { +// if (idMap.containsKey(file.getName())) { +// idMap.remove(file.getName()); +// resultList.add("mediaEvent" + File.separator + file.getName()); +// if (idMap.isEmpty()) { +// deferredResult.setResult(WVPResult.success(resultList)); +// } +// } +// } +// }); +// FileAlterationMonitor monitor = new FileAlterationMonitor(5, observer); +// try { +// monitor.start(); +// } catch (Exception e) { +// log.info("[JT-存储多媒体数据上传命令监听文件失败] param: {}", param ); +// deferredResult.setResult(null); +// return deferredResult; +// } +// taskExecutor.execute(()->{ +// if (param.getMediaId() != null) { +// service.uploadMediaDataForSingle(param.getPhoneNumber(), param.getMediaId(), param.getDelete()); +// }else { +// service.uploadMediaData(param.getPhoneNumber(), param.getQueryMediaDataCommand()); +// } +// +// }); +// return deferredResult; +// } + + @Operation(summary = "JT-开始录音", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "phoneNumber", description = "设备手机号", required = true) + @Parameter(name = "time", description = "录音时间,单位为秒(s) ,0 表示一直录音", required = false) + @Parameter(name = "save", description = "0:实时上传;1:保存", required = false) + @Parameter(name = "samplingRate", description = "音频采样率, 0:8K;1:11K;2:23K;3:32K", required = false) + @GetMapping("/record/start") + public void startRecord(HttpServletRequest request, + @Parameter(required = true) String phoneNumber, + @Parameter(required = false) Integer time, + @Parameter(required = false) Integer save, + @Parameter(required = false) Integer samplingRate + ) { + if (ObjectUtils.isEmpty(time)) { + time = 0; + } + if (ObjectUtils.isEmpty(save)) { + save = 0; + } + if (ObjectUtils.isEmpty(samplingRate)) { + samplingRate = 0; + } + service.record(phoneNumber, 1, time, save, samplingRate); + } + + @Operation(summary = "JT-停止录音", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "phoneNumber", description = "设备手机号", required = true) + @Parameter(name = "time", description = "录音时间,单位为秒(s) ,0 表示一直录音", required = false) + @Parameter(name = "save", description = "0:实时上传;1:保存", required = false) + @Parameter(name = "samplingRate", description = "音频采样率, 0:8K;1:11K;2:23K;3:32K", required = false) + @GetMapping("/record/stop") + public void stopRecord(HttpServletRequest request, + @Parameter(required = true) String phoneNumber, + @Parameter(required = false) Integer time, + @Parameter(required = false) Integer save, + @Parameter(required = false) Integer samplingRate + ) { + if (ObjectUtils.isEmpty(time)) { + time = 0; + } + if (ObjectUtils.isEmpty(save)) { + save = 0; + } + if (ObjectUtils.isEmpty(samplingRate)) { + samplingRate = 0; + } + service.record(phoneNumber, 0, time, save, samplingRate); + } + + @Operation(summary = "JT-查询终端音视频属性", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "phoneNumber", description = "设备手机号", required = true) + @GetMapping("/media/attribute") + public JTMediaAttribute queryMediaAttribute( @Parameter(required = true) String phoneNumber + ) { + return service.queryMediaAttribute(phoneNumber); + } + + // TODO 视频报警上报 + + +} + diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/controller/JT1078TerminalController.java b/src/main/java/com/genersoft/iot/vmp/jt1078/controller/JT1078TerminalController.java new file mode 100644 index 0000000..58058e4 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/controller/JT1078TerminalController.java @@ -0,0 +1,120 @@ +package com.genersoft.iot.vmp.jt1078.controller; + +import com.genersoft.iot.vmp.conf.security.JwtUtils; +import com.genersoft.iot.vmp.jt1078.bean.JTChannel; +import com.genersoft.iot.vmp.jt1078.bean.JTDevice; +import com.genersoft.iot.vmp.jt1078.service.Ijt1078Service; +import com.github.pagehelper.PageInfo; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.web.bind.annotation.*; + + + +@Slf4j +@ConditionalOnProperty(value = "jt1078.enable", havingValue = "true") +@RestController +@Tag(name = "部标终端以及通道管理") +@RequestMapping("/api/jt1078/terminal") +public class JT1078TerminalController { + + @Resource + Ijt1078Service service; + + @Operation(summary = "JT-分页查询部标设备", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "page", description = "当前页", required = true) + @Parameter(name = "count", description = "每页查询数量", required = true) + @Parameter(name = "query", description = "查询内容") + @Parameter(name = "online", description = "是否在线") + @GetMapping("/list") + public PageInfo getDevices(int page, int count, + @RequestParam(required = false) String query, + @RequestParam(required = false) Boolean online) { + return service.getDeviceList(page, count, query, online); + } + + @Operation(summary = "更新设备", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "device", description = "设备", required = true) + @PostMapping("/update") + public void updateDevice(JTDevice device){ + assert device.getId() > 0; + assert device.getPhoneNumber() != null; + service.updateDevice(device); + } + + @Operation(summary = "JT-新增设备", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "device", description = "设备", required = true) + @PostMapping("/add") + public void addDevice(JTDevice device){ + assert device.getPhoneNumber() != null; + String phoneNumber = device.getPhoneNumber().replaceFirst("^0*", ""); + device.setPhoneNumber(phoneNumber); + service.addDevice(device); + } + @Operation(summary = "删除设备", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "phoneNumber", description = "设备手机号", required = true) + @DeleteMapping("/delete") + public void addDevice(String phoneNumber){ + assert phoneNumber != null; + service.deleteDeviceByPhoneNumber(phoneNumber); + } + @Operation(summary = "查询设备", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "phoneNumber", description = "设备手机号", required = true) + @GetMapping("/query") + public JTDevice getDevice(Integer deviceId){ + return service.getDeviceById(deviceId); + } + + + @Operation(summary = "JT-查询部标通道", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "page", description = "当前页", required = true) + @Parameter(name = "count", description = "每页查询数量", required = true) + @Parameter(name = "deviceId", description = "设备ID", required = true) + @Parameter(name = "query", description = "查询内容") + @GetMapping("/channel/list") + public PageInfo getChannels(int page, int count, + @RequestParam(required = true) Integer deviceId, + @RequestParam(required = false) String query) { + assert deviceId != null; + return service.getChannelList(page, count, deviceId, query); + } + + @Operation(summary = "JT-查询单个部标通道", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "id", description = "通道数据库ID", required = true) + @GetMapping("/channel/one") + public JTChannel getChannel(Integer id) { + assert id != null; + return service.getChannelByDbId(id); + } + + @Operation(summary = "JT-更新通道", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "channel", description = "通道", required = true) + @PostMapping("/channel/update") + public void updateChannel(@RequestBody JTChannel channel){ + assert channel.getId() > 0; + assert channel.getChannelId() != null; + service.updateChannel(channel); + } + + @Operation(summary = "JT-新增通道", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "channel", description = "通道", required = true) + @PostMapping("/channel/add") + public JTChannel addChannel(@RequestBody JTChannel channel){ + assert channel.getChannelId() != null; + assert channel.getTerminalDbId() != 0; + service.addChannel(channel); + return channel; + } + @Operation(summary = "JT-删除通道", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "id", description = "通道的数据库ID", required = true) + @DeleteMapping("/channel/delete") + public void deleteChannel(Integer id){ + service.deleteChannelById(id); + } +} + diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/controller/bean/ConfirmationAlarmMessageParam.java b/src/main/java/com/genersoft/iot/vmp/jt1078/controller/bean/ConfirmationAlarmMessageParam.java new file mode 100644 index 0000000..10c3ece --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/controller/bean/ConfirmationAlarmMessageParam.java @@ -0,0 +1,31 @@ +package com.genersoft.iot.vmp.jt1078.controller.bean; + +import com.genersoft.iot.vmp.jt1078.bean.JTConfirmationAlarmMessageType; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; +import lombok.Setter; + +/** + * 人工确认报警消息参数 + */ +@Setter +@Getter +@Schema(description = "人工确认报警消息参数") +public class ConfirmationAlarmMessageParam { + + @Schema(description = "终端手机号") + private String phoneNumber; + @Schema(description = "报警消息流水号") + private int alarmPackageNo; + @Schema(description = "人工确认报警类型") + private JTConfirmationAlarmMessageType alarmMessageType; + + @Override + public String toString() { + return "ConfirmationAlarmMessageParam{" + + "PhoneNumber='" + phoneNumber + '\'' + + ", alarmPackageNo=" + alarmPackageNo + + ", alarmMessageType=" + alarmMessageType + + '}'; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/controller/bean/ConnectionControlParam.java b/src/main/java/com/genersoft/iot/vmp/jt1078/controller/bean/ConnectionControlParam.java new file mode 100644 index 0000000..235c47a --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/controller/bean/ConnectionControlParam.java @@ -0,0 +1,23 @@ +package com.genersoft.iot.vmp.jt1078.controller.bean; + +import com.genersoft.iot.vmp.jt1078.bean.JTDeviceConnectionControl; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; +import lombok.Setter; + +@Setter +@Getter +public class ConnectionControlParam { + + @Schema(description = "终端手机号") + private String phoneNumber; + private JTDeviceConnectionControl control; + + @Override + public String toString() { + return "ConnectionControlParam{" + + "deviceId='" + phoneNumber + '\'' + + ", control=" + control + + '}'; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/controller/bean/QueryMediaDataParam.java b/src/main/java/com/genersoft/iot/vmp/jt1078/controller/bean/QueryMediaDataParam.java new file mode 100644 index 0000000..debacab --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/controller/bean/QueryMediaDataParam.java @@ -0,0 +1,34 @@ +package com.genersoft.iot.vmp.jt1078.controller.bean; + +import com.genersoft.iot.vmp.jt1078.bean.JTQueryMediaDataCommand; +import com.genersoft.iot.vmp.jt1078.bean.JTShootingCommand; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; +import lombok.Setter; + +@Setter +@Getter +@Schema(description = "存储多媒体数据参数") +public class QueryMediaDataParam { + + @Schema(description = "终端手机号") + private String phoneNumber; + + @Schema(description = "多媒体 ID, 单条存储多媒体数据检索上传时有效") + private Long mediaId; + + @Schema(description = "删除标志, 单条存储多媒体数据检索上传时有效") + private int delete; + + @Schema(description = "存储多媒体数据参数") + private JTQueryMediaDataCommand queryMediaDataCommand; + + @Override + public String toString() { + return "QueryMediaDataParam{" + + "设备手机号='" + phoneNumber + '\'' + + ", mediaId=" + mediaId + + ", queryMediaDataCommand=" + queryMediaDataCommand + + '}'; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/controller/bean/SetAreaParam.java b/src/main/java/com/genersoft/iot/vmp/jt1078/controller/bean/SetAreaParam.java new file mode 100644 index 0000000..7a0cb5c --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/controller/bean/SetAreaParam.java @@ -0,0 +1,41 @@ +package com.genersoft.iot.vmp.jt1078.controller.bean; + +import com.genersoft.iot.vmp.jt1078.bean.*; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; +import lombok.Setter; + +import java.util.List; + +@Setter +@Getter +@Schema(description = "设置区域参数") +public class SetAreaParam { + + @Schema(description = "终端手机号") + private String phoneNumber; + + @Schema(description = "圆形区域项") + private List circleAreaList; + + @Schema(description = "矩形区域项") + private List rectangleAreas; + + @Schema(description = "多边形区域") + private JTPolygonArea polygonArea; + + @Schema(description = "路线") + private JTRoute route; + + + @Override + public String toString() { + return "SetAreaParam{" + + "设备手机号='" + phoneNumber + '\'' + + ", circleAreaList=" + circleAreaList + + ", rectangleAreas=" + rectangleAreas + + ", polygonArea=" + polygonArea + + ", route=" + route + + '}'; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/controller/bean/SetConfigParam.java b/src/main/java/com/genersoft/iot/vmp/jt1078/controller/bean/SetConfigParam.java new file mode 100644 index 0000000..e6d0e2f --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/controller/bean/SetConfigParam.java @@ -0,0 +1,26 @@ +package com.genersoft.iot.vmp.jt1078.controller.bean; + +import com.genersoft.iot.vmp.jt1078.bean.JTDeviceConfig; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; +import lombok.Setter; + +@Setter +@Getter +@Schema(description = "终端参数设置") +public class SetConfigParam { + + @Schema(description = "终端手机号") + private String phoneNumber; + + @Schema(description = "终端参数设置") + private JTDeviceConfig config; + + @Override + public String toString() { + return "SetConfigParam{" + + "phoneNumber='" + phoneNumber + '\'' + + ", config=" + config + + '}'; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/controller/bean/SetPhoneBookParam.java b/src/main/java/com/genersoft/iot/vmp/jt1078/controller/bean/SetPhoneBookParam.java new file mode 100644 index 0000000..7493bea --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/controller/bean/SetPhoneBookParam.java @@ -0,0 +1,37 @@ +package com.genersoft.iot.vmp.jt1078.controller.bean; + +import com.genersoft.iot.vmp.jt1078.bean.JTPhoneBookContact; +import com.genersoft.iot.vmp.jt1078.bean.JTTextSign; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; +import lombok.Setter; + +import java.util.List; + +@Setter +@Getter +@Schema(description = "设置电话本") +public class SetPhoneBookParam { + + @Schema(description = "终端手机号") + private String phoneNumber; + + @Schema(description = "设置类型:\n" + + "0: 删除终端上所有存储的联系人,\n" + + "1: 表示更新电话本, 删除终端中已有全部联系人并追加消息中的联系人,\n" + + "2: 表示追加电话本,\n" + + "3: 表示修改电话本$以联系人为索引") + private int type; + + @Schema(description = "联系人") + private List phoneBookContactList; + + @Override + public String toString() { + return "SetPhoneBookParam{" + + "设备手机号='" + phoneNumber + '\'' + + ", type=" + type + + ", phoneBookContactList=" + phoneBookContactList + + '}'; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/controller/bean/ShootingParam.java b/src/main/java/com/genersoft/iot/vmp/jt1078/controller/bean/ShootingParam.java new file mode 100644 index 0000000..101749a --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/controller/bean/ShootingParam.java @@ -0,0 +1,26 @@ +package com.genersoft.iot.vmp.jt1078.controller.bean; + +import com.genersoft.iot.vmp.jt1078.bean.JTShootingCommand; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; +import lombok.Setter; + +@Setter +@Getter +@Schema(description = "摄像头立即拍摄命令参数") +public class ShootingParam { + + @Schema(description = "终端手机号") + private String phoneNumber; + + @Schema(description = "拍摄命令参数") + private JTShootingCommand shootingCommand; + + @Override + public String toString() { + return "ShootingParam{" + + "设备手机号='" + phoneNumber + '\'' + + ", shootingCommand=" + shootingCommand + + '}'; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/controller/bean/TextMessageParam.java b/src/main/java/com/genersoft/iot/vmp/jt1078/controller/bean/TextMessageParam.java new file mode 100644 index 0000000..5df0f89 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/controller/bean/TextMessageParam.java @@ -0,0 +1,34 @@ +package com.genersoft.iot.vmp.jt1078.controller.bean; + +import com.genersoft.iot.vmp.jt1078.bean.JTTextSign; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; +import lombok.Setter; + +/** + * 文本信息下发参数 + */ +@Setter +@Getter +@Schema(description = "人工确认报警消息参数") +public class TextMessageParam { + + @Schema(description = "终端手机号") + private String phoneNumber; + @Schema(description = "标志") + private JTTextSign sign; + @Schema(description = "文本类型,1 = 通知 ,2 = 服务") + private int textType; + @Schema(description = "消息内容,最长为1024字节") + private String content; + + @Override + public String toString() { + return "TextMessageParam{" + + "phoneNumber='" + phoneNumber + '\'' + + ", sign=" + sign + + ", textType=" + textType + + ", content='" + content + '\'' + + '}'; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/dao/JTChannelMapper.java b/src/main/java/com/genersoft/iot/vmp/jt1078/dao/JTChannelMapper.java new file mode 100644 index 0000000..5781026 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/dao/JTChannelMapper.java @@ -0,0 +1,50 @@ +package com.genersoft.iot.vmp.jt1078.dao; + +import com.genersoft.iot.vmp.jt1078.bean.JTChannel; +import com.genersoft.iot.vmp.jt1078.dao.provider.JTChannelProvider; +import org.apache.ibatis.annotations.*; + +import java.util.List; + +@Mapper +public interface JTChannelMapper { + + @SelectProvider(type = JTChannelProvider.class, method = "selectAll") + List selectAll(@Param("terminalDbId") int terminalDbId, @Param("query") String query); + + @Update(value = {" "}) + void update(JTChannel channel); + + @Insert(value = {" "}) + @Options(useGeneratedKeys = true, keyProperty = "id", keyColumn = "id") + void add(JTChannel channel); + + @Delete("delete from wvp_jt_channel where id = #{id}") + void delete(@Param("id") int id); + + @SelectProvider(type = JTChannelProvider.class, method = "selectChannelByChannelId") + JTChannel selectChannelByChannelId(@Param("terminalDbId") int terminalDbId, @Param("channelId") Integer channelId); + + @SelectProvider(type = JTChannelProvider.class, method = "selectChannelById") + JTChannel selectChannelById(@Param("id") Integer id); +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/dao/JTTerminalMapper.java b/src/main/java/com/genersoft/iot/vmp/jt1078/dao/JTTerminalMapper.java new file mode 100644 index 0000000..c27a42e --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/dao/JTTerminalMapper.java @@ -0,0 +1,115 @@ +package com.genersoft.iot.vmp.jt1078.dao; + +import com.genersoft.iot.vmp.jt1078.bean.JTDevice; +import org.apache.ibatis.annotations.*; + +import java.util.List; + +@Mapper +public interface JTTerminalMapper { + + @Select("SELECT * FROM wvp_jt_terminal where phone_number=#{phoneNumber}") + JTDevice getDevice(@Param("phoneNumber") String phoneNumber); + + @Update(value = {" "}) + void updateDevice(JTDevice device); + @Select(value = {" "}) + List getDeviceList(@Param("query") String query, @Param("online") Boolean online); + + @Insert("INSERT INTO wvp_jt_terminal (" + + "terminal_id,"+ + "province_id,"+ + "province_text,"+ + "city_id,"+ + "city_text,"+ + "maker_id,"+ + "phone_number,"+ + "model,"+ + "plate_color,"+ + "plate_no,"+ + "longitude,"+ + "latitude,"+ + "create_time,"+ + "register_time,"+ + "update_time"+ + ") VALUES (" + + "#{terminalId}," + + "#{provinceId}," + + "#{provinceText}," + + "#{cityId}," + + "#{cityText}," + + "#{makerId}," + + "#{phoneNumber}," + + "#{model}," + + "#{plateColor}," + + "#{plateNo}," + + "#{longitude}," + + "#{latitude}," + + "#{createTime}," + + "#{registerTime}," + + "#{updateTime}" + + ")") + void addDevice(JTDevice device); + + @Delete("delete from wvp_jt_terminal where phone_number = #{phoneNumber}") + void deleteDeviceByPhoneNumber(@Param("phoneNumber") String phoneNumber); + + @Update(value = {" "}) + void updateDeviceStatus(@Param("connected") boolean connected, @Param("phoneNumber") String phoneNumber); + + @Select("SELECT * FROM wvp_jt_terminal where id=#{deviceId}") + JTDevice getDeviceById(@Param("deviceId") Integer deviceId); + + @Update({""}) + void batchUpdateDevicePosition(List devices); +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/dao/provider/JTChannelProvider.java b/src/main/java/com/genersoft/iot/vmp/jt1078/dao/provider/JTChannelProvider.java new file mode 100644 index 0000000..c06f0ec --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/dao/provider/JTChannelProvider.java @@ -0,0 +1,37 @@ +package com.genersoft.iot.vmp.jt1078.dao.provider; + +import java.util.Map; + +public class JTChannelProvider { + + public final static String BASE_SQL = + "SELECT jc.*, jc.id as data_device_id, wdc.*, wdc.id as gb_id " + + " from wvp_jt_channel jc " + + " LEFT join wvp_device_channel wdc " + + " on jc.id = wdc.data_device_id and wdc.data_type = 200 "; + + public String selectChannelByChannelId(Map params ){ + StringBuilder sqlBuild = new StringBuilder(); + sqlBuild.append(BASE_SQL); + sqlBuild.append(" WHERE jc.terminal_db_id = #{terminalDbId} and jc.channel_id = #{channelId} "); + return sqlBuild.toString(); + } + public String selectChannelById(Map params ){ + StringBuilder sqlBuild = new StringBuilder(); + sqlBuild.append(BASE_SQL); + sqlBuild.append(" WHERE jc.id = #{id}"); + return sqlBuild.toString(); + } + public String selectAll(Map params ){ + StringBuilder sqlBuild = new StringBuilder(); + sqlBuild.append(BASE_SQL); + sqlBuild.append(" WHERE jc.terminal_db_id = #{terminalDbId} "); + if (params.get("query") != null) { + sqlBuild.append(" AND ") + .append(" jc.name LIKE ").append("'%").append(params.get("query")).append("%'") + ; + } + sqlBuild.append(" ORDER BY jc.channel_id "); + return sqlBuild.toString(); + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/event/ConnectChangeEvent.java b/src/main/java/com/genersoft/iot/vmp/jt1078/event/ConnectChangeEvent.java new file mode 100755 index 0000000..c298c2c --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/event/ConnectChangeEvent.java @@ -0,0 +1,31 @@ +package com.genersoft.iot.vmp.jt1078.event; + +import com.genersoft.iot.vmp.jt1078.bean.JTDevice; +import lombok.Getter; +import lombok.Setter; +import org.springframework.context.ApplicationEvent; + +import java.io.Serial; +import java.time.Clock; + +/** + * 链接断或者连接的事件 + */ + +@Setter +@Getter +public class ConnectChangeEvent extends ApplicationEvent { + + @Serial + private static final long serialVersionUID = 1L; + + public ConnectChangeEvent(Object source) { + super(source); + } + + + private boolean connected; + + private String phoneNumber; + +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/event/DeviceUpdateEvent.java b/src/main/java/com/genersoft/iot/vmp/jt1078/event/DeviceUpdateEvent.java new file mode 100755 index 0000000..552236f --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/event/DeviceUpdateEvent.java @@ -0,0 +1,25 @@ +package com.genersoft.iot.vmp.jt1078.event; + +import com.genersoft.iot.vmp.jt1078.bean.JTDevice; +import lombok.Getter; +import lombok.Setter; +import org.springframework.context.ApplicationEvent; + +import java.io.Serial; + +/** + * 设备更新事件 + */ +public class DeviceUpdateEvent extends ApplicationEvent { + + @Serial + private static final long serialVersionUID = 1L; + + public DeviceUpdateEvent(Object source) { + super(source); + } + + @Getter + @Setter + private JTDevice device; +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/event/FtpUploadEvent.java b/src/main/java/com/genersoft/iot/vmp/jt1078/event/FtpUploadEvent.java new file mode 100644 index 0000000..51c4d43 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/event/FtpUploadEvent.java @@ -0,0 +1,17 @@ +package com.genersoft.iot.vmp.jt1078.event; + +import lombok.Getter; +import lombok.Setter; +import org.springframework.context.ApplicationEvent; + +@Setter +@Getter +public class FtpUploadEvent extends ApplicationEvent { + + public FtpUploadEvent(Object source) { + super(source); + } + + private String fileName; + +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/event/JTPositionEvent.java b/src/main/java/com/genersoft/iot/vmp/jt1078/event/JTPositionEvent.java new file mode 100755 index 0000000..4155ba9 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/event/JTPositionEvent.java @@ -0,0 +1,30 @@ +package com.genersoft.iot.vmp.jt1078.event; + +import com.genersoft.iot.vmp.jt1078.bean.JTDevice; +import com.genersoft.iot.vmp.jt1078.bean.JTPositionBaseInfo; +import lombok.Getter; +import lombok.Setter; +import org.springframework.context.ApplicationEvent; + +import java.io.Serial; + +/** + * 设备更新事件 + */ +public class JTPositionEvent extends ApplicationEvent { + + @Serial + private static final long serialVersionUID = 1L; + + public JTPositionEvent(Object source) { + super(source); + } + + @Getter + @Setter + private String phoneNumber; + + @Getter + @Setter + private JTPositionBaseInfo positionInfo; +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/event/eventListener/ConnectChangeEventListener.java b/src/main/java/com/genersoft/iot/vmp/jt1078/event/eventListener/ConnectChangeEventListener.java new file mode 100644 index 0000000..28fb289 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/event/eventListener/ConnectChangeEventListener.java @@ -0,0 +1,36 @@ +package com.genersoft.iot.vmp.jt1078.event.eventListener; + +import com.genersoft.iot.vmp.jt1078.bean.JTDevice; +import com.genersoft.iot.vmp.jt1078.event.ConnectChangeEvent; +import com.genersoft.iot.vmp.jt1078.service.Ijt1078Service; +import com.genersoft.iot.vmp.jt1078.session.SessionManager; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationListener; +import org.springframework.stereotype.Component; + +@Component +public class ConnectChangeEventListener implements ApplicationListener { + + private final static Logger log = LoggerFactory.getLogger(ConnectChangeEventListener.class); + + @Autowired + private Ijt1078Service service; + + @Override + public void onApplicationEvent(ConnectChangeEvent event) { + if (event.isConnected()) { + log.info("[JT-设备已连接] 终端ID: {}", event.getPhoneNumber()); + }else{ + log.info("[JT-设备连接已断开] 终端ID: {}", event.getPhoneNumber()); + if(SessionManager.INSTANCE.get(event.getPhoneNumber()) != null) { + SessionManager.INSTANCE.get(event.getPhoneNumber()).unregister(); + } + } + JTDevice device = service.getDevice(event.getPhoneNumber()); + if (device != null) { + service.updateDeviceStatus(event.isConnected(), event.getPhoneNumber()); + } + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/proc/Header.java b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/Header.java new file mode 100644 index 0000000..fdf8080 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/Header.java @@ -0,0 +1,48 @@ +package com.genersoft.iot.vmp.jt1078.proc; + +import com.genersoft.iot.vmp.jt1078.util.Bin; +import lombok.Data; + +/** + * @author QingtaiJiang + * @date 2023/4/27 18:22 + * @email qingtaij@163.com + */ +@Data +public class Header { + // 消息ID + String msgId; + + // 消息体属性 + Integer msgPro; + + // 终端手机号 + String phoneNumber; + + // 消息体流水号 + Integer sn; + + // 协议版本号 + Short version = -1; + + + /** + * 判断是否是2019的版本 + * + * @return true 2019后的版本。false 2013 + */ + public boolean is2019Version() { + return Bin.get(msgPro, 14); + } + + @Override + public String toString() { + return "Header{" + + "消息ID='" + msgId + '\'' + + ", 消息体属性=" + msgPro + + ", 终端手机号='" + phoneNumber + '\'' + + ", 消息体流水号=" + sn + + ", 协议版本号=" + version + + '}'; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/proc/entity/Cmd.java b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/entity/Cmd.java new file mode 100644 index 0000000..d6c3a8e --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/entity/Cmd.java @@ -0,0 +1,78 @@ +package com.genersoft.iot.vmp.jt1078.proc.entity; + +import com.genersoft.iot.vmp.jt1078.proc.response.Rs; +import lombok.Data; + +/** + * @author QingtaiJiang + * @date 2023/4/27 18:23 + * @email qingtaij@163.com + */ +@Data +public class Cmd { + String phoneNumber; + Long packageNo; + String msgId; + String respId; + Rs rs; + + public Cmd() { + } + + public Cmd(Builder builder) { + this.phoneNumber = builder.phoneNumber; + this.packageNo = builder.packageNo; + this.msgId = builder.msgId; + this.respId = builder.respId; + this.rs = builder.rs; + } + + public static class Builder { + String phoneNumber; + Long packageNo; + String msgId; + String respId; + Rs rs; + + public Builder setPhoneNumber(String phoneNumber) { + this.phoneNumber = phoneNumber.replaceFirst("^0*", ""); + return this; + } + + public Builder setPackageNo(Long packageNo) { + this.packageNo = packageNo; + return this; + } + + public Builder setMsgId(String msgId) { + this.msgId = msgId; + return this; + } + + public Builder setRespId(String respId) { + this.respId = respId; + return this; + } + + public Builder setRs(Rs re) { + this.rs = re; + return this; + } + + public Cmd build() { + return new Cmd(this); + } + } + + + @Override + public String toString() { + return "Cmd{" + + "devId='" + phoneNumber + '\'' + + ", packageNo=" + packageNo + + ", msgId='" + msgId + '\'' + + ", respId='" + respId + '\'' + + ", rs=" + rs + + '}'; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/proc/factory/CodecFactory.java b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/factory/CodecFactory.java new file mode 100644 index 0000000..8551e39 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/factory/CodecFactory.java @@ -0,0 +1,42 @@ +package com.genersoft.iot.vmp.jt1078.proc.factory; + +import com.genersoft.iot.vmp.jt1078.annotation.MsgId; +import com.genersoft.iot.vmp.jt1078.proc.request.Re; +import com.genersoft.iot.vmp.jt1078.util.ClassUtil; +import lombok.extern.slf4j.Slf4j; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * @author QingtaiJiang + * @date 2023/4/27 18:29 + * @email qingtaij@163.com + */ +@Slf4j +public class CodecFactory { + + private static Map> protocolHash; + + public static void init() { + protocolHash = new HashMap<>(); + List> classList = ClassUtil.getClassList("com.genersoft.iot.vmp.jt1078.proc", MsgId.class); + for (Class handlerClass : classList) { + String id = handlerClass.getAnnotation(MsgId.class).id(); + protocolHash.put(id, handlerClass); + } + if (log.isDebugEnabled()) { + log.debug("消息ID缓存表 protocolHash:{}", protocolHash); + } + } + + public static Re getHandler(String msgId) { + Class aClass = protocolHash.get(msgId); + Object bean = ClassUtil.getBean(aClass); + if (bean instanceof Re) { + return (Re) bean; + } + return null; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/proc/request/J0001.java b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/request/J0001.java new file mode 100644 index 0000000..c775e15 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/request/J0001.java @@ -0,0 +1,49 @@ +package com.genersoft.iot.vmp.jt1078.proc.request; + +import com.genersoft.iot.vmp.jt1078.annotation.MsgId; +import com.genersoft.iot.vmp.jt1078.proc.Header; +import com.genersoft.iot.vmp.jt1078.proc.response.Rs; +import com.genersoft.iot.vmp.jt1078.service.Ijt1078Service; +import com.genersoft.iot.vmp.jt1078.session.Session; +import com.genersoft.iot.vmp.jt1078.session.SessionManager; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufUtil; +import lombok.Getter; +import org.springframework.context.ApplicationEvent; + +/** + * 终端通用应答 + * + * @author QingtaiJiang + * @date 2023/4/27 18:04 + * @email qingtaij@163.com + */ +@Getter +@MsgId(id = "0001") +public class J0001 extends Re { + int respNo; + String respId; + /** + * 0:成功/确认;1:失败;2:消息有误;3:不支持 + */ + int result; + + @Override + protected Rs decode0(ByteBuf buf, Header header, Session session) { + respNo = buf.readUnsignedShort(); + respId = ByteBufUtil.hexDump(buf.readSlice(2)); + result = buf.readUnsignedByte(); + return null; + } + + @Override + protected Rs handler(Header header, Session session, Ijt1078Service service) { + SessionManager.INSTANCE.response(header.getPhoneNumber(), "0001", (long) respNo, result); + return null; + } + + @Override + public ApplicationEvent getEvent() { + return null; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/proc/request/J0002.java b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/request/J0002.java new file mode 100644 index 0000000..71a7523 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/request/J0002.java @@ -0,0 +1,42 @@ +package com.genersoft.iot.vmp.jt1078.proc.request; + +import com.genersoft.iot.vmp.jt1078.annotation.MsgId; +import com.genersoft.iot.vmp.jt1078.proc.Header; +import com.genersoft.iot.vmp.jt1078.proc.response.J8001; +import com.genersoft.iot.vmp.jt1078.proc.response.Rs; +import com.genersoft.iot.vmp.jt1078.service.Ijt1078Service; +import com.genersoft.iot.vmp.jt1078.session.Session; +import io.netty.buffer.ByteBuf; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.ApplicationEvent; + +/** + * 终端心跳 + * + * @author QingtaiJiang + * @date 2023/4/27 18:04 + * @email qingtaij@163.com + */ +@Slf4j +@MsgId(id = "0002") +public class J0002 extends Re { + @Override + protected Rs decode0(ByteBuf buf, Header header, Session session) { + return null; + } + + @Override + protected Rs handler(Header header, Session session, Ijt1078Service service) { + log.info("[终端心跳] {}", header.getPhoneNumber()); + J8001 j8001 = new J8001(); + j8001.setRespNo(header.getSn()); + j8001.setRespId(header.getMsgId()); + j8001.setResult(J8001.SUCCESS); + return j8001; + } + + @Override + public ApplicationEvent getEvent() { + return null; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/proc/request/J0003.java b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/request/J0003.java new file mode 100644 index 0000000..f32a4cb --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/request/J0003.java @@ -0,0 +1,46 @@ +package com.genersoft.iot.vmp.jt1078.proc.request; + +import com.genersoft.iot.vmp.jt1078.annotation.MsgId; +import com.genersoft.iot.vmp.jt1078.proc.Header; +import com.genersoft.iot.vmp.jt1078.proc.response.Rs; +import com.genersoft.iot.vmp.jt1078.service.Ijt1078Service; +import com.genersoft.iot.vmp.jt1078.session.Session; +import com.genersoft.iot.vmp.jt1078.session.SessionManager; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufUtil; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.ApplicationEvent; + +/** + * 终端注销 + */ +@Slf4j +@Getter +@MsgId(id = "0003") +public class J0003 extends Re { + + int respNo; + String respId; + int result; + + @Override + protected Rs decode0(ByteBuf buf, Header header, Session session) { + respNo = buf.readUnsignedShort(); + respId = ByteBufUtil.hexDump(buf.readSlice(2)); + result = buf.readUnsignedByte(); + log.info("[JT-注销] 设备: {}", header.getPhoneNumber()); + return null; + } + + @Override + protected Rs handler(Header header, Session session, Ijt1078Service service) { + SessionManager.INSTANCE.response(header.getPhoneNumber(), "0001", (long) respNo, result); + return null; + } + + @Override + public ApplicationEvent getEvent() { + return null; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/proc/request/J0004.java b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/request/J0004.java new file mode 100644 index 0000000..3db25ce --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/request/J0004.java @@ -0,0 +1,34 @@ +package com.genersoft.iot.vmp.jt1078.proc.request; + +import com.genersoft.iot.vmp.jt1078.annotation.MsgId; +import com.genersoft.iot.vmp.jt1078.proc.Header; +import com.genersoft.iot.vmp.jt1078.proc.response.Rs; +import com.genersoft.iot.vmp.jt1078.service.Ijt1078Service; +import com.genersoft.iot.vmp.jt1078.session.Session; +import io.netty.buffer.ByteBuf; +import org.springframework.context.ApplicationEvent; + +/** + * 查询服务器时间 + * + * @author QingtaiJiang + * @date 2023/4/27 18:06 + * @email qingtaij@163.com + */ +@MsgId(id = "0004") +public class J0004 extends Re { + @Override + protected Rs decode0(ByteBuf buf, Header header, Session session) { + return null; + } + + @Override + protected Rs handler(Header header, Session session, Ijt1078Service service) { + return null; + } + + @Override + public ApplicationEvent getEvent() { + return null; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/proc/request/J0100.java b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/request/J0100.java new file mode 100644 index 0000000..8be3abc --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/request/J0100.java @@ -0,0 +1,132 @@ +package com.genersoft.iot.vmp.jt1078.proc.request; + +import com.genersoft.iot.vmp.common.CivilCodePo; +import com.genersoft.iot.vmp.jt1078.annotation.MsgId; +import com.genersoft.iot.vmp.jt1078.bean.JTDevice; +import com.genersoft.iot.vmp.jt1078.event.DeviceUpdateEvent; +import com.genersoft.iot.vmp.jt1078.proc.Header; +import com.genersoft.iot.vmp.jt1078.proc.response.J8100; +import com.genersoft.iot.vmp.jt1078.proc.response.Rs; +import com.genersoft.iot.vmp.jt1078.service.Ijt1078Service; +import com.genersoft.iot.vmp.jt1078.session.Session; +import com.genersoft.iot.vmp.utils.CivilCodeUtil; +import com.genersoft.iot.vmp.utils.DateUtil; +import io.netty.buffer.ByteBuf; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.ApplicationEvent; + +import java.io.UnsupportedEncodingException; +import java.nio.charset.Charset; +import java.util.UUID; + +/** + * 终端注册 + * + * @author QingtaiJiang + * @date 2023/4/27 18:06 + * @email qingtaij@163.com + */ +@Slf4j +@MsgId(id = "0100") +public class J0100 extends Re { + + private JTDevice device; + private JTDevice deviceForUpdate; + + @Override + protected Rs decode0(ByteBuf buf, Header header, Session session) { + Short version = header.getVersion(); + device = new JTDevice(); + device.setProvinceId(buf.readUnsignedShort() + ""); + if (version >= 1) { + device.setCityId(buf.readUnsignedShort() + ""); + // decode as 2019 + device.setMakerId(buf.readCharSequence(11, Charset.forName("GBK")) + .toString().trim()); + + device.setModel(buf.readCharSequence(30, Charset.forName("GBK")) + .toString().trim()); + + device.setTerminalId(buf.readCharSequence(30, Charset.forName("GBK")) + .toString().trim()); + + device.setPlateColor(buf.readByte()); + device.setPlateNo(buf.readCharSequence(buf.readableBytes(), Charset.forName("GBK")) + .toString().trim()); + } else { + // decode as 2013 + device.setCityId(buf.readUnsignedShort() + ""); + // decode as 2019 + byte[] bytes5 = new byte[5]; + buf.readBytes(bytes5); + device.setMakerId(new String(bytes5).trim()); + + byte[] bytes20 = new byte[20]; + buf.readBytes(bytes20); + device.setModel(new String(bytes20).trim()); + + byte[] bytes7 = new byte[7]; + buf.readBytes(bytes7); + device.setTerminalId(new String(bytes7).trim()); + + device.setPlateColor(buf.readByte()); + byte[] plateColorBytes = new byte[buf.readableBytes()]; + buf.readBytes(plateColorBytes); + try { + device.setPlateNo(new String(plateColorBytes, "GBK").trim()); + } catch (UnsupportedEncodingException e) { + throw new RuntimeException(e); + } + } + return null; + } + + @Override + protected Rs handler(Header header, Session session, Ijt1078Service service) { + J8100 j8100 = new J8100(); + j8100.setRespNo(header.getSn()); + // 从数据库判断这个设备是否合法 + deviceForUpdate = service.getDevice(header.getPhoneNumber()); + if (deviceForUpdate != null) { + j8100.setResult(J8100.SUCCESS); + String authenticationCode = UUID.randomUUID().toString(); + j8100.setCode(authenticationCode); + session.setAuthenticationCode(authenticationCode); + deviceForUpdate.setStatus(true); + deviceForUpdate.setProvinceId(device.getProvinceId()); + deviceForUpdate.setRegisterTime(DateUtil.getNow()); + CivilCodePo provinceCivilCodePo = CivilCodeUtil.INSTANCE.get(device.getProvinceId()); + if (provinceCivilCodePo != null) { + deviceForUpdate.setProvinceText(provinceCivilCodePo.getName()); + } + deviceForUpdate.setCityId(device.getCityId()); + CivilCodePo cityCivilCodePo = CivilCodeUtil.INSTANCE.get(device.getProvinceId() + + String.format("%04d", Integer.parseInt(device.getCityId()))); + if (cityCivilCodePo != null) { + deviceForUpdate.setCityText(cityCivilCodePo.getName()); + } + deviceForUpdate.setModel(device.getModel()); + deviceForUpdate.setMakerId(device.getMakerId()); + deviceForUpdate.setTerminalId(device.getTerminalId()); + // TODO 支持直接展示车牌颜色的描述 + deviceForUpdate.setPlateColor(device.getPlateColor()); + deviceForUpdate.setPlateNo(device.getPlateNo()); + log.info("[JT-注册成功] 设备: {}", deviceForUpdate); + }else { + log.info("[JT-注册失败] 未授权设备: {}", header.getPhoneNumber()); + j8100.setResult(J8100.FAIL); + // 断开连接,清理资源 + if (session.isRegistered()) { + session.unregister(); + } + } + return j8100; + } + + @Override + public ApplicationEvent getEvent() { + DeviceUpdateEvent registerEvent = new DeviceUpdateEvent(this); + registerEvent.setDevice(deviceForUpdate); + return registerEvent; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/proc/request/J0102.java b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/request/J0102.java new file mode 100644 index 0000000..8d3e2a7 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/request/J0102.java @@ -0,0 +1,70 @@ +package com.genersoft.iot.vmp.jt1078.proc.request; + +import com.genersoft.iot.vmp.jt1078.annotation.MsgId; +import com.genersoft.iot.vmp.jt1078.bean.JTDevice; +import com.genersoft.iot.vmp.jt1078.event.DeviceUpdateEvent; +import com.genersoft.iot.vmp.jt1078.proc.Header; +import com.genersoft.iot.vmp.jt1078.proc.response.J8001; +import com.genersoft.iot.vmp.jt1078.proc.response.Rs; +import com.genersoft.iot.vmp.jt1078.service.Ijt1078Service; +import com.genersoft.iot.vmp.jt1078.session.Session; +import io.netty.buffer.ByteBuf; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.ApplicationEvent; + +import java.nio.charset.Charset; + +/** + * 终端鉴权 + * + * @author QingtaiJiang + * @date 2023/4/27 18:06 + * @email qingtaij@163.com + */ +@Slf4j +@MsgId(id = "0102") +public class J0102 extends Re { + + private String authenticationCode; + private JTDevice deviceForUpdate; + + @Override + protected Rs decode0(ByteBuf buf, Header header, Session session) { + if (header.is2019Version()) { + int lenCode = buf.readUnsignedByte(); + authenticationCode = buf.readCharSequence(lenCode, Charset.forName("GBK")).toString(); + }else { + authenticationCode = buf.readCharSequence(buf.readableBytes(), Charset.forName("GBK")).toString(); + } + log.info("设备鉴权: authenticationCode: " + authenticationCode); + return null; + } + + @Override + protected Rs handler(Header header, Session session, Ijt1078Service service) { + J8001 j8001 = new J8001(); + j8001.setRespNo(header.getSn()); + j8001.setRespId(header.getMsgId()); + if (session.getAuthenticationCode() == null || + !session.getAuthenticationCode().equals(authenticationCode)) { + j8001.setResult(J8001.FAIL); + }else { + j8001.setResult(J8001.SUCCESS); + JTDevice device = service.getDevice(header.getPhoneNumber()); + if (device != null && !device.isStatus()) { + deviceForUpdate = device; + deviceForUpdate.setStatus(true); + service.updateDevice(device); + } + } + return j8001; + } + + @Override + public ApplicationEvent getEvent() { + DeviceUpdateEvent registerEvent = new DeviceUpdateEvent(this); + registerEvent.setDevice(deviceForUpdate); + return registerEvent; + } + +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/proc/request/J0104.java b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/request/J0104.java new file mode 100644 index 0000000..fbd8dda --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/request/J0104.java @@ -0,0 +1,191 @@ +package com.genersoft.iot.vmp.jt1078.proc.request; + +import com.genersoft.iot.vmp.jt1078.annotation.MsgId; +import com.genersoft.iot.vmp.jt1078.bean.JTAlarmSign; +import com.genersoft.iot.vmp.jt1078.bean.JTDeviceConfig; +import com.genersoft.iot.vmp.jt1078.bean.common.ConfigAttribute; +import com.genersoft.iot.vmp.jt1078.bean.config.*; +import com.genersoft.iot.vmp.jt1078.proc.Header; +import com.genersoft.iot.vmp.jt1078.proc.response.J8001; +import com.genersoft.iot.vmp.jt1078.proc.response.Rs; +import com.genersoft.iot.vmp.jt1078.service.Ijt1078Service; +import com.genersoft.iot.vmp.jt1078.session.Session; +import com.genersoft.iot.vmp.jt1078.session.SessionManager; +import io.netty.buffer.ByteBuf; +import org.apache.commons.lang3.StringUtils; +import org.springframework.context.ApplicationEvent; + +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.nio.charset.Charset; +import java.util.HashMap; +import java.util.Map; + +/** + * 查询终端参数应答 + * + */ +@MsgId(id = "0104") +public class J0104 extends Re { + + Integer respNo; + Integer paramLength; + + @Override + protected Rs decode0(ByteBuf buf, Header header, Session session) { + respNo = buf.readUnsignedShort(); + paramLength = (int) buf.readUnsignedByte(); + if (paramLength <= 0) { + return null; + } + JTDeviceConfig deviceConfig = new JTDeviceConfig(); + Field[] fields = deviceConfig.getClass().getDeclaredFields(); + Map allFieldMap = new HashMap<>(); + Map allConfigAttributeMap = new HashMap<>(); + for (Field field : fields) { + ConfigAttribute configAttribute = field.getAnnotation(ConfigAttribute.class); + if (configAttribute != null) { + allFieldMap.put(configAttribute.id(), field); + allConfigAttributeMap.put(configAttribute.id(), configAttribute); + } + } + for (int i = 0; i < paramLength; i++) { + long id = buf.readUnsignedInt(); + if (!allFieldMap.containsKey(id)) { + continue; + } + short length = buf.readUnsignedByte(); + Field field = allFieldMap.get(id); + try { + + switch (allConfigAttributeMap.get(id).type()) { + case "Long": + Method methodForLong = deviceConfig.getClass().getDeclaredMethod("set" + StringUtils.capitalize(field.getName()), Long.class); + methodForLong.invoke(deviceConfig, buf.readUnsignedInt()); + continue; + case "String": + Method methodForString = deviceConfig.getClass().getDeclaredMethod("set" + StringUtils.capitalize(field.getName()), String.class); + String val = buf.readCharSequence(length, Charset.forName("GBK")).toString().trim(); + methodForString.invoke(deviceConfig, val); + continue; + case "Integer": + Method methodForInteger = deviceConfig.getClass().getDeclaredMethod("set" + StringUtils.capitalize(field.getName()), Integer.class); + methodForInteger.invoke(deviceConfig, buf.readUnsignedShort()); + continue; + case "Short": + Method methodForShort = deviceConfig.getClass().getDeclaredMethod("set" + StringUtils.capitalize(field.getName()), Short.class); + methodForShort.invoke(deviceConfig, buf.readUnsignedByte()); + continue; + case "IllegalDrivingPeriods": + JTIllegalDrivingPeriods illegalDrivingPeriods = new JTIllegalDrivingPeriods(); + int startHour = buf.readUnsignedByte(); + int startMinute = buf.readUnsignedByte(); + int stopHour = buf.readUnsignedByte(); + int stopMinute = buf.readUnsignedByte(); + illegalDrivingPeriods.setStartTime(startHour + ":" + startMinute); + illegalDrivingPeriods.setEndTime(stopHour + ":" + stopMinute); + Method methodForIllegalDrivingPeriods = deviceConfig.getClass().getDeclaredMethod("set" + StringUtils.capitalize(field.getName()), JTIllegalDrivingPeriods.class); + methodForIllegalDrivingPeriods.invoke(deviceConfig, illegalDrivingPeriods); + continue; + case "CollisionAlarmParams": + JTCollisionAlarmParams collisionAlarmParams = new JTCollisionAlarmParams(); + collisionAlarmParams.setCollisionAlarmTime(buf.readUnsignedByte()); + collisionAlarmParams.setCollisionAcceleration(buf.readUnsignedByte()); + Method methodForCollisionAlarmParams = deviceConfig.getClass().getDeclaredMethod("set" + StringUtils.capitalize(field.getName()), JTCollisionAlarmParams.class); + methodForCollisionAlarmParams.invoke(deviceConfig, collisionAlarmParams); + continue; + case "CameraTimer": + JTCameraTimer cameraTimer = new JTCameraTimer(); + long cameraTimerContent = buf.readUnsignedInt(); + cameraTimer.setSwitchForChannel1((cameraTimerContent & 1) == 1); + cameraTimer.setSwitchForChannel2((cameraTimerContent >>> 1 & 1) == 1); + cameraTimer.setSwitchForChannel3((cameraTimerContent >>> 2 & 1) == 1); + cameraTimer.setSwitchForChannel4((cameraTimerContent >>> 3 & 1) == 1); + cameraTimer.setSwitchForChannel5((cameraTimerContent >>> 4 & 1) == 1); + cameraTimer.setStorageFlagsForChannel1((cameraTimerContent >>> 7 & 1) == 1); + cameraTimer.setStorageFlagsForChannel2((cameraTimerContent >>> 8 & 1) == 1); + cameraTimer.setStorageFlagsForChannel3((cameraTimerContent >>> 9 & 1) == 1); + cameraTimer.setStorageFlagsForChannel4((cameraTimerContent >>> 10 & 1) == 1); + cameraTimer.setStorageFlagsForChannel5((cameraTimerContent >>> 11 & 1) == 1); + cameraTimer.setTimeUnit((cameraTimerContent >>> 15 & 1) == 1); + cameraTimer.setTimeInterval((int)cameraTimerContent >>> 16); + Method methodForCameraTimer = deviceConfig.getClass().getDeclaredMethod("set" + StringUtils.capitalize(field.getName()), JTCameraTimer.class); + methodForCameraTimer.invoke(deviceConfig, cameraTimer); + continue; + case "GnssPositioningMode": + JTGnssPositioningMode gnssPositioningMode = new JTGnssPositioningMode(); + short gnssPositioningModeContent = buf.readUnsignedByte(); + gnssPositioningMode.setGps((gnssPositioningModeContent& 1) == 1); + gnssPositioningMode.setBeidou((gnssPositioningModeContent >>> 1 & 1) == 1); + gnssPositioningMode.setGlonass((gnssPositioningModeContent >>> 2 & 1) == 1); + gnssPositioningMode.setGaLiLeo((gnssPositioningModeContent >>> 3 & 1) == 1); + Method methodForGnssPositioningMode = deviceConfig.getClass().getDeclaredMethod("set" + StringUtils.capitalize(field.getName()), JTGnssPositioningMode.class); + methodForGnssPositioningMode.invoke(deviceConfig, gnssPositioningMode); + continue; + case "VideoParam": + JTVideoParam videoParam = JTVideoParam.decode(buf); + Method methodForVideoParam = deviceConfig.getClass().getDeclaredMethod("set" + StringUtils.capitalize(field.getName()), JTVideoParam.class); + methodForVideoParam.invoke(deviceConfig, videoParam); + continue; + case "ChannelListParam": + JTChannelListParam channelListParam = JTChannelListParam.decode(buf); + Method methodForChannelListParam = deviceConfig.getClass().getDeclaredMethod("set" + StringUtils.capitalize(field.getName()), JTChannelListParam.class); + methodForChannelListParam.invoke(deviceConfig, channelListParam); + continue; + case "ChannelParam": + JTChannelParam channelParam = JTChannelParam.decode(buf); + Method methodForChannelParam = deviceConfig.getClass().getDeclaredMethod("set" + StringUtils.capitalize(field.getName()), JTChannelParam.class); + methodForChannelParam.invoke(deviceConfig, channelParam); + continue; + case "AlarmRecordingParam": + JTAlarmRecordingParam alarmRecordingParam = JTAlarmRecordingParam.decode(buf); + Method methodForAlarmRecordingParam = deviceConfig.getClass().getDeclaredMethod("set" + StringUtils.capitalize(field.getName()), JTAlarmRecordingParam.class); + methodForAlarmRecordingParam.invoke(deviceConfig, alarmRecordingParam); + continue; + case "VideoAlarmBit": + JTVideoAlarmBit videoAlarmBit = JTVideoAlarmBit.decode(buf); + Method methodForVideoAlarmBit = deviceConfig.getClass().getDeclaredMethod("set" + StringUtils.capitalize(field.getName()), JTVideoAlarmBit.class); + methodForVideoAlarmBit.invoke(deviceConfig, videoAlarmBit); + continue; + case "AnalyzeAlarmParam": + JTAnalyzeAlarmParam analyzeAlarmParam = JTAnalyzeAlarmParam.decode(buf); + Method methodForAnalyzeAlarmParam = deviceConfig.getClass().getDeclaredMethod("set" + StringUtils.capitalize(field.getName()), JTAnalyzeAlarmParam.class); + methodForAnalyzeAlarmParam.invoke(deviceConfig, analyzeAlarmParam); + continue; + case "AwakenParam": + JTAwakenParam awakenParamParam = JTAwakenParam.decode(buf); + Method methodForAwakenParam = deviceConfig.getClass().getDeclaredMethod("set" + StringUtils.capitalize(field.getName()), JTAwakenParam.class); + methodForAwakenParam.invoke(deviceConfig, awakenParamParam); + continue; + case "AlarmSign": + JTAlarmSign alarmSign = JTAlarmSign.decode(buf); + Method methodForAlarmSign = deviceConfig.getClass().getDeclaredMethod("set" + StringUtils.capitalize(field.getName()), JTAlarmSign.class); + methodForAlarmSign.invoke(deviceConfig, alarmSign); + continue; + default: + System.err.println(field.getGenericType().getTypeName()); + continue; + } + } catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException e) { + throw new RuntimeException(e); + } + } + SessionManager.INSTANCE.response(header.getPhoneNumber(), "0104", (long) respNo, deviceConfig); + return null; + } + + @Override + protected Rs handler(Header header, Session session, Ijt1078Service service) { + J8001 j8001 = new J8001(); + j8001.setRespNo(header.getSn()); + j8001.setRespId(header.getMsgId()); + j8001.setResult(J8001.SUCCESS); + return j8001; + } + + @Override + public ApplicationEvent getEvent() { + return null; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/proc/request/J0107.java b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/request/J0107.java new file mode 100644 index 0000000..6690b11 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/request/J0107.java @@ -0,0 +1,77 @@ +package com.genersoft.iot.vmp.jt1078.proc.request; + +import com.genersoft.iot.vmp.jt1078.annotation.MsgId; +import com.genersoft.iot.vmp.jt1078.bean.*; +import com.genersoft.iot.vmp.jt1078.proc.Header; +import com.genersoft.iot.vmp.jt1078.proc.response.J8001; +import com.genersoft.iot.vmp.jt1078.proc.response.Rs; +import com.genersoft.iot.vmp.jt1078.service.Ijt1078Service; +import com.genersoft.iot.vmp.jt1078.session.Session; +import com.genersoft.iot.vmp.jt1078.session.SessionManager; +import com.genersoft.iot.vmp.jt1078.util.BCDUtil; +import io.netty.buffer.ByteBuf; +import lombok.extern.slf4j.Slf4j; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.context.ApplicationEvent; + +import java.nio.charset.Charset; + +/** + * 查询终端属性应答 + * + */ +@Slf4j +@MsgId(id = "0107") +public class J0107 extends Re { + + @Override + protected Rs decode0(ByteBuf buf, Header header, Session session) { + + JTDeviceAttribute deviceAttribute = new JTDeviceAttribute(); + + deviceAttribute.setType(JTDeviceType.getInstance(buf.readUnsignedShort())); + + deviceAttribute.setMakerId(buf.readCharSequence(5, Charset.forName("GBK")).toString().trim()); + + deviceAttribute.setDeviceModel(buf.readCharSequence(20, Charset.forName("GBK")).toString().trim()); + if (header.is2019Version()) { + buf.readCharSequence(10, Charset.forName("GBK")); + } + + deviceAttribute.setTerminalId(buf.readCharSequence(7, Charset.forName("GBK")).toString().trim()); + if (header.is2019Version()) { + buf.readCharSequence(23, Charset.forName("GBK")); + } + + byte[] bytes = new byte[10]; + buf.readBytes(bytes); + deviceAttribute.setIccId(BCDUtil.transform(bytes)); + + int hardwareVersionLength = buf.readUnsignedByte(); + deviceAttribute.setHardwareVersion(buf.readCharSequence(hardwareVersionLength, Charset.forName("GBK")).toString().trim()); + + int firmwareVersionLength = buf.readUnsignedByte(); + deviceAttribute.setFirmwareVersion(buf.readCharSequence(firmwareVersionLength, Charset.forName("GBK")).toString().trim()); + + deviceAttribute.setGnssAttribute(JTGnssAttribute.getInstance(buf.readUnsignedByte())); + deviceAttribute.setCommunicationModuleAttribute(JTCommunicationModuleAttribute.getInstance(buf.readUnsignedByte())); + log.info("[查询终端属性应答] {}, {}", header.getPhoneNumber(), deviceAttribute); + SessionManager.INSTANCE.response(header.getPhoneNumber(), "0107", null, deviceAttribute); + return null; + } + + @Override + protected Rs handler(Header header, Session session, Ijt1078Service service) { + J8001 j8001 = new J8001(); + j8001.setRespNo(header.getSn()); + j8001.setRespId(header.getMsgId()); + j8001.setResult(J8001.SUCCESS); + return j8001; + } + + @Override + public ApplicationEvent getEvent() { + return null; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/proc/request/J0200.java b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/request/J0200.java new file mode 100644 index 0000000..f420540 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/request/J0200.java @@ -0,0 +1,105 @@ +package com.genersoft.iot.vmp.jt1078.proc.request; + +import com.genersoft.iot.vmp.jt1078.annotation.MsgId; +import com.genersoft.iot.vmp.jt1078.bean.*; +import com.genersoft.iot.vmp.jt1078.proc.Header; +import com.genersoft.iot.vmp.jt1078.proc.response.J8001; +import com.genersoft.iot.vmp.jt1078.proc.response.Rs; +import com.genersoft.iot.vmp.jt1078.service.Ijt1078Service; +import com.genersoft.iot.vmp.jt1078.session.Session; +import io.netty.buffer.ByteBuf; +import lombok.extern.slf4j.Slf4j; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.context.ApplicationEvent; + +/** + * 位置信息汇报 + * + */ +@Slf4j +@MsgId(id = "0200") +public class J0200 extends Re { + + private JTPositionBaseInfo positionInfo; + + @Override + protected Rs decode0(ByteBuf buf, Header header, Session session) { + positionInfo = JTPositionBaseInfo.decode(buf); + log.debug("[JT-位置汇报]: phoneNumber={} {}", header.getPhoneNumber(), positionInfo.toSimpleString()); + // 读取附加信息 +// JTPositionAdditionalInfo positionAdditionalInfo = new JTPositionAdditionalInfo(); +// Map additionalMsg = new HashMap<>(); +// getAdditionalMsg(buf, positionAdditionalInfo); +// log.info("[JT-位置汇报]: phoneNumber={} {}", header.getPhoneNumber(), positionInfo.toSimpleString()); + return null; + } + + private void getAdditionalMsg(ByteBuf buf, JTPositionAdditionalInfo additionalInfo) { + + if (buf.isReadable()) { + int msgId = buf.readUnsignedByte(); + int length = buf.readUnsignedByte(); + ByteBuf byteBuf = buf.readBytes(length); + switch (msgId) { + case 1: + // 里程 + long mileage = byteBuf.readUnsignedInt(); + log.info("[JT-位置汇报]: 里程: {} km", (double)mileage/10); + break; + case 2: + // 油量 + int oil = byteBuf.readUnsignedShort(); + log.info("[JT-位置汇报]: 油量: {} L", (double)oil/10); + break; + case 3: + // 速度 + int speed = byteBuf.readUnsignedShort(); + log.info("[JT-位置汇报]: 速度: {} km/h", (double)speed/10); + break; + case 4: + // 需要人工确认报警事件的 ID + int alarmId = byteBuf.readUnsignedShort(); + log.info("[JT-位置汇报]: 需要人工确认报警事件的 ID: {}", alarmId); + break; + case 5: + byte[] tirePressureBytes = new byte[30]; + // 胎压 + byteBuf.readBytes(tirePressureBytes); + log.info("[JT-位置汇报]: 胎压 {}", tirePressureBytes); + break; + case 6: + // 车厢温度 + short carriageTemperature = byteBuf.readShort(); + log.info("[JT-位置汇报]: 车厢温度 {}摄氏度", carriageTemperature); + break; + case 11: + // 超速报警 + short positionType = byteBuf.readUnsignedByte(); + long positionId = byteBuf.readUnsignedInt(); + log.info("[JT-位置汇报]: 超速报警, 位置类型: {}, 区域或路段 ID: {}", positionType, positionId); + break; + default: + log.info("[JT-位置汇报]: 附加消息ID: {}, 消息长度: {}", msgId, length); + break; + + } + getAdditionalMsg(buf, additionalInfo); + } + } + + @Override + protected Rs handler(Header header, Session session, Ijt1078Service service) { + J8001 j8001 = new J8001(); + j8001.setRespNo(header.getSn()); + j8001.setRespId(header.getMsgId()); + j8001.setResult(J8001.SUCCESS); + service.updateDevicePosition(header.getPhoneNumber(), positionInfo.getLongitude(), positionInfo.getLatitude()); + return j8001; + } + + @Override + public ApplicationEvent getEvent() { + return null; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/proc/request/J0201.java b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/request/J0201.java new file mode 100644 index 0000000..100812e --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/request/J0201.java @@ -0,0 +1,61 @@ +package com.genersoft.iot.vmp.jt1078.proc.request; + +import com.genersoft.iot.vmp.jt1078.annotation.MsgId; +import com.genersoft.iot.vmp.jt1078.bean.*; +import com.genersoft.iot.vmp.jt1078.event.DeviceUpdateEvent; +import com.genersoft.iot.vmp.jt1078.event.JTPositionEvent; +import com.genersoft.iot.vmp.jt1078.proc.Header; +import com.genersoft.iot.vmp.jt1078.proc.response.J8001; +import com.genersoft.iot.vmp.jt1078.proc.response.Rs; +import com.genersoft.iot.vmp.jt1078.service.Ijt1078Service; +import com.genersoft.iot.vmp.jt1078.session.Session; +import com.genersoft.iot.vmp.jt1078.session.SessionManager; +import io.netty.buffer.ByteBuf; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.context.ApplicationEvent; + +/** + * 位置信息查询应答 + * + * @author QingtaiJiang + * @date 2023/4/27 18:06 + * @email qingtaij@163.com + */ +@MsgId(id = "0201") +public class J0201 extends Re { + + private final static Logger log = LoggerFactory.getLogger(J0100.class); + private JTPositionBaseInfo positionInfo; + private String phoneNumber; + + @Override + protected Rs decode0(ByteBuf buf, Header header, Session session) { + phoneNumber = header.getPhoneNumber(); + int respNo = buf.readUnsignedShort(); + positionInfo = JTPositionBaseInfo.decode(buf); + log.info("[JT-位置信息查询应答]: {}", positionInfo); + SessionManager.INSTANCE.response(header.getPhoneNumber(), "0201", (long) respNo, positionInfo); + return null; + } + + @Override + protected Rs handler(Header header, Session session, Ijt1078Service service) { + J8001 j8001 = new J8001(); + j8001.setRespNo(header.getSn()); + j8001.setRespId(header.getMsgId()); + j8001.setResult(J8001.SUCCESS); + return j8001; + } + + @Override + public ApplicationEvent getEvent() { + if (positionInfo == null || phoneNumber == null ) { + return null; + } + JTPositionEvent registerEvent = new JTPositionEvent(this); + registerEvent.setPhoneNumber(phoneNumber); + registerEvent.setPositionInfo(positionInfo); + return registerEvent; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/proc/request/J0500.java b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/request/J0500.java new file mode 100644 index 0000000..58af88b --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/request/J0500.java @@ -0,0 +1,58 @@ +package com.genersoft.iot.vmp.jt1078.proc.request; + +import com.genersoft.iot.vmp.jt1078.annotation.MsgId; +import com.genersoft.iot.vmp.jt1078.bean.*; +import com.genersoft.iot.vmp.jt1078.event.JTPositionEvent; +import com.genersoft.iot.vmp.jt1078.proc.Header; +import com.genersoft.iot.vmp.jt1078.proc.response.J8001; +import com.genersoft.iot.vmp.jt1078.proc.response.Rs; +import com.genersoft.iot.vmp.jt1078.service.Ijt1078Service; +import com.genersoft.iot.vmp.jt1078.session.Session; +import com.genersoft.iot.vmp.jt1078.session.SessionManager; +import io.netty.buffer.ByteBuf; +import lombok.extern.slf4j.Slf4j; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.context.ApplicationEvent; + +/** + * 车辆控制应答 + * + */ +@Slf4j +@MsgId(id = "0500") +public class J0500 extends Re { + + private JTPositionBaseInfo positionInfo; + private String phoneNumber; + + @Override + protected Rs decode0(ByteBuf buf, Header header, Session session) { + phoneNumber = header.getPhoneNumber(); + int respNo = buf.readUnsignedShort(); + positionInfo = JTPositionBaseInfo.decode(buf); + log.info("[车辆控制应答] {}", header.getPhoneNumber()); + SessionManager.INSTANCE.response(header.getPhoneNumber(), "0500", (long) respNo, positionInfo); + return null; + } + + @Override + protected Rs handler(Header header, Session session, Ijt1078Service service) { + J8001 j8001 = new J8001(); + j8001.setRespNo(header.getSn()); + j8001.setRespId(header.getMsgId()); + j8001.setResult(J8001.SUCCESS); + return j8001; + } + + @Override + public ApplicationEvent getEvent() { + if (positionInfo == null || phoneNumber == null ) { + return null; + } + JTPositionEvent registerEvent = new JTPositionEvent(this); + registerEvent.setPhoneNumber(phoneNumber); + registerEvent.setPositionInfo(positionInfo); + return registerEvent; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/proc/request/J0608.java b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/request/J0608.java new file mode 100644 index 0000000..e366579 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/request/J0608.java @@ -0,0 +1,100 @@ +package com.genersoft.iot.vmp.jt1078.proc.request; + +import com.genersoft.iot.vmp.jt1078.annotation.MsgId; +import com.genersoft.iot.vmp.jt1078.bean.*; +import com.genersoft.iot.vmp.jt1078.proc.Header; +import com.genersoft.iot.vmp.jt1078.proc.response.J8001; +import com.genersoft.iot.vmp.jt1078.proc.response.Rs; +import com.genersoft.iot.vmp.jt1078.service.Ijt1078Service; +import com.genersoft.iot.vmp.jt1078.session.Session; +import com.genersoft.iot.vmp.jt1078.session.SessionManager; +import io.netty.buffer.ByteBuf; +import lombok.extern.slf4j.Slf4j; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.context.ApplicationEvent; + +import java.util.ArrayList; +import java.util.List; + +/** + * 查询区域或线路数据应答 + */ +@Slf4j +@MsgId(id = "0608") +public class J0608 extends Re { + + @Override + protected Rs decode0(ByteBuf buf, Header header, Session session) { + int type = buf.readByte(); + long dataLength = buf.readUnsignedInt(); + log.info("[JT-查询区域或线路数据应答]: 类型: {}, 数量: {}", type, dataLength); + List areaOrRoutes = new ArrayList<>(); + if (dataLength == 0) { + SessionManager.INSTANCE.response(header.getPhoneNumber(), "0608", null, areaOrRoutes); + return null; + } + switch (type) { + case 1: + buf.readUnsignedByte(); + int areaLengthForCircleArea = buf.readUnsignedByte(); + List jtCircleAreas = new ArrayList<>(); + for (int i = 0; i < areaLengthForCircleArea; i++) { + // 查询圆形区域数据 + JTCircleArea jtCircleArea = JTCircleArea.decode(buf); + jtCircleAreas.add(jtCircleArea); + } + SessionManager.INSTANCE.response(header.getPhoneNumber(), "0608", null, jtCircleAreas); + break; + case 2: + buf.readUnsignedByte(); + int areaLengthForRectangleArea = buf.readUnsignedByte(); + // 查询矩形区域数据 + List jtRectangleAreas = new ArrayList<>(); + for (int i = 0; i < areaLengthForRectangleArea; i++) { + // 查询圆形区域数据 + JTRectangleArea jtRectangleArea = JTRectangleArea.decode(buf); + jtRectangleAreas.add(jtRectangleArea); + } + SessionManager.INSTANCE.response(header.getPhoneNumber(), "0608", null, jtRectangleAreas); + break; + case 3: + // 查询多 边形区域数据 + List jtPolygonAreas = new ArrayList<>(); + for (int i = 0; i < dataLength; i++) { + // 查询圆形区域数据 + JTPolygonArea jtRectangleArea = JTPolygonArea.decode(buf); + jtPolygonAreas.add(jtRectangleArea); + } + SessionManager.INSTANCE.response(header.getPhoneNumber(), "0608", null, jtPolygonAreas); + break; + case 4: + // 查询线路数据 + List jtRoutes = new ArrayList<>(); + for (int i = 0; i < dataLength; i++) { + // 查询圆形区域数据 + JTRoute jtRoute = JTRoute.decode(buf); + jtRoutes.add(jtRoute); + } + SessionManager.INSTANCE.response(header.getPhoneNumber(), "0608", null, jtRoutes); + break; + default: + break; + } + + return null; + } + + @Override + protected Rs handler(Header header, Session session, Ijt1078Service service) { + J8001 j8001 = new J8001(); + j8001.setRespNo(header.getSn()); + j8001.setRespId(header.getMsgId()); + return j8001; + } + + @Override + public ApplicationEvent getEvent() { + return null; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/proc/request/J0702.java b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/request/J0702.java new file mode 100644 index 0000000..45ba170 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/request/J0702.java @@ -0,0 +1,45 @@ +package com.genersoft.iot.vmp.jt1078.proc.request; + +import com.genersoft.iot.vmp.jt1078.annotation.MsgId; +import com.genersoft.iot.vmp.jt1078.bean.JTDriverInformation; +import com.genersoft.iot.vmp.jt1078.proc.Header; +import com.genersoft.iot.vmp.jt1078.proc.response.J8001; +import com.genersoft.iot.vmp.jt1078.proc.response.Rs; +import com.genersoft.iot.vmp.jt1078.service.Ijt1078Service; +import com.genersoft.iot.vmp.jt1078.session.Session; +import com.genersoft.iot.vmp.jt1078.session.SessionManager; +import io.netty.buffer.ByteBuf; +import lombok.extern.slf4j.Slf4j; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.context.ApplicationEvent; + +/** + * 驾驶员身份信息采集上报 + * + */ +@Slf4j +@MsgId(id = "0702") +public class J0702 extends Re { + + @Override + protected Rs decode0(ByteBuf buf, Header header, Session session) { + JTDriverInformation driverInformation = JTDriverInformation.decode(buf, header.is2019Version()); + log.info("[JT-驾驶员身份信息采集上报]: {}", driverInformation.toString()); + SessionManager.INSTANCE.response(header.getPhoneNumber(), "0702", null, driverInformation); + return null; + } + + @Override + protected Rs handler(Header header, Session session, Ijt1078Service service) { + J8001 j8001 = new J8001(); + j8001.setRespNo(header.getSn()); + j8001.setRespId(header.getMsgId()); + return j8001; + } + + @Override + public ApplicationEvent getEvent() { + return null; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/proc/request/J0704.java b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/request/J0704.java new file mode 100644 index 0000000..346977a --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/request/J0704.java @@ -0,0 +1,57 @@ +package com.genersoft.iot.vmp.jt1078.proc.request; + +import com.genersoft.iot.vmp.jt1078.annotation.MsgId; +import com.genersoft.iot.vmp.jt1078.bean.JTDriverInformation; +import com.genersoft.iot.vmp.jt1078.bean.JTPositionBaseInfo; +import com.genersoft.iot.vmp.jt1078.proc.Header; +import com.genersoft.iot.vmp.jt1078.proc.response.J8001; +import com.genersoft.iot.vmp.jt1078.proc.response.Rs; +import com.genersoft.iot.vmp.jt1078.service.Ijt1078Service; +import com.genersoft.iot.vmp.jt1078.session.Session; +import com.genersoft.iot.vmp.jt1078.session.SessionManager; +import io.netty.buffer.ByteBuf; +import lombok.extern.slf4j.Slf4j; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.context.ApplicationEvent; + +import java.util.ArrayList; +import java.util.List; + +/** + * 定位数据批量上传 + */ +@Slf4j +@MsgId(id = "0704") +public class J0704 extends Re { + + private final List positionBaseInfoList = new ArrayList<>(); + + @Override + protected Rs decode0(ByteBuf buf, Header header, Session session) { + int length = buf.readUnsignedShort(); + int type = buf.readUnsignedByte(); + for (int i = 0; i < length; i++) { + int dateLength = buf.readUnsignedShort(); + ByteBuf byteBuf = buf.readBytes(dateLength); + JTPositionBaseInfo positionInfo = JTPositionBaseInfo.decode(byteBuf); + byteBuf.release(); + positionBaseInfoList.add(positionInfo); + } + log.info("[JT-定位数据批量上传]: 共{}条", positionBaseInfoList.size()); + return null; + } + + @Override + protected Rs handler(Header header, Session session, Ijt1078Service service) { + J8001 j8001 = new J8001(); + j8001.setRespNo(header.getSn()); + j8001.setRespId(header.getMsgId()); + return j8001; + } + + @Override + public ApplicationEvent getEvent() { + return null; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/proc/request/J0800.java b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/request/J0800.java new file mode 100644 index 0000000..74ee4f8 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/request/J0800.java @@ -0,0 +1,47 @@ +package com.genersoft.iot.vmp.jt1078.proc.request; + +import com.genersoft.iot.vmp.jt1078.annotation.MsgId; +import com.genersoft.iot.vmp.jt1078.bean.JTMediaEventInfo; +import com.genersoft.iot.vmp.jt1078.bean.JTPositionBaseInfo; +import com.genersoft.iot.vmp.jt1078.proc.Header; +import com.genersoft.iot.vmp.jt1078.proc.response.J8001; +import com.genersoft.iot.vmp.jt1078.proc.response.Rs; +import com.genersoft.iot.vmp.jt1078.service.Ijt1078Service; +import com.genersoft.iot.vmp.jt1078.session.Session; +import io.netty.buffer.ByteBuf; +import lombok.extern.slf4j.Slf4j; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.context.ApplicationEvent; + +import java.util.ArrayList; +import java.util.List; + +/** + * 多媒体事件信息上传 + * + */ +@Slf4j +@MsgId(id = "0800") +public class J0800 extends Re { + + @Override + protected Rs decode0(ByteBuf buf, Header header, Session session) { + JTMediaEventInfo mediaEventInfo = JTMediaEventInfo.decode(buf); + log.info("[JT-多媒体事件信息上传]: {}", mediaEventInfo); + return null; + } + + @Override + protected Rs handler(Header header, Session session, Ijt1078Service service) { + J8001 j8001 = new J8001(); + j8001.setRespNo(header.getSn()); + j8001.setRespId(header.getMsgId()); + return j8001; + } + + @Override + public ApplicationEvent getEvent() { + return null; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/proc/request/J0801.java b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/request/J0801.java new file mode 100644 index 0000000..d24579b --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/request/J0801.java @@ -0,0 +1,56 @@ +package com.genersoft.iot.vmp.jt1078.proc.request; + +import com.genersoft.iot.vmp.jt1078.annotation.MsgId; +import com.genersoft.iot.vmp.jt1078.bean.JTMediaEventInfo; +import com.genersoft.iot.vmp.jt1078.proc.Header; +import com.genersoft.iot.vmp.jt1078.proc.response.J8001; +import com.genersoft.iot.vmp.jt1078.proc.response.Rs; +import com.genersoft.iot.vmp.jt1078.service.Ijt1078Service; +import com.genersoft.iot.vmp.jt1078.session.Session; +import com.genersoft.iot.vmp.jt1078.session.SessionManager; +import io.netty.buffer.ByteBuf; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.ApplicationEvent; + +/** + * 多媒体数据上传 + */ +@Slf4j +@MsgId(id = "0801") +public class J0801 extends Re { + + @Override + protected Rs decode0(ByteBuf buf, Header header, Session session) { + JTMediaEventInfo mediaEventInfo = JTMediaEventInfo.decode(buf); + log.info("[JT-多媒体数据上传]: {}", mediaEventInfo); +// try { +// if (mediaEventInfo.getMediaData() != null) { +// File file = new File("./source.jpg"); +// if (file.exists()) { +// file.delete(); +// } +// FileOutputStream fileOutputStream = new FileOutputStream(file); +// fileOutputStream.write(mediaEventInfo.getMediaData()); +// fileOutputStream.flush(); +// fileOutputStream.close(); +// } +// }catch (Exception e) { +// log.error("[JT-多媒体数据上传] 写入文件异常", e); +// } + SessionManager.INSTANCE.response(header.getPhoneNumber(), "0801", null, mediaEventInfo); + return null; + } + + @Override + protected Rs handler(Header header, Session session, Ijt1078Service service) { + J8001 j8001 = new J8001(); + j8001.setRespNo(header.getSn()); + j8001.setRespId(header.getMsgId()); + return j8001; + } + + @Override + public ApplicationEvent getEvent() { + return null; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/proc/request/J0802.java b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/request/J0802.java new file mode 100644 index 0000000..ee18d0a --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/request/J0802.java @@ -0,0 +1,58 @@ +package com.genersoft.iot.vmp.jt1078.proc.request; + +import com.genersoft.iot.vmp.jt1078.annotation.MsgId; +import com.genersoft.iot.vmp.jt1078.bean.JTMediaDataInfo; +import com.genersoft.iot.vmp.jt1078.proc.Header; +import com.genersoft.iot.vmp.jt1078.proc.response.J8001; +import com.genersoft.iot.vmp.jt1078.proc.response.Rs; +import com.genersoft.iot.vmp.jt1078.service.Ijt1078Service; +import com.genersoft.iot.vmp.jt1078.session.Session; +import com.genersoft.iot.vmp.jt1078.session.SessionManager; +import io.netty.buffer.ByteBuf; +import lombok.extern.slf4j.Slf4j; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.context.ApplicationEvent; + +import java.util.ArrayList; +import java.util.List; + +/** + * 存储多媒体数据检索应答 + * + */ +@Slf4j +@MsgId(id = "0802") +public class J0802 extends Re { + + @Override + protected Rs decode0(ByteBuf buf, Header header, Session session) { + int respNo = buf.readUnsignedShort(); + int length = buf.readUnsignedShort(); + if (length == 0) { + log.info("[JT-存储多媒体数据检索应答]: {}", length); + SessionManager.INSTANCE.response(header.getPhoneNumber(), "0802", (long) respNo, new ArrayList<>()); + return null; + } + List mediaDataInfoList = new ArrayList<>(length); + for (int i = 0; i < length; i++) { + mediaDataInfoList.add(JTMediaDataInfo.decode(buf)); + } + log.info("[JT-存储多媒体数据检索应答]: {}", mediaDataInfoList.size()); + SessionManager.INSTANCE.response(header.getPhoneNumber(), "0802", (long) respNo, mediaDataInfoList); + return null; + } + + @Override + protected Rs handler(Header header, Session session, Ijt1078Service service) { + J8001 j8001 = new J8001(); + j8001.setRespNo(header.getSn()); + j8001.setRespId(header.getMsgId()); + return j8001; + } + + @Override + public ApplicationEvent getEvent() { + return null; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/proc/request/J0805.java b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/request/J0805.java new file mode 100644 index 0000000..d0a72ef --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/request/J0805.java @@ -0,0 +1,60 @@ +package com.genersoft.iot.vmp.jt1078.proc.request; + +import com.genersoft.iot.vmp.jt1078.annotation.MsgId; +import com.genersoft.iot.vmp.jt1078.proc.Header; +import com.genersoft.iot.vmp.jt1078.proc.response.Rs; +import com.genersoft.iot.vmp.jt1078.service.Ijt1078Service; +import com.genersoft.iot.vmp.jt1078.session.Session; +import com.genersoft.iot.vmp.jt1078.session.SessionManager; +import io.netty.buffer.ByteBuf; +import lombok.Getter; +import lombok.Setter; +import org.springframework.context.ApplicationEvent; + +import java.util.ArrayList; +import java.util.List; + +/** + * 摄像头立即拍摄命令应答 + */ +@Setter +@Getter +@MsgId(id = "0805") +public class J0805 extends Re { + + private int respNo; + /** + * 0:成功/确认;1:失败;2:消息有误;3:不支持 + */ + private int result; + + /** + * 表示拍摄成功的多媒体个数 + */ + private List ids = new ArrayList<>(); + + @Override + protected Rs decode0(ByteBuf buf, Header header, Session session) { + respNo = buf.readUnsignedShort(); + result = buf.readUnsignedByte(); + if (result == 0) { + int length = buf.readUnsignedShort(); + for (int i = 0; i < length; i++) { + ids.add(buf.readUnsignedInt()); + } + } + SessionManager.INSTANCE.response(header.getPhoneNumber(), "0805", null, ids); + return null; + } + + @Override + protected Rs handler(Header header, Session session, Ijt1078Service service) { + SessionManager.INSTANCE.response(header.getPhoneNumber(), "0001", (long) respNo, result); + return null; + } + + @Override + public ApplicationEvent getEvent() { + return null; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/proc/request/J0900.java b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/request/J0900.java new file mode 100644 index 0000000..9271592 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/request/J0900.java @@ -0,0 +1,55 @@ +package com.genersoft.iot.vmp.jt1078.proc.request; + +import com.genersoft.iot.vmp.jt1078.annotation.MsgId; +import com.genersoft.iot.vmp.jt1078.proc.Header; +import com.genersoft.iot.vmp.jt1078.proc.response.J8001; +import com.genersoft.iot.vmp.jt1078.proc.response.Rs; +import com.genersoft.iot.vmp.jt1078.service.Ijt1078Service; +import com.genersoft.iot.vmp.jt1078.session.Session; +import com.genersoft.iot.vmp.jt1078.session.SessionManager; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import lombok.Getter; +import lombok.Setter; +import org.springframework.context.ApplicationEvent; + +/** + * 数据上行透传 + */ +@Setter +@Getter +@MsgId(id = "0900") +public class J0900 extends Re { + + /** + * 透传消息类型, 0x00: GNSS 模块详细定位数据, 0X0B: 道路运输证 IC卡信息, 0X41: 串口1 透传, 0X42: 串口2 透传, 0XF0 ~ 0XFF: 用户自定义透传 + */ + private Integer type; + + /** + * 透传消息内容 + */ + private byte[] content; + + @Override + protected Rs decode0(ByteBuf buf, Header header, Session session) { + type = (int)buf.readUnsignedByte(); + byte[] content = new byte[buf.readableBytes()]; + buf.readBytes(content); + return null; + } + + @Override + protected Rs handler(Header header, Session session, Ijt1078Service service) { + J8001 j8001 = new J8001(); + j8001.setRespNo(header.getSn()); + j8001.setRespId(header.getMsgId()); + return null; + } + + @Override + public ApplicationEvent getEvent() { + return null; + } + +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/proc/request/J0901.java b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/request/J0901.java new file mode 100644 index 0000000..521c4a3 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/request/J0901.java @@ -0,0 +1,53 @@ +package com.genersoft.iot.vmp.jt1078.proc.request; + +import com.genersoft.iot.vmp.jt1078.annotation.MsgId; +import com.genersoft.iot.vmp.jt1078.proc.Header; +import com.genersoft.iot.vmp.jt1078.proc.response.J8001; +import com.genersoft.iot.vmp.jt1078.proc.response.Rs; +import com.genersoft.iot.vmp.jt1078.service.Ijt1078Service; +import com.genersoft.iot.vmp.jt1078.session.Session; +import io.netty.buffer.ByteBuf; +import lombok.Getter; +import lombok.Setter; +import org.springframework.context.ApplicationEvent; + +/** + * 数据压缩上报 + */ +@Setter +@Getter +@MsgId(id = "0901") +public class J0901 extends Re { + + /** + * 平台 RSA公钥{e ,n}中的 e + */ + private Long e; + + /** + * RSA公钥{e ,n}中的 n + */ + private byte[] n; + + @Override + protected Rs decode0(ByteBuf buf, Header header, Session session) { + e = buf.readUnsignedInt(); + byte[] content = new byte[buf.readableBytes()]; + buf.readBytes(content); + return null; + } + + @Override + protected Rs handler(Header header, Session session, Ijt1078Service service) { + J8001 j8001 = new J8001(); + j8001.setRespNo(header.getSn()); + j8001.setRespId(header.getMsgId()); + return j8001; + } + + @Override + public ApplicationEvent getEvent() { + return null; + } + +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/proc/request/J0A00.java b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/request/J0A00.java new file mode 100644 index 0000000..582691f --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/request/J0A00.java @@ -0,0 +1,54 @@ +package com.genersoft.iot.vmp.jt1078.proc.request; + +import com.genersoft.iot.vmp.jt1078.annotation.MsgId; +import com.genersoft.iot.vmp.jt1078.proc.Header; +import com.genersoft.iot.vmp.jt1078.proc.response.J8001; +import com.genersoft.iot.vmp.jt1078.proc.response.Rs; +import com.genersoft.iot.vmp.jt1078.service.Ijt1078Service; +import com.genersoft.iot.vmp.jt1078.session.Session; +import io.netty.buffer.ByteBuf; +import lombok.Getter; +import lombok.Setter; +import org.springframework.context.ApplicationEvent; + +/** + * 终端 RSA公钥 + */ +@Setter +@Getter +@MsgId(id = "0900") +public class J0A00 extends Re { + + /** + * 透传消息类型, 0x00: GNSS 模块详细定位数据, 0X0B: 道路运输证 IC卡信息, 0X41: 串口1 透传, 0X42: 串口2 透传, 0XF0 ~ 0XFF: 用户自定义透传 + */ + + private Integer type; + + /** + * 透传消息内容 + */ + private byte[] content; + + @Override + protected Rs decode0(ByteBuf buf, Header header, Session session) { + type = (int)buf.readUnsignedByte(); + byte[] content = new byte[buf.readableBytes()]; + buf.readBytes(content); + return null; + } + + @Override + protected Rs handler(Header header, Session session, Ijt1078Service service) { + J8001 j8001 = new J8001(); + j8001.setRespNo(header.getSn()); + j8001.setRespId(header.getMsgId()); + return null; + } + + @Override + public ApplicationEvent getEvent() { + return null; + } + +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/proc/request/J1003.java b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/request/J1003.java new file mode 100644 index 0000000..570f904 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/request/J1003.java @@ -0,0 +1,43 @@ +package com.genersoft.iot.vmp.jt1078.proc.request; + +import com.genersoft.iot.vmp.jt1078.annotation.MsgId; +import com.genersoft.iot.vmp.jt1078.bean.JTMediaAttribute; +import com.genersoft.iot.vmp.jt1078.proc.Header; +import com.genersoft.iot.vmp.jt1078.proc.response.J8001; +import com.genersoft.iot.vmp.jt1078.proc.response.Rs; +import com.genersoft.iot.vmp.jt1078.service.Ijt1078Service; +import com.genersoft.iot.vmp.jt1078.session.Session; +import com.genersoft.iot.vmp.jt1078.session.SessionManager; +import io.netty.buffer.ByteBuf; +import org.springframework.context.ApplicationEvent; + +/** + * 终端上传音视频属性 + * + */ +@MsgId(id = "1003") +public class J1003 extends Re { + + JTMediaAttribute mediaAttribute; + + @Override + protected Rs decode0(ByteBuf buf, Header header, Session session) { + mediaAttribute = JTMediaAttribute.decode(buf); + SessionManager.INSTANCE.response(header.getPhoneNumber(), "1003", null, mediaAttribute); + return null; + } + + @Override + protected Rs handler(Header header, Session session, Ijt1078Service service) { + J8001 j8001 = new J8001(); + j8001.setRespNo(header.getSn()); + j8001.setRespId(header.getMsgId()); + j8001.setResult(J8001.SUCCESS); + return j8001; + } + + @Override + public ApplicationEvent getEvent() { + return null; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/proc/request/J1005.java b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/request/J1005.java new file mode 100644 index 0000000..20ac8ae --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/request/J1005.java @@ -0,0 +1,46 @@ +package com.genersoft.iot.vmp.jt1078.proc.request; + +import com.genersoft.iot.vmp.jt1078.annotation.MsgId; +import com.genersoft.iot.vmp.jt1078.bean.JTMediaAttribute; +import com.genersoft.iot.vmp.jt1078.bean.JTPassengerNum; +import com.genersoft.iot.vmp.jt1078.proc.Header; +import com.genersoft.iot.vmp.jt1078.proc.response.J8001; +import com.genersoft.iot.vmp.jt1078.proc.response.Rs; +import com.genersoft.iot.vmp.jt1078.service.Ijt1078Service; +import com.genersoft.iot.vmp.jt1078.session.Session; +import com.genersoft.iot.vmp.jt1078.session.SessionManager; +import io.netty.buffer.ByteBuf; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.ApplicationEvent; + +/** + * 终端上传乘客流量 + * + */ +@Slf4j +@MsgId(id = "1005") +public class J1005 extends Re { + + JTPassengerNum passengerNum; + + @Override + protected Rs decode0(ByteBuf buf, Header header, Session session) { + passengerNum = JTPassengerNum.decode(buf); + log.info("[终端上传乘客流量] {}", passengerNum); + return null; + } + + @Override + protected Rs handler(Header header, Session session, Ijt1078Service service) { + J8001 j8001 = new J8001(); + j8001.setRespNo(header.getSn()); + j8001.setRespId(header.getMsgId()); + j8001.setResult(J8001.SUCCESS); + return j8001; + } + + @Override + public ApplicationEvent getEvent() { + return null; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/proc/request/J1205.java b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/request/J1205.java new file mode 100644 index 0000000..bbe33a5 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/request/J1205.java @@ -0,0 +1,124 @@ +package com.genersoft.iot.vmp.jt1078.proc.request; + +import com.genersoft.iot.vmp.jt1078.annotation.MsgId; +import com.genersoft.iot.vmp.jt1078.proc.Header; +import com.genersoft.iot.vmp.jt1078.proc.response.J8001; +import com.genersoft.iot.vmp.jt1078.proc.response.Rs; +import com.genersoft.iot.vmp.jt1078.service.Ijt1078Service; +import com.genersoft.iot.vmp.jt1078.session.Session; +import com.genersoft.iot.vmp.jt1078.session.SessionManager; +import com.genersoft.iot.vmp.utils.DateUtil; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufUtil; +import lombok.Getter; +import lombok.Setter; +import org.springframework.context.ApplicationEvent; + +import java.util.ArrayList; +import java.util.List; + +/** + * 终端上传音视频资源列表 + * + * @author QingtaiJiang + * @date 2023/4/28 10:36 + * @email qingtaij@163.com + */ +@Setter +@Getter +@MsgId(id = "1205") +public class J1205 extends Re { + + Integer respNo; + + private List recordList = new ArrayList<>(); + + @Override + protected Rs decode0(ByteBuf buf, Header header, Session session) { + respNo = buf.readUnsignedShort(); + long size = buf.readUnsignedInt(); + + for (int i = 0; i < size; i++) { + JRecordItem item = new JRecordItem(); + item.setChannelId(buf.readUnsignedByte()); + String startTime = ByteBufUtil.hexDump(buf.readSlice(6)); + item.setStartTime(DateUtil.jt1078Toyyyy_MM_dd_HH_mm_ss(startTime)); + String endTime = ByteBufUtil.hexDump(buf.readSlice(6)); + item.setEndTime(DateUtil.jt1078Toyyyy_MM_dd_HH_mm_ss(endTime)); + item.setAlarmSign(buf.readLong()); + item.setMediaType(buf.readUnsignedByte()); + item.setStreamType(buf.readUnsignedByte()); + item.setStorageType(buf.readUnsignedByte()); + item.setSize(buf.readUnsignedInt()); + recordList.add(item); + } + return null; + } + + @Override + protected Rs handler(Header header, Session session, Ijt1078Service service) { + SessionManager.INSTANCE.response(header.getPhoneNumber(), "1205", (long) respNo, recordList); + J8001 j8001 = new J8001(); + j8001.setRespNo(header.getSn()); + j8001.setRespId(header.getMsgId()); + j8001.setResult(J8001.SUCCESS); + return j8001; + } + + + @Setter + @Getter + public static class JRecordItem { + + // 逻辑通道号 + private int channelId; + + // 开始时间 + private String startTime; + + // 结束时间 + private String endTime; + + // 报警标志 + private long alarmSign; + + // 音视频资源类型 + private int mediaType; + + // 码流类型 + private int streamType = 1; + + // 存储器类型 + private int storageType; + + // 文件大小 + private long size; + + @Override + public String toString() { + return "JRecordItem{" + + "channelId=" + channelId + + ", startTime='" + startTime + '\'' + + ", endTime='" + endTime + '\'' + + ", warn=" + alarmSign + + ", mediaType=" + mediaType + + ", streamType=" + streamType + + ", storageType=" + storageType + + ", size=" + size + + '}'; + } + } + + @Override + public String toString() { + return "J1205{" + + "respNo=" + respNo + + ", recordList=" + recordList + + '}'; + } + + @Override + public ApplicationEvent getEvent() { + return null; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/proc/request/J1206.java b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/request/J1206.java new file mode 100644 index 0000000..efef164 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/request/J1206.java @@ -0,0 +1,63 @@ +package com.genersoft.iot.vmp.jt1078.proc.request; + +import com.alibaba.fastjson2.JSON; +import com.genersoft.iot.vmp.jt1078.annotation.MsgId; +import com.genersoft.iot.vmp.jt1078.proc.Header; +import com.genersoft.iot.vmp.jt1078.proc.response.J8001; +import com.genersoft.iot.vmp.jt1078.proc.response.Rs; +import com.genersoft.iot.vmp.jt1078.service.Ijt1078Service; +import com.genersoft.iot.vmp.jt1078.session.Session; +import com.genersoft.iot.vmp.jt1078.session.SessionManager; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufUtil; +import lombok.Getter; +import lombok.Setter; +import org.springframework.context.ApplicationEvent; + +import java.util.ArrayList; +import java.util.List; + +/** + * 文件上传完成通知 + * + */ +@Setter +@Getter +@MsgId(id = "1206") +public class J1206 extends Re { + + Integer respNo; + + // 0:成功; 1:失败 + private int result; + + @Override + protected Rs decode0(ByteBuf buf, Header header, Session session) { + respNo = buf.readUnsignedShort(); + result = buf.readUnsignedByte(); + return null; + } + + @Override + protected Rs handler(Header header, Session session, Ijt1078Service service) { + J8001 j8001 = new J8001(); + j8001.setRespNo(header.getSn()); + j8001.setRespId(header.getMsgId()); + j8001.setResult(J8001.SUCCESS); + return j8001; + } + + + @Override + public String toString() { + return "J1206{" + + "respNo=" + respNo + + ", result=" + result + + '}'; + } + + @Override + public ApplicationEvent getEvent() { + return null; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/proc/request/Re.java b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/request/Re.java new file mode 100644 index 0000000..03a66f4 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/request/Re.java @@ -0,0 +1,45 @@ +package com.genersoft.iot.vmp.jt1078.proc.request; + +import com.genersoft.iot.vmp.jt1078.proc.Header; +import com.genersoft.iot.vmp.jt1078.proc.response.Rs; +import com.genersoft.iot.vmp.jt1078.service.Ijt1078Service; +import com.genersoft.iot.vmp.jt1078.session.Session; +import io.netty.buffer.ByteBuf; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.context.ApplicationEvent; +import lombok.extern.slf4j.Slf4j; +import org.springframework.util.StringUtils; + +/** + * @author QingtaiJiang + * @date 2023/4/27 18:50 + * @email qingtaij@163.com + */ +@Slf4j +public abstract class Re { + + protected abstract Rs decode0(ByteBuf buf, Header header, Session session); + + protected abstract Rs handler(Header header, Session session, Ijt1078Service service); + + public Rs decode(ByteBuf buf, Header header, Session session, Ijt1078Service service) { + if (session != null && !StringUtils.hasLength(session.getPhoneNumber())) { + session.register(header.getPhoneNumber(), (int) header.getVersion(), header); + } + Rs rs = decode0(buf, header, session); + buf.release(); + Rs rsHand = handler(header, session, service); + if (rs == null && rsHand != null) { + rs = rsHand; + } else if (rs != null && rsHand != null) { + log.warn("decode0:{} 与 handler:{} 返回值冲突,采用decode0返回值", rs, rsHand); + } + if (rs != null) { + rs.setHeader(header); + } + return rs; + } + + public abstract ApplicationEvent getEvent(); +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J8001.java b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J8001.java new file mode 100644 index 0000000..07d3aba --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J8001.java @@ -0,0 +1,41 @@ +package com.genersoft.iot.vmp.jt1078.proc.response; + +import com.genersoft.iot.vmp.jt1078.annotation.MsgId; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufUtil; +import io.netty.buffer.Unpooled; +import lombok.Setter; + +/** + * @author QingtaiJiang + * @date 2023/4/27 18:48 + * @email qingtaij@163.com + */ +@Setter +@MsgId(id = "8001") +public class J8001 extends Rs { + + public static final Integer SUCCESS = 0; + + public static final Integer FAIL = 1; + + public static final Integer ERROR = 2; + public static final Integer NOT_SUPPORTED = 3; + public static final Integer ALARM_ACK = 3; + + Integer respNo; + String respId; + Integer result; + + @Override + public ByteBuf encode() { + ByteBuf buffer = Unpooled.buffer(); + buffer.writeShort(respNo); + buffer.writeBytes(ByteBufUtil.decodeHexDump(respId)); + buffer.writeByte(result); + + return buffer; + } + + +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J8100.java b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J8100.java new file mode 100644 index 0000000..af92c21 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J8100.java @@ -0,0 +1,40 @@ +package com.genersoft.iot.vmp.jt1078.proc.response; + +import com.genersoft.iot.vmp.jt1078.annotation.MsgId; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.netty.util.CharsetUtil; +import lombok.Setter; + +/** + * @author QingtaiJiang + * @date 2023/4/27 18:40 + * @email qingtaij@163.com + */ +@Setter +@MsgId(id = "8100") +public class J8100 extends Rs { + /** + * 0 成功 + * 1 车辆已被注册 + * 2 数据库中无该车辆 + * 3 终端已被注册 + * 4 数据库中无该终端 + */ + public static final Integer SUCCESS = 0; + public static final Integer FAIL = 4; + + Integer respNo; + Integer result; + String code; + + @Override + public ByteBuf encode() { + ByteBuf buffer = Unpooled.buffer(); + buffer.writeShort(respNo); + buffer.writeByte(result); + buffer.writeCharSequence(code, CharsetUtil.UTF_8); + return buffer; + } + +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J8103.java b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J8103.java new file mode 100644 index 0000000..04db44b --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J8103.java @@ -0,0 +1,127 @@ +package com.genersoft.iot.vmp.jt1078.proc.response; + +import com.genersoft.iot.vmp.jt1078.annotation.MsgId; +import com.genersoft.iot.vmp.jt1078.bean.JTDeviceConfig; +import com.genersoft.iot.vmp.jt1078.bean.common.ConfigAttribute; +import com.genersoft.iot.vmp.jt1078.bean.config.*; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufUtil; +import io.netty.buffer.Unpooled; +import lombok.Getter; +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.nio.ByteBuffer; +import java.nio.charset.Charset; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; + +/** + * 设置终端参数 + */ +@Getter +@MsgId(id = "8103") +public class J8103 extends Rs { + + private final static Logger log = LoggerFactory.getLogger(J8103.class); + + private JTDeviceConfig config; + + @Override + public ByteBuf encode() { + ByteBuf buffer = Unpooled.buffer(); + Class configClass = config.getClass(); + Field[] declaredFields = configClass.getDeclaredFields(); + Map fieldConfigAttributeMap = new HashMap<>(); + for (Field field : declaredFields) { + try{ + Method method = configClass.getDeclaredMethod("get" + StringUtils.capitalize(field.getName())); + Object invoke = method.invoke(config); + if (invoke == null) { + continue; + } + ConfigAttribute configAttribute = field.getAnnotation(ConfigAttribute.class); + if (configAttribute != null) { + fieldConfigAttributeMap.put(field, configAttribute); + } + }catch (InvocationTargetException | NoSuchMethodException | IllegalAccessException e) { + log.error("[设置终端参数 ] 编码失败", e ); + } + + } + buffer.writeByte(fieldConfigAttributeMap.size()); + + if (!fieldConfigAttributeMap.isEmpty()) { + for (Field field : fieldConfigAttributeMap.keySet()) { + try{ + ConfigAttribute configAttribute = fieldConfigAttributeMap.get(field); + buffer.writeInt((int) (configAttribute.id() & 0xffff)); + switch (configAttribute.type()) { + case "Long": + buffer.writeByte(4); + field.setAccessible(true); + long longVal = (long)field.get(config); + buffer.writeInt((int) (longVal & 0xffffffffL)); + continue; + case "String": + field.setAccessible(true); + String stringVal = (String)field.get(config); + buffer.writeByte(stringVal.getBytes(Charset.forName("GBK")).length); + buffer.writeCharSequence(stringVal, Charset.forName("GBK")); + continue; + case "Integer": + buffer.writeByte(2); + field.setAccessible(true); + Integer integerVal = (Integer)field.get(config); + buffer.writeShort((short)(integerVal & 0xffff)); + continue; + case "Short": + buffer.writeByte(1); + field.setAccessible(true); + Short shortVal = (Short)field.get(config); + buffer.writeByte((int) (shortVal & 0xff)); + continue; + case "IllegalDrivingPeriods": + case "CollisionAlarmParams": + case "CameraTimer": + case "GnssPositioningMode": + case "VideoParam": + case "ChannelListParam": + case "ChannelParam": + case "AlarmRecordingParam": + case "AlarmShielding": + case "VideoAlarmBit": + case "AnalyzeAlarmParam": + case "AwakenParam": + case "AlarmSign": + field.setAccessible(true); + JTDeviceSubConfig subConfig = (JTDeviceSubConfig)field.get(config); + ByteBuf byteBuf = subConfig.encode(); + buffer.writeByte(byteBuf.readableBytes()); + buffer.writeBytes(byteBuf); + continue; + } + }catch (Exception e) { + log.error("[设置终端参数 ] 编码失败", e ); + } + } + } + return buffer; + } + + public void setConfig(JTDeviceConfig config) { + this.config = config; + } + + @Override + public String toString() { + return "J8103{" + + "config=" + config + + '}'; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J8104.java b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J8104.java new file mode 100644 index 0000000..c4d6012 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J8104.java @@ -0,0 +1,20 @@ +package com.genersoft.iot.vmp.jt1078.proc.response; + +import com.genersoft.iot.vmp.jt1078.annotation.MsgId; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; + +import java.util.Arrays; + +/** + * 查询终端参数 + */ +@MsgId(id = "8104") +public class J8104 extends Rs { + + @Override + public ByteBuf encode() { + return Unpooled.buffer(); + } + +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J8105.java b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J8105.java new file mode 100644 index 0000000..5e59096 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J8105.java @@ -0,0 +1,81 @@ +package com.genersoft.iot.vmp.jt1078.proc.response; + +import com.genersoft.iot.vmp.jt1078.annotation.MsgId; +import com.genersoft.iot.vmp.jt1078.bean.JTDeviceConnectionControl; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import lombok.Getter; + +import java.nio.charset.Charset; +import java.util.Arrays; + +/** + * 终端控制 + */ +@Getter +@MsgId(id = "8105") +public class J8105 extends Rs { + + private JTDeviceConnectionControl connectionControl; + + /** + * 终端复位 + */ + private Boolean reset; + + /** + * 终端恢复出厂设置 + */ + private Boolean factoryReset; + + @Override + public ByteBuf encode() { + ByteBuf byteBuf = Unpooled.buffer(); + if (reset != null) { + byteBuf.writeByte(4); + }else if (factoryReset != null) { + byteBuf.writeByte(5); + }else if (connectionControl != null) { + byteBuf.writeByte(2); + StringBuffer stringBuffer = new StringBuffer(); + if (connectionControl.getSwitchOn() != null) { + if (connectionControl.getSwitchOn()) { + stringBuffer.append("1"); + }else { + stringBuffer.append("0"); + stringBuffer.append(";" + connectionControl.getAuthentication()) + .append(";" + connectionControl.getName()) + .append(";" + connectionControl.getUsername()) + .append(";" + connectionControl.getPassword()) + .append(";" + connectionControl.getAddress()) + .append(";" + connectionControl.getTcpPort()) + .append(";" + connectionControl.getUdpPort()) + .append(";" + connectionControl.getTimeLimit()); + } + } + byteBuf.writeCharSequence(stringBuffer.toString(), Charset.forName("GBK")); + } + return byteBuf; + } + + public void setConnectionControl(JTDeviceConnectionControl connectionControl) { + this.connectionControl = connectionControl; + } + + public void setReset(Boolean reset) { + this.reset = reset; + } + + public void setFactoryReset(Boolean factoryReset) { + this.factoryReset = factoryReset; + } + + @Override + public String toString() { + return "J8105{" + + "connectionControl=" + connectionControl + + ", reset=" + reset + + ", factoryReset=" + factoryReset + + '}'; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J8106.java b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J8106.java new file mode 100644 index 0000000..4c7fe9d --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J8106.java @@ -0,0 +1,40 @@ +package com.genersoft.iot.vmp.jt1078.proc.response; + +import com.genersoft.iot.vmp.jt1078.annotation.MsgId; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.netty.util.CharsetUtil; +import lombok.Getter; + +import java.util.Arrays; + +/** + * 查询指定终端参数 + */ +@Getter +@MsgId(id = "8106") +public class J8106 extends Rs { + + private long[] params; + + @Override + public ByteBuf encode() { + ByteBuf buffer = Unpooled.buffer(); + buffer.writeByte(params.length); + for (long param : params) { + buffer.writeInt((int) param); + } + return buffer; + } + + public void setParams(long[] params) { + this.params = params; + } + + @Override + public String toString() { + return "J8106{" + + "params=" + Arrays.toString(params) + + '}'; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J8107.java b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J8107.java new file mode 100644 index 0000000..a516d2c --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J8107.java @@ -0,0 +1,18 @@ +package com.genersoft.iot.vmp.jt1078.proc.response; + +import com.genersoft.iot.vmp.jt1078.annotation.MsgId; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; + +/** + * 查询终端属性 + */ +@MsgId(id = "8107") +public class J8107 extends Rs { + + @Override + public ByteBuf encode() { + return Unpooled.buffer(); + } + +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J8201.java b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J8201.java new file mode 100644 index 0000000..1d360b0 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J8201.java @@ -0,0 +1,18 @@ +package com.genersoft.iot.vmp.jt1078.proc.response; + +import com.genersoft.iot.vmp.jt1078.annotation.MsgId; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; + +/** + * 位置信息查询 + */ +@MsgId(id = "8201") +public class J8201 extends Rs { + + @Override + public ByteBuf encode() { + return Unpooled.buffer(); + } + +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J8202.java b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J8202.java new file mode 100644 index 0000000..1c6ff8a --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J8202.java @@ -0,0 +1,37 @@ +package com.genersoft.iot.vmp.jt1078.proc.response; + +import com.genersoft.iot.vmp.jt1078.annotation.MsgId; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import lombok.Getter; +import lombok.Setter; + +/** + * 临时位置跟踪控制 + */ +@Setter +@Getter +@MsgId(id = "8202") +public class J8202 extends Rs { + + /** + * 时间间隔,单位为秒,时间间隔为0 时停止跟踪,停止跟踪无需带后继字段 + */ + private int timeInterval; + + /** + * 位置跟踪有效期, 单位为秒,终端在接收到位置跟踪控制消息后,在有效期截止时间之前依据消息中的时间间隔发送位置汇报 + */ + private long validityPeriod; + + @Override + public ByteBuf encode() { + ByteBuf buffer = Unpooled.buffer(); + buffer.writeShort((short)(timeInterval & 0xffff)); + if (timeInterval > 0) { + buffer.writeInt((int) (validityPeriod & 0xffffffffL)); + } + return buffer; + } + +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J8203.java b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J8203.java new file mode 100644 index 0000000..d537c4f --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J8203.java @@ -0,0 +1,37 @@ +package com.genersoft.iot.vmp.jt1078.proc.response; + +import com.genersoft.iot.vmp.jt1078.annotation.MsgId; +import com.genersoft.iot.vmp.jt1078.bean.JTConfirmationAlarmMessageType; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import lombok.Getter; +import lombok.Setter; + +/** + * 人工确认报警消息 + */ +@Setter +@Getter +@MsgId(id = "8203") +public class J8203 extends Rs { + + /** + * 报警消息流水号 + */ + private int alarmPackageNo; + /** + * 人工确认报警类型 + */ + private JTConfirmationAlarmMessageType alarmMessageType; + + @Override + public ByteBuf encode() { + ByteBuf buffer = Unpooled.buffer(); + buffer.writeShort((short)(alarmPackageNo & 0xffff)); + if (alarmMessageType != null) { + buffer.writeInt((int) (alarmMessageType.encode() & 0xffffffffL)); + } + return buffer; + } + +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J8204.java b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J8204.java new file mode 100644 index 0000000..7455301 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J8204.java @@ -0,0 +1,18 @@ +package com.genersoft.iot.vmp.jt1078.proc.response; + +import com.genersoft.iot.vmp.jt1078.annotation.MsgId; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; + +/** + * 链路检测 + */ +@MsgId(id = "8204") +public class J8204 extends Rs { + + @Override + public ByteBuf encode() { + return Unpooled.buffer(); + } + +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J8300.java b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J8300.java new file mode 100644 index 0000000..59e3e6e --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J8300.java @@ -0,0 +1,43 @@ +package com.genersoft.iot.vmp.jt1078.proc.response; + +import com.genersoft.iot.vmp.jt1078.annotation.MsgId; +import com.genersoft.iot.vmp.jt1078.bean.JTTextSign; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.nio.charset.Charset; + +/** + * 文本信息下发 + */ +@EqualsAndHashCode(callSuper = true) +@Data +@MsgId(id = "8300") +public class J8300 extends Rs { + + /** + * 标志 + */ + private JTTextSign sign; + + /** + * 文本类型1 = 通知 ,2 = 服务 + */ + private int textType; + + /** + * 文本信息 + */ + private String content; + + @Override + public ByteBuf encode() { + ByteBuf buffer = Unpooled.buffer(); + buffer.writeByte(sign.encode()); + buffer.writeByte(textType); + buffer.writeCharSequence(content, Charset.forName("GBK")); + return buffer; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J8400.java b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J8400.java new file mode 100644 index 0000000..8779118 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J8400.java @@ -0,0 +1,38 @@ +package com.genersoft.iot.vmp.jt1078.proc.response; + +import com.genersoft.iot.vmp.jt1078.annotation.MsgId; +import com.genersoft.iot.vmp.jt1078.bean.JTTextSign; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import lombok.Getter; +import lombok.Setter; + +import java.nio.charset.Charset; + +/** + * 电话回拨 + */ +@Setter +@Getter +@MsgId(id = "8400") +public class J8400 extends Rs { + + /** + * 标志, 0'普通通话,1'监听 + */ + private int sign; + + /** + * 电话号码 + */ + private String phoneNumber; + + @Override + public ByteBuf encode() { + ByteBuf buffer = Unpooled.buffer(); + buffer.writeByte(sign); + buffer.writeCharSequence(phoneNumber, Charset.forName("GBK")); + return buffer; + } + +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J8401.java b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J8401.java new file mode 100644 index 0000000..960fe5e --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J8401.java @@ -0,0 +1,50 @@ +package com.genersoft.iot.vmp.jt1078.proc.response; + +import com.genersoft.iot.vmp.jt1078.annotation.MsgId; +import com.genersoft.iot.vmp.jt1078.bean.JTPhoneBookContact; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import lombok.Getter; +import lombok.Setter; + +import java.nio.charset.Charset; +import java.util.List; + +/** + * 设置电话本 + */ +@Setter +@Getter +@MsgId(id = "8401") +public class J8401 extends Rs { + + /** + * 设置类型: + * 0: 删除终端上所有存储的联系人, + * 1: 表示更新电话本$ 删除终端中已有全部联系人并追加消 息中的联系人, + * 2: 表示追加电话本, + * 3: 表示修改电话本$以联系人为索引 + */ + private int type; + + /** + * 联系人 + */ + private List phoneBookContactList; + + @Override + public ByteBuf encode() { + ByteBuf buffer = Unpooled.buffer(); + buffer.writeByte(type); + if (phoneBookContactList != null && !phoneBookContactList.isEmpty()) { + buffer.writeByte(phoneBookContactList.size()); + for (JTPhoneBookContact jtPhoneBookContact : phoneBookContactList) { + buffer.writeBytes(jtPhoneBookContact.encode()); + } + }else { + buffer.writeByte(0); + } + return buffer; + } + +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J8500.java b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J8500.java new file mode 100644 index 0000000..047ed8b --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J8500.java @@ -0,0 +1,67 @@ +package com.genersoft.iot.vmp.jt1078.proc.response; + +import com.genersoft.iot.vmp.jt1078.annotation.MsgId; +import com.genersoft.iot.vmp.jt1078.bean.JTPhoneBookContact; +import com.genersoft.iot.vmp.jt1078.bean.JTVehicleControl; +import com.genersoft.iot.vmp.jt1078.bean.common.ConfigAttribute; +import com.genersoft.iot.vmp.jt1078.bean.config.JTDeviceSubConfig; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import lombok.Getter; +import lombok.Setter; + +import java.lang.reflect.Field; +import java.nio.charset.Charset; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * 车辆控制 + */ +@Setter +@Getter +@MsgId(id = "8500") +public class J8500 extends Rs { + + /** + * 控制类型 + */ + private JTVehicleControl vehicleControl; + + + @Override + public ByteBuf encode() { + ByteBuf buffer = Unpooled.buffer(); + buffer.writeShort((short)(vehicleControl.getLength() & 0xffff)); + + Field[] fields = vehicleControl.getClass().getDeclaredFields(); + for (Field field : fields) { + field.setAccessible(true); + Object value = null; + try { + value = field.get(vehicleControl); + if (value == null) { + continue; + } + } catch (IllegalAccessException e) { + throw new RuntimeException(e); + } + + ConfigAttribute configAttribute = field.getAnnotation(ConfigAttribute.class); + if (configAttribute == null) { + continue; + } + buffer.writeShort((short)(configAttribute.id() & 0xffff)); + switch (configAttribute.type()) { + case "Byte": + field.setAccessible(true); + buffer.writeByte((int)value); + continue; + } + } + + return buffer; + } + +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J8600.java b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J8600.java new file mode 100644 index 0000000..8fc9063 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J8600.java @@ -0,0 +1,48 @@ +package com.genersoft.iot.vmp.jt1078.proc.response; + +import com.genersoft.iot.vmp.jt1078.annotation.MsgId; +import com.genersoft.iot.vmp.jt1078.bean.JTCircleArea; +import com.genersoft.iot.vmp.jt1078.bean.JTVehicleControl; +import com.genersoft.iot.vmp.jt1078.bean.common.ConfigAttribute; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import lombok.Getter; +import lombok.Setter; + +import java.lang.reflect.Field; +import java.util.List; + +/** + * 设置圆形区域 + */ +@Setter +@Getter +@MsgId(id = "8600") +public class J8600 extends Rs { + + /** + * 设置属性, 0:更新区域; 1:追加区域; 2:修改区域 + */ + private int attribute; + + /** + * 区域项 + */ + private List circleAreaList; + + + @Override + public ByteBuf encode() { + ByteBuf buffer = Unpooled.buffer(); + buffer.writeByte(attribute); + buffer.writeByte(circleAreaList.size()); + if (circleAreaList.isEmpty()) { + return buffer; + } + for (JTCircleArea circleArea : circleAreaList) { + buffer.writeBytes(circleArea.encode()); + } + return buffer; + } + +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J8601.java b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J8601.java new file mode 100644 index 0000000..2d0a7f0 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J8601.java @@ -0,0 +1,42 @@ +package com.genersoft.iot.vmp.jt1078.proc.response; + +import com.genersoft.iot.vmp.jt1078.annotation.MsgId; +import com.genersoft.iot.vmp.jt1078.bean.JTCircleArea; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import lombok.Getter; +import lombok.Setter; + +import java.util.List; + +/** + * 删除圆形区域 + */ +@Setter +@Getter +@MsgId(id = "8601") +public class J8601 extends Rs { + + + /** + * 待删除的区域ID + */ + private List idList; + + + @Override + public ByteBuf encode() { + ByteBuf buffer = Unpooled.buffer(); + if (idList == null || idList.isEmpty()) { + buffer.writeByte(0); + return buffer; + }else { + buffer.writeByte(idList.size()); + } + for (Long id : idList) { + buffer.writeInt((int) (id & 0xffffffffL)); + } + return buffer; + } + +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J8602.java b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J8602.java new file mode 100644 index 0000000..bda25b1 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J8602.java @@ -0,0 +1,46 @@ +package com.genersoft.iot.vmp.jt1078.proc.response; + +import com.genersoft.iot.vmp.jt1078.annotation.MsgId; +import com.genersoft.iot.vmp.jt1078.bean.JTCircleArea; +import com.genersoft.iot.vmp.jt1078.bean.JTRectangleArea; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import lombok.Getter; +import lombok.Setter; + +import java.util.List; + +/** + * 设置矩形区域 + */ +@Setter +@Getter +@MsgId(id = "8602") +public class J8602 extends Rs { + + /** + * 设置属性, 0:更新区域; 1:追加区域; 2:修改区域 + */ + private int attribute; + + /** + * 区域项 + */ + private List rectangleAreas; + + + @Override + public ByteBuf encode() { + ByteBuf buffer = Unpooled.buffer(); + buffer.writeByte(attribute); + buffer.writeByte(rectangleAreas.size()); + if (rectangleAreas.isEmpty()) { + return buffer; + } + for (JTRectangleArea area : rectangleAreas) { + buffer.writeBytes(area.encode()); + } + return buffer; + } + +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J8603.java b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J8603.java new file mode 100644 index 0000000..2b5dbbf --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J8603.java @@ -0,0 +1,41 @@ +package com.genersoft.iot.vmp.jt1078.proc.response; + +import com.genersoft.iot.vmp.jt1078.annotation.MsgId; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import lombok.Getter; +import lombok.Setter; + +import java.util.List; + +/** + * 删除矩形区域 + */ +@Setter +@Getter +@MsgId(id = "8603") +public class J8603 extends Rs { + + + /** + * 待删除的区域ID + */ + private List idList; + + + @Override + public ByteBuf encode() { + ByteBuf buffer = Unpooled.buffer(); + if (idList == null || idList.isEmpty()) { + buffer.writeByte(0); + return buffer; + }else { + buffer.writeByte(idList.size()); + } + for (Long id : idList) { + buffer.writeInt((int) (id & 0xffffffffL)); + } + return buffer; + } + +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J8604.java b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J8604.java new file mode 100644 index 0000000..2e54ded --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J8604.java @@ -0,0 +1,34 @@ +package com.genersoft.iot.vmp.jt1078.proc.response; + +import com.genersoft.iot.vmp.jt1078.annotation.MsgId; +import com.genersoft.iot.vmp.jt1078.bean.JTPolygonArea; +import com.genersoft.iot.vmp.jt1078.bean.JTRectangleArea; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import lombok.Getter; +import lombok.Setter; + +import java.util.List; + +/** + * 设置多边形区域 + */ +@Setter +@Getter +@MsgId(id = "8604") +public class J8604 extends Rs { + + /** + * 多边形区域 + */ + private JTPolygonArea polygonArea; + + + @Override + public ByteBuf encode() { + ByteBuf buffer = Unpooled.buffer(); + buffer.writeBytes(polygonArea.encode()); + return buffer; + } + +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J8605.java b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J8605.java new file mode 100644 index 0000000..492cf6a --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J8605.java @@ -0,0 +1,41 @@ +package com.genersoft.iot.vmp.jt1078.proc.response; + +import com.genersoft.iot.vmp.jt1078.annotation.MsgId; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import lombok.Getter; +import lombok.Setter; + +import java.util.List; + +/** + * 删除多边形区域 + */ +@Setter +@Getter +@MsgId(id = "8605") +public class J8605 extends Rs { + + + /** + * 待删除的区域ID + */ + private List idList; + + + @Override + public ByteBuf encode() { + ByteBuf buffer = Unpooled.buffer(); + if (idList == null || idList.isEmpty()) { + buffer.writeByte(0); + return buffer; + }else { + buffer.writeByte(idList.size()); + } + for (Long id : idList) { + buffer.writeInt((int) (id & 0xffffffffL)); + } + return buffer; + } + +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J8606.java b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J8606.java new file mode 100644 index 0000000..cfcded1 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J8606.java @@ -0,0 +1,32 @@ +package com.genersoft.iot.vmp.jt1078.proc.response; + +import com.genersoft.iot.vmp.jt1078.annotation.MsgId; +import com.genersoft.iot.vmp.jt1078.bean.JTPolygonArea; +import com.genersoft.iot.vmp.jt1078.bean.JTRoute; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import lombok.Getter; +import lombok.Setter; + +/** + * 设置路线 + */ +@Setter +@Getter +@MsgId(id = "8606") +public class J8606 extends Rs { + + /** + * 路线 + */ + private JTRoute route; + + + @Override + public ByteBuf encode() { + ByteBuf buffer = Unpooled.buffer(); + buffer.writeBytes(route.encode()); + return buffer; + } + +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J8607.java b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J8607.java new file mode 100644 index 0000000..1b31235 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J8607.java @@ -0,0 +1,41 @@ +package com.genersoft.iot.vmp.jt1078.proc.response; + +import com.genersoft.iot.vmp.jt1078.annotation.MsgId; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import lombok.Getter; +import lombok.Setter; + +import java.util.List; + +/** + * 删除路线 + */ +@Setter +@Getter +@MsgId(id = "8607") +public class J8607 extends Rs { + + + /** + * 待删除的路线ID + */ + private List idList; + + + @Override + public ByteBuf encode() { + ByteBuf buffer = Unpooled.buffer(); + if (idList == null || idList.isEmpty()) { + buffer.writeByte(0); + return buffer; + }else { + buffer.writeByte(idList.size()); + } + for (Long id : idList) { + buffer.writeInt((int) (id & 0xffffffffL)); + } + return buffer; + } + +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J8608.java b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J8608.java new file mode 100644 index 0000000..5a33918 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J8608.java @@ -0,0 +1,48 @@ +package com.genersoft.iot.vmp.jt1078.proc.response; + +import com.genersoft.iot.vmp.jt1078.annotation.MsgId; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import lombok.Getter; +import lombok.Setter; + +import java.util.List; + +/** + * 查询区域或线路数据 + */ +@Setter +@Getter +@MsgId(id = "8608") +public class J8608 extends Rs { + + + /** + * 查询类型, 1 = 查询圆形区域数据 ,2 = 查询矩形区域数据 ,3 = 查询多 边形区域数据 ,4 = 查询线路数据 + */ + private int type; + + + /** + * 要查询的区域或线路的 ID + */ + private List idList; + + + @Override + public ByteBuf encode() { + ByteBuf buffer = Unpooled.buffer(); + buffer.writeByte(type); + if (idList == null || idList.isEmpty()) { + buffer.writeInt(0); + return buffer; + }else { + buffer.writeInt(idList.size()); + } + for (Long id : idList) { + buffer.writeInt((int) (id & 0xffffffffL)); + } + return buffer; + } + +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J8702.java b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J8702.java new file mode 100644 index 0000000..ba457cd --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J8702.java @@ -0,0 +1,18 @@ +package com.genersoft.iot.vmp.jt1078.proc.response; + +import com.genersoft.iot.vmp.jt1078.annotation.MsgId; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; + +/** + * 上报驾驶员身份信息请求 + */ +@MsgId(id = "8702") +public class J8702 extends Rs { + + @Override + public ByteBuf encode() { + return Unpooled.buffer(); + } + +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J8801.java b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J8801.java new file mode 100644 index 0000000..83d7e4f --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J8801.java @@ -0,0 +1,28 @@ +package com.genersoft.iot.vmp.jt1078.proc.response; + +import com.genersoft.iot.vmp.jt1078.annotation.MsgId; +import com.genersoft.iot.vmp.jt1078.bean.JTShootingCommand; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufUtil; +import io.netty.buffer.Unpooled; +import lombok.Getter; +import lombok.Setter; + +import java.util.List; + +/** + * 摄像头立即拍摄命令 + */ +@Setter +@Getter +@MsgId(id = "8801") +public class J8801 extends Rs { + + JTShootingCommand command; + + @Override + public ByteBuf encode() { + return command.decode(); + } + +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J8802.java b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J8802.java new file mode 100644 index 0000000..f7492c1 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J8802.java @@ -0,0 +1,25 @@ +package com.genersoft.iot.vmp.jt1078.proc.response; + +import com.genersoft.iot.vmp.jt1078.annotation.MsgId; +import com.genersoft.iot.vmp.jt1078.bean.JTQueryMediaDataCommand; +import com.genersoft.iot.vmp.jt1078.bean.JTShootingCommand; +import io.netty.buffer.ByteBuf; +import lombok.Getter; +import lombok.Setter; + +/** + * 存储多媒体数据检索 + */ +@Setter +@Getter +@MsgId(id = "8802") +public class J8802 extends Rs { + + JTQueryMediaDataCommand command; + + @Override + public ByteBuf encode() { + return command.decode(); + } + +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J8803.java b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J8803.java new file mode 100644 index 0000000..59cf091 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J8803.java @@ -0,0 +1,24 @@ +package com.genersoft.iot.vmp.jt1078.proc.response; + +import com.genersoft.iot.vmp.jt1078.annotation.MsgId; +import com.genersoft.iot.vmp.jt1078.bean.JTQueryMediaDataCommand; +import io.netty.buffer.ByteBuf; +import lombok.Getter; +import lombok.Setter; + +/** + * 存储多媒体数据上传命令 + */ +@Setter +@Getter +@MsgId(id = "8803") +public class J8803 extends Rs { + + JTQueryMediaDataCommand command; + + @Override + public ByteBuf encode() { + return command.decode(); + } + +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J8804.java b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J8804.java new file mode 100644 index 0000000..2414d68 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J8804.java @@ -0,0 +1,48 @@ +package com.genersoft.iot.vmp.jt1078.proc.response; + +import com.genersoft.iot.vmp.jt1078.annotation.MsgId; +import com.genersoft.iot.vmp.jt1078.bean.JTQueryMediaDataCommand; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import lombok.Getter; +import lombok.Setter; + +/** + * 录音开始/停止命令 + */ +@Setter +@Getter +@MsgId(id = "8804") +public class J8804 extends Rs { + + /** + * 录音命令, 0:停止录音;0X01:开始录音 + */ + private int commond; + + /** + * 录音时长,单位为秒(s) ,0 表示一直录音 + */ + private int duration; + + /** + * 保存标志, 0:实时上传;1:保存 + */ + private int save; + + /** + * 音频采样率, 0:8K;1:11K;2:23K;3:32K;其他保留 + */ + private int samplingRate; + + @Override + public ByteBuf encode() { + ByteBuf byteBuf = Unpooled.buffer(); + byteBuf.writeByte(commond); + byteBuf.writeShort((short)(duration & 0xffff)); + byteBuf.writeByte(save); + byteBuf.writeByte(samplingRate); + return byteBuf; + } + +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J8805.java b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J8805.java new file mode 100644 index 0000000..c64db88 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J8805.java @@ -0,0 +1,37 @@ +package com.genersoft.iot.vmp.jt1078.proc.response; + +import com.genersoft.iot.vmp.jt1078.annotation.MsgId; +import com.genersoft.iot.vmp.jt1078.bean.JTQueryMediaDataCommand; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; +import lombok.Setter; + +/** + * 单条存储多媒体数据检索上传命令 + */ +@Setter +@Getter +@MsgId(id = "8805") +public class J8805 extends Rs { + + /** + * 多媒体 ID + */ + private Long mediaId; + + /** + * 删除标志, 0:保留;1:删除, 存储多媒体数据上传命令中使用 + */ + private Integer delete; + + @Override + public ByteBuf encode() { + ByteBuf byteBuf = Unpooled.buffer(); + byteBuf.writeInt((int) (mediaId & 0xffffffffL)); + byteBuf.writeByte(delete); + return byteBuf; + } + +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J8900.java b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J8900.java new file mode 100644 index 0000000..b8ef60f --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J8900.java @@ -0,0 +1,35 @@ +package com.genersoft.iot.vmp.jt1078.proc.response; + +import com.genersoft.iot.vmp.jt1078.annotation.MsgId; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import lombok.Getter; +import lombok.Setter; + +/** + * 数据下行透传 + */ +@Setter +@Getter +@MsgId(id = "8900") +public class J8900 extends Rs { + + /** + * 透传消息类型, 0x00: GNSS 模块详细定位数据, 0X0B: 道路运输证 IC卡信息, 0X41: 串口1 透传, 0X42: 串口2 透传, 0XF0 ~ 0XFF: 用户自定义透传 + */ + private Integer type; + + /** + * 透传消息内容 + */ + private byte[] content; + + @Override + public ByteBuf encode() { + ByteBuf byteBuf = Unpooled.buffer(); + byteBuf.writeByte(type); + byteBuf.writeBytes(content); + return byteBuf; + } + +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J8A00.java b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J8A00.java new file mode 100644 index 0000000..51f3df5 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J8A00.java @@ -0,0 +1,35 @@ +package com.genersoft.iot.vmp.jt1078.proc.response; + +import com.genersoft.iot.vmp.jt1078.annotation.MsgId; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import lombok.Getter; +import lombok.Setter; + +/** + * 平台 RSA公钥 + */ +@Setter +@Getter +@MsgId(id = "8A00") +public class J8A00 extends Rs { + + /** + * 平台 RSA公钥{e ,n}中的 e + */ + private Long e; + + /** + * RSA公钥{e ,n}中的 n + */ + private byte[] n; + + @Override + public ByteBuf encode() { + ByteBuf byteBuf = Unpooled.buffer(); + byteBuf.writeInt((int) (e & 0xffffffffL)); + byteBuf.writeBytes(n); + return byteBuf; + } + +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J9003.java b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J9003.java new file mode 100644 index 0000000..9bbd9d0 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J9003.java @@ -0,0 +1,18 @@ +package com.genersoft.iot.vmp.jt1078.proc.response; + +import com.genersoft.iot.vmp.jt1078.annotation.MsgId; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; + +/** + * 查询终端音视频属性 + */ +@MsgId(id = "9003") +public class J9003 extends Rs { + + @Override + public ByteBuf encode() { + return Unpooled.buffer(); + } + +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J9101.java b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J9101.java new file mode 100644 index 0000000..935021a --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J9101.java @@ -0,0 +1,70 @@ +package com.genersoft.iot.vmp.jt1078.proc.response; + +import com.genersoft.iot.vmp.jt1078.annotation.MsgId; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.netty.util.CharsetUtil; +import lombok.Getter; +import lombok.Setter; + +import java.nio.charset.Charset; + +/** + * 实时音视频传输请求 + * + * @author QingtaiJiang + * @date 2023/4/27 18:25 + * @email qingtaij@163.com + */ +@Setter +@Getter +@MsgId(id = "9101") +public class J9101 extends Rs { + String ip; + + // TCP端口 + Integer tcpPort; + + // UDP端口 + Integer udpPort; + + // 逻辑通道号 + Integer channel; + + // 数据类型 + /** + * 0:音视频,1:视频,2:双向对讲,3:监听,4:中心广播,5:透传 + */ + Integer type; + + // 码流类型 + /** + * 0:主码流,1:子码流 + */ + Integer rate; + + @Override + public ByteBuf encode() { + ByteBuf buffer = Unpooled.buffer(); + buffer.writeByte(ip.getBytes(Charset.forName("GBK")).length); + buffer.writeCharSequence(ip, Charset.forName("GBK")); + buffer.writeShort(tcpPort); + buffer.writeShort(udpPort); + buffer.writeByte(channel); + buffer.writeByte(type); + buffer.writeByte(rate); + return buffer; + } + + @Override + public String toString() { + return "J9101{" + + "ip='" + ip + '\'' + + ", tcpPort=" + tcpPort + + ", udpPort=" + udpPort + + ", channel=" + channel + + ", type=" + type + + ", rate=" + rate + + '}'; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J9102.java b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J9102.java new file mode 100644 index 0000000..61d7f0c --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J9102.java @@ -0,0 +1,71 @@ +package com.genersoft.iot.vmp.jt1078.proc.response; + +import com.genersoft.iot.vmp.jt1078.annotation.MsgId; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import lombok.Getter; +import lombok.Setter; + +/** + * 音视频实时传输控制 + * + * @author QingtaiJiang + * @date 2023/4/27 18:49 + * @email qingtaij@163.com + */ +@Setter +@Getter +@MsgId(id = "9102") +public class J9102 extends Rs { + + // 通道号 + Integer channel; + + // 控制指令 + /** + * 0:关闭音视频传输指令; + * 1:切换码流(增加暂停和继续); + * 2:暂停该通道所有流的发送; + * 3:恢复暂停前流的发送,与暂停前的流类型一致; + * 4:关闭双向对讲 + */ + Integer command; + + // 数据类型 + /** + * 0:关闭该通道有关的音视频数据; + * 1:只关闭该通道有关的音频,保留该通道 + * 有关的视频; + * 2:只关闭该通道有关的视频,保留该通道 + * 有关的音频 + */ + Integer closeType; + + // 数据类型 + /** + * 0:主码流; + * 1:子码流 + */ + Integer streamType; + + @Override + public ByteBuf encode() { + ByteBuf buffer = Unpooled.buffer(); + buffer.writeByte(channel); + buffer.writeByte(command); + buffer.writeByte(closeType); + buffer.writeByte(streamType); + return buffer; + } + + + @Override + public String toString() { + return "J9102{" + + "channel=" + channel + + ", command=" + command + + ", closeType=" + closeType + + ", streamType=" + streamType + + '}'; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J9201.java b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J9201.java new file mode 100644 index 0000000..3de9711 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J9201.java @@ -0,0 +1,91 @@ +package com.genersoft.iot.vmp.jt1078.proc.response; + +import com.genersoft.iot.vmp.jt1078.annotation.MsgId; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufUtil; +import io.netty.buffer.Unpooled; +import io.netty.util.CharsetUtil; +import lombok.Getter; +import lombok.Setter; + +import java.nio.charset.Charset; + +/** + * 回放请求 + * + * @author QingtaiJiang + * @date 2023/4/28 10:37 + * @email qingtaij@163.com + */ +@Setter +@Getter +@MsgId(id = "9201") +public class J9201 extends Rs { + // 服务器IP地址 + private String ip; + + // 实时视频服务器TCP端口号 + private int tcpPort; + + // 实时视频服务器UDP端口号 + private int udpPort; + + // 逻辑通道号 + private int channel; + + // 音视频资源类型:0.音视频 1.音频 2.视频 3.视频或音视频 + private int type; + + // 码流类型:0.所有码流 1.主码流 2.子码流(如果此通道只传输音频,此字段置0) + private int rate; + + // 存储器类型:0.所有存储器 1.主存储器 2.灾备存储器" + private int storageType; + + // 回放方式:0.正常回放 1.快进回放 2.关键帧快退回放 3.关键帧播放 4.单帧上传 + private int playbackType; + + // 快进或快退倍数:0.无效 1.1倍 2.2倍 3.4倍 4.8倍 5.16倍 (回放控制为1和2时,此字段内容有效,否则置0) + private int playbackSpeed; + + // 开始时间YYMMDDHHMMSS,回放方式为4时,该字段表示单帧上传时间 + private String startTime; + + // 结束时间YYMMDDHHMMSS,回放方式为4时,该字段无效,为0表示一直回放 + private String endTime; + + @Override + public ByteBuf encode() { + ByteBuf buffer = Unpooled.buffer(); + buffer.writeByte(ip.getBytes(Charset.forName("GBK")).length); + buffer.writeCharSequence(ip, Charset.forName("GBK")); + buffer.writeShort(tcpPort); + buffer.writeShort(udpPort); + buffer.writeByte(channel); + buffer.writeByte(type); + buffer.writeByte(rate); + buffer.writeByte(storageType); + buffer.writeByte(playbackType); + buffer.writeByte(playbackSpeed); + buffer.writeBytes(ByteBufUtil.decodeHexDump(startTime)); + buffer.writeBytes(ByteBufUtil.decodeHexDump(endTime)); + return buffer; + } + + @Override + public String toString() { + return "J9201{" + + "ip='" + ip + '\'' + + ", tcpPort=" + tcpPort + + ", udpPort=" + udpPort + + ", channel=" + channel + + ", type=" + type + + ", rate=" + rate + + ", storageType=" + storageType + + ", playbackType=" + playbackType + + ", playbackSpeed=" + playbackSpeed + + ", startTime='" + startTime + '\'' + + ", endTime='" + endTime + '\'' + + '}'; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J9202.java b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J9202.java new file mode 100644 index 0000000..805d2dd --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J9202.java @@ -0,0 +1,55 @@ +package com.genersoft.iot.vmp.jt1078.proc.response; + +import com.genersoft.iot.vmp.jt1078.annotation.MsgId; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufUtil; +import io.netty.buffer.Unpooled; +import lombok.Getter; +import lombok.Setter; + +/** + * 平台下发远程录像回放控制 + * + * @author QingtaiJiang + * @date 2023/4/28 10:37 + * @email qingtaij@163.com + */ +@Setter +@Getter +@MsgId(id = "9202") +public class J9202 extends Rs { + // 逻辑通道号 + private int channel; + + // 回放控制:0.开始回放 1.暂停回放 2.结束回放 3.快进回放 4.关键帧快退回放 5.拖动回放 6.关键帧播放 + private int playbackType; + + // 快进或快退倍数:0.无效 1.1倍 2.2倍 3.4倍 4.8倍 5.16倍 (回放控制为3和4时,此字段内容有效,否则置0) + private int playbackSpeed; + + // 拖动回放位置(YYMMDDHHMMSS,回放控制为5时,此字段有效) + private String playbackTime; + + @Override + public ByteBuf encode() { + ByteBuf buffer = Unpooled.buffer(); + buffer.writeByte(channel); + buffer.writeByte(playbackType); + buffer.writeByte(playbackSpeed); + if (playbackType == 5) { + buffer.writeBytes(ByteBufUtil.decodeHexDump(playbackTime)); + } + + return buffer; + } + + @Override + public String toString() { + return "J9202{" + + "channel=" + channel + + ", playbackType=" + playbackType + + ", playbackSpeed=" + playbackSpeed + + ", playbackTime='" + playbackTime + '\'' + + '}'; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J9205.java b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J9205.java new file mode 100644 index 0000000..36b858e --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J9205.java @@ -0,0 +1,94 @@ +package com.genersoft.iot.vmp.jt1078.proc.response; + +import com.genersoft.iot.vmp.jt1078.annotation.MsgId; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufUtil; +import io.netty.buffer.Unpooled; + +/** + * 查询资源列表 + * + * @author QingtaiJiang + * @date 2023/4/28 10:36 + * @email qingtaij@163.com + */ +@MsgId(id = "9205") +public class J9205 extends Rs { + // 逻辑通道号 + private int channelId; + + // 开始时间YYMMDDHHMMSS,全0表示无起始时间 + private String startTime; + + // 结束时间YYMMDDHHMMSS,全0表示无终止时间 + private String endTime; + + // 报警标志 + private final int warnType = 0; + + // 音视频资源类型:0.音视频 1.音频 2.视频 3.视频或音视频 + private int mediaType; + + // 码流类型:0.所有码流 1.主码流 2.子码流 + private int streamType = 0; + + // 存储器类型:0.所有存储器 1.主存储器 2.灾备存储器 + private int storageType = 0; + + @Override + public ByteBuf encode() { + ByteBuf buffer = Unpooled.buffer(); + + buffer.writeByte(channelId); + buffer.writeBytes(ByteBufUtil.decodeHexDump(startTime)); + buffer.writeBytes(ByteBufUtil.decodeHexDump(endTime)); + buffer.writeLong(warnType); + buffer.writeByte(mediaType); + buffer.writeByte(streamType); + buffer.writeByte(storageType); + + return buffer; + } + + + public void setChannelId(int channelId) { + this.channelId = channelId; + } + + public void setStartTime(String startTime) { + this.startTime = startTime; + } + + public void setEndTime(String endTime) { + this.endTime = endTime; + } + + public void setMediaType(int mediaType) { + this.mediaType = mediaType; + } + + public void setStreamType(int streamType) { + this.streamType = streamType; + } + + public void setStorageType(int storageType) { + this.storageType = storageType; + } + + public int getWarnType() { + return warnType; + } + + @Override + public String toString() { + return "J9205{" + + "channelId=" + channelId + + ", startTime='" + startTime + '\'' + + ", endTime='" + endTime + '\'' + + ", warnType=" + warnType + + ", mediaType=" + mediaType + + ", streamType=" + streamType + + ", storageType=" + storageType + + '}'; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J9206.java b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J9206.java new file mode 100644 index 0000000..fefa084 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J9206.java @@ -0,0 +1,105 @@ +package com.genersoft.iot.vmp.jt1078.proc.response; + +import com.genersoft.iot.vmp.jt1078.annotation.MsgId; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufUtil; +import io.netty.buffer.Unpooled; +import lombok.Getter; +import lombok.Setter; + +import java.nio.charset.Charset; + +/** + * 文件上传指令 + * + */ +@Setter +@Getter +@MsgId(id = "9206") +public class J9206 extends Rs { + + // 服务器地址 + private String serverIp; + // 服务器端口 + private int port; + // 用户名 + private String username; + // 密码 + private String password; + // 文件上传路径 + private String path; + // 逻辑通道号 + private int channelId; + + // 开始时间YYMMDDHHMMSS,全0表示无起始时间 + private String startTime; + + // 结束时间YYMMDDHHMMSS,全0表示无终止时间 + private String endTime; + + // 报警标志 + private int alarmSign = 0; + + // 音视频资源类型:0.音视频 1.音频 2.视频 3.视频或音视频 + private int mediaType; + + // 码流类型:0.所有码流 1.主码流 2.子码流 + private int streamType = 0; + + // 存储器类型:0.所有存储器 1.主存储器 2.灾备存储器 + private int storageType = 0; + + // 任务执行条件, + // 1:仅WI-FI 下可下载, + // 2: 仅LAN 连接时可下载; + // 3: WI-FI + LAN 连接时可下载; + // 4: 仅3G/ 4G 连接时可下载 + // 5: WI-FI + 3G/ 4G 连接时可下载 + // 6: WI-FI + LAN 连接时可下载 + // 7: WI-FI + LAN + 3G/ 4G 连接时可下载 + private int taskConditions = 7; + + @Override + public ByteBuf encode() { + ByteBuf buffer = Unpooled.buffer(); + + buffer.writeByte(serverIp.getBytes(Charset.forName("GBK")).length); + buffer.writeCharSequence(serverIp, Charset.forName("GBK")); + buffer.writeShort(port); + buffer.writeByte(username.getBytes(Charset.forName("GBK")).length); + buffer.writeCharSequence(username, Charset.forName("GBK")); + buffer.writeByte(password.getBytes(Charset.forName("GBK")).length); + buffer.writeCharSequence(password, Charset.forName("GBK")); + buffer.writeByte(path.getBytes(Charset.forName("GBK")).length); + buffer.writeCharSequence(path, Charset.forName("GBK")); + buffer.writeByte(channelId); + buffer.writeBytes(ByteBufUtil.decodeHexDump(startTime)); + buffer.writeBytes(ByteBufUtil.decodeHexDump(endTime)); + buffer.writeLong(alarmSign); + buffer.writeByte(mediaType); + buffer.writeByte(streamType); + buffer.writeByte(storageType); + buffer.writeByte(taskConditions); + return buffer; + } + + + @Override + public String toString() { + return "J9206{" + + "serverIp='" + serverIp + '\'' + + ", port=" + port + + ", user='" + username + '\'' + + ", password='" + password + '\'' + + ", path='" + path + '\'' + + ", channelId=" + channelId + + ", startTime='" + startTime + '\'' + + ", endTime='" + endTime + '\'' + + ", warnType=" + alarmSign + + ", mediaType=" + mediaType + + ", streamType=" + streamType + + ", storageType=" + storageType + + ", taskConditions=" + taskConditions + + '}'; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J9207.java b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J9207.java new file mode 100644 index 0000000..d7a3c17 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J9207.java @@ -0,0 +1,42 @@ +package com.genersoft.iot.vmp.jt1078.proc.response; + +import com.genersoft.iot.vmp.jt1078.annotation.MsgId; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufUtil; +import io.netty.buffer.Unpooled; +import io.netty.util.CharsetUtil; +import lombok.Getter; +import lombok.Setter; + +/** + * 文件上传控制 + * + */ +@Setter +@Getter +@MsgId(id = "9207") +public class J9207 extends Rs { + + // 对应平台文件上传消息的流水号 + Integer respNo; + + // 控制: 0:暂停; 1:继续; 2:取消 + private int control; + + @Override + public ByteBuf encode() { + ByteBuf buffer = Unpooled.buffer(); + buffer.writeShort(respNo); + buffer.writeByte(control); + return buffer; + } + + + @Override + public String toString() { + return "J9207{" + + "respNo=" + respNo + + ", control=" + control + + '}'; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J9301.java b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J9301.java new file mode 100644 index 0000000..b7852b8 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J9301.java @@ -0,0 +1,44 @@ +package com.genersoft.iot.vmp.jt1078.proc.response; + +import com.genersoft.iot.vmp.jt1078.annotation.MsgId; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufUtil; +import io.netty.buffer.Unpooled; +import lombok.Getter; +import lombok.Setter; + +/** + * 云台控制指令-云台旋转 + * + */ +@Setter +@Getter +@MsgId(id = "9301") +public class J9301 extends Rs { + // 逻辑通道号 + private int channel; + + // 方向: 0:停止; 1:上; 2:下; 3:左; 4:右 + private int direction; + + // 速度:0 ~ 255 + private int speed; + + @Override + public ByteBuf encode() { + ByteBuf buffer = Unpooled.buffer(); + buffer.writeByte(channel); + buffer.writeByte(direction); + buffer.writeByte(speed); + return buffer; + } + + @Override + public String toString() { + return "J9301{" + + "channel=" + channel + + ", direction=" + direction + + ", speed=" + speed + + '}'; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J9302.java b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J9302.java new file mode 100644 index 0000000..457bbf7 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J9302.java @@ -0,0 +1,38 @@ +package com.genersoft.iot.vmp.jt1078.proc.response; + +import com.genersoft.iot.vmp.jt1078.annotation.MsgId; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import lombok.Getter; +import lombok.Setter; + +/** + * 云台控制指令-焦距控制 + * + */ +@Setter +@Getter +@MsgId(id = "9302") +public class J9302 extends Rs { + // 逻辑通道号 + private int channel; + + // 方向: 0:焦距调大; 1:焦距调小 + private int focalDirection; + + @Override + public ByteBuf encode() { + ByteBuf buffer = Unpooled.buffer(); + buffer.writeByte(channel); + buffer.writeByte(focalDirection); + return buffer; + } + + @Override + public String toString() { + return "J9302{" + + "channel=" + channel + + ", zoomDirection=" + focalDirection + + '}'; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J9303.java b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J9303.java new file mode 100644 index 0000000..662ea8c --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J9303.java @@ -0,0 +1,38 @@ +package com.genersoft.iot.vmp.jt1078.proc.response; + +import com.genersoft.iot.vmp.jt1078.annotation.MsgId; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import lombok.Getter; +import lombok.Setter; + +/** + * 云台控制指令-光圈控制 + * + */ +@Setter +@Getter +@MsgId(id = "9303") +public class J9303 extends Rs { + // 逻辑通道号 + private int channel; + + // 调整方式: 0:调大; 1:调小 + private int iris; + + @Override + public ByteBuf encode() { + ByteBuf buffer = Unpooled.buffer(); + buffer.writeByte(channel); + buffer.writeByte(iris); + return buffer; + } + + @Override + public String toString() { + return "J9303{" + + "channel=" + channel + + ", iris=" + iris + + '}'; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J9304.java b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J9304.java new file mode 100644 index 0000000..557a9b2 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J9304.java @@ -0,0 +1,38 @@ +package com.genersoft.iot.vmp.jt1078.proc.response; + +import com.genersoft.iot.vmp.jt1078.annotation.MsgId; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import lombok.Getter; +import lombok.Setter; + +/** + * 云台控制指令-云台雨刷控制 + * + */ +@Setter +@Getter +@MsgId(id = "9304") +public class J9304 extends Rs { + // 逻辑通道号 + private int channel; + + // 启停标识: 0:停止; 1:启动 + private int on; + + @Override + public ByteBuf encode() { + ByteBuf buffer = Unpooled.buffer(); + buffer.writeByte(channel); + buffer.writeByte(on); + return buffer; + } + + @Override + public String toString() { + return "J9304{" + + "channel=" + channel + + ", on=" + on + + '}'; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J9305.java b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J9305.java new file mode 100644 index 0000000..3b34920 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J9305.java @@ -0,0 +1,38 @@ +package com.genersoft.iot.vmp.jt1078.proc.response; + +import com.genersoft.iot.vmp.jt1078.annotation.MsgId; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import lombok.Getter; +import lombok.Setter; + +/** + * 云台控制指令-红外补光控制 + * + */ +@Setter +@Getter +@MsgId(id = "9305") +public class J9305 extends Rs { + // 逻辑通道号 + private int channel; + + // 启停标识: 0:停止; 1:启动 + private int on; + + @Override + public ByteBuf encode() { + ByteBuf buffer = Unpooled.buffer(); + buffer.writeByte(channel); + buffer.writeByte(on); + return buffer; + } + + @Override + public String toString() { + return "J9305{" + + "channel=" + channel + + ", on=" + on + + '}'; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J9306.java b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J9306.java new file mode 100644 index 0000000..ff60132 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/J9306.java @@ -0,0 +1,38 @@ +package com.genersoft.iot.vmp.jt1078.proc.response; + +import com.genersoft.iot.vmp.jt1078.annotation.MsgId; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import lombok.Getter; +import lombok.Setter; + +/** + * 云台控制指令-云台变倍控制 + * + */ +@Setter +@Getter +@MsgId(id = "9306") +public class J9306 extends Rs { + // 逻辑通道号 + private int channel; + + // 0:调大; 1:调小 + private int zoom; + + @Override + public ByteBuf encode() { + ByteBuf buffer = Unpooled.buffer(); + buffer.writeByte(channel); + buffer.writeByte(zoom); + return buffer; + } + + @Override + public String toString() { + return "J9306{" + + "channel=" + channel + + ", zoom=" + zoom + + '}'; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/Rs.java b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/Rs.java new file mode 100644 index 0000000..243cd94 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/proc/response/Rs.java @@ -0,0 +1,27 @@ +package com.genersoft.iot.vmp.jt1078.proc.response; + + +import com.genersoft.iot.vmp.jt1078.proc.Header; +import io.netty.buffer.ByteBuf; + + +/** + * @author QingtaiJiang + * @date 2021/8/30 18:54 + * @email qingtaij@163.com + */ + +public abstract class Rs { + private Header header; + + public abstract ByteBuf encode(); + + + public Header getHeader() { + return header; + } + + public void setHeader(Header header) { + this.header = header; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/service/Ijt1078PlayService.java b/src/main/java/com/genersoft/iot/vmp/jt1078/service/Ijt1078PlayService.java new file mode 100644 index 0000000..c0f8fd3 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/service/Ijt1078PlayService.java @@ -0,0 +1,43 @@ +package com.genersoft.iot.vmp.jt1078.service; + +import com.genersoft.iot.vmp.common.CommonCallback; +import com.genersoft.iot.vmp.common.StreamInfo; +import com.genersoft.iot.vmp.gb28181.bean.CommonGBChannel; +import com.genersoft.iot.vmp.jt1078.bean.*; +import com.genersoft.iot.vmp.jt1078.proc.request.J1205; +import com.genersoft.iot.vmp.service.bean.ErrorCallback; +import com.genersoft.iot.vmp.vmanager.bean.WVPResult; + +import java.util.List; + +public interface Ijt1078PlayService { + + JTMediaStreamType checkStreamFromJt(String stream); + + void play(String phoneNumber, Integer channelId, int type, CommonCallback> callback); + + void playback(String phoneNumber, Integer channelId, String startTime, String endTime, Integer type, + Integer rate, Integer playbackType, Integer playbackSpeed, CommonCallback> callback); + + void stopPlay(String phoneNumber, Integer channelId); + + void pausePlay(String phoneNumber, Integer channelId); + + void continueLivePlay(String phoneNumber, Integer channelId); + + List getRecordList(String phoneNumber, Integer channelId, String startTime, String endTime); + + void stopPlayback(String phoneNumber, Integer channelId); + + StreamInfo startTalk(String phoneNumber, Integer channelId); + + void stopTalk(String phoneNumber, Integer channelId); + + void playbackControl(String phoneNumber, Integer channelId, Integer command, Integer playbackSpeed, String time); + + void start(Integer channelId, Boolean record, ErrorCallback callback); + + void stop(Integer channelId); + + void playBack(Integer channelId, Long startTime, Long stopTime, ErrorCallback callback); +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/service/Ijt1078Service.java b/src/main/java/com/genersoft/iot/vmp/jt1078/service/Ijt1078Service.java new file mode 100644 index 0000000..b6bbba4 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/service/Ijt1078Service.java @@ -0,0 +1,130 @@ +package com.genersoft.iot.vmp.jt1078.service; + +import com.genersoft.iot.vmp.common.CommonCallback; +import com.genersoft.iot.vmp.jt1078.bean.*; +import com.genersoft.iot.vmp.vmanager.bean.WVPResult; +import com.github.pagehelper.PageInfo; +import jakarta.servlet.ServletOutputStream; + +import java.io.OutputStream; +import java.util.List; + +public interface Ijt1078Service { + + JTMediaStreamType checkStreamFromJt(String stream); + + JTDevice getDevice(String phoneNumber); + + JTChannel getChannel(Integer terminalDbId, Integer channelId); + + void updateDevice(JTDevice deviceInDb); + + PageInfo getDeviceList(int page, int count, String query, Boolean online); + + void addDevice(JTDevice device); + + void deleteDeviceByPhoneNumber(String phoneNumber); + + void updateDeviceStatus(boolean connected, String phoneNumber); + + + void ptzControl(String phoneNumber, Integer channelId, String command, int speed); + + void supplementaryLight(String phoneNumber, Integer channelId, String command); + + void wiper(String phoneNumber, Integer channelId, String command); + + JTDeviceConfig queryConfig(String phoneNumber, String[] params); + + void setConfig(String phoneNumber, JTDeviceConfig config); + + void connectionControl(String phoneNumber, JTDeviceConnectionControl control); + + void resetControl(String phoneNumber); + + void factoryResetControl(String phoneNumber); + + JTDeviceAttribute attribute(String phoneNumber); + + JTPositionBaseInfo queryPositionInfo(String phoneNumber); + + void tempPositionTrackingControl(String phoneNumber, Integer timeInterval, Long validityPeriod); + + void confirmationAlarmMessage(String phoneNumber, int alarmPackageNo, JTConfirmationAlarmMessageType alarmMessageType); + + int linkDetection(String phoneNumber); + + int textMessage(String phoneNumber,JTTextSign sign, int textType, String content); + + int telephoneCallback(String phoneNumber, Integer sign, String destPhoneNumber); + + int setPhoneBook(String phoneNumber, int type, List phoneBookContactList); + + JTPositionBaseInfo controlDoor(String phoneNumber, Boolean open); + + int setAreaForCircle(int attribute, String phoneNumber, List circleAreaList); + + int deleteAreaForCircle(String phoneNumber, List ids); + + List queryAreaForCircle(String phoneNumber, List ids); + + int setAreaForRectangle(int i, String phoneNumber, List rectangleAreas); + + int deleteAreaForRectangle(String phoneNumber, List ids); + + List queryAreaForRectangle(String phoneNumber, List ids); + + int setAreaForPolygon(String phoneNumber, JTPolygonArea polygonArea); + + int deleteAreaForPolygon(String phoneNumber, List ids); + + List queryAreaForPolygon(String phoneNumber, List ids); + + int setRoute(String phoneNumber, JTRoute route); + + int deleteRoute(String phoneNumber, List ids); + + List queryRoute(String phoneNumber, List ids); + + JTDriverInformation queryDriverInformation(String phoneNumber); + + List shooting(String phoneNumber, JTShootingCommand shootingCommand); + + List queryMediaData(String phoneNumber, JTQueryMediaDataCommand queryMediaDataCommand); + + void uploadMediaData(String phoneNumber, JTQueryMediaDataCommand queryMediaDataCommand); + + void record(String phoneNumber, int command, Integer time, Integer save, Integer samplingRate); + + void uploadMediaDataForSingle(String phoneNumber, Long mediaId, Integer delete); + + JTMediaAttribute queryMediaAttribute(String phoneNumber); + + void changeStreamType(String phoneNumber, Integer channelId, Integer streamType); + + void recordDownload(String phoneNumber, Integer channelId, String startTime, String endTime, Integer alarmSign, Integer mediaType, Integer streamType, Integer storageType, OutputStream outputStream, CommonCallback> fileCallback); + + PageInfo getChannelList(int page, int count, int deviceId, String query); + + void updateChannel(JTChannel channel); + + void addChannel(JTChannel channel); + + void deleteChannelById(Integer id); + + JTDevice getDeviceById(Integer deviceId); + + void updateDevicePosition(String phoneNumber, Double longitude, Double latitude); + + JTChannel getChannelByDbId(Integer id); + + String getRecordTempUrl(String phoneNumber, Integer channelId, String startTime, String endTime, Integer alarmSign, Integer mediaType, Integer streamType, Integer storageType); + + void recordDownload(String filePath, ServletOutputStream outputStream); + + byte[] snap(String phoneNumber, int channelId); + + void uploadOneMedia(String phoneNumber, Long mediaId, ServletOutputStream outputStream, boolean delete); + + +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/service/impl/SourcePTZServiceForJTImpl.java b/src/main/java/com/genersoft/iot/vmp/jt1078/service/impl/SourcePTZServiceForJTImpl.java new file mode 100644 index 0000000..f88a73d --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/service/impl/SourcePTZServiceForJTImpl.java @@ -0,0 +1,159 @@ +package com.genersoft.iot.vmp.jt1078.service.impl; + +import com.genersoft.iot.vmp.common.enums.ChannelDataType; +import com.genersoft.iot.vmp.conf.exception.ControllerException; +import com.genersoft.iot.vmp.gb28181.bean.*; +import com.genersoft.iot.vmp.gb28181.service.IPTZService; +import com.genersoft.iot.vmp.gb28181.service.ISourcePTZService; +import com.genersoft.iot.vmp.jt1078.bean.JTChannel; +import com.genersoft.iot.vmp.jt1078.bean.JTDevice; +import com.genersoft.iot.vmp.jt1078.cmd.JT1078Template; +import com.genersoft.iot.vmp.jt1078.proc.response.*; +import com.genersoft.iot.vmp.jt1078.service.Ijt1078Service; +import com.genersoft.iot.vmp.service.bean.ErrorCallback; +import com.genersoft.iot.vmp.vmanager.bean.ErrorCode; +import lombok.extern.slf4j.Slf4j; +import org.checkerframework.checker.units.qual.A; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.util.Assert; + +import java.util.List; + +@Slf4j +@Service(ChannelDataType.PTZ_SERVICE + ChannelDataType.JT_1078) +public class SourcePTZServiceForJTImpl implements ISourcePTZService { + + @Autowired + private Ijt1078Service service; + + @Autowired + private JT1078Template jt1078Template; + + @Override + public void ptz(CommonGBChannel channel, FrontEndControlCodeForPTZ frontEndControlCode, ErrorCallback callback) { + JTChannel jtChannel = service.getChannelByDbId(channel.getDataDeviceId()); + if (jtChannel == null) { + callback.run(ErrorCode.ERROR404.getCode(), "通道不存在", null); + return; + } + JTDevice jtDevice = service.getDeviceById(jtChannel.getTerminalDbId()); + if (jtDevice == null) { + callback.run(ErrorCode.ERROR404.getCode(), "设备不存在", null); + return; + } + + if (frontEndControlCode.getPan() == null && frontEndControlCode.getTilt() == null && frontEndControlCode.getZoom() == null) { + J9301 j9301 = new J9301(); + j9301.setChannel(jtChannel.getChannelId()); + j9301.setDirection(0); + j9301.setSpeed(0); + jt1078Template.ptzRotate(jtDevice.getPhoneNumber(), j9301, 6); + callback.run(ErrorCode.SUCCESS.getCode(), ErrorCode.SUCCESS.getMsg(), null); + return; + } + + if (frontEndControlCode.getPan() != null || frontEndControlCode.getTilt() != null) { + J9301 j9301 = new J9301(); + j9301.setChannel(jtChannel.getChannelId()); + if (frontEndControlCode.getTilt() != null) { + if (frontEndControlCode.getTilt() == 0) { + j9301.setDirection(1); + }else if (frontEndControlCode.getTilt() == 1) { + j9301.setDirection(2); + } + j9301.setSpeed((int)(frontEndControlCode.getTilt()/100D * 255)); + } + + if (frontEndControlCode.getPan() != null) { + if (frontEndControlCode.getPan() == 0) { + j9301.setDirection(3); + }else if (frontEndControlCode.getPan() == 1) { + j9301.setDirection(4); + } + j9301.setSpeed((int)(frontEndControlCode.getPanSpeed()/100D * 255)); + } + jt1078Template.ptzRotate(jtDevice.getPhoneNumber(), j9301, 6); + } + if (frontEndControlCode.getZoom() != null) { + J9306 j9306 = new J9306(); + j9306.setChannel(jtChannel.getChannelId()); + j9306.setZoom(1 - frontEndControlCode.getZoom()); + jt1078Template.ptzZoom(jtDevice.getPhoneNumber(), j9306, 6); + } + callback.run(ErrorCode.SUCCESS.getCode(), ErrorCode.SUCCESS.getMsg(), null); + } + + @Override + public void preset(CommonGBChannel channel, FrontEndControlCodeForPreset frontEndControlCode, ErrorCallback callback) { + callback.run(ErrorCode.ERROR486.getCode(), ErrorCode.ERROR486.getMsg(), null); + } + + @Override + public void fi(CommonGBChannel channel, FrontEndControlCodeForFI frontEndControlCode, ErrorCallback callback) { + JTChannel jtChannel = service.getChannelByDbId(channel.getDataDeviceId()); + if (jtChannel == null) { + callback.run(ErrorCode.ERROR404.getCode(), "通道不存在", null); + return; + } + JTDevice jtDevice = service.getDeviceById(jtChannel.getTerminalDbId()); + if (jtDevice == null) { + callback.run(ErrorCode.ERROR404.getCode(), "设备不存在", null); + return; + } + if (frontEndControlCode.getIris() != null) { + J9303 j9303 = new J9303(); + j9303.setChannel(jtChannel.getChannelId()); + j9303.setIris(1 - frontEndControlCode.getIris()); + jt1078Template.ptzIris(jtDevice.getPhoneNumber(), j9303, 6); + } + if (frontEndControlCode.getFocus() != null) { + J9302 j9302 = new J9302(); + j9302.setChannel(jtChannel.getChannelId()); + j9302.setFocalDirection(1 - frontEndControlCode.getFocus()); + jt1078Template.ptzFocal(jtDevice.getPhoneNumber(), j9302, 6); + } + } + + @Override + public void tour(CommonGBChannel channel, FrontEndControlCodeForTour frontEndControlCode, ErrorCallback callback) { + callback.run(ErrorCode.ERROR486.getCode(), ErrorCode.ERROR486.getMsg(), null); + } + + @Override + public void scan(CommonGBChannel channel, FrontEndControlCodeForScan frontEndControlCode, ErrorCallback callback) { + callback.run(ErrorCode.ERROR486.getCode(), ErrorCode.ERROR486.getMsg(), null); + } + + @Override + public void auxiliary(CommonGBChannel channel, FrontEndControlCodeForAuxiliary frontEndControlCode, ErrorCallback callback) { + callback.run(ErrorCode.ERROR486.getCode(), ErrorCode.ERROR486.getMsg(), null); + } + + @Override + public void wiper(CommonGBChannel channel, FrontEndControlCodeForWiper frontEndControlCode, ErrorCallback callback) { + JTChannel jtChannel = service.getChannelByDbId(channel.getDataDeviceId()); + if (jtChannel == null) { + callback.run(ErrorCode.ERROR404.getCode(), "通道不存在", null); + return; + } + JTDevice jtDevice = service.getDeviceById(jtChannel.getTerminalDbId()); + if (jtDevice == null) { + callback.run(ErrorCode.ERROR404.getCode(), "设备不存在", null); + return; + } + J9304 j9304 = new J9304(); + j9304.setChannel(jtChannel.getChannelId()); + if (frontEndControlCode.getCode() == 1) { + j9304.setOn(1); + }else if (frontEndControlCode.getCode() == 0){ + j9304.setOn(0); + } + jt1078Template.ptzWiper(jtDevice.getPhoneNumber(), j9304, 6); + } + + @Override + public void queryPreset(CommonGBChannel channel, ErrorCallback> callback) { + callback.run(ErrorCode.ERROR486.getCode(), ErrorCode.ERROR486.getMsg(), null); + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/service/impl/SourcePlayServiceForJTImpl.java b/src/main/java/com/genersoft/iot/vmp/jt1078/service/impl/SourcePlayServiceForJTImpl.java new file mode 100644 index 0000000..8432a88 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/service/impl/SourcePlayServiceForJTImpl.java @@ -0,0 +1,47 @@ +package com.genersoft.iot.vmp.jt1078.service.impl; + +import com.genersoft.iot.vmp.common.InviteSessionType; +import com.genersoft.iot.vmp.common.StreamInfo; +import com.genersoft.iot.vmp.common.enums.ChannelDataType; +import com.genersoft.iot.vmp.conf.exception.ControllerException; +import com.genersoft.iot.vmp.gb28181.bean.CommonGBChannel; +import com.genersoft.iot.vmp.gb28181.bean.Platform; +import com.genersoft.iot.vmp.gb28181.bean.PlayException; +import com.genersoft.iot.vmp.gb28181.service.IPlayService; +import com.genersoft.iot.vmp.gb28181.service.ISourcePlayService; +import com.genersoft.iot.vmp.jt1078.service.Ijt1078PlayService; +import com.genersoft.iot.vmp.service.bean.ErrorCallback; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import javax.sip.message.Response; + +@Slf4j +@Service(ChannelDataType.PLAY_SERVICE + ChannelDataType.JT_1078) +public class SourcePlayServiceForJTImpl implements ISourcePlayService { + + @Autowired + private Ijt1078PlayService playService; + + @Override + public void play(CommonGBChannel channel, Platform platform, Boolean record, ErrorCallback callback) { + // 部标设备通道 + try { + playService.start(channel.getDataDeviceId(), record, callback); + }catch (Exception e) { + log.info("[通用通道] 部标设备点播异常 {}", e.getMessage()); + callback.run(Response.BUSY_HERE, "busy here", null); + } + } + + @Override + public void stopPlay(CommonGBChannel channel) { + // 推流 + try { + playService.stop(channel.getDataDeviceId()); + }catch (Exception e) { + log.error("[停止点播失败] {}({})", channel.getGbName(), channel.getGbDeviceId(), e); + } + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/service/impl/SourcePlaybackServiceForJTImpl.java b/src/main/java/com/genersoft/iot/vmp/jt1078/service/impl/SourcePlaybackServiceForJTImpl.java new file mode 100644 index 0000000..13881ce --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/service/impl/SourcePlaybackServiceForJTImpl.java @@ -0,0 +1,131 @@ +package com.genersoft.iot.vmp.jt1078.service.impl; + +import com.genersoft.iot.vmp.common.StreamInfo; +import com.genersoft.iot.vmp.common.enums.ChannelDataType; +import com.genersoft.iot.vmp.gb28181.bean.CommonGBChannel; +import com.genersoft.iot.vmp.gb28181.bean.CommonRecordInfo; +import com.genersoft.iot.vmp.gb28181.service.ISourcePlaybackService; +import com.genersoft.iot.vmp.jt1078.bean.JTChannel; +import com.genersoft.iot.vmp.jt1078.bean.JTDevice; +import com.genersoft.iot.vmp.jt1078.cmd.JT1078Template; +import com.genersoft.iot.vmp.jt1078.dao.JTChannelMapper; +import com.genersoft.iot.vmp.jt1078.dao.JTTerminalMapper; +import com.genersoft.iot.vmp.jt1078.proc.request.J1205; +import com.genersoft.iot.vmp.jt1078.service.Ijt1078PlayService; +import com.genersoft.iot.vmp.service.bean.ErrorCallback; +import com.genersoft.iot.vmp.utils.DateUtil; +import com.genersoft.iot.vmp.vmanager.bean.ErrorCode; +import lombok.extern.slf4j.Slf4j; +import org.checkerframework.checker.units.qual.A; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.util.Assert; + +import java.util.ArrayList; +import java.util.List; + +@Slf4j +@Service(ChannelDataType.PLAYBACK_SERVICE + ChannelDataType.JT_1078) +public class SourcePlaybackServiceForJTImpl implements ISourcePlaybackService { + + @Autowired + private JTTerminalMapper jtDeviceMapper; + + @Autowired + private JTChannelMapper jtChannelMapper; + + @Autowired + private JT1078Template jt1078Template; + + @Autowired + private Ijt1078PlayService playService; + + + @Override + public void playback(CommonGBChannel channel, Long startTime, Long stopTime, ErrorCallback callback) { + JTChannel jtChannel = jtChannelMapper.selectChannelById(channel.getDataDeviceId()); + Assert.notNull(channel, "通道不存在"); + JTDevice device = jtDeviceMapper.getDeviceById(jtChannel.getDataDeviceId()); + Assert.notNull(device, "设备不存在"); + jt1078Template.checkTerminalStatus(device.getPhoneNumber()); + + playService.playback(device.getPhoneNumber(), jtChannel.getChannelId(), DateUtil.timestampTo_yyyy_MM_dd_HH_mm_ss(startTime), + DateUtil.timestampTo_yyyy_MM_dd_HH_mm_ss(stopTime), 0, 0, 0, 0, result -> { + callback.run(result.getCode(), result.getMsg(), result.getData()); + }); + } + + @Override + public void stopPlayback(CommonGBChannel channel, String stream) { + JTChannel jtChannel = jtChannelMapper.selectChannelById(channel.getDataDeviceId()); + Assert.notNull(channel, "通道不存在"); + JTDevice device = jtDeviceMapper.getDeviceById(jtChannel.getDataDeviceId()); + Assert.notNull(device, "设备不存在"); + jt1078Template.checkTerminalStatus(device.getPhoneNumber()); + + playService.stopPlayback(device.getPhoneNumber(), jtChannel.getChannelId()); + } + + @Override + public void playbackPause(CommonGBChannel channel, String stream) { + JTChannel jtChannel = jtChannelMapper.selectChannelById(channel.getDataDeviceId()); + Assert.notNull(channel, "通道不存在"); + JTDevice device = jtDeviceMapper.getDeviceById(jtChannel.getDataDeviceId()); + Assert.notNull(device, "设备不存在"); + jt1078Template.checkTerminalStatus(device.getPhoneNumber()); + + playService.playbackControl(device.getPhoneNumber(), jtChannel.getChannelId(), 1, 0, null); + } + + @Override + public void playbackResume(CommonGBChannel channel, String stream) { + JTChannel jtChannel = jtChannelMapper.selectChannelById(channel.getDataDeviceId()); + Assert.notNull(channel, "通道不存在"); + JTDevice device = jtDeviceMapper.getDeviceById(jtChannel.getDataDeviceId()); + Assert.notNull(device, "设备不存在"); + jt1078Template.checkTerminalStatus(device.getPhoneNumber()); + + playService.playbackControl(device.getPhoneNumber(), jtChannel.getChannelId(), 0, 0, null); + } + + @Override + public void playbackSeek(CommonGBChannel channel, String stream, long seekTime) { + // 因为seek是增量,比如15s处, 1078是具体的时间点,比如2025-10-10 23:21:12 这个一个绝对时间点。无法转换,故此处不做支持 + log.warn("[JT-通用通道] 回放seek, 尚不支持"); + } + + @Override + public void playbackSpeed(CommonGBChannel channel, String stream, Double speed) { + JTChannel jtChannel = jtChannelMapper.selectChannelById(channel.getDataDeviceId()); + Assert.notNull(channel, "通道不存在"); + JTDevice device = jtDeviceMapper.getDeviceById(jtChannel.getDataDeviceId()); + Assert.notNull(device, "设备不存在"); + jt1078Template.checkTerminalStatus(device.getPhoneNumber()); + + playService.playbackControl(device.getPhoneNumber(), jtChannel.getChannelId(), 0, (int)Math.floor(speed), null); + } + + @Override + public void queryRecord(CommonGBChannel channel, String startTime, String endTime, ErrorCallback> callback) { + JTChannel jtChannel = jtChannelMapper.selectChannelById(channel.getDataDeviceId()); + Assert.notNull(channel, "通道不存在"); + JTDevice device = jtDeviceMapper.getDeviceById(jtChannel.getDataDeviceId()); + Assert.notNull(device, "设备不存在"); + jt1078Template.checkTerminalStatus(device.getPhoneNumber()); + List recordList = playService.getRecordList(device.getPhoneNumber(), jtChannel.getChannelId(), startTime, endTime); + if (recordList.isEmpty()) { + callback.run(ErrorCode.ERROR100.getCode(), ErrorCode.ERROR100.getMsg(), null); + return; + } + + List recordInfoList = new ArrayList<>(); + for (J1205.JRecordItem jRecordItem : recordList) { + CommonRecordInfo commonRecordInfo = new CommonRecordInfo(); + commonRecordInfo.setStartTime(jRecordItem.getStartTime()); + commonRecordInfo.setEndTime(jRecordItem.getEndTime()); + commonRecordInfo.setFileSize(jRecordItem.getSize() + ""); + recordInfoList.add(commonRecordInfo); + } + callback.run(ErrorCode.SUCCESS.getCode(), ErrorCode.SUCCESS.getMsg(), recordInfoList); + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/service/impl/jt1078PlayServiceImpl.java b/src/main/java/com/genersoft/iot/vmp/jt1078/service/impl/jt1078PlayServiceImpl.java new file mode 100644 index 0000000..f909678 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/service/impl/jt1078PlayServiceImpl.java @@ -0,0 +1,712 @@ +package com.genersoft.iot.vmp.jt1078.service.impl; + +import com.genersoft.iot.vmp.common.CommonCallback; +import com.genersoft.iot.vmp.common.StreamInfo; +import com.genersoft.iot.vmp.common.VideoManagerConstants; +import com.genersoft.iot.vmp.conf.DynamicTask; +import com.genersoft.iot.vmp.conf.UserSetting; +import com.genersoft.iot.vmp.conf.exception.ControllerException; +import com.genersoft.iot.vmp.conf.ftpServer.FtpSetting; +import com.genersoft.iot.vmp.gb28181.bean.*; +import com.genersoft.iot.vmp.jt1078.bean.*; +import com.genersoft.iot.vmp.jt1078.cmd.JT1078Template; +import com.genersoft.iot.vmp.jt1078.event.FtpUploadEvent; +import com.genersoft.iot.vmp.jt1078.proc.request.J1205; +import com.genersoft.iot.vmp.jt1078.proc.response.*; +import com.genersoft.iot.vmp.jt1078.service.Ijt1078PlayService; +import com.genersoft.iot.vmp.jt1078.service.Ijt1078Service; +import com.genersoft.iot.vmp.media.bean.MediaInfo; +import com.genersoft.iot.vmp.media.bean.MediaServer; +import com.genersoft.iot.vmp.media.event.hook.Hook; +import com.genersoft.iot.vmp.media.event.hook.HookData; +import com.genersoft.iot.vmp.media.event.hook.HookSubscribe; +import com.genersoft.iot.vmp.media.event.hook.HookType; +import com.genersoft.iot.vmp.media.event.media.MediaArrivalEvent; +import com.genersoft.iot.vmp.media.event.media.MediaDepartureEvent; +import com.genersoft.iot.vmp.media.event.media.MediaNotFoundEvent; +import com.genersoft.iot.vmp.media.event.mediaServer.MediaSendRtpStoppedEvent; +import com.genersoft.iot.vmp.media.service.IMediaServerService; +import com.genersoft.iot.vmp.service.ISendRtpServerService; +import com.genersoft.iot.vmp.service.bean.ErrorCallback; +import com.genersoft.iot.vmp.service.bean.InviteErrorCode; +import com.genersoft.iot.vmp.service.bean.SSRCInfo; +import com.genersoft.iot.vmp.storager.IRedisCatchStorage; +import com.genersoft.iot.vmp.utils.DateUtil; +import com.genersoft.iot.vmp.utils.MediaServerUtils; +import com.genersoft.iot.vmp.vmanager.bean.ErrorCode; +import com.genersoft.iot.vmp.vmanager.bean.WVPResult; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.ObjectUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.event.EventListener; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Service; +import org.springframework.util.Assert; + +import javax.sip.message.Response; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +@Service +@Slf4j +public class jt1078PlayServiceImpl implements Ijt1078PlayService { + + public static final String talkApp = "jt_talk"; + + @Autowired + private ISendRtpServerService sendRtpServerService; + + @Autowired + private Ijt1078Service jt1078Service; + + @Autowired + private JT1078Template jt1078Template; + + @Autowired + private RedisTemplate redisTemplate; + + @Autowired + private HookSubscribe subscribe; + + @Autowired + private IMediaServerService mediaServerService; + + @Autowired + private DynamicTask dynamicTask; + + @Autowired + private UserSetting userSetting; + + /** + * 流到来的处理 + */ + @Async("taskExecutor") + @EventListener + public void onApplicationEvent(MediaArrivalEvent event) { + if (event.getApp().equals(talkApp) && event.getStream().endsWith("_talk")) { + // 收到对JT讲的流 + if (event.getStream().indexOf("_") <= 0) { + log.info("[JT-对讲流到来] 流格式有误,stream应该为jt_[phoneNumber]_[channelId]_talk"); + return; + } + String[] streamArray = event.getStream().split("_"); + if (streamArray.length != 4) { + log.info("[JT-对讲流到来] 流格式有误,stream应该为jt_[phoneNumber]_[channelId]_talk"); + return; + } + String phoneNumber = streamArray[1]; + String channelId = streamArray[2]; + JTDevice device = jt1078Service.getDevice(phoneNumber); + if (device == null) { + log.info("[JT-对讲流到来] 未找到设备{}", phoneNumber); + return; + } + sendTalk(device, Integer.valueOf(channelId), event.getMediaServer(), event.getApp(), event.getStream()); + + } + } + + /** + * 流离开的处理 + */ + @Async("taskExecutor") + @EventListener + public void onApplicationEvent(MediaDepartureEvent event) { + + } + + /** + * 流未找到的处理 + */ + @Async("taskExecutor") + @EventListener + public void onApplicationEvent(MediaNotFoundEvent event) { + if (!userSetting.getAutoApplyPlay()) { + return; + } + JTMediaStreamType jtMediaStreamType = checkStreamFromJt(event.getStream()); + if (jtMediaStreamType == null){ + return; + } + String[] streamParamArray = event.getStream().split("_"); + String phoneNumber = streamParamArray[1]; + int channelId = Integer.parseInt(streamParamArray[2]); + String params = event.getParams(); + Map paramMap = MediaServerUtils.urlParamToMap(params); + int type = 0; + try { + type = Integer.parseInt(paramMap.get("type")); + }catch (NumberFormatException ignored) {} + if (jtMediaStreamType.equals(JTMediaStreamType.PLAY)) { + play(phoneNumber, channelId, 0, null); + }else if (jtMediaStreamType.equals(JTMediaStreamType.PLAYBACK)) { + String startTimeParam = DateUtil.jt1078Toyyyy_MM_dd_HH_mm_ss(streamParamArray[3]); + String endTimeParam = DateUtil.jt1078Toyyyy_MM_dd_HH_mm_ss(streamParamArray[4]); + int rate = 0; + int playbackType = 0; + int playbackSpeed = 0; + try { + rate = Integer.parseInt(paramMap.get("rate")); + playbackType = Integer.parseInt(paramMap.get("playbackType")); + playbackSpeed = Integer.parseInt(paramMap.get("playbackSpeed")); + }catch (NumberFormatException ignored) {} + playback(phoneNumber, channelId, startTimeParam, endTimeParam, type, rate, playbackType, playbackSpeed, null); + } + } + + + /** + * 校验流是否是属于部标的 + */ + @Override + public JTMediaStreamType checkStreamFromJt(String stream) { + if (!stream.startsWith("jt_")) { + return null; + } + String[] streamParamArray = stream.split("_"); + if (streamParamArray.length == 3) { + return JTMediaStreamType.PLAY; + }else if (streamParamArray.length == 5) { + return JTMediaStreamType.PLAYBACK; + }else if (streamParamArray.length == 4) { + return JTMediaStreamType.TALK; + }else { + return null; + } + } + + private final Map>>> inviteErrorCallbackMap = new ConcurrentHashMap<>(); + + @Override + public void play(String phoneNumber, Integer channelId, int type, CommonCallback> callback) { + JTDevice device = jt1078Service.getDevice(phoneNumber); + if (device == null) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "设备不存在"); + } + jt1078Template.checkTerminalStatus(phoneNumber); + JTChannel channel = jt1078Service.getChannel(device.getId(), channelId); + if (channel == null) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "通道不存在"); + } + play(device, channel, type, callback); + } + + private void play(JTDevice device, JTChannel channel, int type, CommonCallback> callback) { + String phoneNumber = device.getPhoneNumber(); + int channelId = channel.getChannelId(); + String app = "1078"; + String stream = phoneNumber + "_" + channelId; + // 检查流是否已经存在,存在则返回 + String playKey = VideoManagerConstants.INVITE_INFO_1078_PLAY + phoneNumber + ":" + channelId; + List>> errorCallbacks = inviteErrorCallbackMap.computeIfAbsent(playKey, k -> new ArrayList<>()); + errorCallbacks.add(callback); + StreamInfo streamInfo = (StreamInfo) redisTemplate.opsForValue().get(playKey); + if (streamInfo != null) { + MediaServer mediaServer = streamInfo.getMediaServer(); + if (mediaServer != null) { + // 查询流是否存在,不存在则删除缓存数据 + MediaInfo mediaInfo = mediaServerService.getMediaInfo(mediaServer, app, streamInfo.getStream()); + if (mediaInfo != null) { + log.info("[JT-点播] 点播已经存在,直接返回, phoneNumber: {}, channelId: {}", phoneNumber, channelId); + for (CommonCallback> errorCallback : errorCallbacks) { + errorCallback.run(new WVPResult<>(InviteErrorCode.SUCCESS.getCode(), InviteErrorCode.SUCCESS.getMsg(), streamInfo)); + } + return; + } + } + // 清理数据 + redisTemplate.delete(playKey); + } + + MediaServer mediaServer; + if (org.springframework.util.ObjectUtils.isEmpty(device.getMediaServerId()) || "auto".equals(device.getMediaServerId())) { + mediaServer = mediaServerService.getMediaServerForMinimumLoad(null); + } else { + mediaServer = mediaServerService.getOne(device.getMediaServerId()); + } + if (mediaServer == null) { + for (CommonCallback> errorCallback : errorCallbacks) { + errorCallback.run(new WVPResult<>(InviteErrorCode.FAIL.getCode(), "未找到可用的媒体节点", streamInfo)); + } + return; + } + // 设置hook监听 + Hook hook = Hook.getInstance(HookType.on_media_arrival, app, stream, mediaServer.getId()); + subscribe.addSubscribe(hook, (hookData) -> { + dynamicTask.stop(playKey); + log.info("[JT-点播] 点播成功, 手机号: {}, 通道: {}", phoneNumber, channelId); + // TODO 发送9105 实时音视频传输状态通知, 通知丢包率 + StreamInfo info = onPublishHandler(mediaServer, hookData, phoneNumber, channelId); + + for (CommonCallback> errorCallback : errorCallbacks) { + if (errorCallback == null) { + continue; + } + errorCallback.run(new WVPResult<>(InviteErrorCode.SUCCESS.getCode(), InviteErrorCode.SUCCESS.getMsg(), info)); + } + subscribe.removeSubscribe(hook); + redisTemplate.opsForValue().set(playKey, info); + // 截图 + String path = "snap"; + String fileName = phoneNumber + "_" + channelId + ".jpg"; + // 请求截图 + log.info("[请求截图]: " + fileName); + mediaServerService.getSnap(mediaServer, app, stream, 15, 1, path, fileName); + }); + // 开启收流端口 + SSRCInfo ssrcInfo = mediaServerService.openJTTServer(mediaServer, stream, null, false, !channel.isHasAudio(), 1); + if (ssrcInfo == null) { + stopPlay(phoneNumber, channelId); + return; + } + + // 设置超时监听 + dynamicTask.startDelay(playKey, () -> { + log.info("[JT-点播] 超时, phoneNumber: {}, channelId: {}", phoneNumber, channelId); + for (CommonCallback> errorCallback : errorCallbacks) { + errorCallback.run(new WVPResult<>(InviteErrorCode.ERROR_FOR_STREAM_TIMEOUT.getCode(), + InviteErrorCode.ERROR_FOR_STREAM_TIMEOUT.getMsg(), null)); + } + mediaServerService.closeJTTServer(mediaServer, stream, null); + subscribe.removeSubscribe(hook); + stopPlay(phoneNumber, channelId); + }, userSetting.getPlayTimeout()); + + log.info("[JT-点播] phoneNumber: {}, channelId: {},IP: {}, 端口: {}", phoneNumber, channelId, mediaServer.getSdpIp(), ssrcInfo.getPort()); + J9101 j9101 = new J9101(); + j9101.setChannel(channelId); + j9101.setIp(mediaServer.getSdpIp()); + j9101.setRate(1); + j9101.setTcpPort(ssrcInfo.getPort()); + j9101.setUdpPort(ssrcInfo.getPort()); + j9101.setType(type); + jt1078Template.startLive(phoneNumber, j9101, 6); + } + + public StreamInfo onPublishHandler(MediaServer mediaServerItem, HookData hookData, String phoneNumber, Integer channelId) { + StreamInfo streamInfo = mediaServerService.getStreamInfoByAppAndStream(mediaServerItem, "1078", hookData.getStream(), hookData.getMediaInfo(), null); + streamInfo.setDeviceId(phoneNumber); + streamInfo.setChannelId(channelId); + return streamInfo; + } + + @Override + public void stopPlay(String phoneNumber, Integer channelId) { + String playKey = VideoManagerConstants.INVITE_INFO_1078_PLAY + phoneNumber + ":" + channelId; + dynamicTask.stop(playKey); + // 清理回调 + List>> generalCallbacks = inviteErrorCallbackMap.get(playKey); + if (generalCallbacks != null && !generalCallbacks.isEmpty()) { + for (CommonCallback> callback : generalCallbacks) { + callback.run(new WVPResult<>(InviteErrorCode.ERROR_FOR_FINISH.getCode(), InviteErrorCode.ERROR_FOR_FINISH.getMsg(), null)); + } + } + jt1078Template.checkTerminalStatus(phoneNumber); + StreamInfo streamInfo = (StreamInfo) redisTemplate.opsForValue().get(playKey); + // 发送停止命令 + J9102 j9102 = new J9102(); + j9102.setChannel(channelId); + j9102.setCommand(0); + j9102.setCloseType(0); + j9102.setStreamType(1); + jt1078Template.stopLive(phoneNumber, j9102, 6); + log.info("[JT-停止点播] phoneNumber: {}, channelId: {}", phoneNumber, channelId); + // 删除缓存数据 + if (streamInfo != null) { + // 关闭rtpServer + mediaServerService.closeJTTServer(streamInfo.getMediaServer(), streamInfo.getStream(), null); + redisTemplate.delete(playKey); + } + + } + + @Override + public void pausePlay(String phoneNumber, Integer channelId) { + String playKey = VideoManagerConstants.INVITE_INFO_1078_PLAY + phoneNumber + ":" + channelId; + dynamicTask.stop(playKey); + StreamInfo streamInfo = (StreamInfo) redisTemplate.opsForValue().get(playKey); + if (streamInfo == null) { + log.info("[JT-暂停点播] 未找到点播信息 phoneNumber: {}, channelId: {}", phoneNumber, channelId); + } + log.info("[JT-暂停点播] phoneNumber: {}, channelId: {}", phoneNumber, channelId); + // 发送暂停命令 + J9102 j9102 = new J9102(); + j9102.setChannel(channelId); + j9102.setCommand(2); + j9102.setCloseType(0); + j9102.setStreamType(1); + jt1078Template.stopLive(phoneNumber, j9102, 6); + } + + @Override + public void continueLivePlay(String phoneNumber, Integer channelId) { + String playKey = VideoManagerConstants.INVITE_INFO_1078_PLAY + phoneNumber + ":" + channelId; + dynamicTask.stop(playKey); + StreamInfo streamInfo = (StreamInfo) redisTemplate.opsForValue().get(playKey); + if (streamInfo == null) { + log.info("[JT-继续点播] 未找到点播信息 phoneNumber: {}, channelId: {}", phoneNumber, channelId); + } + log.info("[JT-继续点播] phoneNumber: {}, channelId: {}", phoneNumber, channelId); + // 发送暂停命令 + J9102 j9102 = new J9102(); + j9102.setChannel(channelId); + j9102.setCommand(2); + j9102.setCloseType(0); + j9102.setStreamType(1); + jt1078Template.stopLive(phoneNumber, j9102, 6); + } + + @Override + public List getRecordList(String phoneNumber, Integer channelId, String startTime, String endTime) { + log.info("[JT-查询录像列表] phoneNumber: {}, channelId: {}, startTime: {}, endTime: {}" + , phoneNumber, channelId, startTime, endTime); + // 发送请求录像列表命令 + J9205 j9205 = new J9205(); + j9205.setChannelId(channelId); + j9205.setStartTime(DateUtil.yyyy_MM_dd_HH_mm_ssTo1078(startTime)); + j9205.setEndTime(DateUtil.yyyy_MM_dd_HH_mm_ssTo1078(endTime)); + j9205.setMediaType(0); + j9205.setStreamType(0); + j9205.setStorageType(0); + List JRecordItemList = (List) jt1078Template.queryBackTime(phoneNumber, j9205, 20); + if (JRecordItemList == null || JRecordItemList.isEmpty()) { + return null; + } + log.info("[JT-查询录像列表] phoneNumber: {}, channelId: {}, startTime: {}, endTime: {}, 结果: {}条" + , phoneNumber, channelId, startTime, endTime, JRecordItemList.size()); + return JRecordItemList; + } + + + + @Override + public void playback(String phoneNumber, Integer channelId, String startTime, String endTime, Integer type, + Integer rate, Integer playbackType, Integer playbackSpeed, CommonCallback> callback) { + JTDevice device = jt1078Service.getDevice(phoneNumber); + if (device == null) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "设备不存在"); + } + jt1078Template.checkTerminalStatus(phoneNumber); + JTChannel channel = jt1078Service.getChannel(device.getId(), channelId); + if (channel == null) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "通道不存在"); + } + playback(device, channel, startTime, endTime, type, rate, playbackType, playbackSpeed, callback); + + } + + /** + * 回放 + * @param device 设备 + * @param channel 通道 + * @param startTime 开始时间 + * @param endTime 结束时间 + * @param type 音视频资源类型:0.音视频 1.音频 2.视频 3.视频或音视频 + * @param rate 码流类型:0.所有码流 1.主码流 2.子码流(如果此通道只传输音频,此字段置0) + * @param playbackType 回放方式:0.正常回放 1.快进回放 2.关键帧快退回放 3.关键帧播放 4.单帧上传 + * @param playbackSpeed 快进或快退倍数:0.无效 1.1倍 2.2倍 3.4倍 4.8倍 5.16倍 (回放控制为1和2时,此字段内容有效,否则置0) + * @param callback 结束回调 + */ + private void playback(JTDevice device, JTChannel channel, String startTime, String endTime, Integer type, + Integer rate, Integer playbackType, Integer playbackSpeed, CommonCallback> callback) { + + String phoneNumber = device.getPhoneNumber(); + Integer channelId = channel.getChannelId(); + log.info("[JT-回放] 回放,设备:{}, 通道: {}, 开始时间: {}, 结束时间: {}, 音视频类型: {}, 码流类型: {}, " + + "回放方式: {}, 快进或快退倍数: {}", phoneNumber, channelId, startTime, endTime, type, rate, playbackType, playbackSpeed); + // 检查流是否已经存在,存在则返回 + String playbackKey = VideoManagerConstants.INVITE_INFO_1078_PLAYBACK + phoneNumber + ":" + channelId; + List>> errorCallbacks = inviteErrorCallbackMap.computeIfAbsent(playbackKey, k -> new ArrayList<>()); + errorCallbacks.add(callback); + String logInfo = String.format("phoneNumber:%s, channelId:%s, startTime:%s, endTime:%s", phoneNumber, channelId, startTime, endTime); + StreamInfo streamInfo = (StreamInfo) redisTemplate.opsForValue().get(playbackKey); + if (streamInfo != null) { + + mediaServerService.closeJTTServer(streamInfo.getMediaServer(), streamInfo.getStream(), null); + // 清理数据 + redisTemplate.delete(playbackKey); + } + + String app = "1078"; + String stream = String.format("%s_%s_%s_%s", phoneNumber, channelId, + DateUtil.yyyy_MM_dd_HH_mm_ssToUrl(startTime), DateUtil.yyyy_MM_dd_HH_mm_ssToUrl(endTime)); + MediaServer mediaServer; + if (org.springframework.util.ObjectUtils.isEmpty(device.getMediaServerId()) || "auto".equals(device.getMediaServerId())) { + mediaServer = mediaServerService.getMediaServerForMinimumLoad(null); + } else { + mediaServer = mediaServerService.getOne(device.getMediaServerId()); + } + if (mediaServer == null) { + for (CommonCallback> errorCallback : errorCallbacks) { + errorCallback.run(new WVPResult<>(InviteErrorCode.FAIL.getCode(), "未找到可用的媒体节点", streamInfo)); + } + return; + } + // 设置hook监听 + Hook hookSubscribe = Hook.getInstance(HookType.on_media_arrival, app, stream, mediaServer.getId()); + subscribe.addSubscribe(hookSubscribe, (hookData) -> { + dynamicTask.stop(playbackKey); + log.info("[JT-回放] 回放成功, logInfo: {}", logInfo); + StreamInfo info = onPublishHandler(mediaServer, hookData, phoneNumber, channelId); + + for (CommonCallback> errorCallback : errorCallbacks) { + if (errorCallback == null) { + continue; + } + errorCallback.run(new WVPResult<>(InviteErrorCode.SUCCESS.getCode(), InviteErrorCode.SUCCESS.getMsg(), info)); + } + subscribe.removeSubscribe(hookSubscribe); + redisTemplate.opsForValue().set(playbackKey, info); + }); + // 设置超时监听 + dynamicTask.startDelay(playbackKey, () -> { + log.info("[JT-回放] 回放超时, logInfo: {}", logInfo); + for (CommonCallback> errorCallback : errorCallbacks) { + errorCallback.run(new WVPResult<>(InviteErrorCode.ERROR_FOR_SIGNALLING_TIMEOUT.getCode(), + InviteErrorCode.ERROR_FOR_SIGNALLING_TIMEOUT.getMsg(), null)); + } + mediaServerService.closeJTTServer(mediaServer, stream, null); + subscribe.removeSubscribe(hookSubscribe); + }, userSetting.getPlayTimeout()); + + // 开启收流端口 + SSRCInfo ssrcInfo = mediaServerService.openJTTServer(mediaServer, stream, null, false, !channel.isHasAudio(), 1); + log.info("[JT-回放] logInfo: {}, 端口: {}", logInfo, ssrcInfo.getPort()); + J9201 j9201 = new J9201(); + j9201.setChannel(channelId); + j9201.setIp(mediaServer.getSdpIp()); + if (rate != null) { + j9201.setRate(rate); + } + if (playbackType != null) { + j9201.setPlaybackType(playbackType); + } + if (playbackSpeed != null) { + j9201.setPlaybackSpeed(playbackSpeed); + } + + j9201.setTcpPort(ssrcInfo.getPort()); + j9201.setUdpPort(ssrcInfo.getPort()); + j9201.setType(type); + j9201.setStartTime(DateUtil.yyyy_MM_dd_HH_mm_ssTo1078(startTime)); + j9201.setEndTime(DateUtil.yyyy_MM_dd_HH_mm_ssTo1078(endTime)); + jt1078Template.startBackLive(phoneNumber, j9201, 20); + + } + + @Override + public void playbackControl(String phoneNumber, Integer channelId, Integer command, Integer playbackSpeed, String time) { + String playKey = VideoManagerConstants.INVITE_INFO_1078_PLAYBACK + phoneNumber + ":" + channelId; + dynamicTask.stop(playKey); + if (command == 2) { + log.info("[JT-停止回放] phoneNumber: {}, channelId: {}, command: {}, playbackSpeed: {}, time: {}", + phoneNumber, channelId, command, playbackSpeed, time); + // 结束回放 + StreamInfo streamInfo = (StreamInfo) redisTemplate.opsForValue().get(playKey); + // 删除缓存数据 + if (streamInfo != null) { + // 关闭rtpServer + mediaServerService.closeJTTServer(streamInfo.getMediaServer(), streamInfo.getStream(), null); + } + // 清理回调 + List>> generalCallbacks = inviteErrorCallbackMap.get(playKey); + if (generalCallbacks != null && !generalCallbacks.isEmpty()) { + for (CommonCallback> callback : generalCallbacks) { + if (callback == null) { + continue; + } + callback.run(new WVPResult<>(InviteErrorCode.ERROR_FOR_FINISH.getCode(), InviteErrorCode.ERROR_FOR_FINISH.getMsg(), null)); + } + } + }else { + log.info("[JT-回放控制] phoneNumber: {}, channelId: {}, command: {}, playbackSpeed: {}, time: {}", + phoneNumber, channelId, command, playbackSpeed, time); + } + // 发送停止命令 + J9202 j9202 = new J9202(); + j9202.setChannel(channelId); + j9202.setPlaybackType(command); + + if (playbackSpeed != null) { + j9202.setPlaybackSpeed(playbackSpeed); + + } + if (!ObjectUtils.isEmpty(time)) { + j9202.setPlaybackTime(DateUtil.yyyy_MM_dd_HH_mm_ssTo1078(time)); + } + jt1078Template.controlBackLive(phoneNumber, j9202, 4); + } + + @Override + public void stopPlayback(String phoneNumber, Integer channelId) { + playbackControl(phoneNumber, channelId, 2, null, null); + } + + /** + * 监听发流停止 + */ + @EventListener + public void onApplicationEvent(MediaSendRtpStoppedEvent event) { + + List sendRtpInfos = sendRtpServerService.queryByStream(event.getStream()); + if (sendRtpInfos.isEmpty()) { + return; + } + for (SendRtpInfo sendRtpInfo : sendRtpInfos) { + if (!sendRtpInfo.isOnlyAudio() || ObjectUtils.isEmpty(sendRtpInfo.getChannelId())) { + continue; + } + if (!sendRtpInfo.getSsrc().contains("_")) { + continue; + } + sendRtpServerService.delete(sendRtpInfo); + String playKey = VideoManagerConstants.INVITE_INFO_1078_TALK + sendRtpInfo.getApp() + ":" + sendRtpInfo.getStream(); + redisTemplate.delete(playKey); + } + } + + + @Override + public StreamInfo startTalk(String phoneNumber, Integer channelId) { + // 检查流是否已经存在,存在则返回 + String playKey = VideoManagerConstants.INVITE_INFO_1078_TALK + phoneNumber + ":" + channelId; + StreamInfo streamInfo = (StreamInfo) redisTemplate.opsForValue().get(playKey); + + if (streamInfo != null) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "对讲进行中"); + } + + JTDevice device = jt1078Service.getDevice(phoneNumber); + Assert.notNull(device, "部标设备不存在"); + + String stream = "jt_" + phoneNumber + "_" + channelId + "_talk"; + + MediaServer mediaServer; + if (org.springframework.util.ObjectUtils.isEmpty(device.getMediaServerId()) || "auto".equals(device.getMediaServerId())) { + mediaServer = mediaServerService.getMediaServerForMinimumLoad(null); + } else { + mediaServer = mediaServerService.getOne(device.getMediaServerId()); + } + + // 检查待发送的流是否存在, + MediaInfo mediaInfo = mediaServerService.getMediaInfo(mediaServer, talkApp, stream); + Assert.isNull(mediaInfo, "对讲已经存在"); + return mediaServerService.getStreamInfoByAppAndStream(mediaServer, talkApp, stream, null, null, null, false); + + } + private void sendTalk(JTDevice device, Integer channelId, MediaServer mediaServer, String app, String stream) { + // 检查待发送的流是否存在, + MediaInfo mediaInfo = mediaServerService.getMediaInfo(mediaServer, app, stream); + if (mediaInfo == null) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), app + "/" + stream + "流不存在"); + } + + String phoneNumber = device.getPhoneNumber(); + + // 开启收流端口, zlm发送1078的rtp流需要将ssrc字段设置为 imei_channel格式 + String ssrc = device.getPhoneNumber() + "_" + channelId; + SendRtpInfo sendRtpInfo = sendRtpServerService.createSendRtpInfo(mediaServer, null, null, ssrc, phoneNumber, talkApp, stream, channelId, true, false); + sendRtpInfo.setTcpActive(true); + sendRtpInfo.setUsePs(false); + sendRtpInfo.setOnlyAudio(true); + sendRtpInfo.setReceiveStream(stream + "_talk"); + + // 设置hook监听 + Hook hook = Hook.getInstance(HookType.on_media_arrival, "1078", sendRtpInfo.getReceiveStream(), mediaServer.getId()); + subscribe.addSubscribe(hook, (hookData) -> { + log.info("[JT-对讲] 对讲连接建立, phoneNumber: {}, channelId: {}", phoneNumber, channelId); + subscribe.removeSubscribe(hook); + // 存储发流信息 + sendRtpServerService.update(sendRtpInfo); + }); + Hook hookForDeparture = Hook.getInstance(HookType.on_media_departure, app, stream, mediaServer.getId()); + subscribe.addSubscribe(hookForDeparture, (hookData) -> { + log.info("[JT-对讲] 对讲时源流注销, app: {}. stream: {}, phoneNumber: {}, channelId: {}", app, stream, phoneNumber, channelId); + stopTalk(phoneNumber, channelId); + }); + + Integer localPort = mediaServerService.startSendRtpPassive(mediaServer, sendRtpInfo, userSetting.getPlayTimeout()); + + log.info("[JT-对讲] phoneNumber: {}, channelId: {}, 收发端口: {}, app: {}, stream: {}", + phoneNumber, channelId, localPort, app, stream); + J9101 j9101 = new J9101(); + j9101.setChannel(channelId); + j9101.setIp(mediaServer.getSdpIp()); + j9101.setRate(1); + j9101.setTcpPort(sendRtpInfo.getLocalPort()); + j9101.setUdpPort(sendRtpInfo.getLocalPort()); + j9101.setType(4); + jt1078Template.startLive(phoneNumber, j9101, 6); + + log.info("[JT-对讲] 对讲消息下发成功, phoneNumber: {}, channelId: {}", phoneNumber, channelId); + // 存储发流信息 +// sendRtpServerService.update(sendRtpInfo); + } + + @Override + public void stopTalk(String phoneNumber, Integer channelId) { + String playKey = VideoManagerConstants.INVITE_INFO_1078_TALK + phoneNumber + ":" + channelId; + dynamicTask.stop(playKey); + StreamInfo streamInfo = (StreamInfo) redisTemplate.opsForValue().get(playKey); + // 发送停止命令 + J9102 j9102 = new J9102(); + j9102.setChannel(channelId); + j9102.setCommand(4); + j9102.setCloseType(0); + j9102.setStreamType(1); + jt1078Template.stopLive(phoneNumber, j9102, 6); + log.info("[JT-停止对讲] phoneNumber: {}, channelId: {}", phoneNumber, channelId); + // 删除缓存数据 + if (streamInfo != null) { + redisTemplate.delete(playKey); + // 关闭rtpServer + mediaServerService.closeJTTServer(streamInfo.getMediaServer(), streamInfo.getStream(), null); + } + // 清理回调 + List>> generalCallbacks = inviteErrorCallbackMap.get(playKey); + if (generalCallbacks != null && !generalCallbacks.isEmpty()) { + for (CommonCallback> callback : generalCallbacks) { + callback.run(new WVPResult<>(InviteErrorCode.ERROR_FOR_FINISH.getCode(), InviteErrorCode.ERROR_FOR_FINISH.getMsg(), null)); + } + } + } + + @Override + public void start(Integer channelId, Boolean record, ErrorCallback callback) { + JTChannel channel = jt1078Service.getChannelByDbId(channelId); + Assert.notNull(channel, "通道不存在"); + JTDevice device = jt1078Service.getDeviceById(channel.getTerminalDbId()); + Assert.notNull(device, "设备不存在"); + jt1078Template.checkTerminalStatus(device.getPhoneNumber()); + play(device, channel, 0, + result -> callback.run(result.getCode(), result.getMsg(), result.getData())); + } + + @Override + public void stop(Integer channelId) { + JTChannel channel = jt1078Service.getChannelByDbId(channelId); + Assert.notNull(channel, "通道不存在"); + JTDevice device = jt1078Service.getDeviceById(channel.getTerminalDbId()); + Assert.notNull(device, "设备不存在"); + stopPlay(device.getPhoneNumber(), channel.getChannelId()); + } + + @Override + public void playBack(Integer channelId, Long startTime, Long stopTime, ErrorCallback callback) { + if (startTime == null || stopTime == null) { + throw new PlayException(Response.BAD_REQUEST, "bad request"); + } + JTChannel channel = jt1078Service.getChannelByDbId(channelId); + Assert.notNull(channel, "通道不存在"); + JTDevice device = jt1078Service.getDeviceById(channel.getTerminalDbId()); + Assert.notNull(device, "设备不存在"); + jt1078Template.checkTerminalStatus(device.getPhoneNumber()); + String startTimeStr = DateUtil.timestampTo_yyyy_MM_dd_HH_mm_ss(startTime); + String stopTimeStr = DateUtil.timestampTo_yyyy_MM_dd_HH_mm_ss(stopTime); + playback(device, channel, startTimeStr, stopTimeStr, 0, 1, 0, 0, + result -> callback.run(result.getCode(), result.getMsg(), result.getData())); + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/service/impl/jt1078ServiceImpl.java b/src/main/java/com/genersoft/iot/vmp/jt1078/service/impl/jt1078ServiceImpl.java new file mode 100644 index 0000000..6247af8 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/service/impl/jt1078ServiceImpl.java @@ -0,0 +1,922 @@ +package com.genersoft.iot.vmp.jt1078.service.impl; + +import com.genersoft.iot.vmp.common.CommonCallback; +import com.genersoft.iot.vmp.common.StreamInfo; +import com.genersoft.iot.vmp.common.VideoManagerConstants; +import com.genersoft.iot.vmp.conf.DynamicTask; +import com.genersoft.iot.vmp.conf.UserSetting; +import com.genersoft.iot.vmp.conf.exception.ControllerException; +import com.genersoft.iot.vmp.conf.ftpServer.FtpFileSystemFactory; +import com.genersoft.iot.vmp.conf.ftpServer.FtpSetting; +import com.genersoft.iot.vmp.conf.ftpServer.UserManager; +import com.genersoft.iot.vmp.gb28181.bean.CommonGBChannel; +import com.genersoft.iot.vmp.gb28181.service.IGbChannelService; +import com.genersoft.iot.vmp.jt1078.bean.*; +import com.genersoft.iot.vmp.jt1078.bean.common.ConfigAttribute; +import com.genersoft.iot.vmp.jt1078.cmd.JT1078Template; +import com.genersoft.iot.vmp.jt1078.dao.JTChannelMapper; +import com.genersoft.iot.vmp.jt1078.dao.JTTerminalMapper; +import com.genersoft.iot.vmp.jt1078.event.DeviceUpdateEvent; +import com.genersoft.iot.vmp.jt1078.event.JTPositionEvent; +import com.genersoft.iot.vmp.jt1078.proc.response.*; +import com.genersoft.iot.vmp.jt1078.service.Ijt1078Service; +import com.genersoft.iot.vmp.jt1078.session.FtpDownloadManager; +import com.genersoft.iot.vmp.jt1078.session.Session; +import com.genersoft.iot.vmp.jt1078.session.SessionManager; +import com.genersoft.iot.vmp.media.event.media.MediaArrivalEvent; +import com.genersoft.iot.vmp.media.event.media.MediaDepartureEvent; +import com.genersoft.iot.vmp.utils.DateUtil; +import com.genersoft.iot.vmp.vmanager.bean.ErrorCode; +import com.genersoft.iot.vmp.vmanager.bean.WVPResult; +import com.github.pagehelper.PageHelper; +import com.github.pagehelper.PageInfo; +import jakarta.annotation.PostConstruct; +import jakarta.servlet.ServletOutputStream; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.ObjectUtils; +import org.apache.ftpserver.usermanager.impl.BaseUser; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.event.EventListener; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.scheduling.annotation.Async; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.Assert; + +import java.io.IOException; +import java.io.OutputStream; +import java.lang.reflect.Field; +import java.net.InetSocketAddress; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +@Service +@Slf4j +public class jt1078ServiceImpl implements Ijt1078Service { + + @Autowired + private JTTerminalMapper jtDeviceMapper; + + @Autowired + private JTChannelMapper jtChannelMapper; + + @Autowired + private JT1078Template jt1078Template; + + @Autowired + private RedisTemplate redisTemplate; + + @Autowired + private IGbChannelService channelService; + + @Autowired + private DynamicTask dynamicTask; + + @Autowired + private UserSetting userSetting; + + @Autowired + private FtpSetting ftpSetting; + + @Autowired + private UserManager ftpUserManager; + + @Autowired + private FtpFileSystemFactory fileSystemFactory; + + @Autowired + private FtpDownloadManager downloadManager; + + // 服务启动后五分钟内没有尽量连接的设备设置为离线 + @PostConstruct + public void init(){ + // 检查session与在线终端是是否对应 不对应则设置终端离线 + List deviceList = jtDeviceMapper.getDeviceList(null, true); + if (deviceList.isEmpty()) { + return; + } + for (JTDevice device : deviceList) { + Session session = SessionManager.INSTANCE.get(device.getPhoneNumber()); + if (session == null) { + device.setStatus(false); + // 通道发送状态变化通知 + List jtChannels = jtChannelMapper.selectAll(device.getId(), null); + List channelList = new ArrayList<>(); + for (JTChannel jtChannel : jtChannels) { + if (jtChannel.getGbId() > 0) { + jtChannel.setGbStatus("OFF"); + channelList.add(jtChannel); + } + } + channelService.updateStatus(channelList); + updateDevice(device); + } + } + } + + /** + * 流到来的处理 + */ + @Async("taskExecutor") + @org.springframework.context.event.EventListener + public void onApplicationEvent(MediaArrivalEvent event) { + + } + + /** + * 流离开的处理 + */ + @Async("taskExecutor") + @EventListener + public void onApplicationEvent(MediaDepartureEvent event) { + + } + + /** + * 设备更新的通知 + */ + @Async("taskExecutor") + @EventListener + public void onApplicationEvent(DeviceUpdateEvent event) { + JTDevice device = event.getDevice(); + if (device == null || device.getPhoneNumber() == null) { + return; + } + JTDevice deviceInDb = getDevice(event.getDevice().getPhoneNumber()); + if (deviceInDb.isStatus() != device.isStatus()) { + // 通道发送状态变化通知 + List jtChannels = jtChannelMapper.selectAll(deviceInDb.getId(), null); + List channelList = new ArrayList<>(); + for (JTChannel jtChannel : jtChannels) { + if (jtChannel.getGbId() > 0) { + jtChannel.setGbStatus("OFF"); + channelList.add(jtChannel); + } + } + channelService.updateStatus(channelList); + } + updateDevice(event.getDevice()); + } + + /** + * 位置更新的通知 + */ + @Async("taskExecutor") + @EventListener + public void onApplicationEvent(JTPositionEvent event) { + if (event.getPhoneNumber() == null || event.getPositionInfo() == null + || event.getPositionInfo().getLongitude() == null || event.getPositionInfo().getLatitude() == null) { + return; + } + JTDevice device = getDevice(event.getPhoneNumber()); + if (device == null) { + return; + } + device.setLongitude(event.getPositionInfo().getLongitude()); + device.setLatitude(event.getPositionInfo().getLatitude()); + updateDevice(device); + + // 通道发送状态变化通知 + List jtChannels = jtChannelMapper.selectAll(device.getId(), null); + List channelList = new ArrayList<>(); + for (JTChannel jtChannel : jtChannels) { + if (jtChannel.getGbId() > 0) { + jtChannel.setGbLongitude(event.getPositionInfo().getLongitude()); + jtChannel.setGbLatitude(event.getPositionInfo().getLatitude()); + if (event.getPositionInfo().getAltitude() != null) { + jtChannel.setGpsAltitude((double) event.getPositionInfo().getAltitude()); + }else { + jtChannel.setGpsAltitude(0d); + } + if (event.getPositionInfo().getDirection() != null) { + jtChannel.setGpsDirection((double) event.getPositionInfo().getDirection()); + }else { + jtChannel.setGpsDirection(0d); + } + if (event.getPositionInfo().getTime() != null) { + jtChannel.setGpsTime(event.getPositionInfo().getTime()); + }else { + jtChannel.setGpsTime(DateUtil.getNow()); + } + channelList.add(jtChannel); + } + } + channelService.updateGPS(channelList); + } + + + /** + * 校验流是否是属于部标的 + */ + @Override + public JTMediaStreamType checkStreamFromJt(String stream) { + String[] streamParamArray = stream.split("_"); + if (streamParamArray.length == 2) { + return JTMediaStreamType.PLAY; + }else if (streamParamArray.length == 4) { + return JTMediaStreamType.PLAYBACK; + }else if (streamParamArray.length == 5) { + return JTMediaStreamType.TALK; + }else { + return null; + } + } + + @Override + public JTDevice getDevice(String phoneNumber) { + return jtDeviceMapper.getDevice(phoneNumber); + } + + @Override + public JTChannel getChannel(Integer terminalDbId, Integer channelId) { + return jtChannelMapper.selectChannelByChannelId(terminalDbId, channelId); + } + + @Override + public void updateDevice(JTDevice device) { + device.setUpdateTime(DateUtil.getNow()); + jtDeviceMapper.updateDevice(device); + } + + @Override + public PageInfo getDeviceList(int page, int count, String query, Boolean online) { + PageHelper.startPage(page, count); + if (query != null) { + query = query.replaceAll("/", "//") + .replaceAll("%", "/%") + .replaceAll("_", "/_"); + } + List all = jtDeviceMapper.getDeviceList(query, online); + return new PageInfo<>(all); + } + + @Override + public void addDevice(JTDevice device) { + JTDevice deviceInDb = jtDeviceMapper.getDevice(device.getPhoneNumber()); + if (deviceInDb != null) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "设备" + device.getPhoneNumber() + "已存在"); + } + device.setCreateTime(DateUtil.getNow()); + device.setUpdateTime(DateUtil.getNow()); + jtDeviceMapper.addDevice(device); + } + + @Override + public void deleteDeviceByPhoneNumber(String phoneNumber) { + jtDeviceMapper.deleteDeviceByPhoneNumber(phoneNumber); + } + + @Override + public void updateDeviceStatus(boolean connected, String phoneNumber) { + jtDeviceMapper.updateDeviceStatus(connected, phoneNumber); + } + + + @Override + public void recordDownload(String phoneNumber, Integer channelId, String startTime, String endTime, Integer alarmSign, + Integer mediaType, Integer streamType, Integer storageType, OutputStream outputStream, CommonCallback> fileCallback) { + String filePath = UUID.randomUUID().toString(); + fileSystemFactory.addOutputStream(filePath, outputStream); + dynamicTask.startDelay(filePath, ()->{ + fileSystemFactory.removeOutputStream(filePath); + }, 2*60*60*1000); + Session session = SessionManager.INSTANCE.get(phoneNumber); + Assert.notNull(session, "连接不存在"); + InetSocketAddress socketAddress = session.getLoadAddress(); + String hostName = socketAddress.getHostName(); + + BaseUser randomUser = ftpUserManager.getRandomUser(); + + log.info("[JT-录像] 下载,设备:{}, 通道: {}, 开始时间: {}, 结束时间: {} 上传IP: {} 等待上传文件路径: {} 用户名: {}, 密码: {} ", + phoneNumber, channelId, startTime, endTime, hostName, filePath, randomUser.getName(), randomUser.getPassword()); + // 发送停止命令 + J9206 j92026 = new J9206(); + j92026.setChannelId(channelId); + j92026.setStartTime(DateUtil.yyyy_MM_dd_HH_mm_ssTo1078(startTime)); + j92026.setEndTime(DateUtil.yyyy_MM_dd_HH_mm_ssTo1078(endTime)); + j92026.setServerIp(hostName); + j92026.setPort(ftpSetting.getPort()); + j92026.setUsername(randomUser.getName()); + j92026.setPassword(randomUser.getPassword()); + j92026.setPath(filePath); + + if (mediaType != null) { + j92026.setMediaType(mediaType); + } + if (streamType != null) { + j92026.setStreamType(streamType); + } + if (storageType != null) { + j92026.setStorageType(storageType); + } + if (alarmSign != null) { + j92026.setAlarmSign(alarmSign); + } + jt1078Template.fileUpload(phoneNumber, j92026, 7200); + } + + @Override + public void ptzControl(String phoneNumber, Integer channelId, String command, int speed) { + // 发送停止命令 + switch (command) { + case "left": + case "right": + case "up": + case "down": + case "stop": + J9301 j9301 = new J9301(); + j9301.setChannel(channelId); + switch (command) { + case "left": + j9301.setDirection(3); + j9301.setSpeed(speed); + break; + case "right": + j9301.setDirection(4); + j9301.setSpeed(speed); + break; + case "up": + j9301.setDirection(1); + j9301.setSpeed(speed); + break; + case "down": + j9301.setDirection(2); + j9301.setSpeed(speed); + break; + case "stop": + j9301.setDirection(0); + j9301.setSpeed(0); + break; + } + jt1078Template.ptzRotate(phoneNumber, j9301, 6); + break; + + case "zoomin": + case "zoomout": + J9306 j9306 = new J9306(); + j9306.setChannel(channelId); + if (command.equals("zoomin")) { + j9306.setZoom(0); + } else { + j9306.setZoom(1); + } + jt1078Template.ptzZoom(phoneNumber, j9306, 6); + break; + case "irisin": + case "irisout": + J9303 j9303 = new J9303(); + j9303.setChannel(channelId); + if (command.equals("irisin")) { + j9303.setIris(0); + } else { + j9303.setIris(1); + } + jt1078Template.ptzIris(phoneNumber, j9303, 6); + break; + case "focusnear": + case "focusfar": + J9302 j9302 = new J9302(); + j9302.setChannel(channelId); + if (command.equals("focusfar")) { + j9302.setFocalDirection(0); + } else { + j9302.setFocalDirection(1); + } + jt1078Template.ptzFocal(phoneNumber, j9302, 6); + break; + + } + } + + @Override + public void supplementaryLight(String phoneNumber, Integer channelId, String command) { + J9305 j9305 = new J9305(); + j9305.setChannel(channelId); + if (command.equalsIgnoreCase("on")) { + j9305.setOn(1); + } else { + j9305.setOn(0); + } + jt1078Template.ptzSupplementaryLight(phoneNumber, j9305, 6); + } + + @Override + public void wiper(String phoneNumber, Integer channelId, String command) { + J9304 j9304 = new J9304(); + j9304.setChannel(channelId); + if (command.equalsIgnoreCase("on")) { + j9304.setOn(1); + } else { + j9304.setOn(0); + } + jt1078Template.ptzWiper(phoneNumber, j9304, 6); + } + + @Override + public JTDeviceConfig queryConfig(String phoneNumber, String[] params) { + if (phoneNumber == null) { + return null; + } + if (params == null || params.length == 0) { + J8104 j8104 = new J8104(); + return (JTDeviceConfig) jt1078Template.getDeviceConfig(phoneNumber, j8104, 20); + } else { + long[] paramBytes = new long[params.length]; + for (int i = 0; i < params.length; i++) { + try { + Field field = JTDeviceConfig.class.getDeclaredField(params[i]); + if (field.isAnnotationPresent(ConfigAttribute.class)) { + ConfigAttribute configAttribute = field.getAnnotation(ConfigAttribute.class); + if (configAttribute == null) { + log.warn("[查询设备配置] 获取 ConfigAttribute 失败"); + continue; + } + paramBytes[i] = configAttribute.id(); + } + } catch (NoSuchFieldException e) { + throw new RuntimeException(e); + } + } + J8106 j8106 = new J8106(); + j8106.setParams(paramBytes); + return (JTDeviceConfig) jt1078Template.getDeviceSpecifyConfig(phoneNumber, j8106, 20); + } + } + + @Override + public void setConfig(String phoneNumber, JTDeviceConfig config) { + J8103 j8103 = new J8103(); + j8103.setConfig(config); + jt1078Template.setDeviceSpecifyConfig(phoneNumber, j8103, 6); + } + + @Override + public void connectionControl(String phoneNumber, JTDeviceConnectionControl control) { + J8105 j8105 = new J8105(); + j8105.setConnectionControl(control); + jt1078Template.deviceControl(phoneNumber, j8105, 6); + } + + @Override + public void resetControl(String phoneNumber) { + J8105 j8105 = new J8105(); + j8105.setReset(true); + jt1078Template.deviceControl(phoneNumber, j8105, 6); + } + + @Override + public void factoryResetControl(String phoneNumber) { + J8105 j8105 = new J8105(); + j8105.setFactoryReset(true); + jt1078Template.deviceControl(phoneNumber, j8105, 6); + } + + @Override + public JTDeviceAttribute attribute(String phoneNumber) { + J8107 j8107 = new J8107(); + return (JTDeviceAttribute) jt1078Template.deviceAttribute(phoneNumber, j8107, 20); + } + + @Override + public JTPositionBaseInfo queryPositionInfo(String phoneNumber) { + J8201 j8201 = new J8201(); + return (JTPositionBaseInfo) jt1078Template.queryPositionInfo(phoneNumber, j8201, 20); + } + + @Override + public void tempPositionTrackingControl(String phoneNumber, Integer timeInterval, Long validityPeriod) { + J8202 j8202 = new J8202(); + j8202.setTimeInterval(timeInterval); + j8202.setValidityPeriod(validityPeriod); + jt1078Template.tempPositionTrackingControl(phoneNumber, j8202, 20); + } + + @Override + public void confirmationAlarmMessage(String phoneNumber, int alarmPackageNo, JTConfirmationAlarmMessageType alarmMessageType) { + J8203 j8203 = new J8203(); + j8203.setAlarmMessageType(alarmMessageType); + j8203.setAlarmPackageNo(alarmPackageNo); + jt1078Template.confirmationAlarmMessage(phoneNumber, j8203, 6); + } + + @Override + public int linkDetection(String phoneNumber) { + J8204 j8204 = new J8204(); + Object result = jt1078Template.linkDetection(phoneNumber, j8204, 6); + if (result == null) { + return 1; + }else { + return (int) result; + } + } + + @Override + public int textMessage(String phoneNumber, JTTextSign sign, int textType, String content) { + J8300 j8300 = new J8300(); + j8300.setSign(sign); + j8300.setTextType(textType); + j8300.setContent(content); + return (int) jt1078Template.textMessage(phoneNumber, j8300, 6); + } + + @Override + public int telephoneCallback(String phoneNumber, Integer sign, String destPhoneNumber) { + J8400 j8400 = new J8400(); + j8400.setSign(sign); + j8400.setPhoneNumber(destPhoneNumber); + return (int) jt1078Template.telephoneCallback(phoneNumber, j8400, 6); + } + + @Override + public int setPhoneBook(String phoneNumber, int type, List phoneBookContactList) { + J8401 j8401 = new J8401(); + j8401.setType(type); + if (phoneBookContactList != null) { + j8401.setPhoneBookContactList(phoneBookContactList); + } + return (int) jt1078Template.setPhoneBook(phoneNumber, j8401, 6); + } + + @Override + public JTPositionBaseInfo controlDoor(String phoneNumber, Boolean open) { + J8500 j8500 = new J8500(); + JTVehicleControl jtVehicleControl = new JTVehicleControl(); + jtVehicleControl.setControlCarDoor(open ? 1 : 0); + j8500.setVehicleControl(jtVehicleControl); + return (JTPositionBaseInfo) jt1078Template.vehicleControl(phoneNumber, j8500, 20); + } + + @Override + public int setAreaForCircle(int attribute, String phoneNumber, List circleAreaList) { + J8600 j8600 = new J8600(); + j8600.setAttribute(attribute); + j8600.setCircleAreaList(circleAreaList); + return (int) jt1078Template.setAreaForCircle(phoneNumber, j8600, 20); + } + + @Override + public int deleteAreaForCircle(String phoneNumber, List ids) { + J8601 j8601 = new J8601(); + j8601.setIdList(ids); + return (int) jt1078Template.deleteAreaForCircle(phoneNumber, j8601, 20); + } + + @Override + public List queryAreaForCircle(String phoneNumber, List ids) { + J8608 j8608 = new J8608(); + j8608.setType(1); + j8608.setIdList(ids); + return (List) jt1078Template.queryAreaOrRoute(phoneNumber, j8608, 20); + } + + @Override + public int setAreaForRectangle(int attribute, String phoneNumber, List rectangleAreas) { + J8602 j8602 = new J8602(); + j8602.setAttribute(attribute); + j8602.setRectangleAreas(rectangleAreas); + return (int) jt1078Template.setAreaForRectangle(phoneNumber, j8602, 20); + } + + @Override + public int deleteAreaForRectangle(String phoneNumber, List ids) { + J8603 j8603 = new J8603(); + j8603.setIdList(ids); + return (int) jt1078Template.deleteAreaForRectangle(phoneNumber, j8603, 20); + } + + @Override + public List queryAreaForRectangle(String phoneNumber, List ids) { + J8608 j8608 = new J8608(); + j8608.setType(2); + j8608.setIdList(ids); + return (List) jt1078Template.queryAreaOrRoute(phoneNumber, j8608, 20); + } + + @Override + public int setAreaForPolygon(String phoneNumber, JTPolygonArea polygonArea) { + J8604 j8604 = new J8604(); + j8604.setPolygonArea(polygonArea); + return (int) jt1078Template.setAreaForPolygon(phoneNumber, j8604, 20); + } + + @Override + public int deleteAreaForPolygon(String phoneNumber, List ids) { + J8605 j8605 = new J8605(); + j8605.setIdList(ids); + return (int) jt1078Template.deleteAreaForPolygon(phoneNumber, j8605, 20); + } + + @Override + public List queryAreaForPolygon(String phoneNumber, List ids) { + J8608 j8608 = new J8608(); + j8608.setType(3); + j8608.setIdList(ids); + return (List) jt1078Template.queryAreaOrRoute(phoneNumber, j8608, 20); + } + + @Override + public int setRoute(String phoneNumber, JTRoute route) { + J8606 j8606 = new J8606(); + j8606.setRoute(route); + return (int) jt1078Template.setRoute(phoneNumber, j8606, 20); + } + + @Override + public int deleteRoute(String phoneNumber, List ids) { + J8607 j8607 = new J8607(); + j8607.setIdList(ids); + return (int) jt1078Template.deleteRoute(phoneNumber, j8607, 20); + } + + @Override + public List queryRoute(String phoneNumber, List ids) { + J8608 j8608 = new J8608(); + j8608.setType(4); + j8608.setIdList(ids); + return (List) jt1078Template.queryAreaOrRoute(phoneNumber, j8608, 20); + } + + @Override + public JTDriverInformation queryDriverInformation(String phoneNumber) { + J8702 j8702 = new J8702(); + return (JTDriverInformation) jt1078Template.queryDriverInformation(phoneNumber, j8702, 20); + } + + @Override + public List shooting(String phoneNumber, JTShootingCommand shootingCommand) { + J8801 j8801 = new J8801(); + j8801.setCommand(shootingCommand); + return (List) jt1078Template.shooting(phoneNumber, j8801, 300); + } + + @Override + public List queryMediaData(String phoneNumber, JTQueryMediaDataCommand queryMediaDataCommand) { + J8802 j8802 = new J8802(); + j8802.setCommand(queryMediaDataCommand); + return (List) jt1078Template.queryMediaData(phoneNumber, j8802, 300); + } + + @Override + public void uploadMediaData(String phoneNumber, JTQueryMediaDataCommand queryMediaDataCommand) { + J8803 j8803 = new J8803(); + j8803.setCommand(queryMediaDataCommand); + jt1078Template.uploadMediaData(phoneNumber, j8803, 10); + } + + @Override + public void record(String phoneNumber, int command, Integer time, Integer save, Integer samplingRate) { + J8804 j8804 = new J8804(); + j8804.setCommond(command); + j8804.setDuration(time); + j8804.setSave(save); + j8804.setSamplingRate(samplingRate); + jt1078Template.record(phoneNumber, j8804, 10); + } + + @Override + public void uploadMediaDataForSingle(String phoneNumber, Long mediaId, Integer delete) { + J8805 j8805 = new J8805(); + j8805.setMediaId(mediaId); + j8805.setDelete(delete); + jt1078Template.uploadMediaDataForSingle(phoneNumber, j8805, 10); + } + + @Override + public JTMediaAttribute queryMediaAttribute(String phoneNumber) { + J9003 j9003 = new J9003(); + return (JTMediaAttribute) jt1078Template.queryMediaAttribute(phoneNumber, j9003, 300); + } + + @Override + public void changeStreamType(String phoneNumber, Integer channelId, Integer streamType) { + String playKey = VideoManagerConstants.INVITE_INFO_1078_PLAY + phoneNumber + ":" + channelId; + dynamicTask.stop(playKey); + StreamInfo streamInfo = (StreamInfo) redisTemplate.opsForValue().get(playKey); + if (streamInfo == null) { + log.info("[JT-切换码流类型] 未找到点播信息 phoneNumber: {}, channelId: {}, streamType: {}", phoneNumber, channelId, streamType); + } + log.info("[JT-切换码流类型] phoneNumber: {}, channelId: {}, streamType: {}", phoneNumber, channelId, streamType); + // 发送暂停命令 + J9102 j9102 = new J9102(); + j9102.setChannel(channelId); + j9102.setCommand(1); + j9102.setCloseType(0); + j9102.setStreamType(streamType); + jt1078Template.stopLive(phoneNumber, j9102, 6); + } + + @Override + public PageInfo getChannelList(int page, int count, int deviceId, String query) { + + JTDevice device = getDeviceById(deviceId); + if (device == null) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "设备不存在"); + } + PageHelper.startPage(page, count); + List all = jtChannelMapper.selectAll(deviceId, query); + PageInfo jtChannelPageInfo = new PageInfo<>(all); + for (JTChannel jtChannel : jtChannelPageInfo.getList()) { + String playKey = VideoManagerConstants.INVITE_INFO_1078_PLAY + device.getPhoneNumber() + ":" + jtChannel.getChannelId(); + StreamInfo streamInfo = (StreamInfo) redisTemplate.opsForValue().get(playKey); + if (streamInfo != null) { + jtChannel.setStream(streamInfo.getStream()); + } + } + return new PageInfo<>(all); + } + + @Override + @Transactional + public void updateChannel(JTChannel channel) { + channel.setUpdateTime(DateUtil.getNow()); + jtChannelMapper.update(channel); + if (!ObjectUtils.isEmpty(channel.getGbDeviceId())) { + if (channel.getGbId() > 0) { + channelService.update(channel.buildCommonGBChannel()); + }else { + channelService.add(channel.buildCommonGBChannel()); + } + } + } + + @Override + @Transactional + public void addChannel(JTChannel channel) { + JTChannel channelInDb = jtChannelMapper.selectChannelByChannelId(channel.getTerminalDbId(), channel.getChannelId()); + if (channelInDb != null) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "通道已存在"); + } + channel.setCreateTime(DateUtil.getNow()); + channel.setUpdateTime(DateUtil.getNow()); + jtChannelMapper.add(channel); + if (!ObjectUtils.isEmpty(channel.getGbDeviceId())) { + channelService.add(channel.buildCommonGBChannel()); + } + } + + @Override + @Transactional + public void deleteChannelById(Integer id) { + JTChannel jtChannel = jtChannelMapper.selectChannelById(id); + if (jtChannel == null) { + return; + } + if (jtChannel.getGbId() > 0) { + channelService.delete(jtChannel.getGbId()); + } + jtChannelMapper.delete(id); + } + + @Override + public JTDevice getDeviceById(Integer deviceId) { + return jtDeviceMapper.getDeviceById(deviceId); + } + + @Override + public void updateDevicePosition(String phoneNumber, Double longitude, Double latitude) { + JTDevice device = new JTDevice(); + device.setPhoneNumber(phoneNumber); + device.setLongitude(longitude); + device.setLatitude(latitude); + device.setUpdateTime(DateUtil.getNow()); + String key = VideoManagerConstants.INVITE_INFO_1078_POSITION + userSetting.getServerId(); + redisTemplate.opsForList().leftPush(key, device); + } + + @Scheduled(fixedDelay = 1000) + public void positionTask(){ + String key = VideoManagerConstants.INVITE_INFO_1078_POSITION + userSetting.getServerId(); + int count = 1000; + List devices = new ArrayList<>(count); + Long size = redisTemplate.opsForList().size(key); + if (size == null || size == 0) { + return; + } + long readCount = Math.min(count, size); + for (long i = 0L; i < readCount; i++) { + devices.add((JTDevice)redisTemplate.opsForList().rightPop(key)); + } + jtDeviceMapper.batchUpdateDevicePosition(devices); + } + + @Override + public JTChannel getChannelByDbId(Integer id) { + return jtChannelMapper.selectChannelById(id); + } + + + + @Override + public String getRecordTempUrl(String phoneNumber, Integer channelId, String startTime, String endTime, Integer alarmSign, Integer mediaType, Integer streamType, Integer storageType) { + String filePath = UUID.randomUUID().toString(); + + Session session = SessionManager.INSTANCE.get(phoneNumber); + Assert.notNull(session, "连接不存在"); + InetSocketAddress socketAddress = session.getLoadAddress(); + String hostName = socketAddress.getHostName(); + + BaseUser randomUser = ftpUserManager.getRandomUser(); + + log.info("[JT-录像] 下载,设备:{}, 通道: {}, 开始时间: {}, 结束时间: {} 上传IP: {} 等待上传文件路径: {} 用户名: {}, 密码: {} ", + phoneNumber, channelId, startTime, endTime, hostName, filePath, randomUser.getName(), randomUser.getPassword()); + // 文件上传指令 + J9206 j9206 = new J9206(); + j9206.setChannelId(channelId); + j9206.setStartTime(DateUtil.yyyy_MM_dd_HH_mm_ssTo1078(startTime)); + j9206.setEndTime(DateUtil.yyyy_MM_dd_HH_mm_ssTo1078(endTime)); + j9206.setServerIp(hostName); + j9206.setPort(ftpSetting.getPort()); + j9206.setUsername(randomUser.getName()); + j9206.setPassword(randomUser.getPassword()); + j9206.setPath(filePath); + + if (mediaType != null) { + j9206.setMediaType(mediaType); + } + if (streamType != null) { + j9206.setStreamType(streamType); + } + if (storageType != null) { + j9206.setStorageType(storageType); + } + if (alarmSign != null) { + j9206.setAlarmSign(alarmSign); + } + downloadManager.addCatch(filePath, phoneNumber, j9206); + return filePath; + } + + + @Override + public void recordDownload(String filePath, ServletOutputStream outputStream) { + JTRecordDownloadCatch downloadCatch = downloadManager.getCatch(filePath); + Assert.notNull(downloadCatch, "地址不存在"); + fileSystemFactory.addOutputStream(filePath, outputStream); + jt1078Template.fileUpload(downloadCatch.getPhoneNumber(), downloadCatch.getJ9206(), 7200); + downloadManager.runDownload(filePath, 2 * 60 * 60); + fileSystemFactory.removeOutputStream(filePath); + + } + + + @Override + public byte[] snap(String phoneNumber, int channelId) { + J8801 j8801 = new J8801(); + + // 设置抓图默认参数 + JTShootingCommand shootingCommand = new JTShootingCommand(); + shootingCommand.setChanelId(channelId); + shootingCommand.setCommand(1); + shootingCommand.setTime(0); + shootingCommand.setSave(0); + shootingCommand.setResolvingPower(0xFF); + shootingCommand.setQuality(1); + shootingCommand.setBrightness(125); + shootingCommand.setContrastRatio(60); + shootingCommand.setSaturation(60); + shootingCommand.setChroma(125); + + j8801.setCommand(shootingCommand); + log.info("[JT-抓图] 设备编号: {}, 通道编号: {}", phoneNumber, channelId); + // 监听文件上传, 存在设备不回复抓图请求或者回复通用回复,导致缺少抓图编号,但是直接上传文件的,此处通过监听文件上传直接获取文件 + + @SuppressWarnings("unchecked") + List ids = (List) jt1078Template.shooting(phoneNumber, j8801, 300); + log.info("[JT-抓图] 抓图编号: {}, 设备编号: {}, 通道编号: {}", ids.get(0), phoneNumber, channelId); + + log.info("[JT-抓图] 请求上传图片,抓图编号: {}, 设备编号: {}, 通道编号: {}", ids.get(0), phoneNumber, channelId); + J8805 j8805 = new J8805(); + j8805.setMediaId(ids.get(0)); + j8805.setDelete(1); + JTMediaEventInfo mediaEventInfo = (JTMediaEventInfo)jt1078Template.uploadMediaDataForSingle(phoneNumber, j8805, 600); + if (mediaEventInfo == null) { + log.info("[]"); + throw new ControllerException(ErrorCode.ERROR100.getCode(), ErrorCode.ERROR100.getMsg()); + } + log.info("[JT-抓图] 图片上传完成,抓图编号: {}, 设备编号: {}, 通道编号: {}", ids.get(0), phoneNumber, channelId); + return mediaEventInfo.getMediaData(); + } + + @Override + public void uploadOneMedia(String phoneNumber, Long mediaId, ServletOutputStream outputStream, boolean delete) { + log.info("[JT-单条存储多媒体数据上传] 媒体编号: {}, 设备编号: {}", mediaId, phoneNumber); + J8805 j8805 = new J8805(); + j8805.setMediaId(mediaId); + j8805.setDelete(delete ? 1 : 0); + log.info("[JT-单条存储多媒体数据上传] 请求上传,媒体编号: {}, 设备编号: {}", mediaId, phoneNumber); + JTMediaEventInfo mediaEventInfo = (JTMediaEventInfo)jt1078Template.uploadMediaDataForSingle(phoneNumber, j8805, 600); + if (mediaEventInfo == null) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), ErrorCode.ERROR100.getMsg()); + } + log.info("[JT-单条存储多媒体数据上传] 图片上传完成,媒体编号: {}, 设备编号: {}", mediaId, phoneNumber); + try { + outputStream.write(mediaEventInfo.getMediaData()); + outputStream.flush(); + } catch (IOException e) { + log.info("[JT-单条存储多媒体数据上传] 数据写入异常,抓图编号: {}, 设备编号: {}", mediaId, phoneNumber, e); + throw new ControllerException(ErrorCode.ERROR100.getCode(), "数据写入异常"); + } + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/session/FtpDownloadManager.java b/src/main/java/com/genersoft/iot/vmp/jt1078/session/FtpDownloadManager.java new file mode 100644 index 0000000..01ce90b --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/session/FtpDownloadManager.java @@ -0,0 +1,105 @@ +package com.genersoft.iot.vmp.jt1078.session; + +import com.genersoft.iot.vmp.jt1078.bean.JTRecordDownloadCatch; +import com.genersoft.iot.vmp.jt1078.event.FtpUploadEvent; +import com.genersoft.iot.vmp.jt1078.proc.response.J9206; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.event.EventListener; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.DelayQueue; +import java.util.concurrent.SynchronousQueue; +import java.util.concurrent.TimeUnit; + +@Slf4j +@Component +public class FtpDownloadManager { + + private final Map downloadCatchMap = new ConcurrentHashMap<>(); + private final DelayQueue downloadCatchQueue = new DelayQueue<>(); + + private final Map> topicSubscribers = new ConcurrentHashMap<>(); + + // 下载过期检查 + @Scheduled(fixedDelay = 1, timeUnit = TimeUnit.SECONDS) + public void downloadCatchCheck(){ + while (!downloadCatchQueue.isEmpty()) { + try { + JTRecordDownloadCatch take = downloadCatchQueue.take(); + downloadCatchMap.remove(take.getPath()); + } catch (InterruptedException e) { + log.error("[下载过期] ", e); + } + } + } + + public void addCatch(String path, String phoneNumber, J9206 j9206) { + JTRecordDownloadCatch downloadCatch = new JTRecordDownloadCatch(); + downloadCatch.setPhoneNumber(phoneNumber); + downloadCatch.setPath(path); + downloadCatch.setJ9206(j9206); + + // 10分钟临时地址无法访问则删除 + downloadCatch.setDelayTime(System.currentTimeMillis() + 10 * 60 * 1000L); + + downloadCatchMap.put(path, downloadCatch); + downloadCatchQueue.add(downloadCatch); + } + + public JTRecordDownloadCatch getCatch(String path) { + return downloadCatchMap.get(path); + } + + @EventListener + public void onApplicationEvent(FtpUploadEvent event) { + if (topicSubscribers.isEmpty()) { + return; + } + topicSubscribers.keySet().forEach(key -> { + if (!event.getFileName().contains(key)) { + return; + } + SynchronousQueue synchronousQueue = topicSubscribers.get(key); + if (synchronousQueue != null) { + synchronousQueue.offer(null); + } + }); + } + + + public Object runDownload(String path, long timeOut) { + SynchronousQueue subscribe = subscribe(path); + if (subscribe == null) { + log.error("[JT-下载] 暂停进程失败"); + return null; + } + try { + return subscribe.poll(timeOut, TimeUnit.SECONDS); + } catch (InterruptedException e) { + log.warn("[JT-下载] 暂停进程超时", e); + } finally { + this.unsubscribe(path); + JTRecordDownloadCatch downloadCatch = getCatch(path); + if (downloadCatch != null) { + downloadCatchMap.remove(path); + downloadCatchQueue.remove(downloadCatch); + } + } + return null; + } + + private SynchronousQueue subscribe(String key) { + SynchronousQueue queue = null; + if (!topicSubscribers.containsKey(key)) + topicSubscribers.put(key, queue = new SynchronousQueue<>()); + return queue; + } + + private void unsubscribe(String key) { + topicSubscribers.remove(key); + } + +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/session/Session.java b/src/main/java/com/genersoft/iot/vmp/jt1078/session/Session.java new file mode 100644 index 0000000..9c3d13a --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/session/Session.java @@ -0,0 +1,111 @@ +package com.genersoft.iot.vmp.jt1078.session; + +import com.genersoft.iot.vmp.jt1078.proc.Header; +import io.netty.channel.Channel; +import io.netty.channel.ChannelFuture; +import io.netty.util.AttributeKey; +import lombok.Getter; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; + +import java.net.InetSocketAddress; +import java.net.SocketAddress; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * @author QingtaiJiang + * @date 2023/4/27 18:54 + * @email qingtaij@163.com + */ +@Slf4j +public class Session { + + public static final AttributeKey KEY = AttributeKey.newInstance(Session.class.getName()); + + // Netty的channel + protected final Channel channel; + + // 原子类的自增ID + private final AtomicInteger serialNo = new AtomicInteger(0); + + // 是否注册成功 + @Getter + private boolean registered = false; + + // 设备手机号 + @Getter + private String phoneNumber; + + // 设备手机号 + @Setter + @Getter + private String authenticationCode; + + // 创建时间 + @Getter + private final long creationTime; + + // 协议版本号 + @Getter + private Integer protocolVersion; + + @Getter + private Header header; + + protected Session(Channel channel) { + this.channel = channel; + this.creationTime = System.currentTimeMillis(); + } + + public void writeObject(Object message) { + log.info("<<<<<<<<<< cmd{},{}", this, message); + channel.writeAndFlush(message); + } + + /** + * 获得下一个流水号 + * + * @return 流水号 + */ + public int nextSerialNo() { + int current; + int next; + do { + current = serialNo.get(); + next = current > 0xffff ? 0 : current; + } while (!serialNo.compareAndSet(current, next + 1)); + return next; + } + + /** + * 注册session + * + * @param devId 设备ID + */ + public void register(String devId, Integer version, Header header) { + this.phoneNumber = devId; + this.registered = true; + this.protocolVersion = version; + this.header = header; + SessionManager.INSTANCE.put(devId, this); + } + + @Override + public String toString() { + return "[" + + "phoneNumber=" + phoneNumber + + ", reg=" + registered + + ", version=" + protocolVersion + + ",ip=" + channel.remoteAddress() + + ']'; + } + + public void unregister() { + channel.close(); + SessionManager.INSTANCE.remove(this.phoneNumber); + } + + public InetSocketAddress getLoadAddress() { + return (InetSocketAddress)channel.localAddress(); + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/session/SessionManager.java b/src/main/java/com/genersoft/iot/vmp/jt1078/session/SessionManager.java new file mode 100644 index 0000000..48979f8 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/session/SessionManager.java @@ -0,0 +1,151 @@ +package com.genersoft.iot.vmp.jt1078.session; + +import com.genersoft.iot.vmp.jt1078.proc.entity.Cmd; +import io.netty.channel.Channel; +import lombok.extern.slf4j.Slf4j; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.SynchronousQueue; +import java.util.concurrent.TimeUnit; + + +/** + * @author QingtaiJiang + * @date 2023/4/27 19:54 + * @email qingtaij@163.com + */ +@Slf4j +public enum SessionManager { + INSTANCE; + + // 用与消息的缓存 + private final Map> topicSubscribers = new ConcurrentHashMap<>(); + + // session的缓存 + private final Map sessionMap; + + SessionManager() { + this.sessionMap = new ConcurrentHashMap<>(); + } + + /** + * 创建新的Session + * + * @param channel netty通道 + * @return 创建的session对象 + */ + public Session newSession(Channel channel) { + return new Session(channel); + } + + + /** + * 获取指定设备的Session + * + * @param clientId 设备Id + * @return Session + */ + public Session get(Object clientId) { + return sessionMap.get(clientId); + } + + /** + * 放入新设备连接的session + * + * @param clientId 设备ID + * @param newSession session + */ + void put(Object clientId, Session newSession) { + sessionMap.put(clientId, newSession); + } + + + /** + * 发送同步消息,接收响应 + * 默认超时时间6秒 + */ + public Object request(Cmd cmd) { + // 默认6秒 + int timeOut = 6000; + return request(cmd, timeOut); + } + + public Object request(Cmd cmd, Integer timeOut) { + Session session = this.get(cmd.getPhoneNumber()); + if (session == null) { + log.error("DevId: {} not online!", cmd.getPhoneNumber()); + return null; + } + String requestKey = requestKey(cmd.getPhoneNumber(), cmd.getRespId(), cmd.getPackageNo()); + SynchronousQueue subscribe = subscribe(requestKey); + if (subscribe == null) { + log.error("DevId: {} key:{} send repaid", cmd.getPhoneNumber(), requestKey); + return null; + } + session.writeObject(cmd); + try { + return subscribe.poll(timeOut, TimeUnit.SECONDS); + } catch (InterruptedException e) { + log.warn("<<<<<<<<<< timeout" + session, e); + } finally { + this.unsubscribe(requestKey); + } + return null; + } + + public Boolean response(String devId, String respId, Long responseNo, Object data) { + String requestKey = requestKey(devId, respId, responseNo); + + boolean result = false; + if (responseNo == null) { + for (String key : topicSubscribers.keySet()) { + if (key.startsWith(requestKey)) { + SynchronousQueue queue = topicSubscribers.get(key); + if (queue != null) { + result = true; + try { + queue.offer(data, 2, TimeUnit.SECONDS); + } catch (InterruptedException e) { + log.error("{}", e.getMessage(), e); + } + } + } + } + }else { + SynchronousQueue queue = topicSubscribers.get(requestKey); + if (queue != null) { + result = true; + try { + queue.offer(data, 2, TimeUnit.SECONDS); + } catch (InterruptedException e) { + log.error("{}", e.getMessage(), e); + } + } + } + if (!result) { + log.warn("Not find response,key:{} data:{} ", requestKey, data); + } + return result; + } + + private void unsubscribe(String key) { + topicSubscribers.remove(key); + } + + private SynchronousQueue subscribe(String key) { + SynchronousQueue queue = null; + if (!topicSubscribers.containsKey(key)) + topicSubscribers.put(key, queue = new SynchronousQueue<>()); + return queue; + } + + private String requestKey(String devId, String respId, Long requestNo) { + return String.join("_", devId.replaceFirst("^0*", ""), respId, requestNo == null?"":requestNo.toString()); + + } + + public void remove(String devId) { + sessionMap.remove(devId); + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/util/BCDUtil.java b/src/main/java/com/genersoft/iot/vmp/jt1078/util/BCDUtil.java new file mode 100644 index 0000000..b76786e --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/util/BCDUtil.java @@ -0,0 +1,64 @@ +package com.genersoft.iot.vmp.jt1078.util; + +/** + * BCD码转换 + */ +public class BCDUtil { + + public static String transform(byte[] bytes) { + if (bytes.length == 0) { + return null; + } + // BCD + StringBuilder stringBuffer = new StringBuilder(bytes.length * 2); + for (int i = 0; i < bytes.length; i++) { + // 每次取出四位的值,一个byte是八位,第一取出高四位,第二次取出低四位, + // 这里也可以先 & 0xf0再右移4位,0xf0二进制为11110000,与运算后,可以得到高4位是值,低四位清零的结果 + stringBuffer.append((byte) ((bytes[i] >>> 4 & 0xf))); + stringBuffer.append((byte) (bytes[i] & 0x0f)); + } + return stringBuffer.toString(); + } + + /** + * 字符串转BCD码 + * 来自: https://www.cnblogs.com/ranandrun/p/BCD.html + * @param asc ASCII字符串 + * @return BCD + */ + public static byte[] strToBcd(String asc) { + int len = asc.length(); + int mod = len % 2; + if (mod != 0) { + asc = "0" + asc; + len = asc.length(); + } + byte abt[] = new byte[len]; + if (len >= 2) { + len >>= 1; + } + byte bbt[] = new byte[len]; + abt = asc.getBytes(); + int j, k; + for (int p = 0; p < asc.length() / 2; p++) { + if ((abt[2 * p] >= '0') && (abt[2 * p] <= '9')) { + j = abt[2 * p] - '0'; + } else if ((abt[2 * p] >= 'a') && (abt[2 * p] <= 'z')) { + j = abt[2 * p] - 'a' + 0x0a; + } else { + j = abt[2 * p] - 'A' + 0x0a; + } + if ((abt[2 * p + 1] >= '0') && (abt[2 * p + 1] <= '9')) { + k = abt[2 * p + 1] - '0'; + } else if ((abt[2 * p + 1] >= 'a') && (abt[2 * p + 1] <= 'z')) { + k = abt[2 * p + 1] - 'a' + 0x0a; + } else { + k = abt[2 * p + 1] - 'A' + 0x0a; + } + int a = (j << 4) + k; + byte b = (byte) a; + bbt[p] = b; + } + return bbt; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/util/Bin.java b/src/main/java/com/genersoft/iot/vmp/jt1078/util/Bin.java new file mode 100644 index 0000000..31f8b93 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/util/Bin.java @@ -0,0 +1,41 @@ +package com.genersoft.iot.vmp.jt1078.util; + +/** + * 32位整型的二进制读写 + */ +public class Bin { + + private static final int[] bits = new int[32]; + + static { + bits[0] = 1; + for (int i = 1; i < bits.length; i++) { + bits[i] = bits[i - 1] << 1; + } + } + + /** + * 读取n的第i位 + * + * @param n int32 + * @param i 取值范围0-31 + */ + public static boolean get(int n, int i) { + return (n & bits[i]) == bits[i]; + } + + /** + * 不足位数从左边加0 + */ + public static String strHexPaddingLeft(String data, int length) { + int dataLength = data.length(); + if (dataLength < length) { + StringBuilder dataBuilder = new StringBuilder(data); + for (int i = dataLength; i < length; i++) { + dataBuilder.insert(0, "0"); + } + data = dataBuilder.toString(); + } + return data; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/util/ClassUtil.java b/src/main/java/com/genersoft/iot/vmp/jt1078/util/ClassUtil.java new file mode 100644 index 0000000..def0c0e --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/jt1078/util/ClassUtil.java @@ -0,0 +1,116 @@ +package com.genersoft.iot.vmp.jt1078.util; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.core.io.Resource; +import org.springframework.core.io.support.PathMatchingResourcePatternResolver; +import org.springframework.core.io.support.ResourcePatternResolver; + +import java.lang.annotation.Annotation; +import java.util.LinkedList; +import java.util.List; + +@Slf4j +public class ClassUtil { + + public static ConfigurableApplicationContext context; + + public static T getBean(String beanName, Class clazz) { + return context.getBean(beanName, clazz); + } + + public static Object getBean(Class clazz) { + if (clazz != null) { + try { + return clazz.getDeclaredConstructor().newInstance(); + } catch (Exception ex) { + log.error("ClassUtil:找不到指定的类", ex); + } + } + return null; + } + + + public static Object getBean(String className) { + Class clazz = null; + try { + clazz = Class.forName(className); + } catch (Exception ex) { + log.error("ClassUtil:找不到指定的类"); + } + if (clazz != null) { + try { + //获取声明的构造器--》创建实例 + return clazz.getDeclaredConstructor().newInstance(); + } catch (Exception ex) { + log.error("ClassUtil:找不到指定的类", ex); + } + } + return null; + } + + + /** + * 获取包下所有带注解的class + * + * @param packageName 包名 + * @param annotationClass 注解类型 + * @return list + */ + public static List> getClassList(String packageName, Class annotationClass) { + List> classList = getClassList(packageName); + classList.removeIf(next -> !next.isAnnotationPresent(annotationClass)); + return classList; + } + + public static List> getClassList(String... packageName) { + List> classList = new LinkedList<>(); + for (String s : packageName) { + List> c = getClassList(s); + classList.addAll(c); + } + return classList; + } + + public static List> getClassList(String packageName) { + List> classList = new LinkedList<>(); + try { + ResourcePatternResolver resourcePatternResolver = new PathMatchingResourcePatternResolver(); + Resource[] resources = resourcePatternResolver.getResources(packageName.replace(".", "/") + "/**/*.class"); + for (Resource resource : resources) { + String url = resource.getURL().toString(); + + String[] split = url.split(packageName.replace(".", "/")); + String s = split[split.length - 1]; + String className = s.replace("/", "."); + className = className.substring(0, className.lastIndexOf(".")); + doAddClass(classList, packageName + className); + } + + } catch (Exception e) { + throw new RuntimeException(e); + } + return classList; + } + + private static void doAddClass(List> classList, String className) { + Class cls = loadClass(className, false); + classList.add(cls); + } + + public static Class loadClass(String className, boolean isInitialized) { + Class cls; + try { + cls = Class.forName(className, isInitialized, getClassLoader()); + } catch (ClassNotFoundException e) { + throw new RuntimeException(e); + } + return cls; + } + + + public static ClassLoader getClassLoader() { + return Thread.currentThread().getContextClassLoader(); + } + +} diff --git a/src/main/java/com/genersoft/iot/vmp/media/MediaServerConfig.java b/src/main/java/com/genersoft/iot/vmp/media/MediaServerConfig.java new file mode 100755 index 0000000..ba27fb3 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/media/MediaServerConfig.java @@ -0,0 +1,67 @@ +package com.genersoft.iot.vmp.media; + +import com.genersoft.iot.vmp.conf.MediaConfig; +import com.genersoft.iot.vmp.conf.UserSetting; +import com.genersoft.iot.vmp.media.bean.MediaServer; +import com.genersoft.iot.vmp.media.event.mediaServer.MediaServerChangeEvent; +import com.genersoft.iot.vmp.media.service.IMediaServerService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.CommandLineRunner; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.core.annotation.Order; +import org.springframework.stereotype.Component; + +import java.util.List; + +/** + * 启动是从配置文件加载节点信息,以及发送个节点状态管理去控制节点状态 + */ +@Slf4j +@Component +@Order(value=12) +public class MediaServerConfig implements CommandLineRunner { + + @Autowired + private ApplicationEventPublisher applicationEventPublisher; + + @Autowired + private IMediaServerService mediaServerService; + + @Autowired + private MediaConfig mediaConfig; + + @Autowired + private UserSetting userSetting; + + + @Override + public void run(String... strings) throws Exception { + // 清理所有在线节点的缓存信息 + mediaServerService.clearMediaServerForOnline(); + MediaServer defaultMediaServer = mediaServerService.getDefaultMediaServer(); + MediaServer mediaSerItemInConfig = mediaConfig.getMediaSerItem(); + mediaSerItemInConfig.setServerId(userSetting.getServerId()); + if (defaultMediaServer != null && mediaSerItemInConfig.getId().equals(defaultMediaServer.getId())) { + mediaServerService.update(mediaSerItemInConfig); + }else { + if (defaultMediaServer != null) { + mediaServerService.delete(defaultMediaServer); + } + MediaServer mediaServerItem = mediaServerService.getOneFromDatabase(mediaSerItemInConfig.getId()); + if (mediaServerItem == null) { + mediaServerService.add(mediaSerItemInConfig); + }else { + mediaServerService.update(mediaSerItemInConfig); + } + } + // 发送媒体节点变化事件 + mediaServerService.syncCatchFromDatabase(); + // 获取所有的zlm, 并开启主动连接 + List all = mediaServerService.getAllFromDatabase(); + log.info("[媒体节点] 加载节点列表, 共{}个节点", all.size()); + MediaServerChangeEvent event = new MediaServerChangeEvent(this); + event.setMediaServerItemList(all); + applicationEventPublisher.publishEvent(event); + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/media/abl/ABLHttpHookListener.java b/src/main/java/com/genersoft/iot/vmp/media/abl/ABLHttpHookListener.java new file mode 100644 index 0000000..17a6c56 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/media/abl/ABLHttpHookListener.java @@ -0,0 +1,387 @@ +package com.genersoft.iot.vmp.media.abl; + +import com.alibaba.fastjson2.JSONObject; +import com.genersoft.iot.vmp.conf.UserSetting; +import com.genersoft.iot.vmp.conf.exception.ControllerException; +import com.genersoft.iot.vmp.gb28181.service.IDeviceService; +import com.genersoft.iot.vmp.gb28181.service.IInviteStreamService; +import com.genersoft.iot.vmp.gb28181.service.IPlayService; +import com.genersoft.iot.vmp.gb28181.session.AudioBroadcastManager; +import com.genersoft.iot.vmp.gb28181.session.SSRCFactory; +import com.genersoft.iot.vmp.gb28181.transmit.cmd.ISIPCommanderForPlatform; +import com.genersoft.iot.vmp.media.abl.bean.hook.*; +import com.genersoft.iot.vmp.media.abl.event.HookAblServerKeepaliveEvent; +import com.genersoft.iot.vmp.media.abl.event.HookAblServerStartEvent; +import com.genersoft.iot.vmp.media.bean.MediaServer; +import com.genersoft.iot.vmp.media.bean.ResultForOnPublish; +import com.genersoft.iot.vmp.media.event.media.*; +import com.genersoft.iot.vmp.media.service.IMediaServerService; +import com.genersoft.iot.vmp.media.zlm.dto.hook.HookResult; +import com.genersoft.iot.vmp.media.zlm.dto.hook.HookResultForOnPublish; +import com.genersoft.iot.vmp.service.IMediaService; +import com.genersoft.iot.vmp.storager.IRedisCatchStorage; +import io.swagger.v3.oas.annotations.Hidden; +import jakarta.servlet.http.HttpServletRequest; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; +import org.springframework.util.ObjectUtils; +import org.springframework.web.bind.annotation.*; + +import java.util.HashMap; +import java.util.Map; + +/** + * ABL 的hook事件监听 + */ +@RestController +@RequestMapping("/index/hook/abl") +@Hidden +public class ABLHttpHookListener { + + private final static Logger logger = LoggerFactory.getLogger(ABLHttpHookListener.class); + + @Autowired + private ABLRESTfulUtils ablresTfulUtils; + + @Autowired + private ISIPCommanderForPlatform commanderFroPlatform; + + @Autowired + private AudioBroadcastManager audioBroadcastManager; + + @Autowired + private IPlayService playService; + + @Autowired + private IRedisCatchStorage redisCatchStorage; + + @Autowired + private IInviteStreamService inviteStreamService; + + @Autowired + private IDeviceService deviceService; + + @Autowired + private IMediaServerService mediaServerService; + + + @Autowired + private IMediaService mediaService; + + + @Autowired + private UserSetting userSetting; + + @Autowired + private SSRCFactory ssrcFactory; + + @Qualifier("taskExecutor") + @Autowired + private ThreadPoolTaskExecutor taskExecutor; + + @Autowired + private RedisTemplate redisTemplate; + + @Autowired + private ApplicationEventPublisher applicationEventPublisher; + + /** + * 服务器定时上报时间,上报间隔可配置,默认10s上报一次 + */ + @ResponseBody + @PostMapping(value = "/on_server_keepalive", produces = "application/json;charset=UTF-8") + public HookResult onServerKeepalive(@RequestBody OnServerKeepaliveABLHookParam param) { + try { + HookAblServerKeepaliveEvent event = new HookAblServerKeepaliveEvent(this); + MediaServer mediaServerItem = mediaServerService.getOne(param.getMediaServerId()); + if (mediaServerItem != null) { + event.setMediaServerItem(mediaServerItem); + applicationEventPublisher.publishEvent(event); + } + }catch (Exception e) { + logger.info("[ZLM-HOOK-心跳] 发送通知失败 ", e); + } + return HookResult.SUCCESS(); + } + + /** + * 播放器鉴权事件,rtsp/rtmp/http-flv/ws-flv/hls的播放都将触发此鉴权事件。 + */ + @ResponseBody + @PostMapping(value = "/on_play", produces = "application/json;charset=UTF-8") + public HookResult onPlay(@RequestBody OnPlayABLHookParam param) { + + MediaServer mediaServer = mediaServerService.getOne(param.getMediaServerId()); + if (mediaServer == null) { + return new HookResultForOnPublish(0, "success"); + } + + Map paramMap = urlParamToMap(param.getParams()); + // 对于播放流进行鉴权 + boolean authenticateResult = mediaService.authenticatePlay(param.getApp(), param.getStream(), paramMap.get("callId")); + if (!authenticateResult) { + logger.info("[ABL HOOK] 播放鉴权 失败:{}->{}", param.getMediaServerId(), param); + ablresTfulUtils.closeStreams(mediaServer, param.getApp(), param.getStream()); + + } + logger.info("[ABL HOOK] 播放鉴权成功:{}->{}", param.getMediaServerId(), param); + return HookResult.SUCCESS(); + } + + /** + * rtsp/rtmp/rtp推流鉴权事件。 + */ + @ResponseBody + @PostMapping(value = "/on_publish", produces = "application/json;charset=UTF-8") + public HookResult onPublish(@RequestBody OnPublishABLHookParam param) { + + + logger.info("[ABL HOOK] 推流鉴权:{}->{}/{}?{}", param.getMediaServerId(), param.getApp(), param.getStream(), param.getParams()); + // TODO 加快处理速度 + + MediaServer mediaServer = mediaServerService.getOne(param.getMediaServerId()); + if (mediaServer == null) { + return new HookResultForOnPublish(0, "success"); + } + + try { + ResultForOnPublish resultForOnPublish = mediaService.authenticatePublish(mediaServer, param.getApp(), param.getStream(), param.getParams()); + if (resultForOnPublish == null) { + logger.info("[ABL HOOK]推流鉴权 拒绝 响应:{}->{}", param.getMediaServerId(), param); + ablresTfulUtils.closeStreams(mediaServer, param.getApp(), param.getStream()); + } + }catch (ControllerException e) { + ablresTfulUtils.closeStreams(mediaServer, param.getApp(), param.getStream()); + } + return HookResult.SUCCESS(); + } + + /** + * 如果某一个码流进行MP4录像(enable_mp4=1),会触发录像进度通知事件 + */ + @ResponseBody + @PostMapping(value = "/on_record_progress", produces = "application/json;charset=UTF-8") + public HookResult onRecordProgress(@RequestBody OnRecordProgressABLHookParam param) { + + + logger.info("[ABL HOOK] 录像进度通知:{}->{}/{}->{}/{}", param.getMediaServerId(), param.getApp(), param.getStream(), param.getCurrentFileDuration(), param.getTotalVideoDuration()); + + try { + MediaServer mediaServerItem = mediaServerService.getOne(param.getMediaServerId()); + if (mediaServerItem != null) { + MediaRecordProcessEvent event = MediaRecordProcessEvent.getInstance(this, param, mediaServerItem); + event.setMediaServer(mediaServerItem); + applicationEventPublisher.publishEvent(event); + } + }catch (Exception e) { + logger.info("[ZLM-HOOK-录像进度通知] 发送通知失败 ", e); + } + return HookResult.SUCCESS(); + } + + /** + * 当代理拉流、国标接入等等 码流不到达时会发出 码流不到达的事件通知 + */ + @ResponseBody + @PostMapping(value = "/on_stream_not_arrive", produces = "application/json;charset=UTF-8") + public HookResult onStreamNotArrive(@RequestBody ABLHookParam param) { + + + logger.info("[ABL HOOK] 码流不到达通知:{}->{}/{}", param.getMediaServerId(), param.getApp(), param.getStream()); + try { + if ("rtp".equals(param.getApp())) { + return HookResult.SUCCESS(); + } + MediaRtpServerTimeoutEvent event = new MediaRtpServerTimeoutEvent(this); + MediaServer mediaServerItem = mediaServerService.getOne(param.getMediaServerId()); + if (mediaServerItem != null) { + event.setMediaServer(mediaServerItem); + event.setApp("rtp"); + applicationEventPublisher.publishEvent(event); + } + }catch (Exception e) { + logger.info("[ABL-HOOK-码流不到达通知] 发送通知失败 ", e); + } + + return HookResult.SUCCESS(); + } + + /** + * 如果某一个码流进行MP4录像(enable_mp4=1),当某个MP4文件被删除会触发该事件通知 + */ + @ResponseBody + @PostMapping(value = "/on_delete_record_mp4", produces = "application/json;charset=UTF-8") + public HookResult onDeleteRecordMp4(@RequestBody OnRecordMp4ABLHookParam param) { + + + logger.info("[ABL HOOK] MP4文件被删除通知:{}->{}/{}", param.getMediaServerId(), param.getApp(), param.getStream()); + + + return HookResult.SUCCESS(); + } + + + /** + * rtsp/rtmp流注册或注销时触发此事件;此事件对回复不敏感。 + */ + @ResponseBody + @PostMapping(value = "/on_stream_arrive", produces = "application/json;charset=UTF-8") + public HookResult onStreamArrive(@RequestBody OnStreamArriveABLHookParam param) { + + MediaServer mediaServer = mediaServerService.getOne(param.getMediaServerId()); + if (mediaServer == null) { + return HookResult.SUCCESS(); + } + + logger.info("[ABL HOOK] 码流到达, {}->{}/{}", param.getMediaServerId(), param.getApp(), param.getStream()); + MediaArrivalEvent mediaArrivalEvent = MediaArrivalEvent.getInstance(this, param, mediaServer); + applicationEventPublisher.publishEvent(mediaArrivalEvent); + return HookResult.SUCCESS(); + } + + /** + * 流无人观看时事件,用户可以通过此事件选择是否关闭无人看的流。 + */ + @ResponseBody + @PostMapping(value = "/on_stream_none_reader", produces = "application/json;charset=UTF-8") + public JSONObject onStreamNoneReader(@RequestBody ABLHookParam param) { + + logger.info("[ABL HOOK]流无人观看:{}->{}/{}", param.getMediaServerId(), + param.getApp(), param.getStream()); + JSONObject ret = new JSONObject(); + + boolean close = mediaService.closeStreamOnNoneReader(param.getMediaServerId(), param.getApp(), param.getStream(), null); + ret.put("code", close); + return ret; + } + + /** + * 当播放一个url,如果不存在时,会发出一个消息通知 + */ + @ResponseBody + @PostMapping(value = "/on_stream_not_found", produces = "application/json;charset=UTF-8") + public HookResult onStreamNotFound(@RequestBody ABLHookParam param) { + + logger.info("[ABL HOOK] 流未找到:{}->{}/{}", param.getMediaServerId(), param.getApp(), param.getStream()); + MediaServer mediaServer = mediaServerService.getOne(param.getMediaServerId()); + if (!userSetting.getAutoApplyPlay() || mediaServer == null) { + return HookResult.SUCCESS(); + } + MediaNotFoundEvent mediaNotFoundEvent = MediaNotFoundEvent.getInstance(this, param, mediaServer); + applicationEventPublisher.publishEvent(mediaNotFoundEvent); + return HookResult.SUCCESS(); + } + + /** + * ABLMediaServer启动时会发送上线通知 + */ + @ResponseBody + @PostMapping(value = "/on_server_started", produces = "application/json;charset=UTF-8") + public HookResult onServerStarted(HttpServletRequest request, @RequestBody OnServerStaredABLHookParam param) { + + logger.info("[ABL HOOK] 启动 " + param.getMediaServerId()); + try { + HookAblServerStartEvent event = new HookAblServerStartEvent(this); + MediaServer mediaServerItem = mediaServerService.getOne(param.getMediaServerId()); + if (mediaServerItem != null) { + event.setMediaServerItem(mediaServerItem); + applicationEventPublisher.publishEvent(event); + } + }catch (Exception e) { + logger.info("[ABL-HOOK-启动] 发送通知失败 ", e); + } + + return HookResult.SUCCESS(); + } + + /** + * TODO 发送rtp(startSendRtp)被动关闭时回调 + */ +// @ResponseBody +// @PostMapping(value = "/on_send_rtp_stopped", produces = "application/json;charset=UTF-8") +// public HookResult onSendRtpStopped(HttpServletRequest request, @RequestBody OnSendRtpStoppedHookParam param) { +// +// logger.info("[ZLM HOOK] rtp发送关闭:{}->{}/{}", param.getMediaServerId(), param.getApp(), param.getStream()); +// +// // 查找对应的上级推流,发送停止 +// if (!"rtp".equals(param.getApp())) { +// return HookResult.SUCCESS(); +// } +// try { +// MediaSendRtpStoppedEvent event = new MediaSendRtpStoppedEvent(this); +// MediaServer mediaServerItem = mediaServerService.getOne(param.getMediaServerId()); +// if (mediaServerItem != null) { +// event.setMediaServer(mediaServerItem); +// applicationEventPublisher.publishEvent(event); +// } +// }catch (Exception e) { +// logger.info("[ZLM-HOOK-rtp发送关闭] 发送通知失败 ", e); +// } +// +// return HookResult.SUCCESS(); +// } + + /** + * TODO 录像完成事件 + */ + @ResponseBody + @PostMapping(value = "/on_record_mp4", produces = "application/json;charset=UTF-8") + public HookResult onRecordMp4(HttpServletRequest request, @RequestBody OnRecordMp4ABLHookParam param) { + logger.info("[ABL HOOK] 录像完成事件:{}->{}", param.getMediaServerId(), param.getFileName()); + + try { + MediaServer mediaServerItem = mediaServerService.getOne(param.getMediaServerId()); + if (mediaServerItem != null) { + MediaRecordMp4Event event = MediaRecordMp4Event.getInstance(this, param, mediaServerItem); + event.setMediaServer(mediaServerItem); + applicationEventPublisher.publishEvent(event); + } + }catch (Exception e) { + logger.info("[ZLM-HOOK-rtpServer收流超时] 发送通知失败 ", e); + } + + return HookResult.SUCCESS(); + } + + /** + * 当某一路码流断开时会发送通知 + */ + @ResponseBody + @PostMapping(value = "/on_stream_disconnect", produces = "application/json;charset=UTF-8") + public HookResult onRecordMp4(HttpServletRequest request, @RequestBody ABLHookParam param) { + logger.info("[ABL HOOK] 码流断开事件, {}->{}/{}", param.getMediaServerId(), param.getApp(), param.getStream()); + + MediaServer mediaServer = mediaServerService.getOne(param.getMediaServerId()); + if (mediaServer == null) { + return HookResult.SUCCESS(); + } + + MediaDepartureEvent mediaDepartureEvent = MediaDepartureEvent.getInstance(this, param, mediaServer); + applicationEventPublisher.publishEvent(mediaDepartureEvent); + + return HookResult.SUCCESS(); + } + + private Map urlParamToMap(String params) { + HashMap map = new HashMap<>(); + if (ObjectUtils.isEmpty(params)) { + return map; + } + String[] paramsArray = params.split("&"); + if (paramsArray.length == 0) { + return map; + } + for (String param : paramsArray) { + String[] paramArray = param.split("="); + if (paramArray.length == 2) { + map.put(paramArray[0], paramArray[1]); + } + } + return map; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/media/abl/ABLMediaNodeServerService.java b/src/main/java/com/genersoft/iot/vmp/media/abl/ABLMediaNodeServerService.java new file mode 100644 index 0000000..ab24c23 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/media/abl/ABLMediaNodeServerService.java @@ -0,0 +1,539 @@ +package com.genersoft.iot.vmp.media.abl; + +import com.alibaba.fastjson2.JSONArray; +import com.genersoft.iot.vmp.common.CommonCallback; +import com.genersoft.iot.vmp.common.InviteInfo; +import com.genersoft.iot.vmp.common.InviteSessionType; +import com.genersoft.iot.vmp.common.StreamInfo; +import com.genersoft.iot.vmp.conf.SipConfig; +import com.genersoft.iot.vmp.conf.UserSetting; +import com.genersoft.iot.vmp.conf.exception.ControllerException; +import com.genersoft.iot.vmp.gb28181.bean.SendRtpInfo; +import com.genersoft.iot.vmp.gb28181.service.IInviteStreamService; +import com.genersoft.iot.vmp.media.abl.bean.ABLMedia; +import com.genersoft.iot.vmp.media.abl.bean.ABLResult; +import com.genersoft.iot.vmp.media.abl.bean.AblServerConfig; +import com.genersoft.iot.vmp.media.bean.MediaInfo; +import com.genersoft.iot.vmp.media.bean.MediaServer; +import com.genersoft.iot.vmp.media.bean.RecordInfo; +import com.genersoft.iot.vmp.media.event.media.MediaRecordMp4Event; +import com.genersoft.iot.vmp.media.service.IMediaNodeServerService; +import com.genersoft.iot.vmp.service.bean.CloudRecordItem; +import com.genersoft.iot.vmp.service.bean.DownloadFileInfo; +import com.genersoft.iot.vmp.service.bean.ErrorCallback; +import com.genersoft.iot.vmp.storager.dao.CloudRecordServiceMapper; +import com.genersoft.iot.vmp.streamProxy.bean.StreamProxy; +import com.genersoft.iot.vmp.utils.DateUtil; +import com.genersoft.iot.vmp.vmanager.bean.ErrorCode; +import com.genersoft.iot.vmp.vmanager.bean.WVPResult; +import lombok.extern.slf4j.Slf4j; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.event.EventListener; +import org.springframework.stereotype.Service; +import org.springframework.util.ObjectUtils; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@Slf4j +@Service("abl") +public class ABLMediaNodeServerService implements IMediaNodeServerService { + + private final static Logger logger = LoggerFactory.getLogger(ABLMediaNodeServerService.class); + + @Autowired + private ABLRESTfulUtils ablresTfulUtils; + + @Autowired + private SipConfig sipConfig; + + @Autowired + private UserSetting userSetting; + + @Autowired + private CloudRecordServiceMapper cloudRecordServiceMapper; + + @Autowired + private IInviteStreamService inviteStreamService; + + @Override + public boolean initStopSendRtp(MediaServer mediaInfo, String app, String stream, String ssrc) { + return false; + } + + @Override + public int createRTPServer(MediaServer mediaServer, String stream, long ssrc, Integer port, Boolean onlyAuto, Boolean disableAudio, Boolean reUsePort, Integer tcpMode) { + Boolean recordSip = userSetting.getRecordSip(); + return ablresTfulUtils.openRtpServer(mediaServer, "rtp", stream, 96, port, tcpMode, disableAudio?1:0, recordSip, false); + } + + @Override + public void closeRtpServer(MediaServer mediaServer, String streamId, CommonCallback callback) { + if (mediaServer == null) { + return; + } + ABLResult result = ablresTfulUtils.closeStreams(mediaServer, "rtp", streamId); + logger.info("关闭RTP Server " + result); + if (result.getCode() != 0) { + logger.error("[closeRtpServer] 失败: {}", result.getMemo()); + } + } + + @Override + public int createJTTServer(MediaServer mediaServer, String stream, Integer port, Boolean disableVideo, Boolean disableAudio, Integer tcpMode) { + Boolean recordSip = userSetting.getRecordSip(); + return ablresTfulUtils.openRtpServer(mediaServer, "1078", stream, 96, port, tcpMode, disableAudio?1:0, recordSip, true); + } + + @Override + public void closeJTTServer(MediaServer mediaServer, String streamId, CommonCallback callback) { + if (mediaServer == null) { + return; + } + ABLResult result = ablresTfulUtils.closeStreams(mediaServer, "1078", streamId); + logger.info("关闭JT-RTP Server " + result); + if (result.getCode() != 0) { + logger.error("[JT-closeRtpServer] 失败: {}", result.getMemo()); + } + } + + @Override + public void closeStreams(MediaServer mediaServer, String app, String streamId) { + ABLResult result = ablresTfulUtils.closeStreams(mediaServer, app, streamId); + if (result.getCode() != 0) { + logger.error("[closeStreams] 失败: {}", result.getMemo()); + } + } + + @Override + public Boolean updateRtpServerSSRC(MediaServer mediaServerItem, String streamId, String ssrc) { + return null; + } + + @Override + public boolean checkNodeId(MediaServer mediaServerItem) { + logger.warn("[abl-checkNodeId] 未实现"); + return false; + } + + @Override + public void online(MediaServer mediaServerItem) { + logger.warn("[abl-online] 未实现"); + } + + @Override + public MediaServer checkMediaServer(String ip, int port, String secret) { + MediaServer mediaServer = new MediaServer(); + mediaServer.setIp(ip); + mediaServer.setHttpPort(port); + mediaServer.setSecret(secret); + ABLResult result = ablresTfulUtils.getServerConfig(mediaServer); + JSONArray data = result.getParams(); + if (data != null && !data.isEmpty()) { + AblServerConfig config = AblServerConfig.getInstance(data); + config.setServerIp(ip); + config.setHttpServerPort(port); + return new MediaServer(config, sipConfig.getIp()); + } + return null; + } + + @Override + public boolean stopSendRtp(MediaServer mediaInfo, String app, String stream, String ssrc) { + // TODO 需要记录开始发流返回的KEY,暂不做实现 + logger.warn("[abl-stopSendRtp] 未实现"); +// ablresTfulUtils.stopSendRtp() + return false; + } + + @Override + public boolean deleteRecordDirectory(MediaServer mediaServerItem, String app, String stream, String date, String fileName) { + logger.warn("[abl-deleteRecordDirectory] 未实现"); + return false; + } + + @Override + public List getMediaList(MediaServer mediaServer, String app, String stream, String callId) { + ABLResult result = ablresTfulUtils.getMediaList(mediaServer, app, stream); + if (result.getCode() != 0) { + return null; + } + if (result.getMediaList() == null || result.getMediaList().isEmpty()) { + return new ArrayList<>(); + } + List streamInfoList = new ArrayList<>(); + for (int i = 0; i < result.getMediaList().size(); i++) { + ABLMedia ablMedia = result.getMediaList().get(i); + MediaInfo mediaInfo = MediaInfo.getInstance(ablMedia, mediaServer); + StreamInfo streamInfo = getStreamInfoByAppAndStream(mediaServer, app, stream, mediaInfo, null, callId, true); + if (streamInfo != null) { + streamInfoList.add(streamInfo); + } + } + return streamInfoList; + } + + @Override + public StreamInfo getStreamInfoByAppAndStream(MediaServer mediaServer, String app, String stream, MediaInfo mediaInfo, + String addr, String callId, boolean isPlay) { + StreamInfo streamInfoResult = new StreamInfo(); + streamInfoResult.setStream(stream); + streamInfoResult.setApp(app); + if (addr == null) { + addr = mediaServer.getStreamIp(); + } + + streamInfoResult.setIp(addr); + if (mediaInfo != null) { + streamInfoResult.setServerId(mediaInfo.getServerId()); + }else { + streamInfoResult.setServerId(userSetting.getServerId()); + } + + streamInfoResult.setMediaServer(mediaServer); + Map param = new HashMap<>(); + if (!ObjectUtils.isEmpty(callId)) { + param.put("callId", callId); + } + if (mediaInfo != null && !ObjectUtils.isEmpty(mediaInfo.getOriginTypeStr())) { + param.put("originTypeStr", mediaInfo.getOriginTypeStr()); + } + StringBuilder callIdParamBuilder = new StringBuilder(); + if (!param.isEmpty()) { + callIdParamBuilder.append("?"); + for (Map.Entry entry : param.entrySet()) { + callIdParamBuilder.append(entry.getKey()).append("=").append(entry.getValue()); + callIdParamBuilder.append("&"); + } + callIdParamBuilder.deleteCharAt(callIdParamBuilder.length() - 1); + } + + String callIdParam = callIdParamBuilder.toString(); + + streamInfoResult.setRtmp(addr, mediaServer.getRtmpPort(),mediaServer.getRtmpSSlPort(), app, stream, callIdParam); + streamInfoResult.setRtsp(addr, mediaServer.getRtspPort(),mediaServer.getRtspSSLPort(), app, stream, callIdParam); + + String flvFile = String.format("%s/%s.flv%s", app, stream, callIdParam); + if ((mediaServer.getFlvPort() & 1) == 1) { + // 奇数端口 默认ssl端口 + streamInfoResult.setFlv(addr, null, mediaServer.getFlvPort(), flvFile); + }else { + streamInfoResult.setFlv(addr, mediaServer.getFlvPort(),null, flvFile); + } + if ((mediaServer.getWsFlvPort() & 1) == 1) { + // 奇数端口 默认ssl端口 + streamInfoResult.setWsFlv(addr, null, mediaServer.getWsFlvPort(), flvFile); + }else { + streamInfoResult.setWsFlv(addr, mediaServer.getWsFlvPort(),null, flvFile); + } + String mp4File = String.format("%s/%s.mp4%s", app, stream, callIdParam); + if ((mediaServer.getMp4Port() & 1) == 1) { + // 奇数端口 默认ssl端口 + streamInfoResult.setFmp4(addr, null, mediaServer.getMp4Port(), mp4File); + }else { + streamInfoResult.setFmp4(addr, mediaServer.getMp4Port(), null, mp4File); + } + + streamInfoResult.setHls(addr, mediaServer.getHttpPort(), mediaServer.getHttpSSlPort(), app, stream, callIdParam); + streamInfoResult.setTs(addr, mediaServer.getHttpPort(), mediaServer.getHttpSSlPort(), app, stream, callIdParam); + streamInfoResult.setRtc(addr, mediaServer.getHttpPort(), mediaServer.getHttpSSlPort(), app, stream, callIdParam, isPlay); + + streamInfoResult.setMediaInfo(mediaInfo); + + if (!"broadcast".equalsIgnoreCase(app) && !ObjectUtils.isEmpty(mediaServer.getTranscodeSuffix()) && !"null".equalsIgnoreCase(mediaServer.getTranscodeSuffix())) { + String newStream = stream + "_" + mediaServer.getTranscodeSuffix(); + mediaServer.setTranscodeSuffix(null); + StreamInfo transcodeStreamInfo = getStreamInfoByAppAndStream(mediaServer, app, newStream, null, addr, callId, isPlay); + streamInfoResult.setTranscodeStream(transcodeStreamInfo); + } + return streamInfoResult; + } + + @Override + public Boolean connectRtpServer(MediaServer mediaServerItem, String address, int port, String stream) { + logger.warn("[abl-connectRtpServer] 未实现"); + return null; + } + + @Override + public void getSnap(MediaServer mediaServer, String app, String stream, int timeoutSec, int expireSec, String path, String fileName) { + ablresTfulUtils.getSnap(mediaServer, app, stream, timeoutSec, path, fileName); + } + + @Override + public MediaInfo getMediaInfo(MediaServer mediaServer, String app, String stream) { + ABLResult ablResult = ablresTfulUtils.getMediaList(mediaServer, app, stream); + if (ablResult.getCode() != 0) { + return null; + } + if (ablResult.getMediaList() == null || ablResult.getMediaList().isEmpty()) { + return null; + } + return MediaInfo.getInstance(ablResult.getMediaList().get(0), mediaServer); + } + + @Override + public Boolean pauseRtpCheck(MediaServer mediaServer, String streamKey) { + ABLResult ablResult = ablresTfulUtils.pauseRtpServer(mediaServer, streamKey); + return ablResult.getCode() == 0; + } + + @Override + public Boolean resumeRtpCheck(MediaServer mediaServer, String streamKey) { + ABLResult ablResult = ablresTfulUtils.resumeRtpServer(mediaServer, streamKey); + return ablResult.getCode() == 0; + } + + @Override + public String getFfmpegCmd(MediaServer mediaServer, String cmdKey) { + return ""; + } + + @Override + public Boolean delFFmpegSource(MediaServer mediaServer, String streamKey) { + ABLResult ablResult = ablresTfulUtils.delFFmpegProxy(mediaServer, streamKey); + return ablResult.getCode() == 0; + } + + @Override + public Boolean delStreamProxy(MediaServer mediaServer, String streamKey) { + ABLResult ablResult = ablresTfulUtils.delStreamProxy(mediaServer, streamKey); + return ablResult.getCode() == 0; + } + + @Override + public Map getFFmpegCMDs(MediaServer mediaServer) { + return new HashMap<>(); + } + + // 接受进度通知 +// @EventListener +// public void onApplicationEvent(MediaRecordProcessEvent event) { +// CloudRecordItem cloudRecordItem = cloudRecordServiceMapper.getListByFileName(event.getApp(), event.getStream(), event.getFileName()); +// if (cloudRecordItem == null) { +// cloudRecordItem = CloudRecordItem.getInstance(event); +// cloudRecordItem.setStartTime(event.getStartTime()); +// cloudRecordItem.setEndTime(event.getEndTime()); +// cloudRecordServiceMapper.add(cloudRecordItem); +// }else { +// cloudRecordServiceMapper.updateTimeLen(cloudRecordItem.getId(), (long)event.getCurrentFileDuration() * 1000, System.currentTimeMillis()); +// } +// } + @EventListener + public void onApplicationEvent(MediaRecordMp4Event event) { + InviteInfo inviteInfo = inviteStreamService.getInviteInfo(InviteSessionType.DOWNLOAD, null, event.getStream()); + if (inviteInfo == null || inviteInfo.getStreamInfo() == null) { + return; + } + List cloudRecordItemList = cloudRecordServiceMapper.getList(null, event.getApp(), event.getStream(), + null, null, null, null, null, null); + if (cloudRecordItemList.isEmpty()) { + return; + } + long startTime = cloudRecordItemList.get(cloudRecordItemList.size() - 1).getStartTime(); + long endTime = cloudRecordItemList.get(0).getEndTime(); + ABLResult ablResult = ablresTfulUtils.queryRecordList(event.getMediaServer(), event.getApp(), event.getStream(), DateUtil.timestampMsToUrlToyyyy_MM_dd_HH_mm_ss(startTime), + DateUtil.timestampMsToUrlToyyyy_MM_dd_HH_mm_ss(endTime)); + if (ablResult.getCode() != 0) { + return; + } + if (ablResult.getUrl() == null) { + return; + } + String download = ablResult.getUrl().getDownload(); + DownloadFileInfo downloadFileInfo = new DownloadFileInfo(); + downloadFileInfo.setHttpPath(download); + downloadFileInfo.setHttpsPath(download); + inviteInfo.getStreamInfo().setDownLoadFilePath(downloadFileInfo); + inviteStreamService.updateInviteInfo(inviteInfo); + } + + @Override + public Long updateDownloadProcess(MediaServer mediaServer, String app, String stream) { + List list = cloudRecordServiceMapper.getList(null, app, stream, null, + null, null, null, null, null); + if (list.isEmpty()) { + return null; + } + long downloadProcess = 0L; + for (CloudRecordItem cloudRecordItem : list) { + downloadProcess += (long) cloudRecordItem.getTimeLen(); + } + return downloadProcess; + } + + @Override + public WVPResult addStreamProxy(MediaServer mediaServer, String app, String stream, String url, boolean enableAudio, boolean enableMp4, String rtpType, Integer timeout) { + + ABLResult result = ablresTfulUtils.addStreamProxy(mediaServer, app, stream, url, !enableAudio, enableMp4, rtpType, timeout); + if (result.getCode() != 0) { + return WVPResult.fail(ErrorCode.ERROR100.getCode(), result.getMemo()); + }else { + return WVPResult.success(result.getKey()); + } + } + + @Override + public Integer startSendRtpPassive(MediaServer mediaServer, SendRtpInfo sendRtpItem, Integer timeout) { + logger.warn("[abl-startSendRtpPassive] 未实现"); + return 0; + } + + @Override + public Integer startSendRtpTalk(MediaServer mediaServer, SendRtpInfo sendRtpItem, Integer timeout) { + logger.warn("[abl-startSendRtpTalk] 未实现"); + return 0; + } + + @Override + public void startSendRtpStream(MediaServer mediaServer, SendRtpInfo sendRtpItem) { + logger.warn("[abl-startSendRtpStream] 未实现"); + } + + @Override + public String startProxy(MediaServer mediaServer, StreamProxy streamProxy) { + + MediaInfo mediaInfo = getMediaInfo(mediaServer, streamProxy.getApp(), streamProxy.getStream()); + + if (mediaInfo != null) { + closeStreams(mediaServer, streamProxy.getApp(), streamProxy.getStream()); + } + + ABLResult ablResult = null; + if ("ffmpeg".equalsIgnoreCase(streamProxy.getType())){ + if (streamProxy.getTimeout() == 0) { + streamProxy.setTimeout(15); + } + ablResult = ablresTfulUtils.addFFmpegProxy(mediaServer, streamProxy.getApp(), streamProxy.getStream(), streamProxy.getSrcUrl().trim(), + !streamProxy.isEnableAudio(), streamProxy.isEnableMp4(), streamProxy.getRtspType(), streamProxy.getTimeout()); + }else { + ablResult = ablresTfulUtils.addStreamProxy(mediaServer, streamProxy.getApp(), streamProxy.getStream(), streamProxy.getSrcUrl().trim(), + streamProxy.isEnableAudio(), streamProxy.isEnableMp4(), streamProxy.getRtspType(), streamProxy.getTimeout()); + } + if (ablResult.getCode() != 0) { + throw new ControllerException(ablResult.getCode(), ablResult.getMemo()); + }else { + String key = ablResult.getKey(); + if (key == null) { + throw new ControllerException(ablResult.getCode(), "代理结果异常: " + ablResult); + }else { + return key; + } + } + } + + @Override + public void stopProxy(MediaServer mediaServer, String streamKey, String type) { + ABLResult ablResult = null; + if ("ffmpeg".equalsIgnoreCase(type)){ + ablResult = ablresTfulUtils.delFFmpegProxy(mediaServer, streamKey); + }else { + ablResult = ablresTfulUtils.delStreamProxy(mediaServer, streamKey); + } + if (ablResult.getCode() != 0) { + throw new ControllerException(ablResult.getCode(), ablResult.getMemo()); + } + } + + @Override + public List listRtpServer(MediaServer mediaServer) { + ABLResult ablResult = ablresTfulUtils.getMediaList(mediaServer, "rtp", null); + if (ablResult.getCode() != 0) { + return null; + } + if (ablResult.getMediaList() == null || ablResult.getMediaList().isEmpty()) { + return new ArrayList<>(); + } + List result = new ArrayList<>(); + for (int i = 0; i < ablResult.getMediaList().size(); i++) { + ABLMedia ablMedia = ablResult.getMediaList().get(i); + result.add(ablMedia.getStream()); + } + return result; + } + + @Override + public void loadMP4File(MediaServer mediaServer, String app, String stream, String filePath, String fileName, ErrorCallback callback) { + String buildStream = String.format("%s__ReplayFMP4RecordFile__%s", stream, fileName); + StreamInfo streamInfo = getStreamInfoByAppAndStream(mediaServer, app, buildStream, null, null, null, true); + callback.run(ErrorCode.SUCCESS.getCode(), ErrorCode.SUCCESS.getMsg(), streamInfo); + } + + @Override + public void loadMP4FileForDate(MediaServer mediaServer, String app, String stream, String date, String dateDir, ErrorCallback callback) { + // 解析为 LocalDate + LocalDate localDate = LocalDate.parse(date, DateUtil.DateFormatter); + LocalDateTime startOfDay = localDate.atStartOfDay(); + LocalDateTime endOfDay = localDate.atTime(23, 59,59, 999); + String startTime = DateUtil.urlFormatter.format(startOfDay); + String endTime = DateUtil.urlFormatter.format(endOfDay); + + ABLResult ablResult = ablresTfulUtils.queryRecordList(mediaServer, app, stream, startTime, endTime); + if (ablResult.getCode() != 0) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), ablResult.getMemo()); + } + String resultApp = ablResult.getApp(); + String resultStream = ablResult.getStream(); + StreamInfo streamInfo = getStreamInfoByAppAndStream(mediaServer, resultApp, resultStream, null, null,null, true); + streamInfo.setKey(ablResult.getKey()); + if (callback != null) { + callback.run(ErrorCode.SUCCESS.getCode(), ErrorCode.SUCCESS.getMsg(), streamInfo); + } + } + + @Override + public void seekRecordStamp(MediaServer mediaServer, String app, String stream, Double stamp, String schema) { + ABLResult ablResult = ablresTfulUtils.controlRecordPlay(mediaServer, app, stream, "seek", stamp/1000 + ""); + if (ablResult.getCode() != 0) { + log.warn("[abl-seek] 失败:{}", ablResult.getMemo()); + } + } + + @Override + public void setRecordSpeed(MediaServer mediaServer, String app, String stream, Integer speed, String schema) { + ABLResult ablResult = ablresTfulUtils.controlRecordPlay(mediaServer, app, stream, "scale", speed + ""); + if (ablResult.getCode() != 0) { + log.warn("[abl-倍速] 失败:{}", ablResult.getMemo()); + } + } + + @Override + public DownloadFileInfo getDownloadFilePath(MediaServer mediaServer, RecordInfo recordInfo) { + // 将filePath作为独立参数传入,避免%符号解析问题 + String pathTemplate = "%s://%s:%s/%s/%s__ReplayFMP4RecordFile__%s?download_speed=16"; + + DownloadFileInfo info = new DownloadFileInfo(); + if ((mediaServer.getMp4Port() & 1) == 1) { + info.setHttpsPath( + String.format( + pathTemplate, + "https", + mediaServer.getStreamIp(), + mediaServer.getMp4Port(), + recordInfo.getApp(), + recordInfo.getStream(), + recordInfo.getFileName() + ) + ); + }else { + info.setHttpPath( + String.format( + pathTemplate, + "http", + mediaServer.getStreamIp(), + mediaServer.getMp4Port(), + recordInfo.getApp(), + recordInfo.getStream(), + recordInfo.getFileName() + ) + ); + } + return info; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/media/abl/ABLMediaServerStatusManger.java b/src/main/java/com/genersoft/iot/vmp/media/abl/ABLMediaServerStatusManger.java new file mode 100644 index 0000000..a820851 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/media/abl/ABLMediaServerStatusManger.java @@ -0,0 +1,358 @@ +package com.genersoft.iot.vmp.media.abl; + +import com.alibaba.fastjson2.JSONArray; +import com.genersoft.iot.vmp.conf.DynamicTask; +import com.genersoft.iot.vmp.conf.UserSetting; +import com.genersoft.iot.vmp.media.abl.bean.ABLResult; +import com.genersoft.iot.vmp.media.abl.bean.AblServerConfig; +import com.genersoft.iot.vmp.media.abl.bean.ConfigKeyId; +import com.genersoft.iot.vmp.media.abl.event.HookAblServerKeepaliveEvent; +import com.genersoft.iot.vmp.media.abl.event.HookAblServerStartEvent; +import com.genersoft.iot.vmp.media.bean.MediaServer; +import com.genersoft.iot.vmp.media.event.mediaServer.MediaServerChangeEvent; +import com.genersoft.iot.vmp.media.event.mediaServer.MediaServerDeleteEvent; +import com.genersoft.iot.vmp.media.service.IMediaServerService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.event.EventListener; +import org.springframework.scheduling.annotation.Async; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +import java.lang.reflect.Field; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * 管理zlm流媒体节点的状态 + */ +@Component +public class ABLMediaServerStatusManger { + + private final static Logger logger = LoggerFactory.getLogger(ABLMediaServerStatusManger.class); + + private final Map offlineABLPrimaryMap = new ConcurrentHashMap<>(); + private final Map offlineAblsecondaryMap = new ConcurrentHashMap<>(); + private final Map offlineAblTimeMap = new ConcurrentHashMap<>(); + + @Autowired + private ABLRESTfulUtils ablResTfulUtils; + + @Autowired + private IMediaServerService mediaServerService; + + @Autowired + private DynamicTask dynamicTask; + + @Value("${server.ssl.enabled:false}") + private boolean sslEnabled; + + @Value("${server.port}") + private Integer serverPort; + + @Autowired + private UserSetting userSetting; + + private final String type = "abl"; + + @Async("taskExecutor") + @EventListener + public void onApplicationEvent(MediaServerChangeEvent event) { + if (event.getMediaServerItemList() == null + || event.getMediaServerItemList().isEmpty()) { + return; + } + for (MediaServer mediaServer : event.getMediaServerItemList()) { + if (!type.equals(mediaServer.getType())) { + continue; + } + logger.info("[ABL-添加待上线节点] ID:" + mediaServer.getId()); + offlineABLPrimaryMap.put(mediaServer.getId(), mediaServer); + offlineAblTimeMap.put(mediaServer.getId(), System.currentTimeMillis()); + } + execute(); + } + + @Async("taskExecutor") + @EventListener + public void onApplicationEvent(HookAblServerStartEvent event) { + if (event.getMediaServerItem() == null + || !type.equals(event.getMediaServerItem().getType()) + || event.getMediaServerItem().isStatus()) { + return; + } + MediaServer serverItem = mediaServerService.getOne(event.getMediaServerItem().getId()); + if (serverItem == null) { + return; + } + logger.info("[ABL-HOOK事件-服务启动] ID:" + event.getMediaServerItem().getId()); + online(serverItem, null); + } + + @Async("taskExecutor") + @EventListener + public void onApplicationEvent(HookAblServerKeepaliveEvent event) { + if (event.getMediaServerItem() == null) { + return; + } + MediaServer serverItem = mediaServerService.getOne(event.getMediaServerItem().getId()); + if (serverItem == null) { + return; + } + logger.info("[ABL-HOOK事件-心跳] ID:" + event.getMediaServerItem().getId()); + online(serverItem, null); + } + + @Async("taskExecutor") + @EventListener + public void onApplicationEvent(MediaServerDeleteEvent event) { + if (event.getMediaServer() == null) { + return; + } + logger.info("[ABL-节点被移除] ID:" + event.getMediaServer().getServerId()); + offlineABLPrimaryMap.remove(event.getMediaServer().getServerId()); + offlineAblsecondaryMap.remove(event.getMediaServer().getServerId()); + offlineAblTimeMap.remove(event.getMediaServer().getServerId()); + } + + @Scheduled(fixedDelay = 10*1000) //每隔10秒检查一次 + public void execute(){ + // 初次加入的离线节点会在30分钟内,每间隔十秒尝试一次,30分钟后如果仍然没有上线,则每隔30分钟尝试一次连接 + if (offlineABLPrimaryMap.isEmpty() && offlineAblsecondaryMap.isEmpty()) { + return; + } + if (!offlineABLPrimaryMap.isEmpty()) { + for (MediaServer mediaServerItem : offlineABLPrimaryMap.values()) { + if (offlineAblTimeMap.get(mediaServerItem.getId()) < System.currentTimeMillis() - 30*60*1000) { + offlineAblsecondaryMap.put(mediaServerItem.getId(), mediaServerItem); + offlineABLPrimaryMap.remove(mediaServerItem.getId()); + continue; + } + logger.info("[ABL-尝试连接] ID:{}, 地址: {}:{}", mediaServerItem.getId(), mediaServerItem.getIp(), mediaServerItem.getHttpPort()); + ABLResult ablResult = ablResTfulUtils.getServerConfig(mediaServerItem); + AblServerConfig ablServerConfig = null; + if (ablResult.getCode() != 0) { + logger.info("[ABL-尝试连接]失败, ID:{}, 地址: {}:{}", mediaServerItem.getId(), mediaServerItem.getIp(), mediaServerItem.getHttpPort()); + continue; + } + JSONArray params = ablResult.getParams(); + + if (params == null || params.isEmpty()) { + logger.info("[ABL-尝试连接]失败, ID:{}, 地址: {}:{}", mediaServerItem.getId(), mediaServerItem.getIp(), mediaServerItem.getHttpPort()); + }else { + ablServerConfig = AblServerConfig.getInstance(params); + initPort(mediaServerItem, ablServerConfig); + online(mediaServerItem, ablServerConfig); + } + } + } + if (!offlineAblsecondaryMap.isEmpty()) { + for (MediaServer mediaServerItem : offlineAblsecondaryMap.values()) { + if (offlineAblTimeMap.get(mediaServerItem.getId()) < System.currentTimeMillis() - 30*60*1000) { + continue; + } + logger.info("[ABL-尝试连接] ID:{}, 地址: {}:{}", mediaServerItem.getId(), mediaServerItem.getIp(), mediaServerItem.getHttpPort()); + ABLResult ablResult = ablResTfulUtils.getServerConfig(mediaServerItem); + AblServerConfig ablServerConfig = null; + if (ablResult.getCode() != 0) { + logger.info("[ABL-尝试连接]失败, ID:{}, 地址: {}:{}", mediaServerItem.getId(), mediaServerItem.getIp(), mediaServerItem.getHttpPort()); + offlineAblTimeMap.put(mediaServerItem.getId(), System.currentTimeMillis()); + continue; + } + JSONArray params = ablResult.getParams(); + if (params == null || params.isEmpty()) { + logger.info("[ABL-尝试连接]失败, ID:{}, 地址: {}:{}", mediaServerItem.getId(), mediaServerItem.getIp(), mediaServerItem.getHttpPort()); + offlineAblTimeMap.put(mediaServerItem.getId(), System.currentTimeMillis()); + }else { + ablServerConfig = AblServerConfig.getInstance(params); + initPort(mediaServerItem, ablServerConfig); + online(mediaServerItem, ablServerConfig); + } + } + } + } + + private void online(MediaServer mediaServer, AblServerConfig config) { + offlineABLPrimaryMap.remove(mediaServer.getId()); + offlineAblsecondaryMap.remove(mediaServer.getId()); + offlineAblTimeMap.remove(mediaServer.getId()); + if (!mediaServer.isStatus()) { + logger.info("[ABL-连接成功] ID:{}, 地址: {}:{}", mediaServer.getId(), mediaServer.getIp(), mediaServer.getHttpPort()); + mediaServer.setStatus(true); + mediaServer.setHookAliveInterval(10F); + mediaServerService.update(mediaServer); + if(mediaServer.isAutoConfig()) { + if (config == null) { + ABLResult ablResult = ablResTfulUtils.getServerConfig(mediaServer); + JSONArray data = ablResult.getParams(); + if (data != null && !data.isEmpty()) { + config = AblServerConfig.getInstance(data); + } + } + if (config != null) { + initPort(mediaServer, config); + setAblConfig(mediaServer, false, config); + } + } + mediaServerService.update(mediaServer); + } + // 设置两次心跳未收到则认为zlm离线 + String key = "ABL-keepalive-" + mediaServer.getId(); + dynamicTask.startDelay(key, ()->{ + logger.warn("[ABL-心跳超时] ID:{}", mediaServer.getId()); + mediaServer.setStatus(false); + offlineABLPrimaryMap.put(mediaServer.getId(), mediaServer); + offlineAblTimeMap.put(mediaServer.getId(), System.currentTimeMillis()); + // TODO 发送离线通知 + mediaServerService.update(mediaServer); + }, (int)(mediaServer.getHookAliveInterval() * 2 * 1000)); + } + private void initPort(MediaServer mediaServer, AblServerConfig ablServerConfig) { + // 端口只会从配置中读取一次,一旦自己配置或者读取过了将不在配置 + if (ablServerConfig.getRtmpPort() != null && mediaServer.getRtmpPort() != ablServerConfig.getRtmpPort()) { + mediaServer.setRtmpPort(ablServerConfig.getRtmpPort()); + } + if (ablServerConfig.getRtspPort() != null && mediaServer.getRtspPort() != ablServerConfig.getRtspPort()) { + mediaServer.setRtspPort(ablServerConfig.getRtspPort()); + } + if (ablServerConfig.getHttpFlvPort() != null && mediaServer.getFlvPort() != ablServerConfig.getHttpFlvPort()) { + mediaServer.setFlvPort(ablServerConfig.getHttpFlvPort()); + } + if (ablServerConfig.getHttpMp4Port() != null && mediaServer.getMp4Port() != ablServerConfig.getHttpMp4Port()) { + mediaServer.setMp4Port(ablServerConfig.getHttpMp4Port()); + } + if (ablServerConfig.getWsFlvPort() != null && mediaServer.getWsFlvPort() != ablServerConfig.getWsFlvPort()) { + mediaServer.setWsFlvPort(ablServerConfig.getWsFlvPort()); + } + if (ablServerConfig.getPsTsRecvPort() != null && mediaServer.getRtpProxyPort() != ablServerConfig.getPsTsRecvPort()) { + mediaServer.setRtpProxyPort(ablServerConfig.getPsTsRecvPort()); + } + if (ablServerConfig.getJtt1078RecvPort() != null && mediaServer.getRtpProxyPort() != ablServerConfig.getJtt1078RecvPort()) { + mediaServer.setJttProxyPort(ablServerConfig.getJtt1078RecvPort()); + } + mediaServer.setHookAliveInterval(10F); + } + + public void setAblConfig(MediaServer mediaServerItem, boolean restart, AblServerConfig config) { + try { + if (config.getHookEnable() == 0) { + logger.info("[媒体服务节点-ABL] 开启HOOK功能 :{}", mediaServerItem.getId()); + ABLResult ablResult = ablResTfulUtils.setConfigParamValue(mediaServerItem, "hook_enable", "1"); + if (ablResult.getCode() == 0) { + logger.info("[媒体服务节点-ABL] 开启HOOK功能成功 :{}", mediaServerItem.getId()); + }else { + logger.info("[媒体服务节点-ABL] 开启HOOK功能失败 :{}->{}", mediaServerItem.getId(), ablResult.getMemo()); + } + } + }catch (Exception e) { + logger.info("[媒体服务节点-ABL] 开启HOOK功能失败 :{}", mediaServerItem.getId(), e); + } + // 设置相关的HOOK + String[] hookUrlArray = { + "on_stream_arrive", + "on_stream_none_reader", + "on_record_mp4", + "on_stream_disconnect", + "on_stream_not_found", + "on_server_started", + "on_publish", + "on_play", + "on_record_progress", + "on_server_keepalive", + "on_stream_not_arrive", + "on_delete_record_mp4", + }; + + String protocol = sslEnabled ? "https" : "http"; + String hookPrefix = String.format("%s://%s:%s/index/hook/abl", protocol, mediaServerItem.getHookIp(), serverPort); + Field[] fields = AblServerConfig.class.getDeclaredFields(); + for (Field field : fields) { + try { + if (field.isAnnotationPresent(ConfigKeyId.class)) { + ConfigKeyId configKeyId = field.getAnnotation(ConfigKeyId.class); + for (String hook : hookUrlArray) { + if (configKeyId.value().equals(hook)) { + String hookUrl = String.format("%s/%s", hookPrefix, hook); + field.setAccessible(true); + // 利用反射获取值后对比是否与配置中相同,不同则进行设置 + if (!hookUrl.equals(field.get(config))) { + ABLResult ablResult = ablResTfulUtils.setConfigParamValue(mediaServerItem, hook, hookUrl); + if (ablResult.getCode() == 0) { + logger.info("[媒体服务节点-ABL] 设置HOOK {} 成功 :{}", hook, mediaServerItem.getId()); + }else { + logger.info("[媒体服务节点-ABL] 设置HOOK {} 失败 :{}->{}", hook, mediaServerItem.getId(), ablResult.getMemo()); + } + } + } + } + } + }catch (Exception e) { + logger.info("[媒体服务节点-ABL] 设置HOOK 失败 :{}", mediaServerItem.getId(), e); + } + } + + + + +// Map param = new HashMap<>(); +// param.put("api.secret",mediaServerItem.getSecret()); // -profile:v Baseline +// if (mediaServerItem.getRtspPort() != 0) { +// param.put("ffmpeg.snap", "%s -rtsp_transport tcp -i %s -y -f mjpeg -frames:v 1 %s"); +// } +// param.put("hook.enable","1"); +// param.put("hook.on_flow_report",""); +// param.put("hook.on_play",String.format("%s/on_play", hookPrefix)); +// param.put("hook.on_http_access",""); +// param.put("hook.on_publish", String.format("%s/on_publish", hookPrefix)); +// param.put("hook.on_record_ts",""); +// param.put("hook.on_rtsp_auth",""); +// param.put("hook.on_rtsp_realm",""); +// param.put("hook.on_server_started",String.format("%s/on_server_started", hookPrefix)); +// param.put("hook.on_shell_login",""); +// param.put("hook.on_stream_changed",String.format("%s/on_stream_changed", hookPrefix)); +// param.put("hook.on_stream_none_reader",String.format("%s/on_stream_none_reader", hookPrefix)); +// param.put("hook.on_stream_not_found",String.format("%s/on_stream_not_found", hookPrefix)); +// param.put("hook.on_server_keepalive",String.format("%s/on_server_keepalive", hookPrefix)); +// param.put("hook.on_send_rtp_stopped",String.format("%s/on_send_rtp_stopped", hookPrefix)); +// param.put("hook.on_rtp_server_timeout",String.format("%s/on_rtp_server_timeout", hookPrefix)); +// param.put("hook.on_record_mp4",String.format("%s/on_record_mp4", hookPrefix)); +// param.put("hook.timeoutSec","30"); +// param.put("hook.alive_interval", mediaServerItem.getHookAliveInterval()); +// // 推流断开后可以在超时时间内重新连接上继续推流,这样播放器会接着播放。 +// // 置0关闭此特性(推流断开会导致立即断开播放器) +// // 此参数不应大于播放器超时时间 +// // 优化此消息以更快的收到流注销事件 +// param.put("protocol.continue_push_ms", "3000" ); +// // 最多等待未初始化的Track时间,单位毫秒,超时之后会忽略未初始化的Track, 设置此选项优化那些音频错误的不规范流, +// // 等zlm支持给每个rtpServer设置关闭音频的时候可以不设置此选项 +// if (mediaServerItem.isRtpEnable() && !ObjectUtils.isEmpty(mediaServerItem.getRtpPortRange())) { +// param.put("rtp_proxy.port_range", mediaServerItem.getRtpPortRange().replace(",", "-")); +// } +// +// if (!ObjectUtils.isEmpty(mediaServerItem.getRecordPath())) { +// File recordPathFile = new File(mediaServerItem.getRecordPath()); +// param.put("protocol.mp4_save_path", recordPathFile.getParentFile().getPath()); +// param.put("protocol.downloadRoot", recordPathFile.getParentFile().getPath()); +// param.put("record.appName", recordPathFile.getName()); +// } +// +// JSONObject responseJSON = ablResTfulUtils.setConfigParamValue(mediaServerItem, param); +// +// if (responseJSON != null && responseJSON.getInteger("code") == 0) { +// if (restart) { +// logger.info("[媒体服务节点] 设置成功,开始重启以保证配置生效 {} -> {}:{}", +// mediaServerItem.getId(), mediaServerItem.getIp(), mediaServerItem.getHttpPort()); +// ablResTfulUtils.restartServer(mediaServerItem); +// }else { +// logger.info("[媒体服务节点] 设置成功 {} -> {}:{}", +// mediaServerItem.getId(), mediaServerItem.getIp(), mediaServerItem.getHttpPort()); +// } +// }else { +// logger.info("[媒体服务节点] 设置媒体服务节点失败 {} -> {}:{}", +// mediaServerItem.getId(), mediaServerItem.getIp(), mediaServerItem.getHttpPort()); +// } + } + +} diff --git a/src/main/java/com/genersoft/iot/vmp/media/abl/ABLRESTfulUtils.java b/src/main/java/com/genersoft/iot/vmp/media/abl/ABLRESTfulUtils.java new file mode 100644 index 0000000..9c019bd --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/media/abl/ABLRESTfulUtils.java @@ -0,0 +1,540 @@ +package com.genersoft.iot.vmp.media.abl; + +import com.alibaba.fastjson2.JSON; +import com.genersoft.iot.vmp.conf.exception.ControllerException; +import com.genersoft.iot.vmp.media.abl.bean.ABLResult; +import com.genersoft.iot.vmp.media.bean.MediaServer; +import com.genersoft.iot.vmp.vmanager.bean.ErrorCode; +import okhttp3.*; +import org.jetbrains.annotations.NotNull; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.net.ConnectException; +import java.net.SocketTimeoutException; +import java.net.URLEncoder; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.TimeUnit; + +@Component +public class ABLRESTfulUtils { + + private final static Logger logger = LoggerFactory.getLogger(ABLRESTfulUtils.class); + + private OkHttpClient client; + + public interface RequestCallback{ + void run(String response); + } + public interface ResultCallback{ + void run(ABLResult response); + } + + private OkHttpClient getClient(){ + return getClient(null); + } + + private OkHttpClient getClient(Integer readTimeOut){ + if (client == null) { + if (readTimeOut == null) { + readTimeOut = 10; + } + OkHttpClient.Builder httpClientBuilder = new OkHttpClient.Builder(); + //todo 暂时写死超时时间 均为5s + // 设置连接超时时间 + httpClientBuilder.connectTimeout(8,TimeUnit.SECONDS); + // 设置读取超时时间 + httpClientBuilder.readTimeout(readTimeOut,TimeUnit.SECONDS); + // 设置连接池 + httpClientBuilder.connectionPool(new ConnectionPool(16, 5, TimeUnit.MINUTES)); + client = httpClientBuilder.build(); + } + return client; + + } + + public String sendPost(MediaServer mediaServerItem, String api, Map param, RequestCallback callback) { + return sendPost(mediaServerItem, api, param, callback, null); + } + + + public String sendPost(MediaServer mediaServerItem, String api, Map param, RequestCallback callback, Integer readTimeOut) { + OkHttpClient client = getClient(readTimeOut); + + if (mediaServerItem == null) { + return null; + } + String url = String.format("http://%s:%s/index/api/%s", mediaServerItem.getIp(), mediaServerItem.getHttpPort(), api); + String result = null; + + FormBody.Builder builder = new FormBody.Builder(); + builder.add("secret",mediaServerItem.getSecret()); + if (param != null && !param.isEmpty()) { + for (String key : param.keySet()){ + if (param.get(key) != null) { + builder.add(key, param.get(key).toString()); + } + } + } + + FormBody body = builder.build(); + + Request request = new Request.Builder() + .post(body) + .url(url) + .build(); + if (callback == null) { + try { + Response response = client.newCall(request).execute(); + + if (response.isSuccessful()) { + ResponseBody responseBody = response.body(); + if (responseBody != null) { + result = responseBody.string(); + } + }else { + response.close(); + Objects.requireNonNull(response.body()).close(); + } + }catch (IOException e) { + logger.error(String.format("[ %s ]请求失败: %s", url, e.getMessage())); + + if(e instanceof SocketTimeoutException){ + //读取超时超时异常 + logger.error(String.format("读取ABL数据超时失败: %s, %s", url, e.getMessage())); + } + if(e instanceof ConnectException){ + //判断连接异常,我这里是报Failed to connect to 10.7.5.144 + logger.error(String.format("连接ABL连接失败: %s, %s", url, e.getMessage())); + } + + }catch (Exception e){ + logger.error(String.format("访问ABL失败: %s, %s", url, e.getMessage())); + } + }else { + client.newCall(request).enqueue(new Callback(){ + + @Override + public void onResponse(@NotNull Call call, @NotNull Response response){ + if (response.isSuccessful()) { + try { + String responseStr = Objects.requireNonNull(response.body()).string(); + callback.run(responseStr); + } catch (IOException e) { + logger.error(String.format("[ %s ]请求失败: %s", url, e.getMessage())); + } + + }else { + response.close(); + Objects.requireNonNull(response.body()).close(); + } + } + + @Override + public void onFailure(@NotNull Call call, @NotNull IOException e) { + logger.error(String.format("连接ABL失败: %s, %s", call.request().toString(), e.getMessage())); + + if(e instanceof SocketTimeoutException){ + //读取超时超时异常 + logger.error(String.format("读取ABL数据失败: %s, %s", call.request().toString(), e.getMessage())); + } + if(e instanceof ConnectException){ + //判断连接异常,我这里是报Failed to connect to 10.7.5.144 + logger.error(String.format("连接ABL失败: %s, %s", call.request().toString(), e.getMessage())); + } + } + }); + } + return result; + } + + public String sendGet(MediaServer mediaServerItem, String api, Map param) { + OkHttpClient client = getClient(); + + if (mediaServerItem == null) { + return null; + } + String result = null; + StringBuilder stringBuffer = new StringBuilder(); + stringBuffer.append(String.format("http://%s:%s/index/api/%s", mediaServerItem.getIp(), mediaServerItem.getHttpPort(), api)); + if (param != null && !param.keySet().isEmpty()) { + stringBuffer.append("?secret=").append(mediaServerItem.getSecret()).append("&"); + int index = 1; + for (String key : param.keySet()){ + if (param.get(key) != null) { + stringBuffer.append(key + "=" + param.get(key)); + if (index < param.size()) { + stringBuffer.append("&"); + } + } + index++; + } + } + String url = stringBuffer.toString(); + logger.info("[访问ABL]: {}", url); + Request request = new Request.Builder() + .get() + .url(url) + .build(); + try { + Response response = client.newCall(request).execute(); + if (response.isSuccessful()) { + ResponseBody responseBody = response.body(); + if (responseBody != null) { + result = responseBody.string(); + } + }else { + response.close(); + Objects.requireNonNull(response.body()).close(); + } + } catch (ConnectException e) { + logger.error(String.format("连接ABL失败: %s, %s", e.getCause().getMessage(), e.getMessage())); + logger.info("请检查media配置并确认ABL已启动..."); + }catch (IOException e) { + logger.error(String.format("[ %s ]请求失败: %s", url, e.getMessage())); + } + return result; + } + + public void sendGetForImg(MediaServer mediaServerItem, String api, Map params, String targetPath, String fileName) { + String url = String.format("http://%s:%s/index/api/%s", mediaServerItem.getIp(), mediaServerItem.getHttpPort(), api); + HttpUrl parseUrl = HttpUrl.parse(url); + if (parseUrl == null) { + return; + } + HttpUrl.Builder httpBuilder = parseUrl.newBuilder(); + + httpBuilder.addQueryParameter("secret", mediaServerItem.getSecret()); + if (params != null) { + for (Map.Entry param : params.entrySet()) { + httpBuilder.addQueryParameter(param.getKey(), param.getValue().toString()); + } + } + + Request request = new Request.Builder() + .url(httpBuilder.build()) + .build(); + logger.info(request.toString()); + try { + OkHttpClient client = getClient(); + Response response = client.newCall(request).execute(); + if (response.isSuccessful()) { + if (targetPath != null) { + File snapFolder = new File(targetPath); + if (!snapFolder.exists()) { + if (!snapFolder.mkdirs()) { + logger.warn("{}路径创建失败", snapFolder.getAbsolutePath()); + } + } + File snapFile = new File(targetPath + File.separator + fileName); + FileOutputStream outStream = new FileOutputStream(snapFile); + + outStream.write(Objects.requireNonNull(response.body()).bytes()); + outStream.flush(); + outStream.close(); + } else { + logger.error(String.format("[ %s ]请求失败: %s %s", url, response.code(), response.message())); + } + } else { + logger.error(String.format("[ %s ]请求失败: %s %s", url, response.code(), response.message())); + } + Objects.requireNonNull(response.body()).close(); + } catch (ConnectException e) { + logger.error(String.format("连接ABL失败: %s, %s", e.getCause().getMessage(), e.getMessage())); + logger.info("请检查media配置并确认ABL已启动..."); + } catch (IOException e) { + logger.error(String.format("[ %s ]请求失败: %s", url, e.getMessage())); + } + } + + public void sendGetForImgForUrl(String url, String targetPath, String fileName) { + HttpUrl parseUrl = HttpUrl.parse(url); + if (parseUrl == null) { + return; + } + HttpUrl.Builder httpBuilder = parseUrl.newBuilder(); + + Request request = new Request.Builder() + .url(httpBuilder.build()) + .build(); + logger.info(request.toString()); + try { + OkHttpClient client = getClient(); + Response response = client.newCall(request).execute(); + if (response.isSuccessful()) { + if (targetPath != null) { + File snapFolder = new File(targetPath); + if (!snapFolder.exists()) { + if (!snapFolder.mkdirs()) { + logger.warn("{}路径创建失败", snapFolder.getAbsolutePath()); + } + } + File snapFile = new File(targetPath + File.separator + fileName); + FileOutputStream outStream = new FileOutputStream(snapFile); + + outStream.write(Objects.requireNonNull(response.body()).bytes()); + outStream.flush(); + outStream.close(); + } else { + logger.error(String.format("[ %s ]请求失败: %s %s", url, response.code(), response.message())); + } + } else { + logger.error(String.format("[ %s ]请求失败: %s %s", url, response.code(), response.message())); + } + Objects.requireNonNull(response.body()).close(); + } catch (ConnectException e) { + logger.error(String.format("连接ABL失败: %s, %s", e.getCause().getMessage(), e.getMessage())); + logger.info("请检查media配置并确认ABL已启动..."); + } catch (IOException e) { + logger.error(String.format("[ %s ]请求失败: %s", url, e.getMessage())); + } + } + + public Integer openRtpServer(MediaServer mediaServer, String app, String stream, int payload, Integer port, Integer tcpMode, Integer disableAudio, Boolean record, Boolean isJtt) { + Map param = new HashMap<>(); + param.put("vhost", "_defaultVhost_"); + param.put("app", app); + param.put("stream_id", stream); + param.put("payload", payload); + if (isJtt) { + // 1 PS 国标gb28181, 默认为1、 + // 2 ES 视频支持 H246\H265,音频只支持G711A、G711U 、AAC + // 3 XHB (一家公司的打包格式) 只支持视频,音频不能加入打包 + // 4 、Jt1078(2016版本)码流接入 + param.put("RtpPayloadDataType", 4); + param.put("jtt1078_version", "2019"); + } + if (port != null) { + param.put("port", port); + }else { + param.put("port", 0); + } + if (tcpMode != null) { + param.put("enable_tcp", tcpMode); + } + if (disableAudio != null) { + param.put("disableAudio", disableAudio); + } + if (record != null && record) { + param.put("enable_mp4", 1); + } + + String response = sendPost(mediaServer, "openRtpServer", param, null); + if (response == null) { + return 0; + }else { + ABLResult ablResult = JSON.parseObject(response, ABLResult.class); + if (ablResult.getCode() == 0) { + return ablResult.getPort(); + }else { + return 0; + } + } + } + + public ABLResult closeStreams(MediaServer mediaServerItem, String app, String stream) { + Map param = new HashMap<>(); + param.put("vhost", "__defaultVhost__"); + param.put("app", app); + param.put("stream", stream); + param.put("force", 1); + String response = sendPost(mediaServerItem, "close_streams", param, null); + ABLResult ablResult = JSON.parseObject(response, ABLResult.class); + if (ablResult == null) { + return ABLResult.getFailForMediaServer(); + }else { + return ablResult; + } + } + + public ABLResult getServerConfig(MediaServer mediaServerItem){ + String response = sendPost(mediaServerItem, "getServerConfig", null, null); + ABLResult ablResult = JSON.parseObject(response, ABLResult.class); + if (ablResult == null) { + return ABLResult.getFailForMediaServer(); + }else { + return ablResult; + } + } + + public ABLResult setConfigParamValue(MediaServer mediaServerItem, String key, Object value){ + Map param = new HashMap<>(); + param.put("key", key); + param.put("value", value); + String response = sendGet(mediaServerItem, "setConfigParamValue", param); + ABLResult ablResult = JSON.parseObject(response, ABLResult.class); + if (ablResult == null) { + return ABLResult.getFailForMediaServer(); + }else { + return ablResult; + } + } + + public void stopSendRtp(MediaServer mediaServer,String key) { + Map param = new HashMap<>(); + param.put("key", key); + sendPost(mediaServer,"stopSendRtp", param, null); + } + + public ABLResult getMediaList(MediaServer mediaServer, String app, String stream) { + Map param = new HashMap<>(); + param.put("app", app); + if (stream != null) { + param.put("stream", stream); + } + + String response = sendGet(mediaServer, "getMediaList", param); + ABLResult ablResult = JSON.parseObject(response, ABLResult.class); + if (ablResult == null) { + return ABLResult.getFailForMediaServer(); + }else { + return ablResult; + } + } + + public ABLResult queryRecordList(MediaServer mediaServer, String app, String stream, String startTime, String endTime) { + Map param = new HashMap<>(); + param.put("app", app); + param.put("stream", stream); + param.put("starttime", startTime); + param.put("endtime", endTime); + String response = sendGet(mediaServer, "queryRecordList", param); + ABLResult ablResult = JSON.parseObject(response, ABLResult.class); + if (ablResult == null) { + return ABLResult.getFailForMediaServer(); + }else { + return ablResult; + } + } + + public void getSnap(MediaServer mediaServer, String app, String stream, int timeoutSec, String path, String fileName) { + Map param = new HashMap<>(); + param.put("app", app); + param.put("stream", stream); + param.put("timeout_sec", timeoutSec); + param.put("vhost", "_defaultVhost_"); +// JSONObject jsonObject = sendPost(mediaServer, "getSnap", param, null); +// if (jsonObject != null && jsonObject.getInteger("code") == 0) { +// String url = jsonObject.getString("url"); +// sendGetForImgForUrl(url, path, fileName); +// } + sendGetForImg(mediaServer, "getSnap", param, path, fileName); + + } + + public ABLResult addStreamProxy(MediaServer mediaServer, String app, String stream, String url, boolean disableAudio, boolean enableMp4, String rtpType, Integer timeout) { + try { + url = URLEncoder.encode(url, "UTF-8"); + } catch (UnsupportedEncodingException e) { + throw new ControllerException(ErrorCode.ERROR100.getCode(),"url编码失败"); + } + + Map param = new HashMap<>(); + param.put("app", app); + param.put("stream", stream); + param.put("url", url); + param.put("disableAudio", disableAudio? "1" : "0"); + param.put("enable_mp4", enableMp4 ? "1" : "0"); + // TODO rtpType timeout 尚不支持 + String response = sendGet(mediaServer, "addStreamProxy", param); + ABLResult ablResult = JSON.parseObject(response, ABLResult.class); + if (ablResult == null) { + return ABLResult.getFailForMediaServer(); + }else { + return ablResult; + } + } + + public ABLResult addFFmpegProxy(MediaServer mediaServer, String app, String stream, String url, boolean disableAudio, boolean enableMp4, String rtpType, Integer timeout) { + try { + url = URLEncoder.encode(url, "UTF-8"); + } catch (UnsupportedEncodingException e) { + throw new ControllerException(ErrorCode.ERROR100.getCode(),"url编码失败"); + } + Map param = new HashMap<>(); + param.put("app", app); + param.put("stream", stream); + param.put("url", url); + param.put("disableAudio", disableAudio); + param.put("enable_mp4", enableMp4); + // TODO rtpType timeout 尚不支持 + String response = sendGet(mediaServer, "addFFmpegProxy", param); + ABLResult ablResult = JSON.parseObject(response, ABLResult.class); + if (ablResult == null) { + return ABLResult.getFailForMediaServer(); + }else { + return ablResult; + } + } + + public ABLResult delStreamProxy(MediaServer mediaServer, String streamKey) { + Map param = new HashMap<>(); + param.put("key", streamKey); + String response = sendGet(mediaServer, "delStreamProxy", param); + ABLResult ablResult = JSON.parseObject(response, ABLResult.class); + if (ablResult == null) { + return ABLResult.getFailForMediaServer(); + }else { + return ablResult; + } + } + + public ABLResult delFFmpegProxy(MediaServer mediaServer, String streamKey) { + Map param = new HashMap<>(); + param.put("key", streamKey); + String response = sendGet(mediaServer, "delFFmpegProxy", param); + ABLResult ablResult = JSON.parseObject(response, ABLResult.class); + if (ablResult == null) { + return ABLResult.getFailForMediaServer(); + }else { + return ablResult; + } + } + + public ABLResult pauseRtpServer(MediaServer mediaServer, String streamKey) { + Map param = new HashMap<>(); + param.put("key", streamKey); + String response = sendGet(mediaServer, "pauseRtpServer", param); + ABLResult ablResult = JSON.parseObject(response, ABLResult.class); + if (ablResult == null) { + return ABLResult.getFailForMediaServer(); + }else { + return ablResult; + } + } + + public ABLResult resumeRtpServer(MediaServer mediaServer, String streamKey) { + Map param = new HashMap<>(); + param.put("key", streamKey); + String response = sendGet(mediaServer, "resumeRtpServer", param); + ABLResult ablResult = JSON.parseObject(response, ABLResult.class); + if (ablResult == null) { + return ABLResult.getFailForMediaServer(); + }else { + return ablResult; + } + } + + public ABLResult controlRecordPlay(MediaServer mediaServer, String app, String stream, String command, String value) { + Map param = new HashMap<>(); + param.put("app", app); + param.put("stream", stream); + param.put("command", command); + param.put("value", value); + String response = sendGet(mediaServer, "controlRecordPlay", param); + ABLResult ablResult = JSON.parseObject(response, ABLResult.class); + if (ablResult == null) { + return ABLResult.getFailForMediaServer(); + }else { + return ablResult; + } + } + +} diff --git a/src/main/java/com/genersoft/iot/vmp/media/abl/bean/ABLMedia.java b/src/main/java/com/genersoft/iot/vmp/media/abl/bean/ABLMedia.java new file mode 100644 index 0000000..66532c6 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/media/abl/bean/ABLMedia.java @@ -0,0 +1,25 @@ +package com.genersoft.iot.vmp.media.abl.bean; + +import lombok.Data; + +@Data +public class ABLMedia { + private String key; + private String app; + private String stream; + private Integer sourceType; + private Long duration; + private String sim; + private Boolean status; + private Boolean enable_hls; + private Boolean transcodingStatus; + private String sourceURL; + private Integer networkType; + private Integer readerCount; + private String videoCodec; + private Integer width; + private Integer height; + private String audioCodec; + private Integer audioChannels; + private Integer audioSampleRate; +} diff --git a/src/main/java/com/genersoft/iot/vmp/media/abl/bean/ABLRecordFile.java b/src/main/java/com/genersoft/iot/vmp/media/abl/bean/ABLRecordFile.java new file mode 100644 index 0000000..4af189f --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/media/abl/bean/ABLRecordFile.java @@ -0,0 +1,10 @@ +package com.genersoft.iot.vmp.media.abl.bean; + +import lombok.Data; + +@Data +public class ABLRecordFile { + private String file; + private Long duration; + private ABLUrls url; +} diff --git a/src/main/java/com/genersoft/iot/vmp/media/abl/bean/ABLResult.java b/src/main/java/com/genersoft/iot/vmp/media/abl/bean/ABLResult.java new file mode 100644 index 0000000..523e8b1 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/media/abl/bean/ABLResult.java @@ -0,0 +1,49 @@ +package com.genersoft.iot.vmp.media.abl.bean; + +import com.alibaba.fastjson2.JSONArray; +import lombok.Data; + +import java.util.List; + +@Data +public class ABLResult { + private int code; + private String memo; + + + private String key; + private Integer port; + private JSONArray params; + private List mediaList; + + private String app; + private String stream; + private String starttime; + private String endtime; + private Long duration; + private ABLUrls url; + private List recordFileList; + + public static ABLResult getFailForMediaServer() { + ABLResult zlmResult = new ABLResult(); + zlmResult.setCode(-2); + zlmResult.setMemo("流媒体调用失败"); + return zlmResult; + } + + public static ABLResult getMediaServer(int code, String msg) { + ABLResult zlmResult = new ABLResult(); + zlmResult.setCode(code); + zlmResult.setMemo(msg); + return zlmResult; + } + @Override + public String toString() { + return "ZLMResult{" + + "code=" + code + + ", memo='" + memo + '\'' + + (key != null ? (", key=" + key) : "") + + (port != null ? (", port=" + port) : "") + + '}'; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/media/abl/bean/ABLUrls.java b/src/main/java/com/genersoft/iot/vmp/media/abl/bean/ABLUrls.java new file mode 100644 index 0000000..a3b74c8 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/media/abl/bean/ABLUrls.java @@ -0,0 +1,24 @@ +package com.genersoft.iot.vmp.media.abl.bean; + +import com.alibaba.fastjson2.annotation.JSONField; +import lombok.Data; + +@Data +public class ABLUrls { + private String rtsp; + private String rtmp; + + @JSONField(name = "http-flv") + private String httpFlv; + + @JSONField(name = "ws-flv") + private String wsFlv; + + @JSONField(name = "http-mp4") + private String httpMp4; + + @JSONField(name = "http-hls") + private String httpHls; + + private String download; +} diff --git a/src/main/java/com/genersoft/iot/vmp/media/abl/bean/AblServerConfig.java b/src/main/java/com/genersoft/iot/vmp/media/abl/bean/AblServerConfig.java new file mode 100644 index 0000000..af36faf --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/media/abl/bean/AblServerConfig.java @@ -0,0 +1,257 @@ +package com.genersoft.iot.vmp.media.abl.bean; + +import com.alibaba.fastjson2.JSONArray; +import com.alibaba.fastjson2.JSONObject; +import lombok.Data; + +import java.lang.reflect.Field; +import java.util.HashMap; +import java.util.Map; + +@Data +public class AblServerConfig { + + @ConfigKeyId("secret") + private String secret; + + @ConfigKeyId("ServerIP") + private String serverIp; + + @ConfigKeyId("mediaServerID") + private String mediaServerId; + + @ConfigKeyId("hook_enable") + private Integer hookEnable; + + @ConfigKeyId("enable_audio") + private Integer enableAudio; + + @ConfigKeyId("httpServerPort") + private Integer httpServerPort; + + @ConfigKeyId("rtspPort") + private Integer rtspPort; + + @ConfigKeyId("rtmpPort") + private Integer rtmpPort; + + @ConfigKeyId("httpFlvPort") + private Integer httpFlvPort; + + @ConfigKeyId("hls_enable") + private Integer hlsEnable; + + @ConfigKeyId("hlsPort") + private Integer hlsPort; + + @ConfigKeyId("wsFlvPort") + private Integer wsFlvPort; + + @ConfigKeyId("httpMp4Port") + private Integer httpMp4Port; + + @ConfigKeyId("ps_tsRecvPort") + private Integer psTsRecvPort; + + @ConfigKeyId("1078Port") + private Integer jtt1078RecvPort; + + @ConfigKeyId("hlsCutType") + private Integer hlsCutType; + + @ConfigKeyId("h265CutType") + private Integer h265CutType; + + @ConfigKeyId("RecvThreadCount") + private Integer RecvThreadCount; + + @ConfigKeyId("SendThreadCount") + private Integer SendThreadCount; + + @ConfigKeyId("GB28181RtpTCPHeadType") + private Integer GB28181RtpTCPHeadType; + + @ConfigKeyId("ReConnectingCount") + private Integer ReConnectingCount; + + @ConfigKeyId("maxTimeNoOneWatch") + private Integer maxTimeNoOneWatch; + + @ConfigKeyId("pushEnable_mp4") + private Integer pushEnableMp4; + + @ConfigKeyId("fileSecond") + private Integer fileSecond; + + @ConfigKeyId("fileKeepMaxTime") + private Integer fileKeepMaxTime; + + @ConfigKeyId("httpDownloadSpeed") + private Integer httpDownloadSpeed; + + @ConfigKeyId("RecordReplayThread") + private Integer RecordReplayThread; + + @ConfigKeyId("convertMaxObject") + private Integer convertMaxObject; + + @ConfigKeyId("version") + private String version; + + @ConfigKeyId("recordPath") + private String recordPath; + + @ConfigKeyId("picturePath") + private String picturePath; + + @ConfigKeyId("noneReaderDuration") + private Integer noneReaderDuration; + + @ConfigKeyId("on_server_started") + private String onServerStarted; + + @ConfigKeyId("on_server_keepalive") + private String onServerKeepalive; + + @ConfigKeyId("on_play") + private String onPlay; + + @ConfigKeyId("on_publish") + private String onPublish; + + @ConfigKeyId("on_stream_arrive") + private String onStreamArrive; + + @ConfigKeyId("on_stream_not_arrive") + private String onStreamNotArrive; + + @ConfigKeyId("on_stream_none_reader") + private String onStreamNoneReader; + + @ConfigKeyId("on_stream_disconnect") + private String onStreamDisconnect; + + @ConfigKeyId("on_stream_not_found") + private String onStreamNotFound; + + @ConfigKeyId("on_record_mp4") + private String onRecordMp4; + + @ConfigKeyId("on_delete_record_mp4") + private String onDeleteRecordMp4; + + @ConfigKeyId("on_record_progress") + private String onRecordProgress; + + @ConfigKeyId("on_record_ts") + private String onRecordTs; + + @ConfigKeyId("enable_GetFileDuration") + private Integer enableGetFileDuration; + + @ConfigKeyId("keepaliveDuration") + private Integer keepaliveDuration; + + @ConfigKeyId("captureReplayType") + private Integer captureReplayType; + + @ConfigKeyId("pictureMaxCount") + private Integer pictureMaxCount; + + @ConfigKeyId("videoFileFormat") + private Integer videoFileFormat; + + @ConfigKeyId("MaxDiconnectTimeoutSecond") + private Integer maxDiconnectTimeoutSecond; + + @ConfigKeyId("G711ConvertAAC") + private Integer g711ConvertAAC; + + @ConfigKeyId("filterVideo_enable") + private Integer filterVideoEnable; + + @ConfigKeyId("filterVideo_text") + private String filterVideoText; + + @ConfigKeyId("FilterFontSize") + private Integer filterFontSize; + + @ConfigKeyId("FilterFontColor") + private String filterFontColor; + + @ConfigKeyId("FilterFontLeft") + private Integer filterFontLeft; + + @ConfigKeyId("FilterFontTop") + private Integer filterFontTop; + + @ConfigKeyId("FilterFontAlpha") + private Double filterFontAlpha; + + @ConfigKeyId("convertOutWidth") + private Integer convertOutWidth; + + @ConfigKeyId("convertOutHeight") + private Integer convertOutHeight; + + @ConfigKeyId("convertOutBitrate") + private Integer convertOutBitrate; + + @ConfigKeyId("flvPlayAddMute") + private Integer flvPlayAddMute; + + @ConfigKeyId("gb28181LibraryUse") + private Integer gb28181LibraryUse; + + @ConfigKeyId("rtc.listening-ip") + private String rtcListeningIp; + + @ConfigKeyId("rtc.listening-port") + private Integer rtcListeningIpPort; + + @ConfigKeyId("rtc.external-ip") + private String rtcExternalIp; + + @ConfigKeyId("rtc.realm") + private String rtcRealm; + + @ConfigKeyId("rtc.user") + private String rtcUser; + + @ConfigKeyId("rtc.min-port") + private Integer rtcMinPort; + + @ConfigKeyId("rtc.max-port") + private Integer rtcMaxPort; + + public static AblServerConfig getInstance(JSONArray jsonArray) { + if (jsonArray == null || jsonArray.isEmpty()) { + return null; + } + AblServerConfig ablServerConfig = new AblServerConfig(); + Field[] fields = AblServerConfig.class.getDeclaredFields(); + Map fieldMap = new HashMap<>(); + for (Field field : fields) { + if (field.isAnnotationPresent(ConfigKeyId.class)) { + ConfigKeyId configKeyId = field.getAnnotation(ConfigKeyId.class); + fieldMap.put(configKeyId.value(), field); + } + } + for (int i = 0; i < jsonArray.size(); i++) { + JSONObject jsonObject = jsonArray.getJSONObject(i); + if (jsonObject == null) { + continue; + } + for (String key : fieldMap.keySet()) { + if (jsonObject.containsKey(key)) { + Field field = fieldMap.get(key); + field.setAccessible(true); + try { + field.set(ablServerConfig, jsonObject.getObject(key, fieldMap.get(key).getType())); + } catch (IllegalAccessException e) {} + } + } + } + return ablServerConfig; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/media/abl/bean/ConfigKeyId.java b/src/main/java/com/genersoft/iot/vmp/media/abl/bean/ConfigKeyId.java new file mode 100644 index 0000000..244bb94 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/media/abl/bean/ConfigKeyId.java @@ -0,0 +1,10 @@ +package com.genersoft.iot.vmp.media.abl.bean; + +import java.lang.annotation.*; + +@Target(ElementType.FIELD) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface ConfigKeyId { + String value(); +} diff --git a/src/main/java/com/genersoft/iot/vmp/media/abl/bean/hook/ABLHookParam.java b/src/main/java/com/genersoft/iot/vmp/media/abl/bean/hook/ABLHookParam.java new file mode 100644 index 0000000..796935e --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/media/abl/bean/hook/ABLHookParam.java @@ -0,0 +1,65 @@ +package com.genersoft.iot.vmp.media.abl.bean.hook; + +public class ABLHookParam { + private String mediaServerId; + + /** + * 应用名 + */ + private String app; + + /** + * 流id + */ + private String stream; + + /** + * 媒体流来源编号,可以根据这个key进行关闭流媒体 可以调用delMediaStream或close_streams 函数进行关闭 + */ + private String key; + + /** + * 媒体流来源网络编号,可参考附表 + */ + private Integer networkType; + + public String getMediaServerId() { + return mediaServerId; + } + + public void setMediaServerId(String mediaServerId) { + this.mediaServerId = mediaServerId; + } + + public String getApp() { + return app; + } + + public void setApp(String app) { + this.app = app; + } + + public String getStream() { + return stream; + } + + public void setStream(String stream) { + this.stream = stream; + } + + public String getKey() { + return key; + } + + public void setKey(String key) { + this.key = key; + } + + public Integer getNetworkType() { + return networkType; + } + + public void setNetworkType(Integer networkType) { + this.networkType = networkType; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/media/abl/bean/hook/OnPlayABLHookParam.java b/src/main/java/com/genersoft/iot/vmp/media/abl/bean/hook/OnPlayABLHookParam.java new file mode 100644 index 0000000..77c180d --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/media/abl/bean/hook/OnPlayABLHookParam.java @@ -0,0 +1,23 @@ +package com.genersoft.iot.vmp.media.abl.bean.hook; + + +import lombok.Data; +import lombok.EqualsAndHashCode; + +@EqualsAndHashCode(callSuper = true) +@Data +public class OnPlayABLHookParam extends ABLHookParam{ + + private String ip; + private Integer port; + private String params; + + @Override + public String toString() { + return "OnPlayABLHookParam{" + + "ip='" + ip + '\'' + + ", port=" + port + + ", params='" + params + '\'' + + '}'; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/media/abl/bean/hook/OnPublishABLHookParam.java b/src/main/java/com/genersoft/iot/vmp/media/abl/bean/hook/OnPublishABLHookParam.java new file mode 100644 index 0000000..11da274 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/media/abl/bean/hook/OnPublishABLHookParam.java @@ -0,0 +1,31 @@ +package com.genersoft.iot.vmp.media.abl.bean.hook; + +public class OnPublishABLHookParam extends ABLHookParam{ + private String ip; + private Integer port; + private String params; + + public String getIp() { + return ip; + } + + public void setIp(String ip) { + this.ip = ip; + } + + public Integer getPort() { + return port; + } + + public void setPort(Integer port) { + this.port = port; + } + + public String getParams() { + return params; + } + + public void setParams(String params) { + this.params = params; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/media/abl/bean/hook/OnRecordMp4ABLHookParam.java b/src/main/java/com/genersoft/iot/vmp/media/abl/bean/hook/OnRecordMp4ABLHookParam.java new file mode 100644 index 0000000..6aaddfe --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/media/abl/bean/hook/OnRecordMp4ABLHookParam.java @@ -0,0 +1,13 @@ +package com.genersoft.iot.vmp.media.abl.bean.hook; + +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class OnRecordMp4ABLHookParam extends ABLHookParam{ + private String fileName; + private String startTime; + private String endTime; + private long fileSize; +} diff --git a/src/main/java/com/genersoft/iot/vmp/media/abl/bean/hook/OnRecordProgressABLHookParam.java b/src/main/java/com/genersoft/iot/vmp/media/abl/bean/hook/OnRecordProgressABLHookParam.java new file mode 100644 index 0000000..93e6c09 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/media/abl/bean/hook/OnRecordProgressABLHookParam.java @@ -0,0 +1,40 @@ +package com.genersoft.iot.vmp.media.abl.bean.hook; + +public class OnRecordProgressABLHookParam extends OnRecordMp4ABLHookParam{ + private Integer currentFileDuration; + private Integer TotalVideoDuration; + private String startTime; + private String endTime; + + public Integer getCurrentFileDuration() { + return currentFileDuration; + } + + public void setCurrentFileDuration(Integer currentFileDuration) { + this.currentFileDuration = currentFileDuration; + } + + public Integer getTotalVideoDuration() { + return TotalVideoDuration; + } + + public void setTotalVideoDuration(Integer totalVideoDuration) { + TotalVideoDuration = totalVideoDuration; + } + + public String getStartTime() { + return startTime; + } + + public void setStartTime(String startTime) { + this.startTime = startTime; + } + + public String getEndTime() { + return endTime; + } + + public void setEndTime(String endTime) { + this.endTime = endTime; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/media/abl/bean/hook/OnServerKeepaliveABLHookParam.java b/src/main/java/com/genersoft/iot/vmp/media/abl/bean/hook/OnServerKeepaliveABLHookParam.java new file mode 100644 index 0000000..ea1ac97 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/media/abl/bean/hook/OnServerKeepaliveABLHookParam.java @@ -0,0 +1,32 @@ +package com.genersoft.iot.vmp.media.abl.bean.hook; + +public class OnServerKeepaliveABLHookParam { + private String localipAddress; + private String mediaServerId; + private String datetime; + + + public String getLocalipAddress() { + return localipAddress; + } + + public void setLocalipAddress(String localipAddress) { + this.localipAddress = localipAddress; + } + + public String getMediaServerId() { + return mediaServerId; + } + + public void setMediaServerId(String mediaServerId) { + this.mediaServerId = mediaServerId; + } + + public String getDatetime() { + return datetime; + } + + public void setDatetime(String datetime) { + this.datetime = datetime; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/media/abl/bean/hook/OnServerStaredABLHookParam.java b/src/main/java/com/genersoft/iot/vmp/media/abl/bean/hook/OnServerStaredABLHookParam.java new file mode 100644 index 0000000..a9ec44c --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/media/abl/bean/hook/OnServerStaredABLHookParam.java @@ -0,0 +1,32 @@ +package com.genersoft.iot.vmp.media.abl.bean.hook; + +public class OnServerStaredABLHookParam { + private String localipAddress; + private String mediaServerId; + private String datetime; + + + public String getLocalipAddress() { + return localipAddress; + } + + public void setLocalipAddress(String localipAddress) { + this.localipAddress = localipAddress; + } + + public String getMediaServerId() { + return mediaServerId; + } + + public void setMediaServerId(String mediaServerId) { + this.mediaServerId = mediaServerId; + } + + public String getDatetime() { + return datetime; + } + + public void setDatetime(String datetime) { + this.datetime = datetime; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/media/abl/bean/hook/OnStreamArriveABLHookParam.java b/src/main/java/com/genersoft/iot/vmp/media/abl/bean/hook/OnStreamArriveABLHookParam.java new file mode 100644 index 0000000..032a517 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/media/abl/bean/hook/OnStreamArriveABLHookParam.java @@ -0,0 +1,112 @@ +package com.genersoft.iot.vmp.media.abl.bean.hook; + +import com.genersoft.iot.vmp.media.abl.bean.ABLUrls; +import lombok.Getter; +import lombok.Setter; + +/** + * 流到来的事件 + */ +@Getter +@Setter +public class OnStreamArriveABLHookParam extends ABLHookParam{ + + + + /** + * 推流鉴权Id + */ + private String callId; + + /** + * 状态 + */ + private Boolean status; + + + /** + * + */ + private Boolean enableHls; + + + /** + * + */ + private Boolean transcodingStatus; + + + /** + * + */ + private String sourceURL; + + + /** + * + */ + private Integer readerCount; + + + /** + * + */ + private Integer noneReaderDuration; + + + /** + * + */ + private String videoCodec; + + + /** + * + */ + private Integer videoFrameSpeed; + + + /** + * + */ + private Integer width; + + + /** + * + */ + private Integer height; + + + /** + * + */ + private Integer videoBitrate; + + + /** + * + */ + private String audioCodec; + + + /** + * + */ + private Integer audioChannels; + + + /** + * + */ + private Integer audioSampleRate; + + + /** + * + */ + private Integer audioBitrate; + + + private ABLUrls url; +} diff --git a/src/main/java/com/genersoft/iot/vmp/media/abl/event/HookAblServerKeepaliveEvent.java b/src/main/java/com/genersoft/iot/vmp/media/abl/event/HookAblServerKeepaliveEvent.java new file mode 100644 index 0000000..74465e4 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/media/abl/event/HookAblServerKeepaliveEvent.java @@ -0,0 +1,24 @@ +package com.genersoft.iot.vmp.media.abl.event; + +import com.genersoft.iot.vmp.media.bean.MediaServer; +import org.springframework.context.ApplicationEvent; + +/** + * zlm 心跳事件 + */ +public class HookAblServerKeepaliveEvent extends ApplicationEvent { + + public HookAblServerKeepaliveEvent(Object source) { + super(source); + } + + private MediaServer mediaServerItem; + + public MediaServer getMediaServerItem() { + return mediaServerItem; + } + + public void setMediaServerItem(MediaServer mediaServerItem) { + this.mediaServerItem = mediaServerItem; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/media/abl/event/HookAblServerStartEvent.java b/src/main/java/com/genersoft/iot/vmp/media/abl/event/HookAblServerStartEvent.java new file mode 100644 index 0000000..12bdac0 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/media/abl/event/HookAblServerStartEvent.java @@ -0,0 +1,24 @@ +package com.genersoft.iot.vmp.media.abl.event; + +import com.genersoft.iot.vmp.media.bean.MediaServer; +import org.springframework.context.ApplicationEvent; + +/** + * zlm server_start事件 + */ +public class HookAblServerStartEvent extends ApplicationEvent { + + public HookAblServerStartEvent(Object source) { + super(source); + } + + private MediaServer mediaServerItem; + + public MediaServer getMediaServerItem() { + return mediaServerItem; + } + + public void setMediaServerItem(MediaServer mediaServerItem) { + this.mediaServerItem = mediaServerItem; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/media/bean/MediaInfo.java b/src/main/java/com/genersoft/iot/vmp/media/bean/MediaInfo.java new file mode 100644 index 0000000..54cf6eb --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/media/bean/MediaInfo.java @@ -0,0 +1,308 @@ +package com.genersoft.iot.vmp.media.bean; + +import com.alibaba.fastjson2.JSONArray; +import com.alibaba.fastjson2.JSONObject; +import com.genersoft.iot.vmp.media.abl.bean.ABLMedia; +import com.genersoft.iot.vmp.media.abl.bean.hook.OnStreamArriveABLHookParam; +import com.genersoft.iot.vmp.media.zlm.dto.hook.OnStreamChangedHookParam; +import com.genersoft.iot.vmp.media.zlm.dto.hook.OriginType; +import com.genersoft.iot.vmp.utils.MediaServerUtils; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import org.springframework.util.ObjectUtils; + +import java.util.List; +import java.util.Map; + +/** + * 视频信息 + */ +@Data +@Schema(description = "视频信息") +public class MediaInfo { + @Schema(description = "应用名") + private String app; + @Schema(description = "流ID") + private String stream; + @Schema(description = "流媒体节点") + private MediaServer mediaServer; + @Schema(description = "协议") + private String schema; + + @Schema(description = "观看人数") + private Integer readerCount; + @Schema(description = "视频编码类型") + private String videoCodec; + @Schema(description = "视频宽度") + private Integer width; + @Schema(description = "视频高度") + private Integer height; + @Schema(description = "FPS") + private Integer fps; + @Schema(description = "丢包率") + private Integer loss; + @Schema(description = "音频编码类型") + private String audioCodec; + @Schema(description = "音频通道数") + private Integer audioChannels; + @Schema(description = "音频采样率") + private Integer audioSampleRate; + @Schema(description = "时长") + private Long duration; + @Schema(description = "在线") + private Boolean online; + @Schema(description = "unknown = 0,rtmp_push=1,rtsp_push=2,rtp_push=3,pull=4,ffmpeg_pull=5,mp4_vod=6,device_chn=7,rtc_push=8") + private Integer originType; + @Schema(description = "originType的文本描述") + private String originTypeStr; + @Schema(description = "产生流的源流地址") + private String originUrl; + @Schema(description = "存活时间,单位秒") + private Long aliveSecond; + @Schema(description = "数据产生速度,单位byte/s") + private Long bytesSpeed; + @Schema(description = "鉴权参数") + private String callId; + @Schema(description = "额外参数") + private Map paramMap; + @Schema(description = "服务ID") + private String serverId; + + + public static MediaInfo getInstance(JSONObject jsonObject, MediaServer mediaServer, String serverId) { + MediaInfo mediaInfo = new MediaInfo(); + mediaInfo.setMediaServer(mediaServer); + mediaInfo.setServerId(serverId); + String app = jsonObject.getString("app"); + mediaInfo.setApp(app); + String stream = jsonObject.getString("stream"); + mediaInfo.setStream(stream); + String schema = jsonObject.getString("schema"); + mediaInfo.setSchema(schema); + Integer totalReaderCount = jsonObject.getInteger("totalReaderCount"); + Boolean online = jsonObject.getBoolean("online"); + Integer originType = jsonObject.getInteger("originType"); + String originUrl = jsonObject.getString("originUrl"); + String originTypeStr = jsonObject.getString("originTypeStr"); + Long aliveSecond = jsonObject.getLong("aliveSecond"); + String params = jsonObject.getString("params"); + Long bytesSpeed = jsonObject.getLong("bytesSpeed"); + if (totalReaderCount != null) { + mediaInfo.setReaderCount(totalReaderCount); + } else { + mediaInfo.setReaderCount(0); + } + if (online != null) { + mediaInfo.setOnline(online); + } + if (originType != null) { + mediaInfo.setOriginType(originType); + } + if (originTypeStr != null) { + mediaInfo.setOriginTypeStr(originTypeStr); + } + + if (aliveSecond != null) { + mediaInfo.setAliveSecond(aliveSecond); + } + if (bytesSpeed != null) { + mediaInfo.setBytesSpeed(bytesSpeed); + } + if (params != null) { + mediaInfo.setParamMap(MediaServerUtils.urlParamToMap(params)); + if(mediaInfo.getCallId() == null) { + mediaInfo.setCallId(mediaInfo.getParamMap().get("callId")); + } + } + JSONArray jsonArray = jsonObject.getJSONArray("tracks"); + if (!ObjectUtils.isEmpty(jsonArray)) { + for (int i = 0; i < jsonArray.size(); i++) { + JSONObject trackJson = jsonArray.getJSONObject(i); + Integer channels = trackJson.getInteger("channels"); + Integer codecId = trackJson.getInteger("codec_id"); + Integer codecType = trackJson.getInteger("codec_type"); + Integer sampleRate = trackJson.getInteger("sample_rate"); + Integer height = trackJson.getInteger("height"); + Integer width = trackJson.getInteger("width"); + Integer fps = trackJson.getInteger("fps"); + Integer loss = trackJson.getInteger("loss"); + Integer frames = trackJson.getInteger("frames"); + Long keyFrames = trackJson.getLongValue("key_frames"); + Integer gop_interval_ms = trackJson.getInteger("gop_interval_ms"); + Long gop_size = trackJson.getLongValue("gop_size"); + + Long duration = trackJson.getLongValue("duration"); + if (channels != null) { + mediaInfo.setAudioChannels(channels); + } + if (sampleRate != null) { + mediaInfo.setAudioSampleRate(sampleRate); + } + if (height != null) { + mediaInfo.setHeight(height); + } + if (width != null) { + mediaInfo.setWidth(width); + } + if (fps != null) { + mediaInfo.setFps(fps); + } + if (loss != null) { + mediaInfo.setLoss(loss); + } + if (duration > 0L) { + mediaInfo.setDuration(duration); + } + if (codecId != null) { + switch (codecId) { + case 0: + mediaInfo.setVideoCodec("H264"); + break; + case 1: + mediaInfo.setVideoCodec("H265"); + break; + case 2: + mediaInfo.setAudioCodec("AAC"); + break; + case 3: + mediaInfo.setAudioCodec("G711A"); + break; + case 4: + mediaInfo.setAudioCodec("G711U"); + break; + } + } + } + } + return mediaInfo; + } + + public static MediaInfo getInstance(OnStreamChangedHookParam param, MediaServer mediaServer, String serverId) { + + MediaInfo mediaInfo = new MediaInfo(); + mediaInfo.setApp(param.getApp()); + mediaInfo.setStream(param.getStream()); + mediaInfo.setSchema(param.getSchema()); + mediaInfo.setMediaServer(mediaServer); + mediaInfo.setReaderCount(param.getTotalReaderCount()); + mediaInfo.setOnline(param.isRegist()); + mediaInfo.setOriginType(param.getOriginType()); + mediaInfo.setOriginTypeStr(param.getOriginTypeStr()); + mediaInfo.setOriginUrl(param.getOriginUrl()); + mediaInfo.setOriginUrl(param.getOriginUrl()); + mediaInfo.setAliveSecond(param.getAliveSecond()); + mediaInfo.setBytesSpeed(param.getBytesSpeed()); + mediaInfo.setParamMap(param.getParamMap()); + if(mediaInfo.getCallId() == null) { + mediaInfo.setCallId(param.getParamMap().get("callId")); + } + mediaInfo.setServerId(serverId); + List tracks = param.getTracks(); + if (tracks == null || tracks.isEmpty()) { + return mediaInfo; + } + for (OnStreamChangedHookParam.MediaTrack mediaTrack : tracks) { + switch (mediaTrack.getCodec_id()) { + case 0: + mediaInfo.setVideoCodec("H264"); + break; + case 1: + mediaInfo.setVideoCodec("H265"); + break; + case 2: + mediaInfo.setAudioCodec("AAC"); + break; + case 3: + mediaInfo.setAudioCodec("G711A"); + break; + case 4: + mediaInfo.setAudioCodec("G711U"); + break; + } + if (mediaTrack.getSample_rate() > 0) { + mediaInfo.setAudioSampleRate(mediaTrack.getSample_rate()); + } + if (mediaTrack.getChannels() > 0) { + mediaInfo.setAudioChannels(mediaTrack.getChannels()); + } + if (mediaTrack.getHeight() > 0) { + mediaInfo.setHeight(mediaTrack.getHeight()); + } + if (mediaTrack.getWidth() > 0) { + mediaInfo.setWidth(mediaTrack.getWidth()); + } + } + return mediaInfo; + } + + public static MediaInfo getInstance(OnStreamArriveABLHookParam param, MediaServer mediaServer) { + + MediaInfo mediaInfo = new MediaInfo(); + mediaInfo.setApp(param.getApp()); + mediaInfo.setStream(param.getStream()); + mediaInfo.setMediaServer(mediaServer); + mediaInfo.setReaderCount(param.getReaderCount()); + mediaInfo.setOnline(true); + mediaInfo.setVideoCodec(param.getVideoCodec()); + switch (param.getNetworkType()) { + case 21: + mediaInfo.setOriginType(OriginType.RTMP_PUSH.ordinal()); + break; + case 23: + mediaInfo.setOriginType(OriginType.RTSP_PUSH.ordinal()); + break; + case 30: + case 31: + case 32: + case 33: + mediaInfo.setOriginType(OriginType.PULL.ordinal()); + break; + default: + mediaInfo.setOriginType(OriginType.UNKNOWN.ordinal()); + break; + + } + mediaInfo.setWidth(param.getWidth()); + mediaInfo.setHeight(param.getHeight()); + mediaInfo.setAudioCodec(param.getAudioCodec()); + mediaInfo.setAudioChannels(param.getAudioChannels()); + mediaInfo.setAudioSampleRate(param.getAudioSampleRate()); + + return mediaInfo; + } + + public static MediaInfo getInstance(ABLMedia ablMedia, MediaServer mediaServer) { + MediaInfo mediaInfo = new MediaInfo(); + mediaInfo.setApp(ablMedia.getApp()); + mediaInfo.setStream(ablMedia.getStream()); + mediaInfo.setMediaServer(mediaServer); + mediaInfo.setReaderCount(ablMedia.getReaderCount()); + mediaInfo.setOnline(true); + mediaInfo.setVideoCodec(ablMedia.getVideoCodec()); + switch (ablMedia.getNetworkType()) { + case 21: + mediaInfo.setOriginType(OriginType.RTMP_PUSH.ordinal()); + break; + case 23: + mediaInfo.setOriginType(OriginType.RTSP_PUSH.ordinal()); + break; + case 30: + case 31: + case 32: + case 33: + mediaInfo.setOriginType(OriginType.PULL.ordinal()); + break; + default: + mediaInfo.setOriginType(OriginType.UNKNOWN.ordinal()); + break; + + } + mediaInfo.setWidth(ablMedia.getWidth()); + mediaInfo.setHeight(ablMedia.getHeight()); + mediaInfo.setAudioCodec(ablMedia.getAudioCodec()); + mediaInfo.setAudioChannels(ablMedia.getAudioChannels()); + mediaInfo.setAudioSampleRate(ablMedia.getAudioSampleRate()); + + return mediaInfo; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/media/bean/MediaServer.java b/src/main/java/com/genersoft/iot/vmp/media/bean/MediaServer.java new file mode 100755 index 0000000..38de639 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/media/bean/MediaServer.java @@ -0,0 +1,163 @@ +package com.genersoft.iot.vmp.media.bean; + + +import com.genersoft.iot.vmp.media.abl.bean.AblServerConfig; +import com.genersoft.iot.vmp.media.zlm.dto.ZLMServerConfig; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import org.springframework.util.ObjectUtils; + +@Schema(description = "流媒体服务信息") +@Data +public class MediaServer { + + @Schema(description = "ID") + private String id; + + @Schema(description = "IP") + private String ip; + + @Schema(description = "hook使用的IP(zlm访问WVP使用的IP)") + private String hookIp = "127.0.0.1"; + + @Schema(description = "SDP IP") + private String sdpIp; + + @Schema(description = "流IP") + private String streamIp; + + @Schema(description = "HTTP端口") + private int httpPort; + + @Schema(description = "HTTPS端口") + private int httpSSlPort; + + @Schema(description = "RTMP端口") + private int rtmpPort; + + @Schema(description = "flv端口") + private int flvPort; + + @Schema(description = "https-flv端口") + private int flvSSLPort; + + @Schema(description = "mp4端口") + private int mp4Port; + + @Schema(description = "ws-flv端口") + private int wsFlvPort; + + @Schema(description = "wss-flv端口") + private int wsFlvSSLPort; + + @Schema(description = "RTMPS端口") + private int rtmpSSlPort; + + @Schema(description = "RTP收流端口(单端口模式有用)") + private int rtpProxyPort; + + @Schema(description = "1078收流端口(单端口模式有用)") + private int jttProxyPort; + + @Schema(description = "RTSP端口") + private int rtspPort; + + @Schema(description = "RTSPS端口") + private int rtspSSLPort; + + @Schema(description = "是否开启自动配置ZLM") + private boolean autoConfig; + + @Schema(description = "ZLM鉴权参数") + private String secret; + + @Schema(description = "keepalive hook触发间隔,单位秒") + private Float hookAliveInterval; + + @Schema(description = "是否使用多端口模式") + private boolean rtpEnable; + + @Schema(description = "状态") + private boolean status; + + @Schema(description = "多端口RTP收流端口范围") + private String rtpPortRange; + + @Schema(description = "RTP发流端口范围") + private String sendRtpPortRange; + + @Schema(description = "assist服务端口") + private int recordAssistPort; + + @Schema(description = "创建时间") + private String createTime; + + @Schema(description = "更新时间") + private String updateTime; + + @Schema(description = "上次心跳时间") + private String lastKeepaliveTime; + + @Schema(description = "是否是默认ZLM") + private boolean defaultServer; + + @Schema(description = "录像存储时长") + private int recordDay; + + @Schema(description = "录像存储路径") + private String recordPath; + @Schema(description = "类型: zlm/abl") + private String type; + + @Schema(description = "转码的前缀") + private String transcodeSuffix; + + @Schema(description = "服务Id") + private String serverId; + + public MediaServer() { + } + + public MediaServer(ZLMServerConfig zlmServerConfig, String sipIp) { + id = zlmServerConfig.getGeneralMediaServerId(); + ip = zlmServerConfig.getIp(); + hookIp = ObjectUtils.isEmpty(zlmServerConfig.getHookIp())? sipIp: zlmServerConfig.getHookIp(); + sdpIp = ObjectUtils.isEmpty(zlmServerConfig.getSdpIp())? zlmServerConfig.getIp(): zlmServerConfig.getSdpIp(); + streamIp = ObjectUtils.isEmpty(zlmServerConfig.getStreamIp())? zlmServerConfig.getIp(): zlmServerConfig.getStreamIp(); + httpPort = zlmServerConfig.getHttpPort(); + httpSSlPort = zlmServerConfig.getHttpSSLport(); + rtmpPort = zlmServerConfig.getRtmpPort(); + rtmpSSlPort = zlmServerConfig.getRtmpSslPort(); + rtpProxyPort = zlmServerConfig.getRtpProxyPort(); + rtspPort = zlmServerConfig.getRtspPort(); + rtspSSLPort = zlmServerConfig.getRtspSSlport(); + autoConfig = true; // 默认值true; + secret = zlmServerConfig.getApiSecret(); + hookAliveInterval = zlmServerConfig.getHookAliveInterval(); + rtpEnable = false; // 默认使用单端口;直到用户自己设置开启多端口 + rtpPortRange = zlmServerConfig.getPortRange().replace("_",","); // 默认使用30000,30500作为级联时发送流的端口号 + recordAssistPort = 0; // 默认关闭 + transcodeSuffix = zlmServerConfig.getTranscodeSuffix(); + + } + + public MediaServer(AblServerConfig config, String sipIp) { + id = config.getMediaServerId(); + ip = config.getServerIp(); + hookIp = sipIp; + sdpIp = config.getServerIp(); + streamIp = config.getServerIp(); + httpPort = config.getHttpServerPort(); + flvPort = config.getHttpFlvPort(); + mp4Port = config.getHttpMp4Port(); + wsFlvPort = config.getWsFlvPort(); + rtmpPort = config.getRtmpPort(); + rtpProxyPort = config.getJtt1078RecvPort(); + rtspPort = config.getRtspPort(); + autoConfig = true; // 默认值true; + secret = config.getSecret(); + rtpEnable = false; // 默认使用单端口;直到用户自己设置开启多端口 + rtpPortRange = "30000,30500"; // 默认使用30000,30500作为级联时发送流的端口号 + recordAssistPort = 0; // 默认关闭 + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/media/bean/RecordInfo.java b/src/main/java/com/genersoft/iot/vmp/media/bean/RecordInfo.java new file mode 100644 index 0000000..b903058 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/media/bean/RecordInfo.java @@ -0,0 +1,76 @@ +package com.genersoft.iot.vmp.media.bean; + +import com.genersoft.iot.vmp.media.abl.bean.hook.OnRecordMp4ABLHookParam; +import com.genersoft.iot.vmp.media.zlm.dto.hook.OnRecordMp4HookParam; +import com.genersoft.iot.vmp.service.bean.CloudRecordItem; +import com.genersoft.iot.vmp.utils.DateUtil; +import lombok.Data; + +@Data +public class RecordInfo { + private String app; + private String stream; + private String fileName; + private String filePath; + private long fileSize; + private String folder; + private String url; + /** + * 单位毫秒 + */ + private long startTime; + /** + * 单位毫秒 + */ + private double timeLen; + private String params; + + public static RecordInfo getInstance(OnRecordMp4HookParam hookParam) { + RecordInfo recordInfo = new RecordInfo(); + recordInfo.setApp(hookParam.getApp()); + recordInfo.setStream(hookParam.getStream()); + recordInfo.setFileName(hookParam.getFile_name()); + recordInfo.setUrl(hookParam.getUrl()); + recordInfo.setFolder(hookParam.getFolder()); + recordInfo.setFilePath(hookParam.getFile_path()); + recordInfo.setFileSize(hookParam.getFile_size()); + recordInfo.setStartTime(hookParam.getStart_time() * 1000); + recordInfo.setTimeLen(hookParam.getTime_len() * 1000); + return recordInfo; + } + + public static RecordInfo getInstance(OnRecordMp4ABLHookParam hookParam) { + RecordInfo recordInfo = new RecordInfo(); + recordInfo.setApp(hookParam.getApp()); + recordInfo.setStream(hookParam.getStream()); + recordInfo.setFileName(hookParam.getFileName()); + recordInfo.setStartTime(DateUtil.urlToTimestampMs(hookParam.getStartTime())); + recordInfo.setTimeLen(DateUtil.urlToTimestampMs(hookParam.getEndTime()) - recordInfo.getStartTime()); + recordInfo.setFileSize(hookParam.getFileSize()); + return recordInfo; + } + + public static RecordInfo getInstance(CloudRecordItem cloudRecordItem) { + RecordInfo recordInfo = new RecordInfo(); + recordInfo.setApp(cloudRecordItem.getApp()); + recordInfo.setStream(cloudRecordItem.getStream()); + recordInfo.setFileName(cloudRecordItem.getFileName()); + recordInfo.setStartTime(cloudRecordItem.getStartTime()); + recordInfo.setTimeLen(cloudRecordItem.getTimeLen()); + recordInfo.setFileSize(cloudRecordItem.getFileSize()); + recordInfo.setFilePath(cloudRecordItem.getFilePath()); + return recordInfo; + } + + @Override + public String toString() { + return "RecordInfo{" + + "文件名称='" + fileName + '\'' + + ", 文件路径='" + filePath + '\'' + + ", 文件大小=" + fileSize + + ", 开始时间=" + startTime + + ", 时长=" + timeLen + + ", params=" + params + + '}'; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/media/bean/ResultForOnPublish.java b/src/main/java/com/genersoft/iot/vmp/media/bean/ResultForOnPublish.java new file mode 100644 index 0000000..88f7387 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/media/bean/ResultForOnPublish.java @@ -0,0 +1,59 @@ +package com.genersoft.iot.vmp.media.bean; + +public class ResultForOnPublish { + + private boolean enable_audio; + private boolean enable_mp4; + private int mp4_max_second; + private String mp4_save_path; + private String stream_replace; + private Integer modify_stamp; + + public boolean isEnable_audio() { + return enable_audio; + } + + public void setEnable_audio(boolean enable_audio) { + this.enable_audio = enable_audio; + } + + public boolean isEnable_mp4() { + return enable_mp4; + } + + public void setEnable_mp4(boolean enable_mp4) { + this.enable_mp4 = enable_mp4; + } + + public int getMp4_max_second() { + return mp4_max_second; + } + + public void setMp4_max_second(int mp4_max_second) { + this.mp4_max_second = mp4_max_second; + } + + public String getMp4_save_path() { + return mp4_save_path; + } + + public void setMp4_save_path(String mp4_save_path) { + this.mp4_save_path = mp4_save_path; + } + + public String getStream_replace() { + return stream_replace; + } + + public void setStream_replace(String stream_replace) { + this.stream_replace = stream_replace; + } + + public Integer getModify_stamp() { + return modify_stamp; + } + + public void setModify_stamp(Integer modify_stamp) { + this.modify_stamp = modify_stamp; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/media/event/hook/Hook.java b/src/main/java/com/genersoft/iot/vmp/media/event/hook/Hook.java new file mode 100755 index 0000000..0d1c55a --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/media/event/hook/Hook.java @@ -0,0 +1,51 @@ +package com.genersoft.iot.vmp.media.event.hook; + +import lombok.Data; + +/** + * zlm hook事件的参数 + * @author lin + */ +@Data +public class Hook { + + private HookType hookType; + + private String app; + + private String stream; + + private Long expireTime; + + + public static Hook getInstance(HookType hookType, String app, String stream) { + Hook hookSubscribe = new Hook(); + hookSubscribe.setApp(app); + hookSubscribe.setStream(stream); + hookSubscribe.setHookType(hookType); + hookSubscribe.setExpireTime(System.currentTimeMillis() + 5 * 60 * 1000); + return hookSubscribe; + } + + public static Hook getInstance(HookType hookType, String app, String stream, String mediaServer) { + // TODO 后续修改所有方法 + return Hook.getInstance(hookType, app, stream); + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof Hook) { + Hook param = (Hook) obj; + return param.getHookType().equals(this.hookType) + && param.getApp().equals(this.app) + && param.getStream().equals(this.stream); + }else { + return false; + } + } + + @Override + public String toString() { + return this.getHookType() + this.getApp() + this.getStream(); + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/media/event/hook/HookData.java b/src/main/java/com/genersoft/iot/vmp/media/event/hook/HookData.java new file mode 100644 index 0000000..6aa3a67 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/media/event/hook/HookData.java @@ -0,0 +1,78 @@ +package com.genersoft.iot.vmp.media.event.hook; + +import com.genersoft.iot.vmp.media.bean.MediaInfo; +import com.genersoft.iot.vmp.media.bean.RecordInfo; +import com.genersoft.iot.vmp.media.event.media.MediaArrivalEvent; +import com.genersoft.iot.vmp.media.event.media.MediaEvent; +import com.genersoft.iot.vmp.media.event.media.MediaPublishEvent; +import com.genersoft.iot.vmp.media.event.media.MediaRecordMp4Event; +import com.genersoft.iot.vmp.media.bean.MediaServer; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +/** + * Hook返回的内容 + */ +@Data +public class HookData { + /** + * 应用名 + */ + private String app; + /** + * 流ID + */ + private String stream; + /** + * 流媒体节点 + */ + private MediaServer mediaServer; + /** + * 协议 + */ + private String schema; + + /** + * 流信息 + */ + private MediaInfo mediaInfo; + + /** + * 录像信息 + */ + private RecordInfo recordInfo; + + @Schema(description = "推流的额外参数") + private String params; + public static HookData getInstance(MediaEvent mediaEvent) { + HookData hookData = new HookData(); + if (mediaEvent instanceof MediaPublishEvent) { + MediaPublishEvent event = (MediaPublishEvent) mediaEvent; + hookData.setApp(event.getApp()); + hookData.setStream(event.getStream()); + hookData.setSchema(event.getSchema()); + hookData.setMediaServer(event.getMediaServer()); + hookData.setParams(event.getParams()); + }else if (mediaEvent instanceof MediaArrivalEvent) { + MediaArrivalEvent event = (MediaArrivalEvent) mediaEvent; + hookData.setApp(event.getApp()); + hookData.setStream(event.getStream()); + hookData.setSchema(event.getSchema()); + hookData.setMediaServer(event.getMediaServer()); + hookData.setMediaInfo(event.getMediaInfo()); + }else if (mediaEvent instanceof MediaRecordMp4Event) { + MediaRecordMp4Event event = (MediaRecordMp4Event) mediaEvent; + hookData.setApp(event.getApp()); + hookData.setStream(event.getStream()); + hookData.setSchema(event.getSchema()); + hookData.setMediaServer(event.getMediaServer()); + hookData.setRecordInfo(event.getRecordInfo()); + }else { + hookData.setApp(mediaEvent.getApp()); + hookData.setStream(mediaEvent.getStream()); + hookData.setSchema(mediaEvent.getSchema()); + hookData.setMediaServer(mediaEvent.getMediaServer()); + } + return hookData; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/media/event/hook/HookSubscribe.java b/src/main/java/com/genersoft/iot/vmp/media/event/hook/HookSubscribe.java new file mode 100755 index 0000000..c26f3dc --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/media/event/hook/HookSubscribe.java @@ -0,0 +1,114 @@ +package com.genersoft.iot.vmp.media.event.hook; + +import com.genersoft.iot.vmp.media.event.media.*; +import org.springframework.context.event.EventListener; +import org.springframework.scheduling.annotation.Async; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * zlm hook事件的参数 + * @author lin + */ +@Component +public class HookSubscribe { + + /** + * 订阅数据过期时间 + */ + private final long subscribeExpire = 5 * 60 * 1000; + + + @FunctionalInterface + public interface Event{ + void response(HookData data); + } + + /** + * 流到来的处理 + */ + @Async("taskExecutor") + @EventListener + public void onApplicationEvent(MediaArrivalEvent event) { + if (event.getSchema() == null || "rtsp".equals(event.getSchema())) { + sendNotify(HookType.on_media_arrival, event); + } + + } + + /** + * 流结束事件 + */ + @Async("taskExecutor") + @EventListener + public void onApplicationEvent(MediaDepartureEvent event) { + if (event.getSchema() == null || "rtsp".equals(event.getSchema())) { + sendNotify(HookType.on_media_departure, event); + } + + } + /** + * 推流鉴权事件 + */ + @Async("taskExecutor") + @EventListener + public void onApplicationEvent(MediaPublishEvent event) { + sendNotify(HookType.on_publish, event); + } + /** + * 生成录像文件事件 + */ + @Async("taskExecutor") + @EventListener + public void onApplicationEvent(MediaRecordMp4Event event) { + sendNotify(HookType.on_record_mp4, event); + } + + private final Map allSubscribes = new ConcurrentHashMap<>(); + private final Map allHook = new ConcurrentHashMap<>(); + + private void sendNotify(HookType hookType, MediaEvent event) { + Hook paramHook = Hook.getInstance(hookType, event.getApp(), event.getStream()); + Event hookSubscribeEvent = allSubscribes.get(paramHook.toString()); + if (hookSubscribeEvent != null) { + HookData data = HookData.getInstance(event); + hookSubscribeEvent.response(data); + } + } + + public void addSubscribe(Hook hook, HookSubscribe.Event event) { + if (hook.getExpireTime() == null) { + hook.setExpireTime(System.currentTimeMillis() + subscribeExpire); + } + allSubscribes.put(hook.toString(), event); + allHook.put(hook.toString(), hook); + } + + public void removeSubscribe(Hook hook) { + allSubscribes.remove(hook.toString()); + allHook.remove(hook.toString()); + } + + /** + * 对订阅数据进行过期清理 + */ + @Scheduled(fixedRate=subscribeExpire) //每5分钟执行一次 + public void execute(){ + long expireTime = System.currentTimeMillis(); + for (Hook hook : allHook.values()) { + if (hook.getExpireTime() < expireTime) { + allSubscribes.remove(hook.toString()); + allHook.remove(hook.toString()); + } + } + } + + public List getAll() { + return new ArrayList<>(allHook.values()); + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/media/event/hook/HookType.java b/src/main/java/com/genersoft/iot/vmp/media/event/hook/HookType.java new file mode 100755 index 0000000..58bd656 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/media/event/hook/HookType.java @@ -0,0 +1,15 @@ +package com.genersoft.iot.vmp.media.event.hook; + +/** + * hook类型 + * @author lin + */ + +public enum HookType { + + on_publish, + on_record_mp4, + on_media_arrival, + on_media_departure, + on_rtp_server_timeout, +} diff --git a/src/main/java/com/genersoft/iot/vmp/media/event/media/MediaArrivalEvent.java b/src/main/java/com/genersoft/iot/vmp/media/event/media/MediaArrivalEvent.java new file mode 100644 index 0000000..f26f50b --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/media/event/media/MediaArrivalEvent.java @@ -0,0 +1,64 @@ +package com.genersoft.iot.vmp.media.event.media; + +import com.genersoft.iot.vmp.media.abl.bean.hook.OnStreamArriveABLHookParam; +import com.genersoft.iot.vmp.media.bean.MediaInfo; +import com.genersoft.iot.vmp.media.bean.MediaServer; +import com.genersoft.iot.vmp.media.zlm.dto.hook.OnStreamChangedHookParam; +import com.genersoft.iot.vmp.vmanager.bean.StreamContent; +import lombok.Getter; +import lombok.Setter; + +import java.util.Map; + +/** + * 流到来事件 + */ + +public class MediaArrivalEvent extends MediaEvent { + public MediaArrivalEvent(Object source) { + super(source); + } + + public static MediaArrivalEvent getInstance(Object source, OnStreamChangedHookParam hookParam, MediaServer mediaServer, String serverId){ + MediaArrivalEvent mediaArrivalEvent = new MediaArrivalEvent(source); + mediaArrivalEvent.setMediaInfo(MediaInfo.getInstance(hookParam, mediaServer, serverId)); + mediaArrivalEvent.setApp(hookParam.getApp()); + mediaArrivalEvent.setStream(hookParam.getStream()); + mediaArrivalEvent.setMediaServer(mediaServer); + mediaArrivalEvent.setSchema(hookParam.getSchema()); + mediaArrivalEvent.setSchema(hookParam.getSchema()); + mediaArrivalEvent.setParamMap(hookParam.getParamMap()); + return mediaArrivalEvent; + } + public static MediaArrivalEvent getInstance(Object source, OnStreamArriveABLHookParam hookParam, MediaServer mediaServer){ + MediaArrivalEvent mediaArrivalEvent = new MediaArrivalEvent(source); + mediaArrivalEvent.setMediaInfo(MediaInfo.getInstance(hookParam, mediaServer)); + mediaArrivalEvent.setApp(hookParam.getApp()); + mediaArrivalEvent.setStream(hookParam.getStream()); + mediaArrivalEvent.setMediaServer(mediaServer); + mediaArrivalEvent.setCallId(hookParam.getCallId()); + return mediaArrivalEvent; + } + + @Getter + @Setter + private MediaInfo mediaInfo; + + @Getter + @Setter + private String callId; + + @Getter + @Setter + private StreamContent streamInfo; + + @Getter + @Setter + private Map paramMap; + + @Getter + @Setter + private String serverId; + + +} diff --git a/src/main/java/com/genersoft/iot/vmp/media/event/media/MediaDepartureEvent.java b/src/main/java/com/genersoft/iot/vmp/media/event/media/MediaDepartureEvent.java new file mode 100644 index 0000000..02b99f3 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/media/event/media/MediaDepartureEvent.java @@ -0,0 +1,31 @@ +package com.genersoft.iot.vmp.media.event.media; + +import com.genersoft.iot.vmp.media.abl.bean.hook.ABLHookParam; +import com.genersoft.iot.vmp.media.bean.MediaServer; +import com.genersoft.iot.vmp.media.zlm.dto.hook.OnStreamChangedHookParam; + +/** + * 流离开事件 + */ +public class MediaDepartureEvent extends MediaEvent { + public MediaDepartureEvent(Object source) { + super(source); + } + + public static MediaDepartureEvent getInstance(Object source, OnStreamChangedHookParam hookParam, MediaServer mediaServer){ + MediaDepartureEvent mediaDepartureEven = new MediaDepartureEvent(source); + mediaDepartureEven.setApp(hookParam.getApp()); + mediaDepartureEven.setStream(hookParam.getStream()); + mediaDepartureEven.setSchema(hookParam.getSchema()); + mediaDepartureEven.setMediaServer(mediaServer); + return mediaDepartureEven; + } + + public static MediaDepartureEvent getInstance(Object source, ABLHookParam hookParam, MediaServer mediaServer){ + MediaDepartureEvent mediaDepartureEven = new MediaDepartureEvent(source); + mediaDepartureEven.setApp(hookParam.getApp()); + mediaDepartureEven.setStream(hookParam.getStream()); + mediaDepartureEven.setMediaServer(mediaServer); + return mediaDepartureEven; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/media/event/media/MediaEvent.java b/src/main/java/com/genersoft/iot/vmp/media/event/media/MediaEvent.java new file mode 100644 index 0000000..1ddcdd4 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/media/event/media/MediaEvent.java @@ -0,0 +1,66 @@ +package com.genersoft.iot.vmp.media.event.media; + +import com.genersoft.iot.vmp.media.bean.MediaServer; +import org.springframework.context.ApplicationEvent; + +/** + * 流到来事件 + */ +public class MediaEvent extends ApplicationEvent { + + public MediaEvent(Object source) { + super(source); + } + + private String app; + + private String stream; + + private MediaServer mediaServer; + + private String schema; + + private String params; + + public String getParams() { + return params; + } + + public void setParams(String params) { + this.params = params; + } + + + public String getApp() { + return app; + } + + public void setApp(String app) { + this.app = app; + } + + public String getStream() { + return stream; + } + + public void setStream(String stream) { + this.stream = stream; + } + + public MediaServer getMediaServer() { + return mediaServer; + } + + public void setMediaServer(MediaServer mediaServer) { + this.mediaServer = mediaServer; + } + + public String getSchema() { + return schema; + } + + public void setSchema(String schema) { + this.schema = schema; + } + +} diff --git a/src/main/java/com/genersoft/iot/vmp/media/event/media/MediaNotFoundEvent.java b/src/main/java/com/genersoft/iot/vmp/media/event/media/MediaNotFoundEvent.java new file mode 100644 index 0000000..d4152ee --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/media/event/media/MediaNotFoundEvent.java @@ -0,0 +1,32 @@ +package com.genersoft.iot.vmp.media.event.media; + +import com.genersoft.iot.vmp.media.abl.bean.hook.ABLHookParam; +import com.genersoft.iot.vmp.media.bean.MediaServer; +import com.genersoft.iot.vmp.media.zlm.dto.hook.OnStreamNotFoundHookParam; + +/** + * 流未找到 + */ +public class MediaNotFoundEvent extends MediaEvent { + public MediaNotFoundEvent(Object source) { + super(source); + } + + public static MediaNotFoundEvent getInstance(Object source, OnStreamNotFoundHookParam hookParam, MediaServer mediaServer){ + MediaNotFoundEvent mediaDepartureEven = new MediaNotFoundEvent(source); + mediaDepartureEven.setApp(hookParam.getApp()); + mediaDepartureEven.setStream(hookParam.getStream()); + mediaDepartureEven.setSchema(hookParam.getSchema()); + mediaDepartureEven.setMediaServer(mediaServer); + mediaDepartureEven.setParams(hookParam.getParams()); + return mediaDepartureEven; + } + + public static MediaNotFoundEvent getInstance(Object source, ABLHookParam hookParam, MediaServer mediaServer){ + MediaNotFoundEvent mediaDepartureEven = new MediaNotFoundEvent(source); + mediaDepartureEven.setApp(hookParam.getApp()); + mediaDepartureEven.setStream(hookParam.getStream()); + mediaDepartureEven.setMediaServer(mediaServer); + return mediaDepartureEven; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/media/event/media/MediaPublishEvent.java b/src/main/java/com/genersoft/iot/vmp/media/event/media/MediaPublishEvent.java new file mode 100644 index 0000000..4b5fa70 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/media/event/media/MediaPublishEvent.java @@ -0,0 +1,24 @@ +package com.genersoft.iot.vmp.media.event.media; + +import com.genersoft.iot.vmp.media.bean.MediaServer; +import com.genersoft.iot.vmp.media.zlm.dto.hook.OnPublishHookParam; + +/** + * 推流鉴权事件 + */ +public class MediaPublishEvent extends MediaEvent { + public MediaPublishEvent(Object source) { + super(source); + } + + public static MediaPublishEvent getInstance(Object source, OnPublishHookParam hookParam, MediaServer mediaServer){ + MediaPublishEvent mediaPublishEvent = new MediaPublishEvent(source); + mediaPublishEvent.setApp(hookParam.getApp()); + mediaPublishEvent.setStream(hookParam.getStream()); + mediaPublishEvent.setMediaServer(mediaServer); + mediaPublishEvent.setSchema(hookParam.getSchema()); + mediaPublishEvent.setParams(hookParam.getParams()); + return mediaPublishEvent; + } + +} diff --git a/src/main/java/com/genersoft/iot/vmp/media/event/media/MediaRecordMp4Event.java b/src/main/java/com/genersoft/iot/vmp/media/event/media/MediaRecordMp4Event.java new file mode 100644 index 0000000..9fd6def --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/media/event/media/MediaRecordMp4Event.java @@ -0,0 +1,47 @@ +package com.genersoft.iot.vmp.media.event.media; + +import com.genersoft.iot.vmp.media.abl.ABLHttpHookListener; +import com.genersoft.iot.vmp.media.abl.bean.hook.OnRecordMp4ABLHookParam; +import com.genersoft.iot.vmp.media.bean.RecordInfo; +import com.genersoft.iot.vmp.media.bean.MediaServer; +import com.genersoft.iot.vmp.media.zlm.dto.hook.OnRecordMp4HookParam; + +/** + * 录像文件生成事件 + */ +public class MediaRecordMp4Event extends MediaEvent { + public MediaRecordMp4Event(Object source) { + super(source); + } + + private RecordInfo recordInfo; + + public static MediaRecordMp4Event getInstance(Object source, OnRecordMp4HookParam hookParam, MediaServer mediaServer){ + MediaRecordMp4Event mediaRecordMp4Event = new MediaRecordMp4Event(source); + mediaRecordMp4Event.setApp(hookParam.getApp()); + mediaRecordMp4Event.setStream(hookParam.getStream()); + RecordInfo recordInfo = RecordInfo.getInstance(hookParam); + mediaRecordMp4Event.setRecordInfo(recordInfo); + mediaRecordMp4Event.setMediaServer(mediaServer); + return mediaRecordMp4Event; + } + + public static MediaRecordMp4Event getInstance(ABLHttpHookListener source, OnRecordMp4ABLHookParam hookParam, MediaServer mediaServer) { + MediaRecordMp4Event mediaRecordMp4Event = new MediaRecordMp4Event(source); + mediaRecordMp4Event.setApp(hookParam.getApp()); + mediaRecordMp4Event.setStream(hookParam.getStream()); + RecordInfo recordInfo = RecordInfo.getInstance(hookParam); + mediaRecordMp4Event.setRecordInfo(recordInfo); + mediaRecordMp4Event.setMediaServer(mediaServer); + return mediaRecordMp4Event; + } + + public RecordInfo getRecordInfo() { + return recordInfo; + } + + public void setRecordInfo(RecordInfo recordInfo) { + this.recordInfo = recordInfo; + } + +} diff --git a/src/main/java/com/genersoft/iot/vmp/media/event/media/MediaRecordProcessEvent.java b/src/main/java/com/genersoft/iot/vmp/media/event/media/MediaRecordProcessEvent.java new file mode 100644 index 0000000..e4d1a56 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/media/event/media/MediaRecordProcessEvent.java @@ -0,0 +1,77 @@ +package com.genersoft.iot.vmp.media.event.media; + +import com.genersoft.iot.vmp.media.abl.ABLHttpHookListener; +import com.genersoft.iot.vmp.media.abl.bean.hook.OnRecordProgressABLHookParam; +import com.genersoft.iot.vmp.media.bean.MediaServer; +import com.genersoft.iot.vmp.utils.DateUtil; + +import java.util.Date; + +/** + * 录像文件进度通知事件 + */ +public class MediaRecordProcessEvent extends MediaEvent { + + private Integer currentFileDuration; + private Integer TotalVideoDuration; + private String fileName; + private long startTime; + private long endTime; + + public MediaRecordProcessEvent(Object source) { + super(source); + } + + public static MediaRecordProcessEvent getInstance(ABLHttpHookListener source, OnRecordProgressABLHookParam hookParam, MediaServer mediaServer) { + MediaRecordProcessEvent mediaRecordMp4Event = new MediaRecordProcessEvent(source); + mediaRecordMp4Event.setApp(hookParam.getApp()); + mediaRecordMp4Event.setStream(hookParam.getStream()); + mediaRecordMp4Event.setCurrentFileDuration(hookParam.getCurrentFileDuration()); + mediaRecordMp4Event.setTotalVideoDuration(hookParam.getTotalVideoDuration()); + mediaRecordMp4Event.setMediaServer(mediaServer); + mediaRecordMp4Event.setFileName(hookParam.getFileName()); + mediaRecordMp4Event.setStartTime(DateUtil.urlToTimestampMs(hookParam.getStartTime())); + mediaRecordMp4Event.setEndTime(DateUtil.urlToTimestampMs(hookParam.getEndTime())); + return mediaRecordMp4Event; + } + + public Integer getCurrentFileDuration() { + return currentFileDuration; + } + + public void setCurrentFileDuration(Integer currentFileDuration) { + this.currentFileDuration = currentFileDuration; + } + + public Integer getTotalVideoDuration() { + return TotalVideoDuration; + } + + public void setTotalVideoDuration(Integer totalVideoDuration) { + TotalVideoDuration = totalVideoDuration; + } + + public String getFileName() { + return fileName; + } + + public void setFileName(String fileName) { + this.fileName = fileName; + } + + public long getStartTime() { + return startTime; + } + + public void setStartTime(long startTime) { + this.startTime = startTime; + } + + public long getEndTime() { + return endTime; + } + + public void setEndTime(long endTime) { + this.endTime = endTime; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/media/event/media/MediaRtpServerTimeoutEvent.java b/src/main/java/com/genersoft/iot/vmp/media/event/media/MediaRtpServerTimeoutEvent.java new file mode 100644 index 0000000..b700dd5 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/media/event/media/MediaRtpServerTimeoutEvent.java @@ -0,0 +1,22 @@ +package com.genersoft.iot.vmp.media.event.media; + +import com.genersoft.iot.vmp.media.bean.MediaServer; +import com.genersoft.iot.vmp.media.zlm.dto.hook.OnStreamChangedHookParam; + +/** + * RtpServer收流超时事件 + */ +public class MediaRtpServerTimeoutEvent extends MediaEvent { + public MediaRtpServerTimeoutEvent(Object source) { + super(source); + } + + public static MediaRtpServerTimeoutEvent getInstance(Object source, OnStreamChangedHookParam hookParam, MediaServer mediaServer){ + MediaRtpServerTimeoutEvent mediaDepartureEven = new MediaRtpServerTimeoutEvent(source); + mediaDepartureEven.setApp(hookParam.getApp()); + mediaDepartureEven.setStream(hookParam.getStream()); + mediaDepartureEven.setSchema(hookParam.getSchema()); + mediaDepartureEven.setMediaServer(mediaServer); + return mediaDepartureEven; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/media/event/mediaServer/MediaSendRtpStoppedEvent.java b/src/main/java/com/genersoft/iot/vmp/media/event/mediaServer/MediaSendRtpStoppedEvent.java new file mode 100644 index 0000000..d37a4c2 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/media/event/mediaServer/MediaSendRtpStoppedEvent.java @@ -0,0 +1,52 @@ +package com.genersoft.iot.vmp.media.event.mediaServer; + +import com.genersoft.iot.vmp.media.bean.MediaServer; +import com.genersoft.iot.vmp.media.zlm.dto.hook.OnStreamNotFoundHookParam; +import org.springframework.context.ApplicationEvent; + +/** + * 发送流停止事件 + */ +public class MediaSendRtpStoppedEvent extends ApplicationEvent { + public MediaSendRtpStoppedEvent(Object source) { + super(source); + } + + private String app; + + private String stream; + + private MediaServer mediaServer; + + public static MediaSendRtpStoppedEvent getInstance(Object source, OnStreamNotFoundHookParam hookParam, MediaServer mediaServer){ + MediaSendRtpStoppedEvent mediaDepartureEven = new MediaSendRtpStoppedEvent(source); + mediaDepartureEven.setApp(hookParam.getApp()); + mediaDepartureEven.setStream(hookParam.getStream()); + mediaDepartureEven.setMediaServer(mediaServer); + return mediaDepartureEven; + } + + public String getApp() { + return app; + } + + public void setApp(String app) { + this.app = app; + } + + public String getStream() { + return stream; + } + + public void setStream(String stream) { + this.stream = stream; + } + + public MediaServer getMediaServer() { + return mediaServer; + } + + public void setMediaServer(MediaServer mediaServer) { + this.mediaServer = mediaServer; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/media/event/mediaServer/MediaServerChangeEvent.java b/src/main/java/com/genersoft/iot/vmp/media/event/mediaServer/MediaServerChangeEvent.java new file mode 100644 index 0000000..ecbe332 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/media/event/mediaServer/MediaServerChangeEvent.java @@ -0,0 +1,34 @@ +package com.genersoft.iot.vmp.media.event.mediaServer; + +import com.genersoft.iot.vmp.media.bean.MediaServer; +import org.springframework.context.ApplicationEvent; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +public class MediaServerChangeEvent extends ApplicationEvent { + + public MediaServerChangeEvent(Object source) { + super(source); + } + + private List mediaServerItemList; + + public List getMediaServerItemList() { + return mediaServerItemList; + } + + public void setMediaServerItemList(List mediaServerItemList) { + this.mediaServerItemList = mediaServerItemList; + } + + public void setMediaServerItemList(MediaServer... mediaServerItemArray) { + this.mediaServerItemList = new ArrayList<>(); + this.mediaServerItemList.addAll(Arrays.asList(mediaServerItemArray)); + } + + public void setMediaServerItem(List mediaServerItemList) { + this.mediaServerItemList = mediaServerItemList; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/media/event/mediaServer/MediaServerDeleteEvent.java b/src/main/java/com/genersoft/iot/vmp/media/event/mediaServer/MediaServerDeleteEvent.java new file mode 100755 index 0000000..a716ff0 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/media/event/mediaServer/MediaServerDeleteEvent.java @@ -0,0 +1,11 @@ +package com.genersoft.iot.vmp.media.event.mediaServer; + +/** + * zlm在线事件 + */ +public class MediaServerDeleteEvent extends MediaServerEventAbstract { + + public MediaServerDeleteEvent(Object source) { + super(source); + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/media/event/mediaServer/MediaServerEventAbstract.java b/src/main/java/com/genersoft/iot/vmp/media/event/mediaServer/MediaServerEventAbstract.java new file mode 100755 index 0000000..8f80345 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/media/event/mediaServer/MediaServerEventAbstract.java @@ -0,0 +1,25 @@ +package com.genersoft.iot.vmp.media.event.mediaServer; + +import com.genersoft.iot.vmp.media.bean.MediaServer; +import lombok.Getter; +import lombok.Setter; +import org.springframework.context.ApplicationEvent; + +import java.io.Serial; + + +public abstract class MediaServerEventAbstract extends ApplicationEvent { + + @Serial + private static final long serialVersionUID = 1L; + + @Getter + @Setter + private MediaServer mediaServer; + + + public MediaServerEventAbstract(Object source) { + super(source); + } + +} diff --git a/src/main/java/com/genersoft/iot/vmp/media/event/mediaServer/MediaServerOfflineEvent.java b/src/main/java/com/genersoft/iot/vmp/media/event/mediaServer/MediaServerOfflineEvent.java new file mode 100755 index 0000000..2f9ac23 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/media/event/mediaServer/MediaServerOfflineEvent.java @@ -0,0 +1,11 @@ +package com.genersoft.iot.vmp.media.event.mediaServer; + +/** + * zlm离线事件类 + */ +public class MediaServerOfflineEvent extends MediaServerEventAbstract { + + public MediaServerOfflineEvent(Object source) { + super(source); + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/media/event/mediaServer/MediaServerOnlineEvent.java b/src/main/java/com/genersoft/iot/vmp/media/event/mediaServer/MediaServerOnlineEvent.java new file mode 100755 index 0000000..673dce4 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/media/event/mediaServer/MediaServerOnlineEvent.java @@ -0,0 +1,11 @@ +package com.genersoft.iot.vmp.media.event.mediaServer; + +/** + * zlm在线事件 + */ +public class MediaServerOnlineEvent extends MediaServerEventAbstract { + + public MediaServerOnlineEvent(Object source) { + super(source); + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/media/event/mediaServer/MediaServerStatusEventListener.java b/src/main/java/com/genersoft/iot/vmp/media/event/mediaServer/MediaServerStatusEventListener.java new file mode 100755 index 0000000..2e5a6ea --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/media/event/mediaServer/MediaServerStatusEventListener.java @@ -0,0 +1,40 @@ +package com.genersoft.iot.vmp.media.event.mediaServer; + +import com.genersoft.iot.vmp.gb28181.service.IPlayService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.event.EventListener; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Component; + + +/** + * @description: 在线事件监听器,监听到离线后,修改设备离在线状态。 设备在线有两个来源: + * 1、设备主动注销,发送注销指令 + * 2、设备未知原因离线,心跳超时 + * @author: swwheihei + * @date: 2020年5月6日 下午1:51:23 + */ +@Slf4j +@Component +public class MediaServerStatusEventListener { + + @Autowired + private IPlayService playService; + + @Async("taskExecutor") + @EventListener + public void onApplicationEvent(MediaServerOnlineEvent event) { + log.info("[媒体节点] 上线 ID:" + event.getMediaServer().getId()); + playService.zlmServerOnline(event.getMediaServer()); + } + + @Async("taskExecutor") + @EventListener + public void onApplicationEvent(MediaServerOfflineEvent event) { + + log.info("[媒体节点] 离线,ID:" + event.getMediaServer().getId()); + // 处理ZLM离线 + playService.zlmServerOffline(event.getMediaServer()); + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/media/service/IMediaNodeServerService.java b/src/main/java/com/genersoft/iot/vmp/media/service/IMediaNodeServerService.java new file mode 100644 index 0000000..b0a768c --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/media/service/IMediaNodeServerService.java @@ -0,0 +1,91 @@ +package com.genersoft.iot.vmp.media.service; + +import com.genersoft.iot.vmp.common.CommonCallback; +import com.genersoft.iot.vmp.common.StreamInfo; +import com.genersoft.iot.vmp.gb28181.bean.SendRtpInfo; +import com.genersoft.iot.vmp.media.bean.MediaInfo; +import com.genersoft.iot.vmp.media.bean.MediaServer; +import com.genersoft.iot.vmp.media.bean.RecordInfo; +import com.genersoft.iot.vmp.service.bean.DownloadFileInfo; +import com.genersoft.iot.vmp.service.bean.ErrorCallback; +import com.genersoft.iot.vmp.streamProxy.bean.StreamProxy; +import com.genersoft.iot.vmp.vmanager.bean.WVPResult; + +import java.util.List; +import java.util.Map; + +public interface IMediaNodeServerService { + int createRTPServer(MediaServer mediaServer, String streamId, long ssrc, Integer port, Boolean onlyAuto, Boolean disableAudio, Boolean reUsePort, Integer tcpMode); + + void closeRtpServer(MediaServer mediaServer, String streamId, CommonCallback callback); + + + int createJTTServer(MediaServer mediaServer, String streamId, Integer port, Boolean disableVideo, Boolean disableAudio, Integer tcpMode); + + void closeJTTServer(MediaServer mediaServer, String streamId, CommonCallback callback); + + + void closeStreams(MediaServer mediaServer, String app, String stream); + + Boolean updateRtpServerSSRC(MediaServer mediaServer, String stream, String ssrc); + + boolean checkNodeId(MediaServer mediaServer); + + void online(MediaServer mediaServer); + + MediaServer checkMediaServer(String ip, int port, String secret); + + boolean stopSendRtp(MediaServer mediaInfo, String app, String stream, String ssrc); + + boolean initStopSendRtp(MediaServer mediaInfo, String app, String stream, String ssrc); + + boolean deleteRecordDirectory(MediaServer mediaServer, String app, String stream, String date, String fileName); + + List getMediaList(MediaServer mediaServer, String app, String stream, String callId); + + Boolean connectRtpServer(MediaServer mediaServer, String address, int port, String stream); + + void getSnap(MediaServer mediaServer, String app, String stream, int timeoutSec, int expireSec, String path, String fileName); + + MediaInfo getMediaInfo(MediaServer mediaServer, String app, String stream); + + Boolean pauseRtpCheck(MediaServer mediaServer, String streamKey); + + Boolean resumeRtpCheck(MediaServer mediaServer, String streamKey); + + String getFfmpegCmd(MediaServer mediaServer, String cmdKey); + + WVPResult addStreamProxy(MediaServer mediaServer, String app, String stream, String url, boolean enableAudio, boolean enableMp4, String rtpType, Integer timeout); + + Boolean delFFmpegSource(MediaServer mediaServer, String streamKey); + + Boolean delStreamProxy(MediaServer mediaServer, String streamKey); + + Map getFFmpegCMDs(MediaServer mediaServer); + + Integer startSendRtpPassive(MediaServer mediaServer, SendRtpInfo sendRtpItem, Integer timeout); + + void startSendRtpStream(MediaServer mediaServer, SendRtpInfo sendRtpItem); + + Integer startSendRtpTalk(MediaServer mediaServer, SendRtpInfo sendRtpItem, Integer timeout); + + Long updateDownloadProcess(MediaServer mediaServer, String app, String stream); + + String startProxy(MediaServer mediaServer, StreamProxy streamProxy); + + void stopProxy(MediaServer mediaServer, String streamKey, String type); + + List listRtpServer(MediaServer mediaServer); + + void loadMP4FileForDate(MediaServer mediaServer, String app, String stream, String datePath, String dateDir, ErrorCallback callback); + + void seekRecordStamp(MediaServer mediaServer, String app, String stream, Double stamp, String schema); + + void setRecordSpeed(MediaServer mediaServer, String app, String stream, Integer speed, String schema); + + DownloadFileInfo getDownloadFilePath(MediaServer mediaServer, RecordInfo recordInfo); + + StreamInfo getStreamInfoByAppAndStream(MediaServer mediaServer, String app, String stream, MediaInfo mediaInfo, String addr, String callId, boolean isPlay); + + void loadMP4File(MediaServer mediaServer, String app, String stream, String filePath, String fileName, ErrorCallback callback); +} diff --git a/src/main/java/com/genersoft/iot/vmp/media/service/IMediaServerService.java b/src/main/java/com/genersoft/iot/vmp/media/service/IMediaServerService.java new file mode 100755 index 0000000..4246030 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/media/service/IMediaServerService.java @@ -0,0 +1,176 @@ +package com.genersoft.iot.vmp.media.service; + +import com.genersoft.iot.vmp.common.CommonCallback; +import com.genersoft.iot.vmp.common.StreamInfo; +import com.genersoft.iot.vmp.gb28181.bean.SendRtpInfo; +import com.genersoft.iot.vmp.media.bean.MediaInfo; +import com.genersoft.iot.vmp.media.bean.MediaServer; +import com.genersoft.iot.vmp.media.bean.RecordInfo; +import com.genersoft.iot.vmp.service.bean.*; +import com.genersoft.iot.vmp.streamProxy.bean.StreamProxy; +import com.genersoft.iot.vmp.vmanager.bean.WVPResult; + +import java.util.List; +import java.util.Map; + +/** + * 媒体服务节点 + */ +public interface IMediaServerService { + + List getAllOnlineList(); + + List getAll(); + + List getAllFromDatabase(); + + List getAllOnline(); + + MediaServer getOne(String generalMediaServerId); + + void syncCatchFromDatabase(); + + MediaServer getMediaServerForMinimumLoad(Boolean hasAssist); + + void updateVmServer(List mediaServerItemList); + + SSRCInfo openRTPServer(MediaServer mediaServerItem, String streamId, String presetSsrc, boolean ssrcCheck, + boolean isPlayback, Integer port, Boolean onlyAuto, Boolean disableAudio, Boolean reUsePort, Integer tcpMode); + + void closeRTPServer(MediaServer mediaServerItem, String streamId); + + void closeRTPServer(MediaServer mediaServerItem, String streamId, CommonCallback callback); + + SSRCInfo openJTTServer(MediaServer mediaServerItem, String streamId, Integer port, Boolean disableVideo, Boolean disableAudio, Integer tcpMode); + + void closeJTTServer(MediaServer mediaServerItem, String streamId, CommonCallback callback); + + Boolean updateRtpServerSSRC(MediaServer mediaServerItem, String streamId, String ssrc); + + void closeRTPServer(String mediaServerId, String streamId); + + void clearRTPServer(MediaServer mediaServerItem); + + void update(MediaServer mediaSerItem); + + void addCount(String mediaServerId); + + void removeCount(String mediaServerId); + + void releaseSsrc(String mediaServerItemId, String ssrc); + + void clearMediaServerForOnline(); + + void add(MediaServer mediaSerItem); + + void resetOnlineServerItem(MediaServer serverItem); + + MediaServer checkMediaServer(String ip, int port, String secret, String type); + + boolean checkMediaRecordServer(String ip, int port); + + void delete(MediaServer mediaServer); + + MediaServer getDefaultMediaServer(); + + MediaServerLoad getLoad(MediaServer mediaServerItem); + + List getAllWithAssistPort(); + + MediaServer getOneFromDatabase(String id); + + boolean stopSendRtp(MediaServer mediaInfo, String app, String stream, String ssrc); + + boolean initStopSendRtp(MediaServer mediaInfo, String app, String stream, String ssrc); + + boolean deleteRecordDirectory(MediaServer mediaServerItem, String app, String stream, String date, String fileName); + + List getMediaList(MediaServer mediaInfo, String app, String stream, String callId); + + Boolean connectRtpServer(MediaServer mediaServerItem, String address, int port, String stream); + + void getSnap(MediaServer mediaServer, String app, String stream, int timeoutSec, int expireSec, String path, String fileName); + + MediaInfo getMediaInfo(MediaServer mediaServerItem, String app, String stream); + + Boolean pauseRtpCheck(MediaServer mediaServerItem, String streamKey); + + boolean resumeRtpCheck(MediaServer mediaServerItem, String streamKey); + + String getFfmpegCmd(MediaServer mediaServer, String cmdKey); + + void closeStreams(MediaServer mediaServerItem, String app, String stream); + + WVPResult addStreamProxy(MediaServer mediaServerItem, String app, String stream, String url, boolean enableAudio, boolean enableMp4, String rtpType, Integer timeout); + + Boolean delFFmpegSource(MediaServer mediaServerItem, String streamKey); + + Boolean delStreamProxy(MediaServer mediaServerItem, String streamKey); + + Map getFFmpegCMDs(MediaServer mediaServer); + + /** + * 根据应用名和流ID获取播放地址, 通过zlm接口检查是否存在 + * @param app + * @param stream + * @return + */ + StreamInfo getStreamInfoByAppAndStreamWithCheck(String app, String stream, String mediaServerId,String addr, boolean authority); + + + /** + * 根据应用名和流ID获取播放地址, 通过zlm接口检查是否存在, 返回的ip使用远程访问ip,适用与zlm与wvp在一台主机的情况 + * @param app + * @param stream + * @return + */ + StreamInfo getStreamInfoByAppAndStreamWithCheck(String app, String stream, String mediaServerId, boolean authority); + + /** + * 根据应用名和流ID获取播放地址, 只是地址拼接 + * @param app + * @param stream + * @return + */ + StreamInfo getStreamInfoByAppAndStream(MediaServer mediaServerItem, String app, String stream, MediaInfo mediaInfo, String callId); + + /** + * 根据应用名和流ID获取播放地址, 只是地址拼接,返回的ip使用远程访问ip,适用与zlm与wvp在一台主机的情况 + * @param app + * @param stream + * @return + */ + StreamInfo getStreamInfoByAppAndStream(MediaServer mediaServer, String app, String stream, MediaInfo mediaInfo, String addr, String callId, boolean isPlay); + + Boolean isStreamReady(MediaServer mediaServer, String rtp, String streamId); + + Integer startSendRtpPassive(MediaServer mediaServer, SendRtpInfo sendRtpItem, Integer timeout); + + Integer startSendRtpTalk(MediaServer mediaServer, SendRtpInfo sendRtpItem, Integer timeout); + + void startSendRtp(MediaServer mediaServer, SendRtpInfo sendRtpItem); + + MediaServer getMediaServerByAppAndStream(String app, String stream); + + Long updateDownloadProcess(MediaServer mediaServerItem, String app, String stream); + + String startProxy(MediaServer mediaServer, StreamProxy streamProxy); + + void stopProxy(MediaServer mediaServer, String streamKey, String type); + + StreamInfo getMediaByAppAndStream(String app, String stream); + + int createRTPServer(MediaServer mediaServerItem, String streamId, long ssrc, Integer port, boolean onlyAuto, boolean disableAudio, boolean reUsePort, Integer tcpMode); + + List listRtpServer(MediaServer mediaServer); + + void loadMP4FileForDate(MediaServer mediaServer, String app, String stream, String datePath, String dateDir, ErrorCallback callback); + + void seekRecordStamp(MediaServer mediaServer, String app, String stream, Double stamp, String schema); + + void setRecordSpeed(MediaServer mediaServer, String app, String stream, Integer speed, String schema); + + DownloadFileInfo getDownloadFilePath(MediaServer mediaServer, RecordInfo recordInfo); + + void loadMP4File(MediaServer mediaServer, String app, String stream, String filePath, String fileName, ErrorCallback callback); +} diff --git a/src/main/java/com/genersoft/iot/vmp/media/service/impl/MediaServerServiceImpl.java b/src/main/java/com/genersoft/iot/vmp/media/service/impl/MediaServerServiceImpl.java new file mode 100755 index 0000000..8b1ee8c --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/media/service/impl/MediaServerServiceImpl.java @@ -0,0 +1,1007 @@ +package com.genersoft.iot.vmp.media.service.impl; + +import com.genersoft.iot.vmp.common.CommonCallback; +import com.genersoft.iot.vmp.common.StreamInfo; +import com.genersoft.iot.vmp.common.VideoManagerConstants; +import com.genersoft.iot.vmp.conf.MediaConfig; +import com.genersoft.iot.vmp.conf.UserSetting; +import com.genersoft.iot.vmp.conf.exception.ControllerException; +import com.genersoft.iot.vmp.gb28181.bean.SendRtpInfo; +import com.genersoft.iot.vmp.gb28181.service.IInviteStreamService; +import com.genersoft.iot.vmp.gb28181.session.SSRCFactory; +import com.genersoft.iot.vmp.media.bean.MediaInfo; +import com.genersoft.iot.vmp.media.bean.MediaServer; +import com.genersoft.iot.vmp.media.bean.RecordInfo; +import com.genersoft.iot.vmp.media.event.media.MediaArrivalEvent; +import com.genersoft.iot.vmp.media.event.media.MediaDepartureEvent; +import com.genersoft.iot.vmp.media.event.mediaServer.MediaServerDeleteEvent; +import com.genersoft.iot.vmp.media.event.mediaServer.MediaServerOfflineEvent; +import com.genersoft.iot.vmp.media.event.mediaServer.MediaServerOnlineEvent; +import com.genersoft.iot.vmp.media.service.IMediaNodeServerService; +import com.genersoft.iot.vmp.media.service.IMediaServerService; +import com.genersoft.iot.vmp.media.zlm.dto.StreamAuthorityInfo; +import com.genersoft.iot.vmp.media.zlm.dto.hook.OriginType; +import com.genersoft.iot.vmp.service.bean.DownloadFileInfo; +import com.genersoft.iot.vmp.service.bean.ErrorCallback; +import com.genersoft.iot.vmp.service.bean.MediaServerLoad; +import com.genersoft.iot.vmp.service.bean.SSRCInfo; +import com.genersoft.iot.vmp.storager.IRedisCatchStorage; +import com.genersoft.iot.vmp.storager.dao.MediaServerMapper; +import com.genersoft.iot.vmp.streamProxy.bean.StreamProxy; +import com.genersoft.iot.vmp.utils.DateUtil; +import com.genersoft.iot.vmp.vmanager.bean.ErrorCode; +import com.genersoft.iot.vmp.vmanager.bean.WVPResult; +import jakarta.validation.constraints.NotNull; +import lombok.extern.slf4j.Slf4j; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.Response; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.context.event.EventListener; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.ObjectUtils; + +import java.time.LocalDateTime; +import java.util.*; + +/** + * 媒体服务器节点管理 + */ +@Slf4j +@Service +public class MediaServerServiceImpl implements IMediaServerService { + + @Autowired + private SSRCFactory ssrcFactory; + + @Autowired + private UserSetting userSetting; + + @Autowired + private MediaServerMapper mediaServerMapper; + + @Autowired + private IRedisCatchStorage redisCatchStorage; + + @Autowired + private IInviteStreamService inviteStreamService; + + @Autowired + private RedisTemplate redisTemplate; + + @Autowired + private Map nodeServerServiceMap; + + @Autowired + private ApplicationEventPublisher applicationEventPublisher; + + @Autowired + private MediaConfig mediaConfig; + + + /** + * 流到来的处理 + */ + @Async("taskExecutor") + @org.springframework.context.event.EventListener + public void onApplicationEvent(MediaArrivalEvent event) { + if ("rtsp".equals(event.getSchema())) { + log.info("流变化:注册 app->{}, stream->{}", event.getApp(), event.getStream()); + addCount(event.getMediaServer().getId()); + String type = OriginType.values()[event.getMediaInfo().getOriginType()].getType(); + redisCatchStorage.addStream(event.getMediaServer(), type, event.getApp(), event.getStream(), event.getMediaInfo()); + } + } + + /** + * 流离开的处理 + */ + @Async("taskExecutor") + @EventListener + public void onApplicationEvent(MediaDepartureEvent event) { + if ("rtsp".equals(event.getSchema())) { + log.info("流变化:注销, app->{}, stream->{}", event.getApp(), event.getStream()); + removeCount(event.getMediaServer().getId()); + MediaInfo mediaInfo = redisCatchStorage.getStreamInfo( + event.getApp(), event.getStream(), event.getMediaServer().getId()); + if (mediaInfo == null) { + return; + } + String type = OriginType.values()[mediaInfo.getOriginType()].getType(); + redisCatchStorage.removeStream(mediaInfo.getMediaServer().getId(), type, event.getApp(), event.getStream()); + } + } + + /** + * 流媒体节点上线 + */ + @Async("taskExecutor") + @EventListener + @Transactional + public void onApplicationEvent(MediaServerOnlineEvent event) { + // 查看是否有未处理的RTP流 + + } + + /** + * 流媒体节点离线 + */ + @Async("taskExecutor") + @EventListener + @Transactional + public void onApplicationEvent(MediaServerOfflineEvent event) { + + } + + + /** + * 初始化 + */ + @Override + public void updateVmServer(List mediaServerList) { + log.info("[媒体服务节点] 缓存初始化 "); + for (MediaServer mediaServer : mediaServerList) { + if (ObjectUtils.isEmpty(mediaServer.getId())) { + continue; + } + // 更新 + if (!ssrcFactory.hasMediaServerSSRC(mediaServer.getId())) { + ssrcFactory.initMediaServerSSRC(mediaServer.getId(), null); + } + // 查询redis是否存在此mediaServer + String key = VideoManagerConstants.MEDIA_SERVER_PREFIX + userSetting.getServerId(); + Boolean hasKey = redisTemplate.hasKey(key); + if (hasKey != null && !hasKey) { + redisTemplate.opsForHash().put(key, mediaServer.getId(), mediaServer); + } + } + } + + + @Override + public SSRCInfo openRTPServer(MediaServer mediaServer, String streamId, String presetSsrc, boolean ssrcCheck, + boolean isPlayback, Integer port, Boolean onlyAuto, Boolean disableAudio, Boolean reUsePort, Integer tcpMode) { + if (mediaServer == null || mediaServer.getId() == null) { + log.info("[openRTPServer] 失败, mediaServer == null || mediaServer.getId() == null"); + return null; + } + // 获取mediaServer可用的ssrc + String ssrc; + if (presetSsrc != null) { + ssrc = presetSsrc; + }else { + if (isPlayback) { + ssrc = ssrcFactory.getPlayBackSsrc(mediaServer.getId()); + }else { + ssrc = ssrcFactory.getPlaySsrc(mediaServer.getId()); + } + } + + if (streamId == null) { + streamId = String.format("%08x", Long.parseLong(ssrc)).toUpperCase(); + } + if (ssrcCheck && tcpMode > 0) { + // 目前zlm不支持 tcp模式更新ssrc,暂时关闭ssrc校验 + log.warn("[openRTPServer] 平台对接时下级可能自定义ssrc,但是tcp模式zlm收流目前无法更新ssrc,可能收流超时,此时请使用udp收流或者关闭ssrc校验"); + } + int rtpServerPort; + if (mediaServer.isRtpEnable()) { + IMediaNodeServerService mediaNodeServerService = nodeServerServiceMap.get(mediaServer.getType()); + if (mediaNodeServerService == null) { + log.info("[openRTPServer] 失败, mediaServer的类型: {},未找到对应的实现类", mediaServer.getType()); + return null; + } + rtpServerPort = mediaNodeServerService.createRTPServer(mediaServer, streamId, ssrcCheck ? Long.parseLong(ssrc) : 0, port, onlyAuto, disableAudio, reUsePort, tcpMode); + } else { + rtpServerPort = mediaServer.getRtpProxyPort(); + } + return new SSRCInfo(rtpServerPort, ssrc, "rtp", streamId, null); + } + + @Override + public int createRTPServer(MediaServer mediaServer, String streamId, long ssrc, Integer port, boolean onlyAuto, boolean disableAudio, boolean reUsePort, Integer tcpMode) { + int rtpServerPort; + if (mediaServer.isRtpEnable()) { + IMediaNodeServerService mediaNodeServerService = nodeServerServiceMap.get(mediaServer.getType()); + if (mediaNodeServerService == null) { + log.info("[openRTPServer] 失败, mediaServer的类型: {},未找到对应的实现类", mediaServer.getType()); + return 0; + } + rtpServerPort = mediaNodeServerService.createRTPServer(mediaServer, streamId, ssrc, port, onlyAuto, disableAudio, reUsePort, tcpMode); + } else { + rtpServerPort = mediaServer.getRtpProxyPort(); + } + return rtpServerPort; + } + + @Override + public SSRCInfo openJTTServer(MediaServer mediaServer, @NotNull String streamId, Integer port, Boolean disableVideo, Boolean disableAudio, Integer tcpMode) { + if (mediaServer == null || mediaServer.getId() == null) { + log.info("[openJTTServer] 失败, mediaServer == null || mediaServer.getId() == null"); + return null; + } + + int rtpServerPort; + if (mediaServer.isRtpEnable()) { + IMediaNodeServerService mediaNodeServerService = nodeServerServiceMap.get(mediaServer.getType()); + if (mediaNodeServerService == null) { + log.info("[openJTTServer] 失败, mediaServer的类型: {},未找到对应的实现类", mediaServer.getType()); + return null; + } + rtpServerPort = mediaNodeServerService.createJTTServer(mediaServer, streamId, port, disableVideo, disableAudio, tcpMode); + } else { + rtpServerPort = mediaServer.getJttProxyPort(); + } + return new SSRCInfo(rtpServerPort, null, "1078", streamId, null); + } + + @Override + public List listRtpServer(MediaServer mediaServer) { + IMediaNodeServerService mediaNodeServerService = nodeServerServiceMap.get(mediaServer.getType()); + if (mediaNodeServerService == null) { + log.info("[openRTPServer] 失败, mediaServer的类型: {},未找到对应的实现类", mediaServer.getType()); + return new ArrayList<>(); + } + return mediaNodeServerService.listRtpServer(mediaServer); + } + + @Override + public void closeRTPServer(MediaServer mediaServer, String streamId) { + if (mediaServer == null) { + return; + } + IMediaNodeServerService mediaNodeServerService = nodeServerServiceMap.get(mediaServer.getType()); + if (mediaNodeServerService == null) { + log.info("[closeRTPServer] 失败, mediaServer的类型: {},未找到对应的实现类", mediaServer.getType()); + return; + } + mediaNodeServerService.closeRtpServer(mediaServer, streamId, null); + } + + @Override + public void closeRTPServer(MediaServer mediaServer, String streamId, CommonCallback callback) { + if (mediaServer == null) { + callback.run(false); + return; + } + IMediaNodeServerService mediaNodeServerService = nodeServerServiceMap.get(mediaServer.getType()); + if (mediaNodeServerService == null) { + log.info("[closeRTPServer] 失败, mediaServer的类型: {},未找到对应的实现类", mediaServer.getType()); + return; + } + mediaNodeServerService.closeRtpServer(mediaServer, streamId, callback); + } + + @Override + public void closeRTPServer(String mediaServerId, String streamId) { + MediaServer mediaServer = this.getOne(mediaServerId); + if (mediaServer == null) { + return; + } + if (mediaServer.isRtpEnable()) { + closeRTPServer(mediaServer, streamId); + } + IMediaNodeServerService mediaNodeServerService = nodeServerServiceMap.get(mediaServer.getType()); + if (mediaNodeServerService == null) { + log.info("[closeRTPServer] 失败, mediaServer的类型: {},未找到对应的实现类", mediaServer.getType()); + return; + } + mediaNodeServerService.closeStreams(mediaServer, "rtp", streamId); + } + + @Override + public void closeJTTServer(MediaServer mediaServer, String streamId, CommonCallback callback) { + if (mediaServer == null) { + callback.run(false); + return; + } + IMediaNodeServerService mediaNodeServerService = nodeServerServiceMap.get(mediaServer.getType()); + if (mediaNodeServerService == null) { + log.info("[closeJTTServer] 失败, mediaServer的类型: {},未找到对应的实现类", mediaServer.getType()); + return; + } + mediaNodeServerService.closeJTTServer(mediaServer, streamId, callback); + } + + @Override + public Boolean updateRtpServerSSRC(MediaServer mediaServer, String streamId, String ssrc) { + if (mediaServer == null) { + return false; + } + IMediaNodeServerService mediaNodeServerService = nodeServerServiceMap.get(mediaServer.getType()); + if (mediaNodeServerService == null) { + log.info("[updateRtpServerSSRC] 失败, mediaServer的类型: {},未找到对应的实现类", mediaServer.getType()); + return false; + } + return mediaNodeServerService.updateRtpServerSSRC(mediaServer, streamId, ssrc); + } + + @Override + public void releaseSsrc(String mediaServerId, String ssrc) { + MediaServer mediaServer = getOne(mediaServerId); + if (mediaServer == null || ssrc == null) { + return; + } + ssrcFactory.releaseSsrc(mediaServerId, ssrc); + } + + /** + * 媒体服务节点 重启后重置他的推流信息, TODO 给正在使用的设备发送停止命令 + */ + @Override + public void clearRTPServer(MediaServer mediaServer) { + ssrcFactory.reset(mediaServer.getId()); + } + + @Override + public void update(MediaServer mediaSerItem) { + mediaServerMapper.update(mediaSerItem); + MediaServer mediaServerInRedis = getOne(mediaSerItem.getId()); + // 获取完整数据 + MediaServer mediaServerInDataBase = mediaServerMapper.queryOne(mediaSerItem.getId(), userSetting.getServerId()); + if (mediaServerInDataBase == null) { + return; + } + mediaServerInDataBase.setStatus(mediaSerItem.isStatus()); + if (mediaServerInRedis == null || !ssrcFactory.hasMediaServerSSRC(mediaServerInDataBase.getId())) { + ssrcFactory.initMediaServerSSRC(mediaServerInDataBase.getId(),null); + } + if (mediaSerItem.getSecret() != null && !mediaServerInDataBase.getSecret().equals(mediaSerItem.getSecret())) { + mediaServerInDataBase.setSecret(mediaSerItem.getSecret()); + } + mediaServerInDataBase.setSecret(mediaSerItem.getSecret()); + String key = VideoManagerConstants.MEDIA_SERVER_PREFIX + userSetting.getServerId(); + redisTemplate.opsForHash().put(key, mediaServerInDataBase.getId(), mediaServerInDataBase); + if (mediaServerInDataBase.isStatus()) { + resetOnlineServerItem(mediaServerInDataBase); + } + } + + + @Override + public List getAllOnlineList() { + List result = new ArrayList<>(); + String key = VideoManagerConstants.MEDIA_SERVER_PREFIX + userSetting.getServerId(); + String onlineKey = VideoManagerConstants.ONLINE_MEDIA_SERVERS_PREFIX + userSetting.getServerId(); + List values = redisTemplate.opsForHash().values(key); + for (Object value : values) { + if (Objects.isNull(value)) { + continue; + } + MediaServer mediaServer = (MediaServer) value; + // 检查状态 + Double aDouble = redisTemplate.opsForZSet().score(onlineKey, mediaServer.getId()); + if (aDouble != null) { + mediaServer.setStatus(true); + } + result.add(mediaServer); + } + result.sort((serverItem1, serverItem2)->{ + int sortResult = 0; + LocalDateTime localDateTime1 = LocalDateTime.parse(serverItem1.getCreateTime(), DateUtil.formatter); + LocalDateTime localDateTime2 = LocalDateTime.parse(serverItem2.getCreateTime(), DateUtil.formatter); + + sortResult = localDateTime1.compareTo(localDateTime2); + return sortResult; + }); + return result; + } + + @Override + public List getAll() { + List mediaServerList = mediaServerMapper.queryAll(userSetting.getServerId()); + if (mediaServerList.isEmpty()) { + return new ArrayList<>(); + } + for (MediaServer mediaServer : mediaServerList) { + MediaServer mediaServerInRedis = getOne(mediaServer.getId()); + if (mediaServerInRedis != null) { + mediaServer.setStatus(mediaServerInRedis.isStatus()); + } + } + return mediaServerList; + } + + + @Override + public List getAllFromDatabase() { + return mediaServerMapper.queryAll(userSetting.getServerId()); + } + + @Override + public List getAllOnline() { + String key = VideoManagerConstants.ONLINE_MEDIA_SERVERS_PREFIX + userSetting.getServerId(); + Set mediaServerIdSet = redisTemplate.opsForZSet().reverseRange(key, 0, -1); + + List result = new ArrayList<>(); + if (mediaServerIdSet != null && !mediaServerIdSet.isEmpty()) { + for (Object mediaServerId : mediaServerIdSet) { + String mediaServerIdStr = (String) mediaServerId; + String serverKey = VideoManagerConstants.MEDIA_SERVER_PREFIX + userSetting.getServerId(); + result.add((MediaServer) redisTemplate.opsForHash().get(serverKey, mediaServerIdStr)); + } + } + Collections.reverse(result); + return result; + } + + /** + * 获取单个媒体服务节点服务器 + * @param mediaServerId 服务id + * @return mediaServer + */ + @Override + public MediaServer getOne(String mediaServerId) { + if (mediaServerId == null) { + return null; + } + String key = VideoManagerConstants.MEDIA_SERVER_PREFIX + userSetting.getServerId(); + return (MediaServer) redisTemplate.opsForHash().get(key, mediaServerId); + } + + + @Override + public MediaServer getDefaultMediaServer() { + return mediaServerMapper.queryDefault(userSetting.getServerId()); + } + + @Override + public void clearMediaServerForOnline() { + String key = VideoManagerConstants.ONLINE_MEDIA_SERVERS_PREFIX + userSetting.getServerId(); + redisTemplate.delete(key); + } + + @Override + public void add(MediaServer mediaServer) { + mediaServer.setCreateTime(DateUtil.getNow()); + mediaServer.setUpdateTime(DateUtil.getNow()); + if (mediaServer.getHookAliveInterval() == null || mediaServer.getHookAliveInterval() == 0F) { + mediaServer.setHookAliveInterval(10F); + } + if (mediaServer.getType() == null) { + log.info("[添加媒体节点] 失败, mediaServer的类型:为空"); + return; + } + if (mediaServerMapper.queryOne(mediaServer.getId(), userSetting.getServerId()) != null) { + log.info("[添加媒体节点] 失败, 媒体服务ID已存在,请修改媒体服务器配置, {}", mediaServer.getId()); + throw new ControllerException(ErrorCode.ERROR100.getCode(),"保存失败,媒体服务ID [ " + mediaServer.getId() + " ] 已存在,请修改媒体服务器配置"); + } + IMediaNodeServerService mediaNodeServerService = nodeServerServiceMap.get(mediaServer.getType()); + if (mediaNodeServerService == null) { + log.info("[添加媒体节点] 失败, mediaServer的类型: {},未找到对应的实现类", mediaServer.getType()); + return; + } + + mediaServerMapper.add(mediaServer); + if (mediaServer.isStatus()) { + mediaNodeServerService.online(mediaServer); + } + } + + @Override + public void resetOnlineServerItem(MediaServer serverItem) { + // 更新缓存 + String key = VideoManagerConstants.ONLINE_MEDIA_SERVERS_PREFIX + userSetting.getServerId(); + // 使用zset的分数作为当前并发量, 默认值设置为0 + if (redisTemplate.opsForZSet().score(key, serverItem.getId()) == null) { // 不存在则设置默认值 已存在则重置 + redisTemplate.opsForZSet().add(key, serverItem.getId(), 0L); + // 查询服务流数量 + int count = getMediaList(serverItem); + redisTemplate.opsForZSet().add(key, serverItem.getId(), count); + }else { + clearRTPServer(serverItem); + } + } + + private int getMediaList(MediaServer serverItem) { + + return 0; + } + + + @Override + public void addCount(String mediaServerId) { + if (mediaServerId == null) { + return; + } + String key = VideoManagerConstants.ONLINE_MEDIA_SERVERS_PREFIX + userSetting.getServerId(); + redisTemplate.opsForZSet().incrementScore(key, mediaServerId, 1); + + } + + @Override + public void removeCount(String mediaServerId) { + String key = VideoManagerConstants.ONLINE_MEDIA_SERVERS_PREFIX + userSetting.getServerId(); + redisTemplate.opsForZSet().incrementScore(key, mediaServerId, - 1); + } + + /** + * 获取负载最低的节点 + * @return mediaServer + */ + @Override + public MediaServer getMediaServerForMinimumLoad(Boolean hasAssist) { + String key = VideoManagerConstants.ONLINE_MEDIA_SERVERS_PREFIX + userSetting.getServerId(); + Long size = redisTemplate.opsForZSet().zCard(key); + if (size == null || size == 0) { + log.info("获取负载最低的节点时无在线节点"); + return null; + } + + // 获取分数最低的,及并发最低的 + Set objects = redisTemplate.opsForZSet().range(key, 0, -1); + ArrayList mediaServerObjectS = new ArrayList<>(objects); + MediaServer mediaServer = null; + if (hasAssist == null) { + String mediaServerId = (String)mediaServerObjectS.get(0); + mediaServer = getOne(mediaServerId); + }else if (hasAssist) { + for (Object mediaServerObject : mediaServerObjectS) { + String mediaServerId = (String)mediaServerObject; + MediaServer serverItem = getOne(mediaServerId); + if (serverItem.getRecordAssistPort() > 0) { + mediaServer = serverItem; + break; + } + } + }else if (!hasAssist) { + for (Object mediaServerObject : mediaServerObjectS) { + String mediaServerId = (String)mediaServerObject; + MediaServer serverItem = getOne(mediaServerId); + if (serverItem.getRecordAssistPort() == 0) { + mediaServer = serverItem; + break; + } + } + } + + return mediaServer; + } + + @Override + public MediaServer checkMediaServer(String ip, int port, String secret, String type) { + if (mediaServerMapper.queryOneByHostAndPort(ip, port, userSetting.getServerId()) != null) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "此连接已存在"); + } + + IMediaNodeServerService mediaNodeServerService = nodeServerServiceMap.get(type); + if (mediaNodeServerService == null) { + log.info("[closeRTPServer] 失败, mediaServer的类型: {},未找到对应的实现类", type); + return null; + } + MediaServer mediaServer = mediaNodeServerService.checkMediaServer(ip, port, secret); + if (mediaServer != null) { + if (mediaServerMapper.queryOne(mediaServer.getId(), userSetting.getServerId()) != null) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "媒体服务ID [" + mediaServer.getId() + " ] 已存在,请修改媒体服务器配置"); + } + } + return mediaServer; + } + + @Override + public boolean checkMediaRecordServer(String ip, int port) { + boolean result = false; + OkHttpClient client = new OkHttpClient(); + String url = String.format("http://%s:%s/index/api/record", ip, port); + Request request = new Request.Builder() + .get() + .url(url) + .build(); + try { + Response response = client.newCall(request).execute(); + if (response != null) { + result = true; + } + } catch (Exception e) {} + + return result; + } + + @Override + public void delete(MediaServer mediaServer) { + mediaServerMapper.delOne(mediaServer.getId(), userSetting.getServerId()); + redisTemplate.opsForZSet().remove(VideoManagerConstants.ONLINE_MEDIA_SERVERS_PREFIX + userSetting.getServerId(), mediaServer.getId()); + String key = VideoManagerConstants.MEDIA_SERVER_PREFIX + userSetting.getServerId() + ":" + mediaServer.getId(); + redisTemplate.delete(key); + // 发送节点移除通知 + MediaServerDeleteEvent event = new MediaServerDeleteEvent(this); + event.setMediaServer(mediaServer); + applicationEventPublisher.publishEvent(event); + } + + @Override + public MediaServer getOneFromDatabase(String mediaServerId) { + return mediaServerMapper.queryOne(mediaServerId, userSetting.getServerId()); + } + + @Override + public void syncCatchFromDatabase() { + List allInCatch = getAllOnlineList(); + List allInDatabase = mediaServerMapper.queryAll(userSetting.getServerId()); + Map mediaServerMap = new HashMap<>(); + + for (MediaServer mediaServer : allInDatabase) { + mediaServerMap.put(mediaServer.getId(), mediaServer); + } + for (MediaServer mediaServer : allInCatch) { + // 清除数据中不存在但redis缓存数据 + if (!mediaServerMap.containsKey(mediaServer.getId())) { + delete(mediaServer); + } + } + } + + @Override + public MediaServerLoad getLoad(MediaServer mediaServer) { + MediaServerLoad result = new MediaServerLoad(); + result.setId(mediaServer.getId()); + result.setPush(redisCatchStorage.getPushStreamCount(mediaServer.getId())); + result.setProxy(redisCatchStorage.getProxyStreamCount(mediaServer.getId())); + + result.setGbReceive(inviteStreamService.getStreamInfoCount(mediaServer.getId())); + result.setGbSend(redisCatchStorage.getGbSendCount(mediaServer.getId())); + return result; + } + + @Override + public List getAllWithAssistPort() { + return mediaServerMapper.queryAllWithAssistPort(userSetting.getServerId()); + } + + + @Override + public boolean stopSendRtp(MediaServer mediaInfo, String app, String stream, String ssrc) { + IMediaNodeServerService mediaNodeServerService = nodeServerServiceMap.get(mediaInfo.getType()); + if (mediaNodeServerService == null) { + log.info("[stopSendRtp] 失败, mediaServer的类型: {},未找到对应的实现类", mediaInfo.getType()); + return false; + } + return mediaNodeServerService.stopSendRtp(mediaInfo, app, stream, ssrc); + } + + @Override + public boolean initStopSendRtp(MediaServer mediaInfo, String app, String stream, String ssrc) { + IMediaNodeServerService mediaNodeServerService = nodeServerServiceMap.get(mediaInfo.getType()); + if (mediaNodeServerService == null) { + log.info("[stopSendRtp] 失败, mediaServer的类型: {},未找到对应的实现类", mediaInfo.getType()); + return false; + } + return mediaNodeServerService.initStopSendRtp(mediaInfo, app, stream, ssrc); + } + + @Override + public boolean deleteRecordDirectory(MediaServer mediaServer, String app, String stream, String date, String fileName) { + IMediaNodeServerService mediaNodeServerService = nodeServerServiceMap.get(mediaServer.getType()); + if (mediaNodeServerService == null) { + log.info("[stopSendRtp] 失败, mediaServer的类型: {},未找到对应的实现类", mediaServer.getType()); + return false; + } + return mediaNodeServerService.deleteRecordDirectory(mediaServer, app, stream, date, fileName); + } + + @Override + public List getMediaList(MediaServer mediaServer, String app, String stream, String callId) { + IMediaNodeServerService mediaNodeServerService = nodeServerServiceMap.get(mediaServer.getType()); + if (mediaNodeServerService == null) { + log.info("[getMediaList] 失败, mediaServer的类型: {},未找到对应的实现类", mediaServer.getType()); + return new ArrayList<>(); + } + return mediaNodeServerService.getMediaList(mediaServer, app, stream, callId); + } + + @Override + public Boolean connectRtpServer(MediaServer mediaServer, String address, int port, String stream) { + IMediaNodeServerService mediaNodeServerService = nodeServerServiceMap.get(mediaServer.getType()); + if (mediaNodeServerService == null) { + log.info("[connectRtpServer] 失败, mediaServer的类型: {},未找到对应的实现类", mediaServer.getType()); + return false; + } + return mediaNodeServerService.connectRtpServer(mediaServer, address, port, stream); + } + + @Override + public void getSnap(MediaServer mediaServer, String app, String stream, int timeoutSec, int expireSec, String path, String fileName) { + IMediaNodeServerService mediaNodeServerService = nodeServerServiceMap.get(mediaServer.getType()); + if (mediaNodeServerService == null) { + log.info("[getSnap] 失败, mediaServer的类型: {},未找到对应的实现类", mediaServer.getType()); + return; + } + mediaNodeServerService.getSnap(mediaServer, app, stream, timeoutSec, expireSec, path, fileName); + } + + @Override + public MediaInfo getMediaInfo(MediaServer mediaServer, String app, String stream) { + IMediaNodeServerService mediaNodeServerService = nodeServerServiceMap.get(mediaServer.getType()); + if (mediaNodeServerService == null) { + log.info("[getMediaInfo] 失败, mediaServer的类型: {},未找到对应的实现类", mediaServer.getType()); + return null; + } + return mediaNodeServerService.getMediaInfo(mediaServer, app, stream); + } + + @Override + public Boolean pauseRtpCheck(MediaServer mediaServer, String streamKey) { + IMediaNodeServerService mediaNodeServerService = nodeServerServiceMap.get(mediaServer.getType()); + if (mediaNodeServerService == null) { + log.info("[pauseRtpCheck] 失败, mediaServer的类型: {},未找到对应的实现类", mediaServer.getType()); + return false; + } + return mediaNodeServerService.pauseRtpCheck(mediaServer, streamKey); + } + + @Override + public boolean resumeRtpCheck(MediaServer mediaServer, String streamKey) { + IMediaNodeServerService mediaNodeServerService = nodeServerServiceMap.get(mediaServer.getType()); + if (mediaNodeServerService == null) { + log.info("[pauseRtpCheck] 失败, mediaServer的类型: {},未找到对应的实现类", mediaServer.getType()); + return false; + } + return mediaNodeServerService.resumeRtpCheck(mediaServer, streamKey); + } + + @Override + public String getFfmpegCmd(MediaServer mediaServer, String cmdKey) { + IMediaNodeServerService mediaNodeServerService = nodeServerServiceMap.get(mediaServer.getType()); + if (mediaNodeServerService == null) { + log.info("[getFfmpegCmd] 失败, mediaServer的类型: {},未找到对应的实现类", mediaServer.getType()); + return null; + } + return mediaNodeServerService.getFfmpegCmd(mediaServer, cmdKey); + } + + @Override + public void closeStreams(MediaServer mediaServer, String app, String stream) { + IMediaNodeServerService mediaNodeServerService = nodeServerServiceMap.get(mediaServer.getType()); + if (mediaNodeServerService == null) { + log.info("[closeStreams] 失败, mediaServer的类型: {},未找到对应的实现类", mediaServer.getType()); + return; + } + mediaNodeServerService.closeStreams(mediaServer, app, stream); + } + + @Override + public WVPResult addStreamProxy(MediaServer mediaServer, String app, String stream, String url, + boolean enableAudio, boolean enableMp4, String rtpType, Integer timeout) { + IMediaNodeServerService mediaNodeServerService = nodeServerServiceMap.get(mediaServer.getType()); + if (mediaNodeServerService == null) { + log.info("[addStreamProxy] 失败, mediaServer的类型: {},未找到对应的实现类", mediaServer.getType()); + return WVPResult.fail(ErrorCode.ERROR400); + } + return mediaNodeServerService.addStreamProxy(mediaServer, app, stream, url, enableAudio, enableMp4, rtpType, timeout); + } + + @Override + public Boolean delFFmpegSource(MediaServer mediaServer, String streamKey) { + IMediaNodeServerService mediaNodeServerService = nodeServerServiceMap.get(mediaServer.getType()); + if (mediaNodeServerService == null) { + log.info("[delFFmpegSource] 失败, mediaServer的类型: {},未找到对应的实现类", mediaServer.getType()); + return false; + } + return mediaNodeServerService.delFFmpegSource(mediaServer, streamKey); + } + + @Override + public Boolean delStreamProxy(MediaServer mediaServerItem, String streamKey) { + IMediaNodeServerService mediaNodeServerService = nodeServerServiceMap.get(mediaServerItem.getType()); + if (mediaNodeServerService == null) { + log.info("[delStreamProxy] 失败, mediaServer的类型: {},未找到对应的实现类", mediaServerItem.getType()); + return false; + } + return mediaNodeServerService.delStreamProxy(mediaServerItem, streamKey); + } + + @Override + public Map getFFmpegCMDs(MediaServer mediaServer) { + IMediaNodeServerService mediaNodeServerService = nodeServerServiceMap.get(mediaServer.getType()); + if (mediaNodeServerService == null) { + log.info("[getFFmpegCMDs] 失败, mediaServer的类型: {},未找到对应的实现类", mediaServer.getType()); + return new HashMap<>(); + } + return mediaNodeServerService.getFFmpegCMDs(mediaServer); + } + + @Override + public StreamInfo getStreamInfoByAppAndStream(MediaServer mediaServerItem, String app, String stream, MediaInfo mediaInfo, String callId) { + return getStreamInfoByAppAndStream(mediaServerItem, app, stream, mediaInfo, null, callId, true); + } + + @Override + public StreamInfo getStreamInfoByAppAndStreamWithCheck(String app, String stream, String mediaServerId, String addr, boolean authority) { + if (mediaServerId == null) { + mediaServerId = mediaConfig.getId(); + } + MediaServer mediaInfo = getOne(mediaServerId); + if (mediaInfo == null) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "未找到使用的媒体节点"); + } + String calld = null; + StreamAuthorityInfo streamAuthorityInfo = redisCatchStorage.getStreamAuthorityInfo(app, stream); + if (streamAuthorityInfo != null) { + calld = streamAuthorityInfo.getCallId(); + } + List streamInfoList = getMediaList(mediaInfo, app, stream, calld); + if (streamInfoList == null || streamInfoList.isEmpty()) { + return null; + }else { + StreamInfo streamInfo = streamInfoList.get(0); + if (addr != null && !addr.isEmpty()) { + streamInfo.changeStreamIp(addr); + } + return streamInfo; + } + } + + + + @Override + public StreamInfo getStreamInfoByAppAndStreamWithCheck(String app, String stream, String mediaServerId, boolean authority) { + return getStreamInfoByAppAndStreamWithCheck(app, stream, mediaServerId, null, authority); + } + + @Override + public StreamInfo getStreamInfoByAppAndStream(MediaServer mediaServer, String app, String stream, MediaInfo mediaInfo, String addr, String callId, boolean isPlay) { + IMediaNodeServerService mediaNodeServerService = nodeServerServiceMap.get(mediaServer.getType()); + if (mediaNodeServerService == null) { + log.info("[getStreamInfoByAppAndStream] 失败, mediaServer的类型: {},未找到对应的实现类", mediaServer.getType()); + return null; + } + return mediaNodeServerService.getStreamInfoByAppAndStream(mediaServer, app, stream, mediaInfo, addr, callId, isPlay); + } + + @Override + public Boolean isStreamReady(MediaServer mediaServer, String app, String streamId) { + IMediaNodeServerService mediaNodeServerService = nodeServerServiceMap.get(mediaServer.getType()); + if (mediaNodeServerService == null) { + log.info("[isStreamReady] 失败, mediaServer的类型: {},未找到对应的实现类", mediaServer.getType()); + return false; + } + MediaInfo mediaInfo = mediaNodeServerService.getMediaInfo(mediaServer, app, streamId); + return mediaInfo != null; + } + + @Override + public Integer startSendRtpPassive(MediaServer mediaServer, SendRtpInfo sendRtpItem, Integer timeout) { + IMediaNodeServerService mediaNodeServerService = nodeServerServiceMap.get(mediaServer.getType()); + if (mediaNodeServerService == null) { + log.info("[startSendRtpPassive] 失败, mediaServer的类型: {},未找到对应的实现类", mediaServer.getType()); + throw new ControllerException(ErrorCode.ERROR100.getCode(), "未找到mediaServer对应的实现类"); + } + return mediaNodeServerService.startSendRtpPassive(mediaServer, sendRtpItem, timeout); + } + + @Override + public Integer startSendRtpTalk(MediaServer mediaServer, SendRtpInfo sendRtpItem, Integer timeout) { + IMediaNodeServerService mediaNodeServerService = nodeServerServiceMap.get(mediaServer.getType()); + if (mediaNodeServerService == null) { + log.info("[startSendRtpPassive] 失败, mediaServer的类型: {},未找到对应的实现类", mediaServer.getType()); + throw new ControllerException(ErrorCode.ERROR100.getCode(), "未找到mediaServer对应的实现类"); + } + return mediaNodeServerService.startSendRtpTalk(mediaServer, sendRtpItem, timeout); + } + + @Override + public void startSendRtp(MediaServer mediaServer, SendRtpInfo sendRtpItem) { + IMediaNodeServerService mediaNodeServerService = nodeServerServiceMap.get(mediaServer.getType()); + if (mediaNodeServerService == null) { + log.info("[startSendRtpStream] 失败, mediaServer的类型: {},未找到对应的实现类", mediaServer.getType()); + throw new ControllerException(ErrorCode.ERROR100.getCode(), "未找到mediaServer对应的实现类"); + } + sendRtpItem.setRtcp(true); + + log.info("[开始推流] {}/{}, 目标={}:{},SSRC={}, RTCP={}", sendRtpItem.getApp(), sendRtpItem.getStream(), + sendRtpItem.getIp(), sendRtpItem.getPort(), sendRtpItem.getSsrc(), sendRtpItem.isRtcp()); + mediaNodeServerService.startSendRtpStream(mediaServer, sendRtpItem); + } + + + + @Override + public MediaServer getMediaServerByAppAndStream(String app, String stream) { + List mediaServerList = getAll(); + for (MediaServer mediaServer : mediaServerList) { + MediaInfo mediaInfo = getMediaInfo(mediaServer, app, stream); + if (mediaInfo != null) { + return mediaServer; + } + } + return null; + } + + @Override + public StreamInfo getMediaByAppAndStream(String app, String stream) { + + List mediaServerList = getAll(); + for (MediaServer mediaServer : mediaServerList) { + MediaInfo mediaInfo = getMediaInfo(mediaServer, app, stream); + if (mediaInfo != null) { + return getStreamInfoByAppAndStream(mediaServer, app, stream, mediaInfo, mediaInfo.getCallId()); + } + } + return null; + } + + @Override + public Long updateDownloadProcess(MediaServer mediaServer, String app, String stream) { + IMediaNodeServerService mediaNodeServerService = nodeServerServiceMap.get(mediaServer.getType()); + if (mediaNodeServerService == null) { + log.info("[updateDownloadProcess] 失败, mediaServer的类型: {},未找到对应的实现类", mediaServer.getType()); + throw new ControllerException(ErrorCode.ERROR100.getCode(), "未找到mediaServer对应的实现类"); + } + return mediaNodeServerService.updateDownloadProcess(mediaServer, app, stream); + } + + @Override + public String startProxy(MediaServer mediaServer, StreamProxy streamProxy) { + IMediaNodeServerService mediaNodeServerService = nodeServerServiceMap.get(mediaServer.getType()); + if (mediaNodeServerService == null) { + log.info("[startProxy] 失败, mediaServer的类型: {},未找到对应的实现类", mediaServer.getType()); + throw new ControllerException(ErrorCode.ERROR100.getCode(), "未找到mediaServer对应的实现类"); + } + return mediaNodeServerService.startProxy(mediaServer, streamProxy); + } + + @Override + public void stopProxy(MediaServer mediaServer, String streamKey, String type) { + IMediaNodeServerService mediaNodeServerService = nodeServerServiceMap.get(mediaServer.getType()); + if (mediaNodeServerService == null) { + log.info("[stopProxy] 失败, mediaServer的类型: {},未找到对应的实现类", mediaServer.getType()); + throw new ControllerException(ErrorCode.ERROR100.getCode(), "未找到mediaServer对应的实现类"); + } + mediaNodeServerService.stopProxy(mediaServer, streamKey, type); + } + + @Override + public void loadMP4FileForDate(MediaServer mediaServer, String app, String stream, String date, String dateDir, ErrorCallback callback) { + IMediaNodeServerService mediaNodeServerService = nodeServerServiceMap.get(mediaServer.getType()); + if (mediaNodeServerService == null) { + log.info("[loadMP4FileForDate] 失败, mediaServer的类型: {},未找到对应的实现类", mediaServer.getType()); + throw new ControllerException(ErrorCode.ERROR100.getCode(), "未找到mediaServer对应的实现类"); + } + mediaNodeServerService.loadMP4FileForDate(mediaServer, app, stream, date, dateDir, callback); + + } + + @Override + public void loadMP4File(MediaServer mediaServer, String app, String stream, String filePath, String fileName, ErrorCallback callback) { + IMediaNodeServerService mediaNodeServerService = nodeServerServiceMap.get(mediaServer.getType()); + if (mediaNodeServerService == null) { + log.info("[loadMP4File] 失败, mediaServer的类型: {},未找到对应的实现类", mediaServer.getType()); + throw new ControllerException(ErrorCode.ERROR100.getCode(), "未找到mediaServer对应的实现类"); + } + mediaNodeServerService.loadMP4File(mediaServer, app, stream, filePath, fileName, callback); + } + + @Override + public void seekRecordStamp(MediaServer mediaServer, String app, String stream, Double stamp, String schema) { + IMediaNodeServerService mediaNodeServerService = nodeServerServiceMap.get(mediaServer.getType()); + if (mediaNodeServerService == null) { + log.info("[seekRecordStamp] 失败, mediaServer的类型: {},未找到对应的实现类", mediaServer.getType()); + throw new ControllerException(ErrorCode.ERROR100.getCode(), "未找到mediaServer对应的实现类"); + } + mediaNodeServerService.seekRecordStamp(mediaServer, app, stream, stamp, schema); + } + + @Override + public void setRecordSpeed(MediaServer mediaServer, String app, String stream, Integer speed, String schema) { + IMediaNodeServerService mediaNodeServerService = nodeServerServiceMap.get(mediaServer.getType()); + if (mediaNodeServerService == null) { + log.info("[setRecordSpeed] 失败, mediaServer的类型: {},未找到对应的实现类", mediaServer.getType()); + throw new ControllerException(ErrorCode.ERROR100.getCode(), "未找到mediaServer对应的实现类"); + } + mediaNodeServerService.setRecordSpeed(mediaServer, app, stream, speed, schema); + } + + @Override + public DownloadFileInfo getDownloadFilePath(MediaServer mediaServer, RecordInfo recordInfo) { + IMediaNodeServerService mediaNodeServerService = nodeServerServiceMap.get(mediaServer.getType()); + if (mediaNodeServerService == null) { + log.info("[setRecordSpeed] 失败, mediaServer的类型: {},未找到对应的实现类", mediaServer.getType()); + throw new ControllerException(ErrorCode.ERROR100.getCode(), "未找到mediaServer对应的实现类"); + } + return mediaNodeServerService.getDownloadFilePath(mediaServer, recordInfo); + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/media/zlm/AssistRESTfulUtils.java b/src/main/java/com/genersoft/iot/vmp/media/zlm/AssistRESTfulUtils.java new file mode 100644 index 0000000..23b8b37 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/media/zlm/AssistRESTfulUtils.java @@ -0,0 +1,288 @@ +package com.genersoft.iot.vmp.media.zlm; + +import com.alibaba.fastjson2.JSON; +import com.alibaba.fastjson2.JSONObject; +import com.genersoft.iot.vmp.media.bean.MediaServer; +import com.genersoft.iot.vmp.utils.SSLSocketClientUtil; +import lombok.extern.slf4j.Slf4j; +import okhttp3.*; +import okhttp3.logging.HttpLoggingInterceptor; +import org.jetbrains.annotations.NotNull; +import org.springframework.stereotype.Component; +import org.springframework.util.ObjectUtils; + +import javax.net.ssl.X509TrustManager; +import java.io.IOException; +import java.net.ConnectException; +import java.net.SocketTimeoutException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.TimeUnit; + +@Slf4j +@Component +public class AssistRESTfulUtils { + + private OkHttpClient client; + + + public interface RequestCallback{ + void run(JSONObject response); + } + + private OkHttpClient getClient(){ + return getClient(null); + } + + private OkHttpClient getClient(Integer readTimeOut){ + if (client == null) { + if (readTimeOut == null) { + readTimeOut = 10; + } + OkHttpClient.Builder httpClientBuilder = new OkHttpClient.Builder(); + // 设置连接超时时间 + httpClientBuilder.connectTimeout(8, TimeUnit.SECONDS); + // 设置读取超时时间 + httpClientBuilder.readTimeout(readTimeOut,TimeUnit.SECONDS); + // 设置连接池 + httpClientBuilder.connectionPool(new ConnectionPool(16, 5, TimeUnit.MINUTES)); + if (log.isDebugEnabled()) { + HttpLoggingInterceptor logging = new HttpLoggingInterceptor(message -> { + log.debug("http请求参数:" + message); + }); + logging.setLevel(HttpLoggingInterceptor.Level.BASIC); + // OkHttp進行添加攔截器loggingInterceptor + httpClientBuilder.addInterceptor(logging); + } + X509TrustManager manager = SSLSocketClientUtil.getX509TrustManager(); + // 设置ssl + httpClientBuilder.sslSocketFactory(SSLSocketClientUtil.getSocketFactory(manager), manager); + httpClientBuilder.hostnameVerifier(SSLSocketClientUtil.getHostnameVerifier());//忽略校验 + client = httpClientBuilder.build(); + } + return client; + + } + + + public JSONObject sendGet(MediaServer mediaServerItem, String api, Map param, RequestCallback callback) { + OkHttpClient client = getClient(); + + if (mediaServerItem == null) { + return null; + } + if (mediaServerItem.getRecordAssistPort() <= 0) { + log.warn("未启用Assist服务"); + return null; + } + StringBuilder stringBuffer = new StringBuilder(); + stringBuffer.append(api); + JSONObject responseJSON = null; + + if (param != null && !param.keySet().isEmpty()) { + stringBuffer.append("?"); + int index = 1; + for (String key : param.keySet()){ + if (param.get(key) != null) { + stringBuffer.append(key + "=" + param.get(key)); + if (index < param.size()) { + stringBuffer.append("&"); + } + } + index++; + } + } + + String url = stringBuffer.toString(); + log.info("[访问assist]: {}", url); + Request request = new Request.Builder() + .get() + .url(url) + .build(); + if (callback == null) { + try { + Response response = client.newCall(request).execute(); + if (response.isSuccessful()) { + ResponseBody responseBody = response.body(); + if (responseBody != null) { + String responseStr = responseBody.string(); + responseJSON = JSON.parseObject(responseStr); + } + }else { + response.close(); + Objects.requireNonNull(response.body()).close(); + } + } catch (ConnectException e) { + log.error(String.format("连接Assist失败: %s, %s", e.getCause().getMessage(), e.getMessage())); + log.info("请检查media配置并确认Assist已启动..."); + }catch (IOException e) { + log.error(String.format("[ %s ]请求失败: %s", url, e.getMessage())); + } + }else { + client.newCall(request).enqueue(new Callback(){ + + @Override + public void onResponse(@NotNull Call call, @NotNull Response response){ + if (response.isSuccessful()) { + try { + String responseStr = Objects.requireNonNull(response.body()).string(); + callback.run(JSON.parseObject(responseStr)); + } catch (IOException e) { + log.error(String.format("[ %s ]请求失败: %s", url, e.getMessage())); + } + + }else { + response.close(); + Objects.requireNonNull(response.body()).close(); + } + } + + @Override + public void onFailure(@NotNull Call call, @NotNull IOException e) { + log.error(String.format("连接Assist失败: %s, %s", e.getCause().getMessage(), e.getMessage())); + log.info("请检查media配置并确认Assist已启动..."); + } + }); + } + + + + return responseJSON; + } + + public JSONObject sendPost(MediaServer mediaServerItem, String url, + JSONObject param, ZLMRESTfulUtils.RequestCallback callback, + Integer readTimeOut) { + OkHttpClient client = getClient(readTimeOut); + + if (mediaServerItem == null) { + return null; + } + log.info("[访问assist]: {}, 参数: {}", url, param); + JSONObject responseJSON = new JSONObject(); + //-2自定义流媒体 调用错误码 + responseJSON.put("code",-2); + responseJSON.put("msg","ASSIST调用失败"); + + RequestBody requestBodyJson = RequestBody.create(MediaType.parse("application/json; charset=utf-8"), param.toString()); + + Request request = new Request.Builder() + .post(requestBodyJson) + .url(url) + .addHeader("Content-Type", "application/json") + .build(); + if (callback == null) { + try { + Response response = client.newCall(request).execute(); + if (response.isSuccessful()) { + ResponseBody responseBody = response.body(); + if (responseBody != null) { + String responseStr = responseBody.string(); + responseJSON = JSON.parseObject(responseStr); + } + }else { + response.close(); + Objects.requireNonNull(response.body()).close(); + } + }catch (IOException e) { + log.error(String.format("[ %s ]ASSIST请求失败: %s", url, e.getMessage())); + + if(e instanceof SocketTimeoutException){ + //读取超时超时异常 + log.error(String.format("读取ASSIST数据失败: %s, %s", url, e.getMessage())); + } + if(e instanceof ConnectException){ + //判断连接异常,我这里是报Failed to connect to 10.7.5.144 + log.error(String.format("连接ASSIST失败: %s, %s", url, e.getMessage())); + } + + }catch (Exception e){ + log.error(String.format("访问ASSIST失败: %s, %s", url, e.getMessage())); + } + }else { + client.newCall(request).enqueue(new Callback(){ + + @Override + public void onResponse(@NotNull Call call, @NotNull Response response){ + if (response.isSuccessful()) { + try { + String responseStr = Objects.requireNonNull(response.body()).string(); + callback.run(responseStr); + } catch (IOException e) { + log.error(String.format("[ %s ]请求失败: %s", url, e.getMessage())); + } + + }else { + response.close(); + Objects.requireNonNull(response.body()).close(); + } + } + + @Override + public void onFailure(@NotNull Call call, @NotNull IOException e) { + log.error(String.format("连接ZLM失败: %s, %s", call.request().toString(), e.getMessage())); + + if(e instanceof SocketTimeoutException){ + //读取超时超时异常 + log.error(String.format("读取ZLM数据失败: %s, %s", call.request().toString(), e.getMessage())); + } + if(e instanceof ConnectException){ + //判断连接异常,我这里是报Failed to connect to 10.7.5.144 + log.error(String.format("连接ZLM失败: %s, %s", call.request().toString(), e.getMessage())); + } + } + }); + } + + + + return responseJSON; + } + + public JSONObject getInfo(MediaServer mediaServerItem, RequestCallback callback){ + Map param = new HashMap<>(); + return sendGet(mediaServerItem, "api/record/info",param, callback); + } + + public JSONObject addTask(MediaServer mediaServerItem, String app, String stream, String startTime, + String endTime, String callId, List filePathList, String remoteHost) { + + JSONObject videoTaskInfoJSON = new JSONObject(); + videoTaskInfoJSON.put("app", app); + videoTaskInfoJSON.put("stream", stream); + videoTaskInfoJSON.put("startTime", startTime); + videoTaskInfoJSON.put("endTime", endTime); + videoTaskInfoJSON.put("callId", callId); + videoTaskInfoJSON.put("filePathList", filePathList); + if (!ObjectUtils.isEmpty(remoteHost)) { + videoTaskInfoJSON.put("remoteHost", remoteHost); + } + String urlStr = String.format("%s/api/record/file/download/task/add", remoteHost);; + return sendPost(mediaServerItem, urlStr, videoTaskInfoJSON, null, 30); + } + + public JSONObject queryTaskList(MediaServer mediaServerItem, String app, String stream, String callId, + String taskId, Boolean isEnd, String scheme) { + Map param = new HashMap<>(); + if (!ObjectUtils.isEmpty(app)) { + param.put("app", app); + } + if (!ObjectUtils.isEmpty(stream)) { + param.put("stream", stream); + } + if (!ObjectUtils.isEmpty(callId)) { + param.put("callId", callId); + } + if (!ObjectUtils.isEmpty(taskId)) { + param.put("taskId", taskId); + } + if (!ObjectUtils.isEmpty(isEnd)) { + param.put("isEnd", isEnd); + } + String urlStr = String.format("%s://%s:%s/api/record/file/download/task/list", + scheme, mediaServerItem.getIp(), mediaServerItem.getRecordAssistPort());; + return sendGet(mediaServerItem, urlStr, param, null); + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/media/zlm/ZLMHttpHookListener.java b/src/main/java/com/genersoft/iot/vmp/media/zlm/ZLMHttpHookListener.java new file mode 100755 index 0000000..605d301 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/media/zlm/ZLMHttpHookListener.java @@ -0,0 +1,316 @@ +package com.genersoft.iot.vmp.media.zlm; + +import com.alibaba.fastjson2.JSON; +import com.alibaba.fastjson2.JSONObject; +import com.genersoft.iot.vmp.conf.UserSetting; +import com.genersoft.iot.vmp.media.bean.MediaServer; +import com.genersoft.iot.vmp.media.bean.ResultForOnPublish; +import com.genersoft.iot.vmp.media.event.media.*; +import com.genersoft.iot.vmp.media.event.mediaServer.MediaSendRtpStoppedEvent; +import com.genersoft.iot.vmp.media.service.IMediaServerService; +import com.genersoft.iot.vmp.media.zlm.dto.ZLMServerConfig; +import com.genersoft.iot.vmp.media.zlm.dto.hook.*; +import com.genersoft.iot.vmp.media.zlm.event.HookZlmServerKeepaliveEvent; +import com.genersoft.iot.vmp.media.zlm.event.HookZlmServerStartEvent; +import com.genersoft.iot.vmp.service.IMediaService; +import com.genersoft.iot.vmp.utils.MediaServerUtils; +import io.swagger.v3.oas.annotations.Hidden; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.util.ObjectUtils; +import org.springframework.web.bind.annotation.*; + +import jakarta.servlet.http.HttpServletRequest; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.HashMap; +import java.util.Map; + +/** + * @description:针对 ZLMediaServer的hook事件监听 + * @author: swwheihei + * @date: 2020年5月8日 上午10:46:48 + */ +@Slf4j +@RestController +@RequestMapping("/index/hook") +@Hidden +public class ZLMHttpHookListener { + + @Autowired + private IMediaServerService mediaServerService; + + @Autowired + private IMediaService mediaService; + + @Autowired + private UserSetting userSetting; + + @Autowired + private ApplicationEventPublisher applicationEventPublisher; + + + /** + * 服务器定时上报时间,上报间隔可配置,默认10s上报一次 + */ + @ResponseBody + @PostMapping(value = "/on_server_keepalive", produces = "application/json;charset=UTF-8") + public HookResult onServerKeepalive(@RequestBody OnServerKeepaliveHookParam param) { + try { + HookZlmServerKeepaliveEvent event = new HookZlmServerKeepaliveEvent(this); + MediaServer mediaServerItem = mediaServerService.getOne(param.getMediaServerId()); + if (mediaServerItem != null) { + event.setMediaServerItem(mediaServerItem); + applicationEventPublisher.publishEvent(event); + } + }catch (Exception e) { + log.info("[ZLM-HOOK-心跳] 发送通知失败 ", e); + } + return HookResult.SUCCESS(); + } + + /** + * 播放器鉴权事件,rtsp/rtmp/http-flv/ws-flv/hls的播放都将触发此鉴权事件。 + */ + @ResponseBody + @PostMapping(value = "/on_play", produces = "application/json;charset=UTF-8") + public HookResult onPlay(@RequestBody OnPlayHookParam param) { + + Map paramMap = MediaServerUtils.urlParamToMap(param.getParams()); + // 对于播放流进行鉴权 + boolean authenticateResult = mediaService.authenticatePlay(param.getApp(), param.getStream(), paramMap.get("callId")); + if (!authenticateResult) { + log.info("[ZLM HOOK] 播放鉴权 失败:{}->{}", param.getMediaServerId(), param); + return new HookResult(401, "Unauthorized"); + } + if (log.isDebugEnabled()){ + log.debug("[ZLM HOOK] 播放鉴权成功:{}->{}", param.getMediaServerId(), param); + } + return HookResult.SUCCESS(); + } + + /** + * rtsp/rtmp/rtp推流鉴权事件。 + */ + @ResponseBody + @PostMapping(value = "/on_publish", produces = "application/json;charset=UTF-8") + public HookResultForOnPublish onPublish(@RequestBody OnPublishHookParam param) { + + JSONObject json = (JSONObject) JSON.toJSON(param); + + log.info("[ZLM HOOK]推流鉴权:{}->{}", param.getMediaServerId(), param); + // TODO 加快处理速度 + + String mediaServerId = json.getString("mediaServerId"); + MediaServer mediaServer = mediaServerService.getOne(mediaServerId); + if (mediaServer == null) { + HookResultForOnPublish fail = HookResultForOnPublish.Fail(); + log.warn("[ZLM HOOK]推流鉴权 响应:{}->找不到对应的mediaServer", param.getMediaServerId()); + return fail; + } + + ResultForOnPublish resultForOnPublish = mediaService.authenticatePublish(mediaServer, param.getApp(), param.getStream(), param.getParams()); + if (resultForOnPublish != null) { + HookResultForOnPublish successResult = HookResultForOnPublish.getInstance(resultForOnPublish); + log.info("[ZLM HOOK]推流鉴权 响应:{}->{}->>>>{}", param.getMediaServerId(), param, successResult); + return successResult; + }else { + HookResultForOnPublish fail = HookResultForOnPublish.Fail(); + log.info("[ZLM HOOK]推流鉴权 响应:{}->{}->>>>{}", param.getMediaServerId(), param, fail); + return fail; + } + } + + /** + * rtsp/rtmp流注册或注销时触发此事件;此事件对回复不敏感。 + */ + @ResponseBody + @PostMapping(value = "/on_stream_changed", produces = "application/json;charset=UTF-8") + public HookResult onStreamChanged(@RequestBody OnStreamChangedHookParam param) { + MediaServer mediaServer = mediaServerService.getOne(param.getMediaServerId()); + if (mediaServer == null) { + return HookResult.SUCCESS(); + } + if (!ObjectUtils.isEmpty(mediaServer.getTranscodeSuffix()) + && !"null".equalsIgnoreCase(mediaServer.getTranscodeSuffix()) + && param.getStream().endsWith(mediaServer.getTranscodeSuffix()) ) { + return HookResult.SUCCESS(); + } + if (param.getSchema().equalsIgnoreCase("rtsp")) { + if (param.isRegist()) { + log.info("[ZLM HOOK] 流注册, {}->{}->{}/{}", param.getMediaServerId(), param.getSchema(), param.getApp(), param.getStream()); + String queryParams = param.getParams(); + if (queryParams == null) { + try { + URL url = new URL("http" + param.getOriginUrl().substring(4)); + queryParams = url.getQuery(); + }catch (MalformedURLException ignored) {} + } + if (queryParams != null) { + param.setParamMap(MediaServerUtils.urlParamToMap(queryParams)); + }else { + param.setParamMap(new HashMap<>()); + } + MediaArrivalEvent mediaArrivalEvent = MediaArrivalEvent.getInstance(this, param, mediaServer, userSetting.getServerId()); + applicationEventPublisher.publishEvent(mediaArrivalEvent); + } else { + log.info("[ZLM HOOK] 流注销, {}->{}->{}/{}", param.getMediaServerId(), param.getSchema(), param.getApp(), param.getStream()); + MediaDepartureEvent mediaDepartureEvent = MediaDepartureEvent.getInstance(this, param, mediaServer); + applicationEventPublisher.publishEvent(mediaDepartureEvent); + } + } + + return HookResult.SUCCESS(); + } + + /** + * 流无人观看时事件,用户可以通过此事件选择是否关闭无人看的流。 + */ + @ResponseBody + @PostMapping(value = "/on_stream_none_reader", produces = "application/json;charset=UTF-8") + public JSONObject onStreamNoneReader(@RequestBody OnStreamNoneReaderHookParam param) { + + log.info("[ZLM HOOK]流无人观看:{}->{}->{}/{}", param.getMediaServerId(), param.getSchema(), + param.getApp(), param.getStream()); + + MediaServer mediaInfo = mediaServerService.getOne(param.getMediaServerId()); + if (mediaInfo == null) { + JSONObject ret = new JSONObject(); + ret.put("code", 0); + return ret; + } + if (mediaInfo.getTranscodeSuffix() != null && param.getStream().endsWith(mediaInfo.getTranscodeSuffix())) { + param.setStream(param.getStream().substring(0, param.getStream().lastIndexOf(mediaInfo.getTranscodeSuffix()) - 1)); + } + if (!ObjectUtils.isEmpty(mediaInfo.getTranscodeSuffix()) + && !"null".equalsIgnoreCase(mediaInfo.getTranscodeSuffix()) + && param.getStream().endsWith(mediaInfo.getTranscodeSuffix()) ) { + param.setStream(param.getStream().substring(0, param.getStream().lastIndexOf(mediaInfo.getTranscodeSuffix()) -1 )); + } + + JSONObject ret = new JSONObject(); + boolean close = mediaService.closeStreamOnNoneReader(param.getMediaServerId(), param.getApp(), param.getStream(), param.getSchema()); + log.info("[ZLM HOOK]流无人观看是否触发关闭:{}, {}->{}->{}/{}", close, param.getMediaServerId(), param.getSchema(), + param.getApp(), param.getStream()); + ret.put("code", 0); + ret.put("close", close); + return ret; + } + + /** + * 流未找到事件,用户可以在此事件触发时,立即去拉流,这样可以实现按需拉流;此事件对回复不敏感。 + */ + @ResponseBody + @PostMapping(value = "/on_stream_not_found", produces = "application/json;charset=UTF-8") + public HookResult onStreamNotFound(@RequestBody OnStreamNotFoundHookParam param) { + log.info("[ZLM HOOK] 流未找到:{}->{}->{}/{}", param.getMediaServerId(), param.getSchema(), param.getApp(), param.getStream()); + + + MediaServer mediaServer = mediaServerService.getOne(param.getMediaServerId()); + if (!userSetting.getAutoApplyPlay() || mediaServer == null) { + return HookResult.SUCCESS(); + } + MediaNotFoundEvent mediaNotFoundEvent = MediaNotFoundEvent.getInstance(this, param, mediaServer); + applicationEventPublisher.publishEvent(mediaNotFoundEvent); + return HookResult.SUCCESS(); + } + + /** + * 服务器启动事件,可以用于监听服务器崩溃重启;此事件对回复不敏感。 + */ + @ResponseBody + @PostMapping(value = "/on_server_started", produces = "application/json;charset=UTF-8") + public HookResult onServerStarted(HttpServletRequest request, @RequestBody JSONObject jsonObject) { + + jsonObject.put("ip", request.getRemoteAddr()); + ZLMServerConfig zlmServerConfig = JSON.to(ZLMServerConfig.class, jsonObject); + zlmServerConfig.setIp(request.getRemoteAddr()); + log.info("[ZLM HOOK] zlm 启动 " + zlmServerConfig.getGeneralMediaServerId()); + try { + HookZlmServerStartEvent event = new HookZlmServerStartEvent(this); + MediaServer mediaServerItem = mediaServerService.getOne(zlmServerConfig.getMediaServerId()); + if (mediaServerItem != null) { + event.setMediaServerItem(mediaServerItem); + applicationEventPublisher.publishEvent(event); + } + }catch (Exception e) { + log.info("[ZLM-HOOK-ZLM启动] 发送通知失败 ", e); + } + + return HookResult.SUCCESS(); + } + + /** + * 发送rtp(startSendRtp)被动关闭时回调 + */ + @ResponseBody + @PostMapping(value = "/on_send_rtp_stopped", produces = "application/json;charset=UTF-8") + public HookResult onSendRtpStopped(HttpServletRequest request, @RequestBody OnSendRtpStoppedHookParam param) { + + log.info("[ZLM HOOK] rtp发送关闭:{}->{}/{}", param.getMediaServerId(), param.getApp(), param.getStream()); + + // 查找对应的上级推流,发送停止 + if (!"rtp".equals(param.getApp())) { + return HookResult.SUCCESS(); + } + try { + MediaSendRtpStoppedEvent event = new MediaSendRtpStoppedEvent(this); + MediaServer mediaServerItem = mediaServerService.getOne(param.getMediaServerId()); + if (mediaServerItem != null) { + event.setMediaServer(mediaServerItem); + applicationEventPublisher.publishEvent(event); + } + }catch (Exception e) { + log.info("[ZLM-HOOK-rtp发送关闭] 发送通知失败 ", e); + } + + return HookResult.SUCCESS(); + } + + /** + * rtpServer收流超时 + */ + @ResponseBody + @PostMapping(value = "/on_rtp_server_timeout", produces = "application/json;charset=UTF-8") + public HookResult onRtpServerTimeout(@RequestBody OnRtpServerTimeoutHookParam + param) { + log.info("[ZLM HOOK] rtpServer收流超时:{}->{}({})", param.getMediaServerId(), param.getStream_id(), param.getSsrc()); + + try { + MediaRtpServerTimeoutEvent event = new MediaRtpServerTimeoutEvent(this); + MediaServer mediaServerItem = mediaServerService.getOne(param.getMediaServerId()); + if (mediaServerItem != null) { + event.setMediaServer(mediaServerItem); + event.setApp("rtp"); + applicationEventPublisher.publishEvent(event); + } + }catch (Exception e) { + log.info("[ZLM-HOOK-rtpServer收流超时] 发送通知失败 ", e); + } + + return HookResult.SUCCESS(); + } + + /** + * 录像完成事件 + */ + @ResponseBody + @PostMapping(value = "/on_record_mp4", produces = "application/json;charset=UTF-8") + public HookResult onRecordMp4(HttpServletRequest request, @RequestBody OnRecordMp4HookParam param) { + log.info("[ZLM HOOK] 录像完成:时长: {}, {}->{}",param.getTime_len(), param.getMediaServerId(), param.getFile_path()); + + try { + MediaServer mediaServerItem = mediaServerService.getOne(param.getMediaServerId()); + if (mediaServerItem != null) { + MediaRecordMp4Event event = MediaRecordMp4Event.getInstance(this, param, mediaServerItem); + event.setMediaServer(mediaServerItem); + applicationEventPublisher.publishEvent(event); + } + }catch (Exception e) { + log.info("[ZLM-HOOK-rtpServer收流超时] 发送通知失败 ", e); + } + + return HookResult.SUCCESS(); + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/media/zlm/ZLMMediaNodeServerService.java b/src/main/java/com/genersoft/iot/vmp/media/zlm/ZLMMediaNodeServerService.java new file mode 100644 index 0000000..628ee20 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/media/zlm/ZLMMediaNodeServerService.java @@ -0,0 +1,690 @@ +package com.genersoft.iot.vmp.media.zlm; + +import com.alibaba.fastjson2.JSON; +import com.alibaba.fastjson2.JSONArray; +import com.alibaba.fastjson2.JSONObject; +import com.genersoft.iot.vmp.common.CommonCallback; +import com.genersoft.iot.vmp.common.StreamInfo; +import com.genersoft.iot.vmp.conf.UserSetting; +import com.genersoft.iot.vmp.conf.exception.ControllerException; +import com.genersoft.iot.vmp.gb28181.bean.SendRtpInfo; +import com.genersoft.iot.vmp.media.bean.MediaInfo; +import com.genersoft.iot.vmp.media.bean.MediaServer; +import com.genersoft.iot.vmp.media.bean.RecordInfo; +import com.genersoft.iot.vmp.media.event.hook.Hook; +import com.genersoft.iot.vmp.media.event.hook.HookSubscribe; +import com.genersoft.iot.vmp.media.event.hook.HookType; +import com.genersoft.iot.vmp.media.service.IMediaNodeServerService; +import com.genersoft.iot.vmp.media.zlm.dto.*; +import com.genersoft.iot.vmp.service.bean.DownloadFileInfo; +import com.genersoft.iot.vmp.service.bean.ErrorCallback; +import com.genersoft.iot.vmp.streamProxy.bean.StreamProxy; +import com.genersoft.iot.vmp.vmanager.bean.ErrorCode; +import com.genersoft.iot.vmp.vmanager.bean.WVPResult; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.RandomStringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.util.ObjectUtils; + +import java.util.*; + +@Slf4j +@Service("zlm") +public class ZLMMediaNodeServerService implements IMediaNodeServerService { + + + @Autowired + private ZLMRESTfulUtils zlmresTfulUtils; + + @Autowired + private ZLMServerFactory zlmServerFactory; + + @Autowired + private UserSetting userSetting; + + @Autowired + private HookSubscribe subscribe; + + @Override + public int createRTPServer(MediaServer mediaServer, String streamId, long ssrc, Integer port, Boolean onlyAuto, Boolean disableAudio, Boolean reUsePort, Integer tcpMode) { + return zlmServerFactory.createRTPServer(mediaServer, "rtp", streamId, ssrc, port, onlyAuto, disableAudio, reUsePort, tcpMode); + } + + @Override + public void closeRtpServer(MediaServer mediaServer, String streamId, CommonCallback callback) { + zlmServerFactory.closeRtpServer(mediaServer, streamId, callback); + } + + @Override + public int createJTTServer(MediaServer mediaServer, String streamId, Integer port, Boolean disableVideo, Boolean disableAudio, Integer tcpMode) { + return zlmServerFactory.createRTPServer(mediaServer, "1078", streamId, 0, port, disableVideo, disableAudio, false, tcpMode); + } + + @Override + public void closeJTTServer(MediaServer mediaServer, String streamId, CommonCallback callback) { + zlmServerFactory.closeRtpServer(mediaServer, streamId, callback); + } + + @Override + public void closeStreams(MediaServer mediaServer, String app, String stream) { + zlmresTfulUtils.closeStreams(mediaServer, app, stream); + } + + @Override + public Boolean updateRtpServerSSRC(MediaServer mediaServer, String streamId, String ssrc) { + return zlmServerFactory.updateRtpServerSSRC(mediaServer, streamId, ssrc); + } + + @Override + public boolean checkNodeId(MediaServer mediaServer) { + if (mediaServer == null) { + return false; + } + ZLMResult> mediaServerConfig = zlmresTfulUtils.getMediaServerConfig(mediaServer); + if (mediaServerConfig != null) { + List data = mediaServerConfig.getData(); + if (data != null && !data.isEmpty()) { + ZLMServerConfig zlmServerConfig= JSON.parseObject(JSON.toJSONString(data.get(0)), ZLMServerConfig.class); + return zlmServerConfig.getGeneralMediaServerId().equals(mediaServer.getId()); + }else { + return false; + } + + }else { + return false; + } + } + + @Override + public void online(MediaServer mediaServer) { + + } + + @Override + public MediaServer checkMediaServer(String ip, int port, String secret) { + MediaServer mediaServer = new MediaServer(); + mediaServer.setServerId(userSetting.getServerId()); + mediaServer.setIp(ip); + mediaServer.setHttpPort(port); + mediaServer.setSecret(secret); + ZLMResult> mediaServerConfigResult = zlmresTfulUtils.getMediaServerConfig(mediaServer); + if (mediaServerConfigResult == null) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "连接失败"); + } + List configList = mediaServerConfigResult.getData(); + if (configList == null) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "读取配置失败"); + } + ZLMServerConfig zlmServerConfig = JSON.parseObject(JSON.toJSONString(configList.get(0)), ZLMServerConfig.class); + if (zlmServerConfig == null) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "读取配置失败"); + } + mediaServer.setId(zlmServerConfig.getGeneralMediaServerId()); + mediaServer.setHttpSSlPort(zlmServerConfig.getHttpSSLport()); + mediaServer.setRtmpPort(zlmServerConfig.getRtmpPort()); + mediaServer.setRtmpSSlPort(zlmServerConfig.getRtmpSslPort()); + mediaServer.setRtspPort(zlmServerConfig.getRtspPort()); + mediaServer.setRtspSSLPort(zlmServerConfig.getRtspSSlport()); + mediaServer.setRtpProxyPort(zlmServerConfig.getRtpProxyPort()); + mediaServer.setStreamIp(ip); + + mediaServer.setHookIp("127.0.0.1"); + mediaServer.setSdpIp(ip); + mediaServer.setType("zlm"); + return mediaServer; + } + + @Override + public boolean stopSendRtp(MediaServer mediaInfo, String app, String stream, String ssrc) { + Map param = new HashMap<>(); + param.put("vhost", "__defaultVhost__"); + param.put("app", app); + param.put("stream", stream); + if (!ObjectUtils.isEmpty(ssrc)) { + param.put("ssrc", ssrc); + } + ZLMResult zlmResult = zlmresTfulUtils.stopSendRtp(mediaInfo, param); + if (zlmResult.getCode() == 0) { + log.info("[停止发流] 成功: 参数:{}", JSON.toJSONString(param)); + return true; + }else { + log.info("停止发流结果: {}, 参数:{}", zlmResult.getMsg(), JSON.toJSONString(param)); + return false; + } + } + + @Override + public boolean initStopSendRtp(MediaServer mediaInfo, String app, String stream, String ssrc) { + Map param = new HashMap<>(); + param.put("vhost", "__defaultVhost__"); + param.put("app", app); + param.put("stream", stream); + if (!ObjectUtils.isEmpty(ssrc)) { + param.put("ssrc", ssrc); + } + ZLMResult zlmResult = zlmresTfulUtils.stopSendRtp(mediaInfo, param); + if (zlmResult.getCode() != 0 ) { + log.error("停止发流失败: {}, 参数:{}", zlmResult.getMsg(), JSON.toJSONString(param)); + return false; + } + return true; + } + + @Override + public boolean deleteRecordDirectory(MediaServer mediaServer, String app, String stream, String date, String fileName) { + log.info("[zlm-deleteRecordDirectory] 删除磁盘文件, server: {} {}:{}->{}/{}", mediaServer.getId(), app, stream, date, fileName); + ZLMResult zlmResult = zlmresTfulUtils.deleteRecordDirectory(mediaServer, app, + stream, date, fileName); + if (zlmResult.getCode() == 0) { + return true; + }else { + log.info("[zlm-deleteRecordDirectory] 删除磁盘文件错误, server: {} {}:{}->{}/{}, 结果: {}", mediaServer.getId(), app, stream, date, fileName, zlmResult); + throw new ControllerException(ErrorCode.ERROR100.getCode(), "删除磁盘文件失败"); + } + } + + @Override + public List getMediaList(MediaServer mediaServer, String app, String stream, String callId) { + List streamInfoList = new ArrayList<>(); + ZLMResult zlmResult = zlmresTfulUtils.getMediaList(mediaServer, app, stream); + if (zlmResult != null) { + if (zlmResult.getCode() == 0) { + if (zlmResult.getData() == null) { + return streamInfoList; + } + for (int i = 0; i < zlmResult.getData().size(); i++) { + JSONObject mediaJSON = zlmResult.getData().getJSONObject(0); + MediaInfo mediaInfo = MediaInfo.getInstance(mediaJSON, mediaServer, userSetting.getServerId()); + StreamInfo streamInfo = getStreamInfoByAppAndStream(mediaServer, mediaInfo.getApp(), + mediaInfo.getStream(), mediaInfo, null, callId, true); + if (streamInfo != null) { + streamInfoList.add(streamInfo); + } + } + } + } + return streamInfoList; + } + + @Override + public Boolean connectRtpServer(MediaServer mediaServer, String address, int port, String stream) { + ZLMResult zlmResult = zlmresTfulUtils.connectRtpServer(mediaServer, address, port, stream); + log.info("[TCP主动连接对方] 结果: {}", zlmResult); + return zlmResult.getCode() == 0; + } + + @Override + public void getSnap(MediaServer mediaServer, String app, String stream, int timeoutSec, int expireSec, String path, String fileName) { + String streamUrl; + if (mediaServer.getRtspPort() != 0) { + streamUrl = String.format("rtsp://127.0.0.1:%s/%s/%s", mediaServer.getRtspPort(), "rtp", stream); + } else { + streamUrl = String.format("http://127.0.0.1:%s/%s/%s.live.mp4", mediaServer.getHttpPort(), "rtp", stream); + } + zlmresTfulUtils.getSnap(mediaServer, streamUrl, timeoutSec, expireSec, path, fileName); + } + + @Override + public MediaInfo getMediaInfo(MediaServer mediaServer, String app, String stream) { + ZLMResult zlmResult = zlmresTfulUtils.getMediaInfo(mediaServer, app, "rtsp", stream); + if (zlmResult.getCode() != 0 || zlmResult.getData() == null || zlmResult.getData().getString("app") == null ) { + return null; + } + return MediaInfo.getInstance(zlmResult.getData(), mediaServer, userSetting.getServerId()); + } + + @Override + public Boolean pauseRtpCheck(MediaServer mediaServer, String streamKey) { + ZLMResult zlmResult = zlmresTfulUtils.pauseRtpCheck(mediaServer, streamKey); + return zlmResult.getCode() == 0; + } + + @Override + public Boolean resumeRtpCheck(MediaServer mediaServer, String streamKey) { + ZLMResult zlmResult = zlmresTfulUtils.resumeRtpCheck(mediaServer, streamKey); + return zlmResult.getCode() == 0; + } + + @Override + public String getFfmpegCmd(MediaServer mediaServer, String cmdKey) { + ZLMResult> mediaServerConfigResult = zlmresTfulUtils.getMediaServerConfig(mediaServer); + if (mediaServerConfigResult == null || mediaServerConfigResult.getCode() != 0) { + log.warn("[getFfmpegCmd] 获取流媒体配置失败"); + throw new ControllerException(ErrorCode.ERROR100.getCode(), "获取流媒体配置失败"); + } + List data = mediaServerConfigResult.getData(); + JSONObject mediaServerConfig = data.get(0); + if (ObjectUtils.isEmpty(cmdKey)) { + cmdKey = "ffmpeg.cmd"; + } + return mediaServerConfig.getString(cmdKey); + } + + @Override + public WVPResult addStreamProxy(MediaServer mediaServer, String app, String stream, String url, + boolean enableAudio, boolean enableMp4, String rtpType, Integer timeout) { + ZLMResult zlmResult = zlmresTfulUtils.addStreamProxy(mediaServer, app, stream, url, enableAudio, enableMp4, rtpType, timeout); + if (zlmResult.getCode() != 0) { + return WVPResult.fail(ErrorCode.ERROR100.getCode(), "添加代理失败"); + }else { + StreamProxyResult data = zlmResult.getData(); + if (data == null) { + return WVPResult.fail(ErrorCode.ERROR100.getCode(), "代理结果异常"); + }else { + return WVPResult.success(data.getKey()); + } + } + } + + @Override + public Boolean delFFmpegSource(MediaServer mediaServer, String streamKey) { + ZLMResult flagDataZLMResult = zlmresTfulUtils.delFFmpegSource(mediaServer, streamKey); + return flagDataZLMResult != null && flagDataZLMResult.getCode() == 0; + } + + @Override + public Boolean delStreamProxy(MediaServer mediaServer, String streamKey) { + ZLMResult flagDataZLMResult = zlmresTfulUtils.delStreamProxy(mediaServer, streamKey); + return flagDataZLMResult != null && flagDataZLMResult.getCode() == 0; + } + + @Override + public Map getFFmpegCMDs(MediaServer mediaServer) { + Map result = new HashMap<>(); + ZLMResult> mediaServerConfigResult = zlmresTfulUtils.getMediaServerConfig(mediaServer); + if (mediaServerConfigResult != null && mediaServerConfigResult.getCode() == 0 + && !mediaServerConfigResult.getData().isEmpty()){ + JSONObject jsonObject = mediaServerConfigResult.getData().get(0); + + for (String key : jsonObject.keySet()) { + if (key.startsWith("ffmpeg.cmd")){ + result.put(key, jsonObject.getString(key)); + } + } + } + return result; + } + + @Override + public Integer startSendRtpPassive(MediaServer mediaServer, SendRtpInfo sendRtpItem, Integer timeout) { + Map param = new HashMap<>(12); + param.put("vhost","__defaultVhost__"); + param.put("app", sendRtpItem.getApp()); + param.put("stream", sendRtpItem.getStream()); + param.put("ssrc", sendRtpItem.getSsrc()); + param.put("src_port", sendRtpItem.getLocalPort()); + param.put("pt", sendRtpItem.getPt()); + param.put("use_ps", sendRtpItem.isUsePs() ? "1" : "0"); + param.put("only_audio", sendRtpItem.isOnlyAudio() ? "1" : "0"); + param.put("is_udp", sendRtpItem.isTcp() ? "0" : "1"); + param.put("recv_stream_id", sendRtpItem.getReceiveStream()); + param.put("enable_origin_recv_limit", "1"); + if (timeout != null) { + param.put("close_delay_ms", timeout); + } + if (!sendRtpItem.isTcp()) { + // 开启rtcp保活 + param.put("udp_rtcp_timeout", sendRtpItem.isRtcp()? "1":"0"); + } + if (!sendRtpItem.isTcpActive()) { + param.put("dst_url",sendRtpItem.getIp()); + param.put("dst_port", sendRtpItem.getPort()); + } + + ZLMResult zlmResult = zlmServerFactory.startSendRtpPassive(mediaServer, param, null); + if (zlmResult.getCode() != 0 ) { + log.error("启动监听TCP被动推流失败: {}, 参数:{}", zlmResult.getMsg(), JSON.toJSONString(param)); + throw new ControllerException(zlmResult.getCode(), zlmResult.getMsg()); + } + log.info("调用ZLM-TCP被动推流接口成功: 本地端口: {}", zlmResult.getLocal_port()); + return zlmResult.getLocal_port(); + } + + @Override + public void startSendRtpStream(MediaServer mediaServer, SendRtpInfo sendRtpItem) { + Map param = new HashMap<>(12); + param.put("vhost", "__defaultVhost__"); + param.put("app", sendRtpItem.getApp()); + param.put("stream", sendRtpItem.getStream()); + param.put("ssrc", sendRtpItem.getSsrc()); + param.put("src_port", sendRtpItem.getLocalPort()); + param.put("pt", sendRtpItem.getPt()); + param.put("use_ps", sendRtpItem.isUsePs() ? "1" : "0"); + param.put("only_audio", sendRtpItem.isOnlyAudio() ? "1" : "0"); + param.put("is_udp", sendRtpItem.isTcp() ? "0" : "1"); + param.put("enable_origin_recv_limit", "1"); + if (!sendRtpItem.isTcp()) { + // udp模式下开启rtcp保活 + param.put("udp_rtcp_timeout", sendRtpItem.isRtcp() ? "500" : "0"); + } + param.put("dst_url", sendRtpItem.getIp()); + param.put("dst_port", sendRtpItem.getPort()); + ZLMResult zlmResult = zlmresTfulUtils.startSendRtp(mediaServer, param); + if (zlmResult == null ) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "连接zlm失败"); + }else if (zlmResult.getCode() != 0) { + throw new ControllerException(zlmResult.getCode(), zlmResult.getMsg()); + } + log.info("[推流结果]:{} ,参数: {}", zlmResult, JSONObject.toJSONString(param)); + } + + @Override + public Integer startSendRtpTalk(MediaServer mediaServer, SendRtpInfo sendRtpItem, Integer timeout) { + Map param = new HashMap<>(12); + param.put("vhost","__defaultVhost__"); + param.put("app", sendRtpItem.getApp()); + param.put("stream", sendRtpItem.getStream()); + param.put("ssrc", sendRtpItem.getSsrc()); + param.put("pt", sendRtpItem.getPt()); + param.put("type", sendRtpItem.isUsePs() ? "1" : "0"); + param.put("only_audio", sendRtpItem.isOnlyAudio() ? "1" : "0"); + param.put("recv_stream_id", sendRtpItem.getReceiveStream()); + param.put("enable_origin_recv_limit", "1"); + ZLMResult zlmResult = zlmServerFactory.startSendRtpTalk(mediaServer, param, null); + if (zlmResult.getCode() != 0 ) { + log.error("启动监听TCP被动推流失败: {}, 参数:{}", zlmResult.getMsg(), JSON.toJSONString(param)); + throw new ControllerException(zlmResult.getCode(), zlmResult.getMsg()); + } + log.info("调用ZLM-TCP被动推流接口, 成功 本地端口: {}", zlmResult.getLocal_port()); + return zlmResult.getLocal_port(); + } + + @Override + public Long updateDownloadProcess(MediaServer mediaServer, String app, String stream) { + MediaInfo mediaInfo = getMediaInfo(mediaServer, app, stream); + if (mediaInfo == null) { + log.warn("[获取下载进度] 查询进度失败, 节点Id: {}, {}/{}", mediaServer.getId(), app, stream); + return null; + } + return mediaInfo.getDuration(); + } + + @Override + public String startProxy(MediaServer mediaServer, StreamProxy streamProxy) { + String dstUrl; + if ("ffmpeg".equalsIgnoreCase(streamProxy.getType())) { + + String ffmpegCmd = getFfmpegCmd(mediaServer, streamProxy.getFfmpegCmdKey()); + + if (ffmpegCmd == null) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "ffmpeg拉流代理无法获取ffmpeg cmd"); + } + String schema = getSchemaFromFFmpegCmd(ffmpegCmd); + if (schema == null) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "ffmpeg拉流代理无法从ffmpeg cmd中获取到输出格式"); + } + int port; + String schemaForUri; + if (schema.equalsIgnoreCase("rtsp")) { + port = mediaServer.getRtspPort(); + schemaForUri = schema; + }else if (schema.equalsIgnoreCase("flv")) { + if (mediaServer.getRtmpPort() == 0) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "ffmpeg拉流代理播放时发现未设置rtmp端口"); + } + port = mediaServer.getRtmpPort(); + schemaForUri = "rtmp"; + }else { + port = mediaServer.getRtmpPort(); + schemaForUri = schema; + } + + dstUrl = String.format("%s://%s:%s/%s/%s", schemaForUri, "127.0.0.1", port, streamProxy.getApp(), + streamProxy.getStream()); + }else { + dstUrl = String.format("rtsp://%s:%s/%s/%s", "127.0.0.1", mediaServer.getRtspPort(), streamProxy.getApp(), + streamProxy.getStream()); + } + MediaInfo mediaInfo = getMediaInfo(mediaServer, streamProxy.getApp(), streamProxy.getStream()); + + if (mediaInfo != null) { + closeStreams(mediaServer, streamProxy.getApp(), streamProxy.getStream()); + } + + ZLMResult zlmResult = null; + if ("ffmpeg".equalsIgnoreCase(streamProxy.getType())){ + if (streamProxy.getTimeout() == 0) { + streamProxy.setTimeout(15); + } + zlmResult = zlmresTfulUtils.addFFmpegSource(mediaServer, streamProxy.getSrcUrl().trim(), dstUrl, + streamProxy.getTimeout(), streamProxy.isEnableAudio(), streamProxy.isEnableMp4(), + streamProxy.getFfmpegCmdKey()); + }else { + zlmResult = zlmresTfulUtils.addStreamProxy(mediaServer, streamProxy.getApp(), streamProxy.getStream(), streamProxy.getSrcUrl().trim(), + streamProxy.isEnableAudio(), streamProxy.isEnableMp4(), streamProxy.getRtspType(), streamProxy.getTimeout()); + } + if (zlmResult.getCode() != 0) { + throw new ControllerException(zlmResult.getCode(), zlmResult.getMsg()); + }else { + StreamProxyResult data = zlmResult.getData(); + if (data == null) { + throw new ControllerException(zlmResult.getCode(), "代理结果异常: " + zlmResult); + }else { + return data.getKey(); + } + } + } + + private String getSchemaFromFFmpegCmd(String ffmpegCmd) { + ffmpegCmd = ffmpegCmd.replaceAll(" + ", " "); + String[] paramArray = ffmpegCmd.split(" "); + if (paramArray.length == 0) { + return null; + } + for (int i = 0; i < paramArray.length; i++) { + if (paramArray[i].equalsIgnoreCase("-f")) { + if (i + 1 < paramArray.length - 1) { + return paramArray[i+1]; + }else { + return null; + } + + } + } + return null; + } + + @Override + public void stopProxy(MediaServer mediaServer, String streamKey, String type) { + ZLMResult zlmResult = zlmresTfulUtils.delStreamProxy(mediaServer, streamKey); + if (zlmResult == null) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "请求失败"); + }else if (zlmResult.getCode() != 0) { + throw new ControllerException(zlmResult.getCode(), zlmResult.getMsg()); + } + } + + @Override + public List listRtpServer(MediaServer mediaServer) { + ZLMResult> zlmResult = zlmresTfulUtils.listRtpServer(mediaServer); + List result = new ArrayList<>(); + if (zlmResult.getCode() != 0) { + return result; + } + List data = zlmResult.getData(); + if (data == null || data.isEmpty()) { + return result; + } + for (RtpServerResult datum : data) { + result.add(datum.getStream_id()); + } + return result; + } + + @Override + public void loadMP4File(MediaServer mediaServer, String app, String stream, String filePath, String fileName, ErrorCallback callback) { + String buildApp = "mp4_record"; + String buildStream = app + "_" + stream + "_" + fileName + "_" + RandomStringUtils.randomAlphabetic(6).toLowerCase(); + + Hook hook = Hook.getInstance(HookType.on_media_arrival, buildApp, buildStream, mediaServer.getServerId()); + subscribe.addSubscribe(hook, (hookData) -> { + StreamInfo streamInfo = getStreamInfoByAppAndStream(mediaServer, buildApp, buildStream, hookData.getMediaInfo(), null, null, true); + if (callback != null) { + callback.run(ErrorCode.SUCCESS.getCode(), ErrorCode.SUCCESS.getMsg(), streamInfo); + } + }); + + ZLMResult zlmResult = zlmresTfulUtils.loadMP4File(mediaServer, buildApp, buildStream, filePath); + + if (zlmResult == null) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "请求失败"); + } + if (zlmResult.getCode() != 0) { + throw new ControllerException(zlmResult.getCode(), zlmResult.getMsg()); + } + } + + @Override + public void loadMP4FileForDate(MediaServer mediaServer, String app, String stream, String date, String dateDir, ErrorCallback callback) { + String buildApp = "mp4_record"; + String buildStream = app + "_" + stream + "_" + date; + MediaInfo mediaInfo = getMediaInfo(mediaServer, buildApp, buildStream); + if (mediaInfo != null) { + if (callback != null) { + StreamInfo streamInfo = getStreamInfoByAppAndStream(mediaServer, buildApp, buildStream, mediaInfo, null, null, true); + callback.run(ErrorCode.SUCCESS.getCode(), ErrorCode.SUCCESS.getMsg(), streamInfo); + } + return; + } + + Hook hook = Hook.getInstance(HookType.on_media_arrival, buildApp, buildStream, mediaServer.getServerId()); + subscribe.addSubscribe(hook, (hookData) -> { + StreamInfo streamInfo = getStreamInfoByAppAndStream(mediaServer, buildApp, buildStream, hookData.getMediaInfo(), null, null, true); + if (callback != null) { + callback.run(ErrorCode.SUCCESS.getCode(), ErrorCode.SUCCESS.getMsg(), streamInfo); + } + }); + + ZLMResult zlmResult = zlmresTfulUtils.loadMP4File(mediaServer, buildApp, buildStream, dateDir); + + if (zlmResult == null) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "请求失败"); + } + if (zlmResult.getCode() != 0) { + throw new ControllerException(zlmResult.getCode(), zlmResult.getMsg()); + } + } + + @Override + public void seekRecordStamp(MediaServer mediaServer, String app, String stream, Double stamp, String schema) { + ZLMResult zlmResult = zlmresTfulUtils.seekRecordStamp(mediaServer, app, stream, stamp, schema); + if (zlmResult == null) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "请求失败"); + } + if (zlmResult.getCode() != 0) { + throw new ControllerException(zlmResult.getCode(), zlmResult.getMsg()); + } + } + + @Override + public void setRecordSpeed(MediaServer mediaServer, String app, String stream, Integer speed, String schema) { + ZLMResult zlmResult = zlmresTfulUtils.setRecordSpeed(mediaServer, app, stream, speed, schema); + if (zlmResult == null) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "请求失败"); + } + if (zlmResult.getCode() != 0) { + throw new ControllerException(zlmResult.getCode(), zlmResult.getMsg()); + } + } + + @Override + public DownloadFileInfo getDownloadFilePath(MediaServer mediaServerItem, RecordInfo recordInfo) { + + // 将filePath作为独立参数传入,避免%符号解析问题 + String pathTemplate = "%s://%s:%s/index/api/downloadFile?file_path=%s"; + + DownloadFileInfo info = new DownloadFileInfo(); + + // filePath作为第4个参数 + info.setHttpPath(String.format(pathTemplate, + "http", + mediaServerItem.getStreamIp(), + mediaServerItem.getHttpPort(), + recordInfo.getFilePath())); + + // 同样作为第4个参数 + if (mediaServerItem.getHttpSSlPort() > 0) { + info.setHttpsPath(String.format(pathTemplate, + "https", + mediaServerItem.getStreamIp(), + mediaServerItem.getHttpSSlPort(), + recordInfo.getFilePath())); + } + return info; + } + + @Override + public StreamInfo getStreamInfoByAppAndStream(MediaServer mediaServer, String app, String stream, MediaInfo mediaInfo, String addr, String callId, boolean isPlay) { + StreamInfo streamInfoResult = new StreamInfo(); + streamInfoResult.setStream(stream); + streamInfoResult.setApp(app); + if (addr == null) { + addr = mediaServer.getStreamIp(); + } + + streamInfoResult.setIp(addr); + if (mediaInfo != null) { + streamInfoResult.setServerId(mediaInfo.getServerId()); + }else { + streamInfoResult.setServerId(userSetting.getServerId()); + } + + streamInfoResult.setMediaServer(mediaServer); + Map param = new HashMap<>(); + if (!ObjectUtils.isEmpty(callId)) { + param.put("callId", callId); + } + if (mediaInfo != null && !ObjectUtils.isEmpty(mediaInfo.getOriginTypeStr())) { + if (!ObjectUtils.isEmpty(mediaInfo.getOriginTypeStr())) { + param.put("originTypeStr", mediaInfo.getOriginTypeStr()); + } + if (!ObjectUtils.isEmpty(mediaInfo.getVideoCodec())) { + param.put("videoCodec", mediaInfo.getVideoCodec()); + } + if (!ObjectUtils.isEmpty(mediaInfo.getAudioCodec())) { + param.put("audioCodec", mediaInfo.getAudioCodec()); + } + } + StringBuilder callIdParamBuilder = new StringBuilder(); + if (!param.isEmpty()) { + callIdParamBuilder.append("?"); + for (Map.Entry entry : param.entrySet()) { + callIdParamBuilder.append(entry.getKey()).append("=").append(entry.getValue()); + callIdParamBuilder.append("&"); + } + callIdParamBuilder.deleteCharAt(callIdParamBuilder.length() - 1); + } + + String callIdParam = callIdParamBuilder.toString(); + + streamInfoResult.setRtmp(addr, mediaServer.getRtmpPort(),mediaServer.getRtmpSSlPort(), app, stream, callIdParam); + streamInfoResult.setRtsp(addr, mediaServer.getRtspPort(),mediaServer.getRtspSSLPort(), app, stream, callIdParam); + + String flvFile = String.format("%s/%s.live.flv%s", app, stream, callIdParam); + streamInfoResult.setFlv(addr, mediaServer.getHttpPort(),mediaServer.getHttpSSlPort(), flvFile); + streamInfoResult.setWsFlv(addr, mediaServer.getHttpPort(),mediaServer.getHttpSSlPort(), flvFile); + + String mp4File = String.format("%s/%s.live.mp4%s", app, stream, callIdParam); + streamInfoResult.setFmp4(addr, mediaServer.getHttpPort(),mediaServer.getHttpSSlPort(), mp4File); + streamInfoResult.setWsMp4(addr, mediaServer.getHttpPort(),mediaServer.getHttpSSlPort(), mp4File); + + streamInfoResult.setHls(addr, mediaServer.getHttpPort(), mediaServer.getHttpSSlPort(), app, stream, callIdParam); + streamInfoResult.setWsHls(addr, mediaServer.getHttpPort(), mediaServer.getHttpSSlPort(), app, stream, callIdParam); + + streamInfoResult.setTs(addr, mediaServer.getHttpPort(), mediaServer.getHttpSSlPort(), app, stream, callIdParam); + streamInfoResult.setWsTs(addr, mediaServer.getHttpPort(), mediaServer.getHttpSSlPort(), app, stream, callIdParam); + + streamInfoResult.setRtc(addr, mediaServer.getHttpPort(), mediaServer.getHttpSSlPort(), app, stream, callIdParam, isPlay); + + streamInfoResult.setMediaInfo(mediaInfo); + + if (!"broadcast".equalsIgnoreCase(app) && !ObjectUtils.isEmpty(mediaServer.getTranscodeSuffix()) && !"null".equalsIgnoreCase(mediaServer.getTranscodeSuffix())) { + String newStream = stream + "_" + mediaServer.getTranscodeSuffix(); + mediaServer.setTranscodeSuffix(null); + StreamInfo transcodeStreamInfo = getStreamInfoByAppAndStream(mediaServer, app, newStream, null, addr, callId, isPlay); + streamInfoResult.setTranscodeStream(transcodeStreamInfo); + } + return streamInfoResult; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/media/zlm/ZLMMediaServerStatusManager.java b/src/main/java/com/genersoft/iot/vmp/media/zlm/ZLMMediaServerStatusManager.java new file mode 100644 index 0000000..0281ffe --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/media/zlm/ZLMMediaServerStatusManager.java @@ -0,0 +1,319 @@ +package com.genersoft.iot.vmp.media.zlm; + +import com.alibaba.fastjson2.JSON; +import com.alibaba.fastjson2.JSONObject; +import com.genersoft.iot.vmp.conf.DynamicTask; +import com.genersoft.iot.vmp.gb28181.event.EventPublisher; +import com.genersoft.iot.vmp.media.bean.MediaServer; +import com.genersoft.iot.vmp.media.event.mediaServer.MediaServerChangeEvent; +import com.genersoft.iot.vmp.media.event.mediaServer.MediaServerDeleteEvent; +import com.genersoft.iot.vmp.media.service.IMediaServerService; +import com.genersoft.iot.vmp.media.zlm.dto.ZLMResult; +import com.genersoft.iot.vmp.media.zlm.dto.ZLMServerConfig; +import com.genersoft.iot.vmp.media.zlm.event.HookZlmServerKeepaliveEvent; +import com.genersoft.iot.vmp.media.zlm.event.HookZlmServerStartEvent; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.event.EventListener; +import org.springframework.scheduling.annotation.Async; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; +import org.springframework.util.ObjectUtils; + +import java.io.File; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.ConcurrentHashMap; + +/** + * 管理zlm流媒体节点的状态 + */ +@Slf4j +@Component +public class ZLMMediaServerStatusManager { + + private final Map offlineZlmPrimaryMap = new ConcurrentHashMap<>(); + private final Map offlineZlmsecondaryMap = new ConcurrentHashMap<>(); + private final Map offlineZlmTimeMap = new ConcurrentHashMap<>(); + + @Autowired + private ZLMRESTfulUtils zlmresTfulUtils; + + @Autowired + private IMediaServerService mediaServerService; + + @Autowired + private DynamicTask dynamicTask; + + @Value("${server.ssl.enabled:false}") + private boolean sslEnabled; + + @Value("${server.port}") + private Integer serverPort; + + @Value("${server.servlet.context-path:}") + private String serverServletContextPath; + + @Autowired + private EventPublisher eventPublisher; + + private final String type = "zlm"; + + @Async("taskExecutor") + @EventListener + public void onApplicationEvent(MediaServerChangeEvent event) { + if (event.getMediaServerItemList() == null + || event.getMediaServerItemList().isEmpty()) { + return; + } + for (MediaServer mediaServerItem : event.getMediaServerItemList()) { + if (!type.equals(mediaServerItem.getType())) { + continue; + } + log.info("[ZLM-添加待上线节点] ID:" + mediaServerItem.getId()); + offlineZlmPrimaryMap.put(mediaServerItem.getId(), mediaServerItem); + offlineZlmTimeMap.put(mediaServerItem.getId(), System.currentTimeMillis()); + execute(); + } + } + + @Async("taskExecutor") + @EventListener + public void onApplicationEvent(HookZlmServerStartEvent event) { + if (event.getMediaServerItem() == null + || !type.equals(event.getMediaServerItem().getType()) + || event.getMediaServerItem().isStatus()) { + return; + } + MediaServer serverItem = mediaServerService.getOne(event.getMediaServerItem().getId()); + if (serverItem == null) { + return; + } + log.info("[ZLM-HOOK事件-服务启动] ID:" + event.getMediaServerItem().getId()); + online(serverItem, null); + } + + @Async("taskExecutor") + @EventListener + public void onApplicationEvent(HookZlmServerKeepaliveEvent event) { + if (event.getMediaServerItem() == null) { + return; + } + MediaServer serverItem = mediaServerService.getOne(event.getMediaServerItem().getId()); + if (serverItem == null) { + return; + } + log.debug("[ZLM-HOOK事件-心跳] ID:" + event.getMediaServerItem().getId()); + online(serverItem, null); + } + + @Async("taskExecutor") + @EventListener + public void onApplicationEvent(MediaServerDeleteEvent event) { + if (event.getMediaServer() == null) { + return; + } + log.info("[ZLM-节点被移除] ID:" + event.getMediaServer().getId()); + offlineZlmPrimaryMap.remove(event.getMediaServer().getId()); + offlineZlmsecondaryMap.remove(event.getMediaServer().getId()); + offlineZlmTimeMap.remove(event.getMediaServer().getId()); + } + + @Scheduled(fixedDelay = 10*1000) //每隔10秒检查一次 + public void execute(){ + // 初次加入的离线节点会在30分钟内,每间隔十秒尝试一次,30分钟后如果仍然没有上线,则每隔30分钟尝试一次连接 + if (offlineZlmPrimaryMap.isEmpty() && offlineZlmsecondaryMap.isEmpty()) { + return; + } + if (!offlineZlmPrimaryMap.isEmpty()) { + for (MediaServer mediaServerItem : offlineZlmPrimaryMap.values()) { + if (offlineZlmTimeMap.get(mediaServerItem.getId()) != null + && offlineZlmTimeMap.get(mediaServerItem.getId()) < System.currentTimeMillis() - 30*60*1000) { + offlineZlmsecondaryMap.put(mediaServerItem.getId(), mediaServerItem); + offlineZlmPrimaryMap.remove(mediaServerItem.getId()); + continue; + } + log.info("[ZLM-尝试连接] ID:{}, 地址: {}:{}", mediaServerItem.getId(), mediaServerItem.getIp(), mediaServerItem.getHttpPort()); + ZLMResult> mediaServerConfigResult = zlmresTfulUtils.getMediaServerConfig(mediaServerItem); + ZLMServerConfig zlmServerConfig = null; + if (mediaServerConfigResult == null) { + log.info("[ZLM-尝试连接]失败, ID:{}, 地址: {}:{}", mediaServerItem.getId(), mediaServerItem.getIp(), mediaServerItem.getHttpPort()); + continue; + } + List data = mediaServerConfigResult.getData(); + if (data == null || data.isEmpty()) { + log.info("[ZLM-尝试连接]失败, ID:{}, 地址: {}:{}", mediaServerItem.getId(), mediaServerItem.getIp(), mediaServerItem.getHttpPort()); + }else { + zlmServerConfig = JSON.parseObject(JSON.toJSONString(data.get(0)), ZLMServerConfig.class); + initPort(mediaServerItem, zlmServerConfig); + online(mediaServerItem, zlmServerConfig); + } + } + } + if (!offlineZlmsecondaryMap.isEmpty()) { + for (MediaServer mediaServerItem : offlineZlmsecondaryMap.values()) { + if (offlineZlmTimeMap.get(mediaServerItem.getId()) < System.currentTimeMillis() - 30*60*1000) { + continue; + } + log.info("[ZLM-尝试连接] ID:{}, 地址: {}:{}", mediaServerItem.getId(), mediaServerItem.getIp(), mediaServerItem.getHttpPort()); + ZLMResult> mediaServerConfig = zlmresTfulUtils.getMediaServerConfig(mediaServerItem); + ZLMServerConfig zlmServerConfig = null; + if (mediaServerConfig == null) { + log.info("[ZLM-尝试连接]失败, ID:{}, 地址: {}:{}", mediaServerItem.getId(), mediaServerItem.getIp(), mediaServerItem.getHttpPort()); + offlineZlmTimeMap.put(mediaServerItem.getId(), System.currentTimeMillis()); + continue; + } + List data = mediaServerConfig.getData(); + if (data == null || data.isEmpty()) { + log.info("[ZLM-尝试连接]失败, ID:{}, 地址: {}:{}", mediaServerItem.getId(), mediaServerItem.getIp(), mediaServerItem.getHttpPort()); + offlineZlmTimeMap.put(mediaServerItem.getId(), System.currentTimeMillis()); + }else { + zlmServerConfig = JSON.parseObject(JSON.toJSONString(data.get(0)), ZLMServerConfig.class); + initPort(mediaServerItem, zlmServerConfig); + online(mediaServerItem, zlmServerConfig); + } + } + } + } + + private void online(MediaServer mediaServerItem, ZLMServerConfig config) { + offlineZlmPrimaryMap.remove(mediaServerItem.getId()); + offlineZlmsecondaryMap.remove(mediaServerItem.getId()); + offlineZlmTimeMap.remove(mediaServerItem.getId()); + if (!mediaServerItem.isStatus()) { + log.info("[ZLM-连接成功] ID:{}, 地址: {}:{}", mediaServerItem.getId(), mediaServerItem.getIp(), mediaServerItem.getHttpPort()); + mediaServerItem.setStatus(true); + mediaServerItem.setHookAliveInterval(10F); + // 发送上线通知 + eventPublisher.mediaServerOnlineEventPublish(mediaServerItem); + if(mediaServerItem.isAutoConfig()) { + if (config == null) { + ZLMResult> mediaServerConfig = zlmresTfulUtils.getMediaServerConfig(mediaServerItem); + List data = mediaServerConfig.getData(); + if (data != null && !data.isEmpty()) { + config = JSON.parseObject(JSON.toJSONString(data.get(0)), ZLMServerConfig.class); + } + } + if (config != null) { + initPort(mediaServerItem, config); + setZLMConfig(mediaServerItem, "0".equals(config.getHookEnable()) + || !Objects.equals(mediaServerItem.getHookAliveInterval(), config.getHookAliveInterval())); + } + } + mediaServerService.update(mediaServerItem); + } + // 设置两次心跳未收到则认为zlm离线 + String key = "zlm-keepalive-" + mediaServerItem.getId(); + dynamicTask.startDelay(key, ()->{ + log.warn("[ZLM-心跳超时] ID:{}", mediaServerItem.getId()); + mediaServerItem.setStatus(false); + offlineZlmPrimaryMap.put(mediaServerItem.getId(), mediaServerItem); + offlineZlmTimeMap.put(mediaServerItem.getId(), System.currentTimeMillis()); + // 发送离线通知 + eventPublisher.mediaServerOfflineEventPublish(mediaServerItem); + mediaServerService.update(mediaServerItem); + }, (int)(mediaServerItem.getHookAliveInterval() * 2 * 1000)); + } + private void initPort(MediaServer mediaServerItem, ZLMServerConfig zlmServerConfig) { + // 端口只会从配置中读取一次,一旦自己配置或者读取过了将不在配置 + if (mediaServerItem.getHttpSSlPort() == 0) { + mediaServerItem.setHttpSSlPort(zlmServerConfig.getHttpSSLport()); + } + if (mediaServerItem.getRtmpPort() == 0) { + mediaServerItem.setRtmpPort(zlmServerConfig.getRtmpPort()); + } + if (mediaServerItem.getRtmpSSlPort() == 0) { + mediaServerItem.setRtmpSSlPort(zlmServerConfig.getRtmpSslPort()); + } + if (mediaServerItem.getRtspPort() == 0) { + mediaServerItem.setRtspPort(zlmServerConfig.getRtspPort()); + } + if (mediaServerItem.getRtspSSLPort() == 0) { + mediaServerItem.setRtspSSLPort(zlmServerConfig.getRtspSSlport()); + } + if (mediaServerItem.getRtpProxyPort() == 0) { + mediaServerItem.setRtpProxyPort(zlmServerConfig.getRtpProxyPort()); + } + if (mediaServerItem.getFlvSSLPort() == 0) { + mediaServerItem.setFlvSSLPort(zlmServerConfig.getHttpSSLport()); + } + if (mediaServerItem.getWsFlvSSLPort() == 0) { + mediaServerItem.setWsFlvSSLPort(zlmServerConfig.getHttpSSLport()); + } + if (Objects.isNull(zlmServerConfig.getTranscodeSuffix())) { + mediaServerItem.setTranscodeSuffix(null); + }else { + mediaServerItem.setTranscodeSuffix(zlmServerConfig.getTranscodeSuffix()); + } + mediaServerItem.setRtpProxyPort(zlmServerConfig.getRtpProxyPort()); + mediaServerItem.setHookAliveInterval(10F); + } + + public void setZLMConfig(MediaServer mediaServerItem, boolean restart) { + log.info("[媒体服务节点] 正在设置 :{} -> {}:{}", + mediaServerItem.getId(), mediaServerItem.getIp(), mediaServerItem.getHttpPort()); + String protocol = sslEnabled ? "https" : "http"; + String hookPrefix = String.format("%s://%s:%s%s/index/hook", protocol, mediaServerItem.getHookIp(), serverPort, (serverServletContextPath == null || "/".equals(serverServletContextPath)) ? "" : serverServletContextPath); + + Map param = new HashMap<>(); + if (mediaServerItem.getRtspPort() != 0) { + param.put("ffmpeg.snap", "%s -rtsp_transport tcp -i %s -y -f mjpeg -frames:v 1 %s"); + } + param.put("hook.enable","1"); + param.put("hook.on_flow_report",""); + param.put("hook.on_play",String.format("%s/on_play", hookPrefix)); + param.put("hook.on_http_access",""); + param.put("hook.on_publish", String.format("%s/on_publish", hookPrefix)); + param.put("hook.on_record_ts",""); + param.put("hook.on_rtsp_auth",""); + param.put("hook.on_rtsp_realm",""); + param.put("hook.on_server_started",String.format("%s/on_server_started", hookPrefix)); + param.put("hook.on_shell_login",""); + param.put("hook.on_stream_changed",String.format("%s/on_stream_changed", hookPrefix)); + param.put("hook.on_stream_none_reader",String.format("%s/on_stream_none_reader", hookPrefix)); + param.put("hook.on_stream_not_found",String.format("%s/on_stream_not_found", hookPrefix)); + param.put("hook.on_server_keepalive",String.format("%s/on_server_keepalive", hookPrefix)); + param.put("hook.on_send_rtp_stopped",String.format("%s/on_send_rtp_stopped", hookPrefix)); + param.put("hook.on_rtp_server_timeout",String.format("%s/on_rtp_server_timeout", hookPrefix)); + param.put("hook.on_record_mp4",String.format("%s/on_record_mp4", hookPrefix)); + param.put("hook.timeoutSec","30"); + param.put("hook.alive_interval", mediaServerItem.getHookAliveInterval()); + // 推流断开后可以在超时时间内重新连接上继续推流,这样播放器会接着播放。 + // 置0关闭此特性(推流断开会导致立即断开播放器) + // 此参数不应大于播放器超时时间 + // 优化此消息以更快的收到流注销事件 + param.put("protocol.continue_push_ms", "3000" ); + // 最多等待未初始化的Track时间,单位毫秒,超时之后会忽略未初始化的Track, 设置此选项优化那些音频错误的不规范流, + // 等zlm支持给每个rtpServer设置关闭音频的时候可以不设置此选项 + if (mediaServerItem.isRtpEnable() && !ObjectUtils.isEmpty(mediaServerItem.getRtpPortRange())) { + param.put("rtp_proxy.port_range", mediaServerItem.getRtpPortRange().replace(",", "-")); + } + + if (!ObjectUtils.isEmpty(mediaServerItem.getRecordPath())) { + File recordPathFile = new File(mediaServerItem.getRecordPath()); + param.put("protocol.mp4_save_path", recordPathFile.getParentFile().getPath()); + param.put("protocol.downloadRoot", recordPathFile.getParentFile().getPath()); + param.put("record.appName", recordPathFile.getName()); + } + + ZLMResult zlmResult = zlmresTfulUtils.setServerConfig(mediaServerItem, param); + + if (zlmResult != null && zlmResult.getCode() == 0) { + if (restart) { + log.info("[媒体服务节点] 设置成功,开始重启以保证配置生效 {} -> {}:{}", + mediaServerItem.getId(), mediaServerItem.getIp(), mediaServerItem.getHttpPort()); + zlmresTfulUtils.restartServer(mediaServerItem); + }else { + log.info("[媒体服务节点] 设置成功 {} -> {}:{}", + mediaServerItem.getId(), mediaServerItem.getIp(), mediaServerItem.getHttpPort()); + } + }else { + log.info("[媒体服务节点] 设置媒体服务节点失败 {} -> {}:{}", + mediaServerItem.getId(), mediaServerItem.getIp(), mediaServerItem.getHttpPort()); + } + } + +} diff --git a/src/main/java/com/genersoft/iot/vmp/media/zlm/ZLMRESTfulUtils.java b/src/main/java/com/genersoft/iot/vmp/media/zlm/ZLMRESTfulUtils.java new file mode 100755 index 0000000..66bad9a --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/media/zlm/ZLMRESTfulUtils.java @@ -0,0 +1,814 @@ +package com.genersoft.iot.vmp.media.zlm; + +import com.alibaba.fastjson2.JSON; +import com.alibaba.fastjson2.JSONArray; +import com.alibaba.fastjson2.JSONObject; +import com.alibaba.fastjson2.TypeReference; +import com.genersoft.iot.vmp.conf.exception.ControllerException; +import com.genersoft.iot.vmp.media.bean.MediaServer; +import com.genersoft.iot.vmp.media.zlm.dto.*; +import com.genersoft.iot.vmp.vmanager.bean.ErrorCode; +import lombok.extern.slf4j.Slf4j; +import okhttp3.*; +import okhttp3.logging.HttpLoggingInterceptor; +import org.jetbrains.annotations.NotNull; +import org.springframework.stereotype.Component; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.math.BigDecimal; +import java.net.ConnectException; +import java.net.SocketTimeoutException; +import java.net.URLEncoder; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.TimeUnit; + +@Slf4j +@Component +public class ZLMRESTfulUtils { + + private OkHttpClient client; + + + public interface RequestCallback{ + void run(String response); + } + public interface ResultCallback{ + void run(ZLMResult response); + } + + private OkHttpClient getClient(){ + return getClient(null); + } + + private OkHttpClient getClient(Integer readTimeOut){ + if (client == null) { + if (readTimeOut == null) { + readTimeOut = 10; + } + OkHttpClient.Builder httpClientBuilder = new OkHttpClient.Builder(); + //todo 暂时写死超时时间 均为5s + // 设置连接超时时间 + httpClientBuilder.connectTimeout(8,TimeUnit.SECONDS); + // 设置读取超时时间 + httpClientBuilder.readTimeout(readTimeOut,TimeUnit.SECONDS); + // 设置连接池 + httpClientBuilder.connectionPool(new ConnectionPool(16, 5, TimeUnit.MINUTES)); + if (log.isDebugEnabled()) { + HttpLoggingInterceptor logging = new HttpLoggingInterceptor(message -> { + log.debug("http请求参数:" + message); + }); + logging.setLevel(HttpLoggingInterceptor.Level.BASIC); + // OkHttp進行添加攔截器loggingInterceptor + httpClientBuilder.addInterceptor(logging); + } + client = httpClientBuilder.build(); + } + return client; + + } + + public String sendPost(MediaServer mediaServer, String api, Map param, RequestCallback callback) { + return sendPost(mediaServer, api, param, callback, null); + } + + + public String sendPost(MediaServer mediaServer, String api, Map param, RequestCallback callback, Integer readTimeOut) { + OkHttpClient client = getClient(readTimeOut); + + if (mediaServer == null) { + return null; + } + String url = String.format("http://%s:%s/index/api/%s", mediaServer.getIp(), mediaServer.getHttpPort(), api); + String result = null; + FormBody.Builder builder = new FormBody.Builder(); + builder.add("secret",mediaServer.getSecret()); + if (param != null && !param.isEmpty()) { + for (String key : param.keySet()){ + if (param.get(key) != null) { + builder.add(key, param.get(key).toString()); + } + } + } + + FormBody body = builder.build(); + + Request request = new Request.Builder() + .post(body) + .url(url) + .build(); + if (callback == null) { + try { + Response response = client.newCall(request).execute(); + if (response.isSuccessful()) { + ResponseBody responseBody = response.body(); + if (responseBody != null) { + result = responseBody.string(); + } + }else { + response.close(); + Objects.requireNonNull(response.body()).close(); + } + }catch (IOException e) { + log.error(String.format("[ %s ]请求失败: %s", url, e.getMessage())); + + if(e instanceof SocketTimeoutException){ + //读取超时超时异常 + log.error(String.format("读取ZLM数据超时失败: %s, %s", url, e.getMessage())); + } + if(e instanceof ConnectException){ + //判断连接异常,我这里是报Failed to connect to 10.7.5.144 + log.error(String.format("连接ZLM连接失败: %s, %s", url, e.getMessage())); + } + + }catch (Exception e){ + log.error(String.format("访问ZLM失败: %s, %s", url, e.getMessage())); + } + }else { + client.newCall(request).enqueue(new Callback(){ + + @Override + public void onResponse(@NotNull Call call, @NotNull Response response){ + if (response.isSuccessful()) { + try { + String responseStr = Objects.requireNonNull(response.body()).string(); + callback.run(responseStr); + } catch (IOException e) { + log.error(String.format("[ %s ]请求失败: %s", url, e.getMessage())); + } + + }else { + response.close(); + Objects.requireNonNull(response.body()).close(); + } + } + + @Override + public void onFailure(@NotNull Call call, @NotNull IOException e) { + log.error(String.format("连接ZLM失败: %s, %s", call.request().toString(), e.getMessage())); + + if(e instanceof SocketTimeoutException){ + //读取超时超时异常 + log.error(String.format("读取ZLM数据失败: %s, %s", call.request().toString(), e.getMessage())); + } + if(e instanceof ConnectException){ + //判断连接异常,我这里是报Failed to connect to 10.7.5.144 + log.error(String.format("连接ZLM失败: %s, %s", call.request().toString(), e.getMessage())); + } + } + }); + } + + return result; + } + + public void sendGetForImg(MediaServer mediaServer, String api, Map params, String targetPath, String fileName) { + String url = String.format("http://%s:%s/index/api/%s", mediaServer.getIp(), mediaServer.getHttpPort(), api); + HttpUrl parseUrl = HttpUrl.parse(url); + if (parseUrl == null) { + return; + } + HttpUrl.Builder httpBuilder = parseUrl.newBuilder(); + + httpBuilder.addQueryParameter("secret", mediaServer.getSecret()); + if (params != null) { + for (Map.Entry param : params.entrySet()) { + httpBuilder.addQueryParameter(param.getKey(), param.getValue().toString()); + } + } + + Request request = new Request.Builder() + .url(httpBuilder.build()) + .build(); + if (log.isDebugEnabled()){ + log.debug(request.toString()); + } + try { + OkHttpClient client = getClient(); + Response response = client.newCall(request).execute(); + if (response.isSuccessful()) { + if (targetPath != null) { + File snapFolder = new File(targetPath); + if (!snapFolder.exists()) { + if (!snapFolder.mkdirs()) { + log.warn("{}路径创建失败", snapFolder.getAbsolutePath()); + } + + } + File snapFile = new File(targetPath + File.separator + fileName); + FileOutputStream outStream = new FileOutputStream(snapFile); + + outStream.write(Objects.requireNonNull(response.body()).bytes()); + outStream.flush(); + outStream.close(); + } else { + log.error(String.format("[ %s ]请求失败: %s %s", url, response.code(), response.message())); + } + } else { + log.error(String.format("[ %s ]请求失败: %s %s", url, response.code(), response.message())); + } + Objects.requireNonNull(response.body()).close(); + } catch (ConnectException e) { + log.error(String.format("连接ZLM失败: %s, %s", e.getCause().getMessage(), e.getMessage())); + log.info("请检查media配置并确认ZLM已启动..."); + } catch (IOException e) { + log.error(String.format("[ %s ]请求失败: %s", url, e.getMessage())); + } + } + + public ZLMResult isMediaOnline(MediaServer mediaServer, String app, String stream, String schema){ + Map param = new HashMap<>(); + if (app != null) { + param.put("app",app); + } + if (stream != null) { + param.put("stream",stream); + } + if (schema != null) { + param.put("schema",schema); + } + param.put("vhost","__defaultVhost__"); + String response = sendPost(mediaServer, "isMediaOnline", param, null); + if (response == null) { + return ZLMResult.getFailForMediaServer(); + }else { + ZLMResult zlmResult = JSON.parseObject(response, ZLMResult.class); + if (zlmResult == null) { + return ZLMResult.getFailForMediaServer(); + }else { + return zlmResult; + } + } + } + + public ZLMResult getMediaList(MediaServer mediaServer, String app, String stream, String schema, ResultCallback callback){ + Map param = new HashMap<>(); + if (app != null) { + param.put("app",app); + } + if (stream != null) { + param.put("stream",stream); + } + if (schema != null) { + param.put("schema",schema); + } + param.put("vhost","__defaultVhost__"); + RequestCallback requestCallback = null; + if (callback != null) { + requestCallback = (responseStr -> { + if (callback == null) { + return; + } + if (responseStr == null) { + callback.run(ZLMResult.getFailForMediaServer()); + }else { + ZLMResult zlmResult = JSON.parseObject(responseStr, new TypeReference>() {}); + if (zlmResult == null) { + callback.run(ZLMResult.getFailForMediaServer()); + }else { + callback.run(zlmResult); + } + + } + }); + } + + String response = sendPost(mediaServer, "getMediaList",param, requestCallback); + if (response == null) { + return ZLMResult.getFailForMediaServer(); + }else { + ZLMResult zlmResult = JSON.parseObject(response, new TypeReference>() {}); + if (zlmResult == null) { + return ZLMResult.getFailForMediaServer(); + }else { + return zlmResult; + } + } + } + + public ZLMResult getMediaList(MediaServer mediaServer, String app, String stream){ + return getMediaList(mediaServer, app, stream,null, null); + } + + public ZLMResult getMediaInfo(MediaServer mediaServer, String app, String schema, String stream){ + Map param = new HashMap<>(); + param.put("app",app); + param.put("schema",schema); + param.put("stream",stream); + param.put("vhost","__defaultVhost__"); + + String response = sendPost(mediaServer, "getMediaInfo",param, null); + if (response == null) { + return ZLMResult.getFailForMediaServer(); + }else { + JSONObject jsonObject = JSON.parseObject(response); + if (jsonObject == null) { + return ZLMResult.getFailForMediaServer(); + }else { + ZLMResult zlmResult = new ZLMResult<>(); + zlmResult.setCode(0); + zlmResult.setData(jsonObject); + return zlmResult; + } + } + } + + public ZLMResult getRtpInfo(MediaServer mediaServer, String stream_id){ + Map param = new HashMap<>(); + param.put("stream_id",stream_id); + String response = sendPost(mediaServer, "getRtpInfo",param, null); + if (response == null) { + return ZLMResult.getFailForMediaServer(); + }else { + ZLMResult zlmResult = JSON.parseObject(response, ZLMResult.class); + if (zlmResult == null) { + return ZLMResult.getFailForMediaServer(); + }else { + return zlmResult; + } + } + } + + public ZLMResult addFFmpegSource(MediaServer mediaServer, String src_url, String dst_url, Integer timeout_sec, + boolean enable_audio, boolean enable_mp4, String ffmpeg_cmd_key){ + try { + src_url = URLEncoder.encode(src_url, "UTF-8"); + } catch (UnsupportedEncodingException e) { + throw new ControllerException(ErrorCode.ERROR100.getCode(),"url编码失败"); + } + + Map param = new HashMap<>(); + param.put("src_url", src_url); + param.put("dst_url", dst_url); + param.put("timeout_ms", timeout_sec*1000); + param.put("enable_mp4", enable_mp4); + param.put("ffmpeg_cmd_key", ffmpeg_cmd_key); + + String response = sendPost(mediaServer, "addFFmpegSource",param, null); + if (response == null) { + return ZLMResult.getFailForMediaServer(); + }else { + ZLMResult zlmResult = JSON.parseObject(response, new TypeReference>() {}); + if (zlmResult == null) { + return ZLMResult.getFailForMediaServer(); + }else { + return zlmResult; + } + } + } + + public ZLMResult delFFmpegSource(MediaServer mediaServer, String key){ + Map param = new HashMap<>(); + param.put("key", key); + + String response = sendPost(mediaServer, "delFFmpegSource",param, null); + if (response == null) { + return ZLMResult.getFailForMediaServer(); + }else { + ZLMResult zlmResult = JSON.parseObject(response, new TypeReference>() {}); + if (zlmResult == null) { + return ZLMResult.getFailForMediaServer(); + }else { + return zlmResult; + } + } + } + + public ZLMResult delStreamProxy(MediaServer mediaServer, String key){ + Map param = new HashMap<>(); + param.put("key", key); + String response = sendPost(mediaServer, "delStreamProxy",param, null); + if (response == null) { + return ZLMResult.getFailForMediaServer(); + }else { + ZLMResult zlmResult = JSON.parseObject(response, new TypeReference>() {}); + if (zlmResult == null) { + return ZLMResult.getFailForMediaServer(); + }else { + return zlmResult; + } + } + } + + public ZLMResult> getMediaServerConfig(MediaServer mediaServer ){ + + String response = sendPost(mediaServer, "getServerConfig",null, null); + if (response == null) { + return ZLMResult.getFailForMediaServer(); + }else { + ZLMResult> zlmResult = JSON.parseObject(response, new TypeReference>>() {}); + if (zlmResult == null) { + return ZLMResult.getFailForMediaServer(); + }else { + return zlmResult; + } + } + } + + public ZLMResult setServerConfig(MediaServer mediaServer, Map param){ + String response = sendPost(mediaServer, "setServerConfig",param, null); + if (response == null) { + return ZLMResult.getFailForMediaServer(); + }else { + ZLMResult zlmResult = JSON.parseObject(response, ZLMResult.class); + if (zlmResult == null) { + return ZLMResult.getFailForMediaServer(); + }else { + return zlmResult; + } + } + } + + public ZLMResult openRtpServer(MediaServer mediaServer, Map param){ + String response = sendPost(mediaServer, "openRtpServer",param, null); + if (response == null) { + return ZLMResult.getFailForMediaServer(); + }else { + ZLMResult zlmResult = JSON.parseObject(response, ZLMResult.class); + if (zlmResult == null) { + return ZLMResult.getFailForMediaServer(); + }else { + return zlmResult; + } + } + } + + public ZLMResult closeRtpServer(MediaServer mediaServer, Map param) { + String response = sendPost(mediaServer, "closeRtpServer",param, null); + if (response == null) { + return ZLMResult.getFailForMediaServer(); + }else { + ZLMResult zlmResult = JSON.parseObject(response, ZLMResult.class); + if (zlmResult == null) { + return ZLMResult.getFailForMediaServer(); + }else { + return zlmResult; + } + } + } + + public void closeRtpServer(MediaServer mediaServer, Map param, ResultCallback callback) { + sendPost(mediaServer, "closeRtpServer",param, (response -> { + if (callback == null) { + return; + } + if (response == null) { + callback.run(ZLMResult.getFailForMediaServer()); + }else { + callback.run(JSON.parseObject(response, ZLMResult.class)); + } + })); + + } + + public ZLMResult> listRtpServer(MediaServer mediaServer) { + String response = sendPost(mediaServer, "listRtpServer",null, null); + if (response == null) { + return ZLMResult.getFailForMediaServer(); + }else { + ZLMResult> zlmResult = JSON.parseObject(response, new TypeReference>>() {}); + if (zlmResult == null) { + return ZLMResult.getFailForMediaServer(); + }else { + return zlmResult; + } + } + } + + public ZLMResult startSendRtp(MediaServer mediaServer, Map param) { + String response = sendPost(mediaServer, "startSendRtp",param, null); + if (response == null) { + return ZLMResult.getFailForMediaServer(); + }else { + ZLMResult zlmResult = JSON.parseObject(response, ZLMResult.class); + if (zlmResult == null) { + return ZLMResult.getFailForMediaServer(); + }else { + return zlmResult; + } + } + } + + public ZLMResult startSendRtpPassive(MediaServer mediaServer, Map param) { + String response = sendPost(mediaServer, "startSendRtpPassive",param, null); + if (response == null) { + return ZLMResult.getFailForMediaServer(); + }else { + ZLMResult zlmResult = JSON.parseObject(response, ZLMResult.class); + if (zlmResult == null) { + return ZLMResult.getFailForMediaServer(); + }else { + return zlmResult; + } + } + } + + public ZLMResult startSendRtpPassive(MediaServer mediaServer, Map param, ResultCallback callback) { + String response = sendPost(mediaServer, "startSendRtpPassive",param, (responseStr -> { + if (callback == null) { + return; + } + if (responseStr == null) { + callback.run(ZLMResult.getFailForMediaServer()); + }else { + ZLMResult zlmResult = JSON.parseObject(responseStr, ZLMResult.class); + if (zlmResult == null) { + callback.run(ZLMResult.getFailForMediaServer()); + }else { + callback.run(zlmResult); + } + } + })); + if (response == null) { + return ZLMResult.getFailForMediaServer(); + }else { + ZLMResult zlmResult = JSON.parseObject(response, ZLMResult.class); + if (zlmResult == null) { + return ZLMResult.getFailForMediaServer(); + }else { + return zlmResult; + } + } + } + + public ZLMResult startSendRtpTalk(MediaServer mediaServer, Map param, ResultCallback callback) { + String response = sendPost(mediaServer, "startSendRtpTalk",param, (responseStr -> { + if (callback == null) { + return; + } + if (responseStr == null) { + callback.run(ZLMResult.getFailForMediaServer()); + }else { + callback.run(JSON.parseObject(responseStr, ZLMResult.class)); + } + })); + if (response == null) { + return ZLMResult.getFailForMediaServer(); + }else { + ZLMResult zlmResult = JSON.parseObject(response, ZLMResult.class); + if (zlmResult == null) { + return ZLMResult.getFailForMediaServer(); + }else { + return zlmResult; + } + } + } + + public ZLMResult stopSendRtp(MediaServer mediaServer, Map param) { + String response = sendPost(mediaServer, "stopSendRtp",param, null); + if (response == null) { + return ZLMResult.getFailForMediaServer(); + }else { + ZLMResult zlmResult = JSON.parseObject(response, ZLMResult.class); + if (zlmResult == null) { + return ZLMResult.getFailForMediaServer(); + }else { + return zlmResult; + } + } + } + + public ZLMResult restartServer(MediaServer mediaServer) { + String response = sendPost(mediaServer, "restartServer",null, null); + if (response == null) { + return ZLMResult.getFailForMediaServer(); + }else { + ZLMResult zlmResult = JSON.parseObject(response, ZLMResult.class); + if (zlmResult == null) { + return ZLMResult.getFailForMediaServer(); + }else { + return zlmResult; + } + } + } + + public ZLMResult addStreamProxy(MediaServer mediaServer, String app, String stream, String url, boolean enable_audio, boolean enable_mp4, String rtp_type, Integer timeOut) { + Map param = new HashMap<>(); + param.put("vhost", "__defaultVhost__"); + param.put("app", app); + param.put("stream", stream); + param.put("url", url); + param.put("enable_mp4", enable_mp4?1:0); + param.put("enable_audio", enable_audio?1:0); + param.put("rtp_type", rtp_type); + param.put("timeout_sec", timeOut); + // 拉流重试次数,默认为3 + param.put("retry_count", 3); + + String response = sendPost(mediaServer, "addStreamProxy",param, null); + if (response == null) { + return ZLMResult.getFailForMediaServer(); + }else { + ZLMResult zlmResult = JSON.parseObject(response, new TypeReference>() {}); + if (zlmResult == null) { + return ZLMResult.getFailForMediaServer(); + }else { + return zlmResult; + } + } + } + + public ZLMResult closeStreams(MediaServer mediaServer, String app, String stream) { + Map param = new HashMap<>(); + param.put("vhost", "__defaultVhost__"); + param.put("app", app); + param.put("stream", stream); + param.put("force", 1); + + String response = sendPost(mediaServer, "close_streams",param, null); + if (response == null) { + return ZLMResult.getFailForMediaServer(); + }else { + ZLMResult zlmResult = JSON.parseObject(response, new TypeReference>() {}); + if (zlmResult == null) { + return ZLMResult.getFailForMediaServer(); + }else { + return zlmResult; + } + } + } + + public ZLMResult> getAllSession(MediaServer mediaServer) { + String response = sendPost(mediaServer, "getAllSession",null, null); + if (response == null) { + return ZLMResult.getFailForMediaServer(); + }else { + ZLMResult> zlmResult = JSON.parseObject(response, new TypeReference>>() {}); + if (zlmResult == null) { + return ZLMResult.getFailForMediaServer(); + }else { + return zlmResult; + } + } + } + + public void kickSessions(MediaServer mediaServer, String localPortSStr) { + Map param = new HashMap<>(); + param.put("local_port", localPortSStr); + sendPost(mediaServer, "kick_sessions",param, null); + } + + public void getSnap(MediaServer mediaServer, String streamUrl, int timeout_sec, int expire_sec, String targetPath, String fileName) { + Map param = new HashMap<>(3); + param.put("url", streamUrl); + param.put("timeout_sec", timeout_sec); + param.put("expire_sec", expire_sec); + param.put("async", 1); + sendGetForImg(mediaServer, "getSnap", param, targetPath, fileName); + } + + public ZLMResult pauseRtpCheck(MediaServer mediaServer, String streamId) { + Map param = new HashMap<>(1); + param.put("stream_id", streamId); + String response = sendPost(mediaServer, "pauseRtpCheck", param, null); + if (response == null) { + return ZLMResult.getFailForMediaServer(); + }else { + ZLMResult zlmResult = JSON.parseObject(response, ZLMResult.class); + if (zlmResult == null) { + return ZLMResult.getFailForMediaServer(); + }else { + return zlmResult; + } + } + } + + public ZLMResult resumeRtpCheck(MediaServer mediaServer, String streamId) { + Map param = new HashMap<>(1); + param.put("stream_id", streamId); + String response = sendPost(mediaServer, "resumeRtpCheck", param, null); + if (response == null) { + return ZLMResult.getFailForMediaServer(); + }else { + ZLMResult zlmResult = JSON.parseObject(response, ZLMResult.class); + if (zlmResult == null) { + return ZLMResult.getFailForMediaServer(); + }else { + return zlmResult; + } + } + } + + public ZLMResult connectRtpServer(MediaServer mediaServer, String dst_url, int dst_port, String stream_id) { + Map param = new HashMap<>(1); + param.put("dst_url", dst_url); + param.put("dst_port", dst_port); + param.put("stream_id", stream_id); + String response = sendPost(mediaServer, "connectRtpServer", param, null); + if (response == null) { + return ZLMResult.getFailForMediaServer(); + }else { + ZLMResult zlmResult = JSON.parseObject(response, ZLMResult.class); + if (zlmResult == null) { + return ZLMResult.getFailForMediaServer(); + }else { + return zlmResult; + } + } + } + + public ZLMResult updateRtpServerSSRC(MediaServer mediaServer, String streamId, String ssrc) { + Map param = new HashMap<>(1); + param.put("ssrc", ssrc); + param.put("stream_id", streamId); + + String response = sendPost(mediaServer, "updateRtpServerSSRC", param, null); + if (response == null) { + return ZLMResult.getFailForMediaServer(); + }else { + ZLMResult zlmResult = JSON.parseObject(response, ZLMResult.class); + if (zlmResult == null) { + return ZLMResult.getFailForMediaServer(); + }else { + return zlmResult; + } + } + } + + public ZLMResult deleteRecordDirectory(MediaServer mediaServer, String app, String stream, String date, String fileName) { + Map param = new HashMap<>(1); + param.put("vhost", "__defaultVhost__"); + param.put("app", app); + param.put("stream", stream); + param.put("period", date); + param.put("name", fileName); + String response = sendPost(mediaServer, "deleteRecordDirectory", param, null); + if (response == null) { + return ZLMResult.getFailForMediaServer(); + }else { + ZLMResult zlmResult = JSON.parseObject(response, ZLMResult.class); + if (zlmResult == null) { + return ZLMResult.getFailForMediaServer(); + }else { + return zlmResult; + } + } + } + + public ZLMResult loadMP4File(MediaServer mediaServer, String app, String stream, String datePath) { + Map param = new HashMap<>(1); + param.put("vhost", "__defaultVhost__"); + param.put("app", app); + param.put("stream", stream); + param.put("file_path", datePath); + param.put("file_repeat", "0"); + String response = sendPost(mediaServer, "loadMP4File", param, null); + if (response == null) { + return ZLMResult.getFailForMediaServer(); + }else { + ZLMResult zlmResult = JSON.parseObject(response, ZLMResult.class); + if (zlmResult == null) { + return ZLMResult.getFailForMediaServer(); + }else { + return zlmResult; + } + } + } + + public ZLMResult setRecordSpeed(MediaServer mediaServer, String app, String stream, int speed, String schema) { + Map param = new HashMap<>(1); + param.put("vhost", "__defaultVhost__"); + param.put("app", app); + param.put("stream", stream); + param.put("speed", speed); + param.put("schema", schema); + String response = sendPost(mediaServer, "setRecordSpeed", param, null); + if (response == null) { + return ZLMResult.getFailForMediaServer(); + }else { + ZLMResult zlmResult = JSON.parseObject(response, ZLMResult.class); + if (zlmResult == null) { + return ZLMResult.getFailForMediaServer(); + }else { + return zlmResult; + } + } + } + + public ZLMResult seekRecordStamp(MediaServer mediaServer, String app, String stream, Double stamp, String schema) { + Map param = new HashMap<>(1); + param.put("vhost", "__defaultVhost__"); + param.put("app", app); + param.put("stream", stream); + BigDecimal bigDecimal = new BigDecimal(stamp); + param.put("stamp", bigDecimal); + param.put("schema", schema); + + String response = sendPost(mediaServer, "seekRecordStamp", param, null); + if (response == null) { + return ZLMResult.getFailForMediaServer(); + }else { + ZLMResult zlmResult = JSON.parseObject(response, ZLMResult.class); + if (zlmResult == null) { + return ZLMResult.getFailForMediaServer(); + }else { + return zlmResult; + } + } + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/media/zlm/ZLMServerFactory.java b/src/main/java/com/genersoft/iot/vmp/media/zlm/ZLMServerFactory.java new file mode 100755 index 0000000..4349445 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/media/zlm/ZLMServerFactory.java @@ -0,0 +1,249 @@ +package com.genersoft.iot.vmp.media.zlm; + +import com.alibaba.fastjson2.JSONArray; +import com.genersoft.iot.vmp.common.CommonCallback; +import com.genersoft.iot.vmp.gb28181.bean.SendRtpInfo; +import com.genersoft.iot.vmp.media.bean.MediaServer; +import com.genersoft.iot.vmp.media.zlm.dto.ZLMResult; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.util.HashMap; +import java.util.Map; + +@Slf4j +@Component +public class ZLMServerFactory { + @Autowired + private ZLMRESTfulUtils zlmresTfulUtils; + + + /** + * 开启rtpServer + * @param mediaServerItem zlm服务实例 + * @param streamId 流Id + * @param ssrc ssrc + * @param port 端口, 0/null为使用随机 + * @param reUsePort 是否重用端口 + * @param tcpMode 0/null udp 模式,1 tcp 被动模式, 2 tcp 主动模式。 + * @return + */ + public int createRTPServer(MediaServer mediaServerItem, String app, String streamId, long ssrc, Integer port, Boolean onlyAuto, Boolean disableAudio, Boolean reUsePort, Integer tcpMode) { + int result = -1; + // 查询此rtp server 是否已经存在 + ZLMResult rtpInfoResult = zlmresTfulUtils.getRtpInfo(mediaServerItem, streamId); + if(rtpInfoResult.getCode() == 0){ + if (rtpInfoResult.getExist() != null && rtpInfoResult.getExist()) { + result = rtpInfoResult.getLocal_port(); + if (result == 0) { + // 此时说明rtpServer已经创建但是流还没有推上来 + // 此时重新打开rtpServer + Map param = new HashMap<>(); + param.put("stream_id", streamId); + ZLMResult zlmResult = zlmresTfulUtils.closeRtpServer(mediaServerItem, param); + if (zlmResult != null ) { + if (zlmResult.getCode() == 0) { + return createRTPServer(mediaServerItem, streamId, app, ssrc, port,onlyAuto, reUsePort,disableAudio, tcpMode); + }else { + log.warn("[开启rtpServer], 重启RtpServer错误"); + } + } + } + return result; + } + }else if(rtpInfoResult.getCode() == -2){ + return result; + } + + Map param = new HashMap<>(); + + if (tcpMode == null) { + tcpMode = 0; + } + param.put("tcp_mode", tcpMode); + param.put("app", app); + param.put("stream_id", streamId); + if (disableAudio != null) { + param.put("only_track", disableAudio?2:0); + } + + if (reUsePort != null) { + param.put("re_use_port", reUsePort?"1":"0"); + } + // 推流端口设置0则使用随机端口 + if (port == null) { + param.put("port", 0); + }else { + param.put("port", port); + } + if (onlyAuto != null) { + param.put("only_audio", onlyAuto?"1":"0"); + } + if (ssrc != 0) { + param.put("ssrc", ssrc); + } + + ZLMResult zlmResult = zlmresTfulUtils.openRtpServer(mediaServerItem, param); + if (zlmResult != null) { + if (zlmResult.getCode() == 0) { + result= zlmResult.getPort(); + }else { + log.error("创建RTP Server 失败 {}: ", zlmResult.getMsg()); + } + }else { + // 检查ZLM状态 + log.error("创建RTP Server 失败 {}: 请检查ZLM服务", param.get("port")); + } + return result; + } + + public boolean closeRtpServer(MediaServer serverItem, String streamId) { + boolean result = false; + if (serverItem !=null){ + Map param = new HashMap<>(); + param.put("stream_id", streamId); + ZLMResult zlmResult = zlmresTfulUtils.closeRtpServer(serverItem, param); + if (zlmResult != null ) { + if (zlmResult.getCode() == 0) { + result = zlmResult.getHit() >= 1; + }else { + log.error("关闭RTP Server 失败: " + zlmResult.getMsg()); + } + }else { + // 检查ZLM状态 + log.error("关闭RTP Server 失败: 请检查ZLM服务"); + } + } + return result; + } + + public void closeRtpServer(MediaServer serverItem, String streamId, CommonCallback callback) { + if (serverItem == null) { + if (callback != null) { + callback.run(false); + } + return; + } + Map param = new HashMap<>(); + param.put("stream_id", streamId); + zlmresTfulUtils.closeRtpServer(serverItem, param, zlmResult -> { + if (zlmResult.getCode() == 0) { + if (callback != null) { + callback.run(zlmResult.getHit() >= 1); + } + return; + }else { + log.error("关闭RTP Server 失败: " + zlmResult.getMsg()); + } + if (callback != null) { + callback.run(false); + } + }); + } + + + /** + * 调用zlm RESTFUL API —— startSendRtp + */ + public ZLMResult startSendRtpStream(MediaServer mediaServerItem, Mapparam) { + return zlmresTfulUtils.startSendRtp(mediaServerItem, param); + } + + /** + * 调用zlm RESTFUL API —— startSendRtpPassive + */ + public ZLMResult startSendRtpPassive(MediaServer mediaServerItem, Mapparam) { + return zlmresTfulUtils.startSendRtpPassive(mediaServerItem, param); + } + + public ZLMResult startSendRtpPassive(MediaServer mediaServerItem, Map param, ZLMRESTfulUtils.ResultCallback callback) { + return zlmresTfulUtils.startSendRtpPassive(mediaServerItem, param, callback); + } + + public ZLMResult startSendRtpTalk(MediaServer mediaServer, Map param, ZLMRESTfulUtils.ResultCallback callback) { + return zlmresTfulUtils.startSendRtpTalk(mediaServer, param, callback); + } + + /** + * 查询待转推的流是否就绪 + */ + public Boolean isStreamReady(MediaServer mediaServerItem, String app, String streamId) { + ZLMResult zlmResult = zlmresTfulUtils.getMediaList(mediaServerItem, app, streamId); + if (zlmResult == null || zlmResult.getCode() == -2) { + return null; + } + ZLMResult result = (ZLMResult) zlmResult; + return (result.getCode() == 0 + && result.getData() != null + && !result.getData().isEmpty()); + } + + public ZLMResult startSendRtp(MediaServer mediaInfo, SendRtpInfo sendRtpItem) { + String is_Udp = sendRtpItem.isTcp() ? "0" : "1"; + log.info("rtp/{}开始推流, 目标={}:{},SSRC={}", sendRtpItem.getStream(), sendRtpItem.getIp(), sendRtpItem.getPort(), sendRtpItem.getSsrc()); + Map param = new HashMap<>(12); + param.put("vhost","__defaultVhost__"); + param.put("app",sendRtpItem.getApp()); + param.put("stream",sendRtpItem.getStream()); + param.put("ssrc", sendRtpItem.getSsrc()); + param.put("src_port", sendRtpItem.getLocalPort()); + param.put("pt", sendRtpItem.getPt()); + param.put("use_ps", sendRtpItem.isUsePs() ? "1" : "0"); + param.put("only_audio", sendRtpItem.isOnlyAudio() ? "1" : "0"); + if (!sendRtpItem.isTcp()) { + // udp模式下开启rtcp保活 + param.put("udp_rtcp_timeout", sendRtpItem.isRtcp()? "1":"0"); + } + + if (mediaInfo == null) { + return null; + } + // 如果是非严格模式,需要关闭端口占用 + ZLMResult zlmResult = null; + if (sendRtpItem.getLocalPort() != 0) { + if (sendRtpItem.isTcpActive()) { + zlmResult = startSendRtpPassive(mediaInfo, param); + }else { + param.put("is_udp", is_Udp); + param.put("dst_url", sendRtpItem.getIp()); + param.put("dst_port", sendRtpItem.getPort()); + zlmResult = startSendRtpStream(mediaInfo, param); + } + }else { + if (sendRtpItem.isTcpActive()) { + zlmResult = startSendRtpPassive(mediaInfo, param); + }else { + param.put("is_udp", is_Udp); + param.put("dst_url", sendRtpItem.getIp()); + param.put("dst_port", sendRtpItem.getPort()); + zlmResult = startSendRtpStream(mediaInfo, param); + } + } + return zlmResult; + } + + public Boolean updateRtpServerSSRC(MediaServer mediaServerItem, String streamId, String ssrc) { + boolean result = false; + ZLMResult zlmResult = zlmresTfulUtils.updateRtpServerSSRC(mediaServerItem, streamId, ssrc); + if (zlmResult.getCode() == 0) { + result= true; + log.info("[更新RTPServer] 成功"); + } else { + log.error("[更新RTPServer] 失败: {}, streamId:{},ssrc:{}", zlmResult.getMsg(), + streamId, ssrc); + } + return result; + } + + public ZLMResult stopSendRtpStream(MediaServer mediaServerItem, SendRtpInfo sendRtpItem) { + Map param = new HashMap<>(); + param.put("vhost", "__defaultVhost__"); + param.put("app", sendRtpItem.getApp()); + param.put("stream", sendRtpItem.getStream()); + param.put("ssrc", sendRtpItem.getSsrc()); + return zlmresTfulUtils.stopSendRtp(mediaServerItem, param); + } + + +} diff --git a/src/main/java/com/genersoft/iot/vmp/media/zlm/dto/ChannelOnlineEvent.java b/src/main/java/com/genersoft/iot/vmp/media/zlm/dto/ChannelOnlineEvent.java new file mode 100755 index 0000000..207ad4b --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/media/zlm/dto/ChannelOnlineEvent.java @@ -0,0 +1,13 @@ +package com.genersoft.iot.vmp.media.zlm.dto; + +import com.genersoft.iot.vmp.gb28181.bean.SendRtpInfo; + +import java.text.ParseException; + +/** + * @author lin + */ +public interface ChannelOnlineEvent { + + void run(SendRtpInfo sendRtpItem) throws ParseException; +} diff --git a/src/main/java/com/genersoft/iot/vmp/media/zlm/dto/FlagData.java b/src/main/java/com/genersoft/iot/vmp/media/zlm/dto/FlagData.java new file mode 100644 index 0000000..a5bbdf7 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/media/zlm/dto/FlagData.java @@ -0,0 +1,8 @@ +package com.genersoft.iot.vmp.media.zlm.dto; + +import lombok.Data; + +@Data +public class FlagData { + private boolean flag; +} diff --git a/src/main/java/com/genersoft/iot/vmp/media/zlm/dto/RtpServerResult.java b/src/main/java/com/genersoft/iot/vmp/media/zlm/dto/RtpServerResult.java new file mode 100644 index 0000000..f7e61d6 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/media/zlm/dto/RtpServerResult.java @@ -0,0 +1,10 @@ +package com.genersoft.iot.vmp.media.zlm.dto; + +import lombok.Data; + +@Data +public class RtpServerResult { + private Integer port; + private String stream_id; + +} diff --git a/src/main/java/com/genersoft/iot/vmp/media/zlm/dto/ServerKeepaliveData.java b/src/main/java/com/genersoft/iot/vmp/media/zlm/dto/ServerKeepaliveData.java new file mode 100755 index 0000000..0cc81f2 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/media/zlm/dto/ServerKeepaliveData.java @@ -0,0 +1,4 @@ +package com.genersoft.iot.vmp.media.zlm.dto; + +public class ServerKeepaliveData { +} diff --git a/src/main/java/com/genersoft/iot/vmp/media/zlm/dto/SessionData.java b/src/main/java/com/genersoft/iot/vmp/media/zlm/dto/SessionData.java new file mode 100644 index 0000000..fb8a33b --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/media/zlm/dto/SessionData.java @@ -0,0 +1,14 @@ +package com.genersoft.iot.vmp.media.zlm.dto; + +import lombok.Data; + +@Data +public class SessionData { + private String id; + private String local_ip; + private Integer local_port; + private String peer_ip; + private Integer peer_port; + private String typeid; + +} diff --git a/src/main/java/com/genersoft/iot/vmp/media/zlm/dto/StreamAuthorityInfo.java b/src/main/java/com/genersoft/iot/vmp/media/zlm/dto/StreamAuthorityInfo.java new file mode 100755 index 0000000..8b722a9 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/media/zlm/dto/StreamAuthorityInfo.java @@ -0,0 +1,118 @@ +package com.genersoft.iot.vmp.media.zlm.dto; + +import com.genersoft.iot.vmp.media.event.media.MediaArrivalEvent; + +/** + * 流的鉴权信息 + * @author lin + */ +public class StreamAuthorityInfo { + + private String id; + private String app; + private String stream; + + /** + * 产生源类型, + * unknown = 0, + * rtmp_push=1, + * rtsp_push=2, + * rtp_push=3, + * pull=4, + * ffmpeg_pull=5, + * mp4_vod=6, + * device_chn=7 + */ + private int originType; + + /** + * 产生源类型的字符串描述 + */ + private String originTypeStr; + + /** + * 推流时自定义的播放鉴权ID + */ + private String callId; + + /** + * 推流的鉴权签名 + */ + private String sign; + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getApp() { + return app; + } + + public void setApp(String app) { + this.app = app; + } + + public String getStream() { + return stream; + } + + public void setStream(String stream) { + this.stream = stream; + } + + public int getOriginType() { + return originType; + } + + public void setOriginType(int originType) { + this.originType = originType; + } + + public String getOriginTypeStr() { + return originTypeStr; + } + + public void setOriginTypeStr(String originTypeStr) { + this.originTypeStr = originTypeStr; + } + + public String getCallId() { + return callId; + } + + public void setCallId(String callId) { + this.callId = callId; + } + + public String getSign() { + return sign; + } + + public void setSign(String sign) { + this.sign = sign; + } + + public static StreamAuthorityInfo getInstanceByHook(String app, String stream, String id) { + StreamAuthorityInfo streamAuthorityInfo = new StreamAuthorityInfo(); + streamAuthorityInfo.setApp(app); + streamAuthorityInfo.setStream(stream); + streamAuthorityInfo.setId(id); + return streamAuthorityInfo; + } + + public static StreamAuthorityInfo getInstanceByHook(MediaArrivalEvent event) { + StreamAuthorityInfo streamAuthorityInfo = new StreamAuthorityInfo(); + streamAuthorityInfo.setApp(event.getApp()); + streamAuthorityInfo.setStream(event.getStream()); + streamAuthorityInfo.setId(event.getMediaServer().getId()); + if (event.getMediaInfo() != null) { + streamAuthorityInfo.setOriginType(event.getMediaInfo().getOriginType()); + } + + return streamAuthorityInfo; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/media/zlm/dto/StreamProxyResult.java b/src/main/java/com/genersoft/iot/vmp/media/zlm/dto/StreamProxyResult.java new file mode 100644 index 0000000..7276582 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/media/zlm/dto/StreamProxyResult.java @@ -0,0 +1,9 @@ +package com.genersoft.iot.vmp.media.zlm.dto; + +import lombok.Data; + +@Data +public class StreamProxyResult { + + private String key; +} diff --git a/src/main/java/com/genersoft/iot/vmp/media/zlm/dto/ZLMResult.java b/src/main/java/com/genersoft/iot/vmp/media/zlm/dto/ZLMResult.java new file mode 100644 index 0000000..5dd1838 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/media/zlm/dto/ZLMResult.java @@ -0,0 +1,58 @@ +package com.genersoft.iot.vmp.media.zlm.dto; + +import lombok.Data; + +@Data +public class ZLMResult { + private int code; + private String msg; + private T data; + + + private Boolean online; + private Boolean exist; + private String peer_ip; + private Integer peer_port; + private String local_ip; + private Integer local_port; + private Integer changed; + private Integer port; + private Integer hit; + + public static ZLMResult getFailForMediaServer() { + ZLMResult zlmResult = new ZLMResult<>(); + zlmResult.setCode(-2); + zlmResult.setMsg("流媒体调用失败"); + return zlmResult; + } + + public static ZLMResult getMediaServer(int code, String msg) { + return getMediaServer(code, msg, null); + } + + public static ZLMResult getMediaServer(int code, String msg, T data) { + ZLMResult zlmResult = new ZLMResult<>(); + zlmResult.setCode(code); + zlmResult.setMsg(msg); + zlmResult.setData(data); + return zlmResult; + } + + @Override + public String toString() { + return "ZLMResult{" + + "code=" + code + + ", msg='" + msg + '\'' + + ", data=" + data + + (online != null ? (", online=" + online) : "") + + (exist != null ? (", exist=" + exist) : "") + + (peer_ip != null ? (", peer_ip=" + peer_ip) : "") + + (peer_port != null ? (", peer_port=" + peer_port) : "") + + (local_ip != null ? (", local_ip=" + local_ip) : "") + + (local_port != null ? (", local_port=" + local_port) : "") + + (changed != null ? (", changed=" + changed) : "") + + (port != null ? (", port=" + port) : "") + + (hit != null ? (", hit=" + hit) : "") + + '}'; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/media/zlm/dto/ZLMRunInfo.java b/src/main/java/com/genersoft/iot/vmp/media/zlm/dto/ZLMRunInfo.java new file mode 100755 index 0000000..624e4e1 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/media/zlm/dto/ZLMRunInfo.java @@ -0,0 +1,33 @@ +package com.genersoft.iot.vmp.media.zlm.dto; + +/** + * 记录zlm运行中一些参数 + */ +public class ZLMRunInfo { + + /** + * zlm当前流数量 + */ + private int mediaCount; + + /** + * 在线状态 + */ + private boolean online; + + public int getMediaCount() { + return mediaCount; + } + + public void setMediaCount(int mediaCount) { + this.mediaCount = mediaCount; + } + + public boolean isOnline() { + return online; + } + + public void setOnline(boolean online) { + this.online = online; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/media/zlm/dto/ZLMServerConfig.java b/src/main/java/com/genersoft/iot/vmp/media/zlm/dto/ZLMServerConfig.java new file mode 100755 index 0000000..84ebc3e --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/media/zlm/dto/ZLMServerConfig.java @@ -0,0 +1,341 @@ +package com.genersoft.iot.vmp.media.zlm.dto; + +import com.alibaba.fastjson2.annotation.JSONField; +import com.genersoft.iot.vmp.media.zlm.dto.hook.HookParam; +import lombok.Data; +import lombok.EqualsAndHashCode; + +@EqualsAndHashCode(callSuper = true) +@Data +public class ZLMServerConfig extends HookParam { + + @JSONField(name = "api.apiDebug") + private String apiDebug; + + @JSONField(name = "api.secret") + private String apiSecret; + + @JSONField(name = "api.snapRoot") + private String apiSnapRoot; + + @JSONField(name = "api.defaultSnap") + private String apiDefaultSnap; + + @JSONField(name = "ffmpeg.bin") + private String ffmpegBin; + + @JSONField(name = "ffmpeg.cmd") + private String ffmpegCmd; + + @JSONField(name = "ffmpeg.snap") + private String ffmpegSnap; + + @JSONField(name = "ffmpeg.log") + private String ffmpegLog; + + @JSONField(name = "ffmpeg.restart_sec") + private String ffmpegRestartSec; + + @JSONField(name = "protocol.modify_stamp") + private String protocolModifyStamp; + + @JSONField(name = "protocol.enable_audio") + private String protocolEnableAudio; + + @JSONField(name = "protocol.add_mute_audio") + private String protocolAddMuteAudio; + + @JSONField(name = "protocol.continue_push_ms") + private String protocolContinuePushMs; + + @JSONField(name = "protocol.enable_hls") + private String protocolEnableHls; + + @JSONField(name = "protocol.enable_mp4") + private String protocolEnableMp4; + + @JSONField(name = "protocol.enable_rtsp") + private String protocolEnableRtsp; + + @JSONField(name = "protocol.enable_rtmp") + private String protocolEnableRtmp; + + @JSONField(name = "protocol.enable_ts") + private String protocolEnableTs; + + @JSONField(name = "protocol.enable_fmp4") + private String protocolEnableFmp4; + + @JSONField(name = "protocol.mp4_as_player") + private String protocolMp4AsPlayer; + + @JSONField(name = "protocol.mp4_max_second") + private String protocolMp4MaxSecond; + + @JSONField(name = "protocol.mp4_save_path") + private String protocolMp4SavePath; + + @JSONField(name = "protocol.hls_save_path") + private String protocolHlsSavePath; + + @JSONField(name = "protocol.hls_demand") + private String protocolHlsDemand; + + @JSONField(name = "protocol.rtsp_demand") + private String protocolRtspDemand; + + @JSONField(name = "protocol.rtmp_demand") + private String protocolRtmpDemand; + + @JSONField(name = "protocol.ts_demand") + private String protocolTsDemand; + + @JSONField(name = "protocol.fmp4_demand") + private String protocolFmp4Demand; + + @JSONField(name = "general.enableVhost") + private String generalEnableVhost; + + @JSONField(name = "general.flowThreshold") + private String generalFlowThreshold; + + @JSONField(name = "general.maxStreamWaitMS") + private String generalMaxStreamWaitMS; + + @JSONField(name = "general.streamNoneReaderDelayMS") + private int generalStreamNoneReaderDelayMS; + + @JSONField(name = "general.resetWhenRePlay") + private String generalResetWhenRePlay; + + @JSONField(name = "general.mergeWriteMS") + private String generalMergeWriteMS; + + @JSONField(name = "general.mediaServerId") + private String generalMediaServerId; + + @JSONField(name = "general.wait_track_ready_ms") + private String generalWaitTrackReadyMs; + + @JSONField(name = "general.wait_add_track_ms") + private String generalWaitAddTrackMs; + + @JSONField(name = "general.unready_frame_cache") + private String generalUnreadyFrameCache; + + + @JSONField(name = "ip") + private String ip; + + private String sdpIp; + + private String streamIp; + + private String hookIp; + + private String updateTime; + + private String createTime; + + @JSONField(name = "hls.fileBufSize") + private String hlsFileBufSize; + + @JSONField(name = "hls.filePath") + private String hlsFilePath; + + @JSONField(name = "hls.segDur") + private String hlsSegDur; + + @JSONField(name = "hls.segNum") + private String hlsSegNum; + + @JSONField(name = "hls.segRetain") + private String hlsSegRetain; + + @JSONField(name = "hls.broadcastRecordTs") + private String hlsBroadcastRecordTs; + + @JSONField(name = "hls.deleteDelaySec") + private String hlsDeleteDelaySec; + + @JSONField(name = "hls.segKeep") + private String hlsSegKeep; + + @JSONField(name = "hook.access_file_except_hls") + private String hookAccessFileExceptHLS; + + @JSONField(name = "hook.admin_params") + private String hookAdminParams; + + @JSONField(name = "hook.alive_interval") + private Float hookAliveInterval; + + @JSONField(name = "hook.enable") + private String hookEnable; + + @JSONField(name = "hook.on_flow_report") + private String hookOnFlowReport; + + @JSONField(name = "hook.on_http_access") + private String hookOnHttpAccess; + + @JSONField(name = "hook.on_play") + private String hookOnPlay; + + @JSONField(name = "hook.on_publish") + private String hookOnPublish; + + @JSONField(name = "hook.on_record_mp4") + private String hookOnRecordMp4; + + @JSONField(name = "hook.on_rtsp_auth") + private String hookOnRtspAuth; + + @JSONField(name = "hook.on_rtsp_realm") + private String hookOnRtspRealm; + + @JSONField(name = "hook.on_shell_login") + private String hookOnShellLogin; + + @JSONField(name = "hook.on_stream_changed") + private String hookOnStreamChanged; + + @JSONField(name = "hook.on_stream_none_reader") + private String hookOnStreamNoneReader; + + @JSONField(name = "hook.on_stream_not_found") + private String hookOnStreamNotFound; + + @JSONField(name = "hook.on_server_started") + private String hookOnServerStarted; + + @JSONField(name = "hook.on_server_keepalive") + private String hookOnServerKeepalive; + + @JSONField(name = "hook.on_send_rtp_stopped") + private String hookOnSendRtpStopped; + + @JSONField(name = "hook.on_rtp_server_timeout") + private String hookOnRtpServerTimeout; + + @JSONField(name = "hook.timeoutSec") + private String hookTimeoutSec; + + @JSONField(name = "http.charSet") + private String httpCharSet; + + @JSONField(name = "http.keepAliveSecond") + private String httpKeepAliveSecond; + + @JSONField(name = "http.maxReqCount") + private String httpMaxReqCount; + + @JSONField(name = "http.maxReqSize") + private String httpMaxReqSize; + + @JSONField(name = "http.notFound") + private String httpNotFound; + + @JSONField(name = "http.port") + private int httpPort; + + @JSONField(name = "http.rootPath") + private String httpRootPath; + + @JSONField(name = "http.sendBufSize") + private String httpSendBufSize; + + @JSONField(name = "http.sslport") + private int httpSSLport; + + @JSONField(name = "multicast.addrMax") + private String multicastAddrMax; + + @JSONField(name = "multicast.addrMin") + private String multicastAddrMin; + + @JSONField(name = "multicast.udpTTL") + private String multicastUdpTTL; + + @JSONField(name = "record.appName") + private String recordAppName; + + @JSONField(name = "record.filePath") + private String recordFilePath; + + @JSONField(name = "record.fileSecond") + private String recordFileSecond; + + @JSONField(name = "record.sampleMS") + private String recordFileSampleMS; + + @JSONField(name = "rtmp.handshakeSecond") + private String rtmpHandshakeSecond; + + @JSONField(name = "rtmp.keepAliveSecond") + private String rtmpKeepAliveSecond; + + @JSONField(name = "rtmp.modifyStamp") + private String rtmpModifyStamp; + + @JSONField(name = "rtmp.port") + private int rtmpPort; + + @JSONField(name = "rtmp.sslport") + private int rtmpSslPort; + + @JSONField(name = "rtp.audioMtuSize") + private String rtpAudioMtuSize; + + @JSONField(name = "rtp.clearCount") + private String rtpClearCount; + + @JSONField(name = "rtp.cycleMS") + private String rtpCycleMS; + + @JSONField(name = "rtp.maxRtpCount") + private String rtpMaxRtpCount; + + @JSONField(name = "rtp.videoMtuSize") + private String rtpVideoMtuSize; + + @JSONField(name = "rtp_proxy.checkSource") + private String rtpProxyCheckSource; + + @JSONField(name = "rtp_proxy.dumpDir") + private String rtpProxyDumpDir; + + @JSONField(name = "rtp_proxy.port") + private int rtpProxyPort; + + @JSONField(name = "rtp_proxy.port_range") + private String portRange; + + @JSONField(name = "rtp_proxy.timeoutSec") + private String rtpProxyTimeoutSec; + + @JSONField(name = "rtsp.authBasic") + private String rtspAuthBasic; + + @JSONField(name = "rtsp.handshakeSecond") + private String rtspHandshakeSecond; + + @JSONField(name = "rtsp.keepAliveSecond") + private String rtspKeepAliveSecond; + + @JSONField(name = "rtsp.port") + private int rtspPort; + + @JSONField(name = "rtsp.sslport") + private int rtspSSlport; + + @JSONField(name = "shell.maxReqSize") + private String shellMaxReqSize; + + @JSONField(name = "shell.shell") + private String shellPhell; + + @JSONField(name = "transcode.suffix") + private String transcodeSuffix; + +} diff --git a/src/main/java/com/genersoft/iot/vmp/media/zlm/dto/hook/HookParam.java b/src/main/java/com/genersoft/iot/vmp/media/zlm/dto/hook/HookParam.java new file mode 100755 index 0000000..8ae9e6f --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/media/zlm/dto/hook/HookParam.java @@ -0,0 +1,13 @@ +package com.genersoft.iot.vmp.media.zlm.dto.hook; + +import lombok.Data; + +/** + * zlm hook事件的参数 + * @author lin + */ +@Data +public class HookParam { + private String mediaServerId; + +} diff --git a/src/main/java/com/genersoft/iot/vmp/media/zlm/dto/hook/HookResult.java b/src/main/java/com/genersoft/iot/vmp/media/zlm/dto/hook/HookResult.java new file mode 100755 index 0000000..dee9d66 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/media/zlm/dto/hook/HookResult.java @@ -0,0 +1,40 @@ +package com.genersoft.iot.vmp.media.zlm.dto.hook; + +public class HookResult { + + private int code; + private String msg; + + + public HookResult() { + } + + public HookResult(int code, String msg) { + this.code = code; + this.msg = msg; + } + + public static HookResult SUCCESS(){ + return new HookResult(0, "success"); + } + + public static HookResultForOnPublish Fail(){ + return new HookResultForOnPublish(-1, "fail"); + } + + public int getCode() { + return code; + } + + public void setCode(int code) { + this.code = code; + } + + public String getMsg() { + return msg; + } + + public void setMsg(String msg) { + this.msg = msg; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/media/zlm/dto/hook/HookResultForOnPublish.java b/src/main/java/com/genersoft/iot/vmp/media/zlm/dto/hook/HookResultForOnPublish.java new file mode 100755 index 0000000..3777e19 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/media/zlm/dto/hook/HookResultForOnPublish.java @@ -0,0 +1,52 @@ +package com.genersoft.iot.vmp.media.zlm.dto.hook; + +import com.genersoft.iot.vmp.media.bean.ResultForOnPublish; +import lombok.Getter; +import lombok.Setter; + +@Setter +@Getter +public class HookResultForOnPublish extends HookResult{ + + private boolean enable_audio; + private boolean enable_mp4; + private int mp4_max_second; + private String mp4_save_path; + private String stream_replace; + private Integer modify_stamp; + + public HookResultForOnPublish() { + } + + public static HookResultForOnPublish SUCCESS(){ + return new HookResultForOnPublish(0, "success"); + } + + public static HookResultForOnPublish getInstance(ResultForOnPublish resultForOnPublish){ + HookResultForOnPublish successResult = new HookResultForOnPublish(0, "success"); + successResult.setEnable_audio(resultForOnPublish.isEnable_audio()); + successResult.setEnable_mp4(resultForOnPublish.isEnable_mp4()); + successResult.setModify_stamp(resultForOnPublish.getModify_stamp()); + successResult.setStream_replace(resultForOnPublish.getStream_replace()); + successResult.setMp4_max_second(resultForOnPublish.getMp4_max_second()); + successResult.setMp4_save_path(resultForOnPublish.getMp4_save_path()); + return successResult; + } + + public HookResultForOnPublish(int code, String msg) { + setCode(code); + setMsg(msg); + } + + @Override + public String toString() { + return "HookResultForOnPublish{" + + "enable_audio=" + enable_audio + + ", enable_mp4=" + enable_mp4 + + ", mp4_max_second=" + mp4_max_second + + ", mp4_save_path='" + mp4_save_path + '\'' + + ", stream_replace='" + stream_replace + '\'' + + ", modify_stamp='" + modify_stamp + '\'' + + '}'; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/media/zlm/dto/hook/OnPlayHookParam.java b/src/main/java/com/genersoft/iot/vmp/media/zlm/dto/hook/OnPlayHookParam.java new file mode 100755 index 0000000..6e41cce --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/media/zlm/dto/hook/OnPlayHookParam.java @@ -0,0 +1,95 @@ +package com.genersoft.iot.vmp.media.zlm.dto.hook; + +/** + * zlm hook事件中的on_play事件的参数 + * @author lin + */ +public class OnPlayHookParam extends HookParam{ + private String id; + private String app; + private String stream; + private String ip; + private String params; + private int port; + private String schema; + private String vhost; + + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getApp() { + return app; + } + + public void setApp(String app) { + this.app = app; + } + + public String getStream() { + return stream; + } + + public void setStream(String stream) { + this.stream = stream; + } + + public String getIp() { + return ip; + } + + public void setIp(String ip) { + this.ip = ip; + } + + public String getParams() { + return params; + } + + public void setParams(String params) { + this.params = params; + } + + public int getPort() { + return port; + } + + public void setPort(int port) { + this.port = port; + } + + public String getSchema() { + return schema; + } + + public void setSchema(String schema) { + this.schema = schema; + } + + public String getVhost() { + return vhost; + } + + public void setVhost(String vhost) { + this.vhost = vhost; + } + + @Override + public String toString() { + return "OnPlayHookParam{" + + "id='" + id + '\'' + + ", app='" + app + '\'' + + ", stream='" + stream + '\'' + + ", ip='" + ip + '\'' + + ", params='" + params + '\'' + + ", port=" + port + + ", schema='" + schema + '\'' + + ", vhost='" + vhost + '\'' + + '}'; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/media/zlm/dto/hook/OnPublishHookParam.java b/src/main/java/com/genersoft/iot/vmp/media/zlm/dto/hook/OnPublishHookParam.java new file mode 100755 index 0000000..e117213 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/media/zlm/dto/hook/OnPublishHookParam.java @@ -0,0 +1,59 @@ +package com.genersoft.iot.vmp.media.zlm.dto.hook; + +import lombok.Getter; +import lombok.Setter; + +/** + * zlm hook事件中的on_publish事件的参数 + * @author lin + */ + +public class OnPublishHookParam extends HookParam{ + + @Getter + @Setter + private String id; + + @Getter + @Setter + private String app; + + @Getter + @Setter + private String stream; + + @Getter + @Setter + private String ip; + + @Getter + @Setter + private String params; + + @Getter + @Setter + private int port; + + @Getter + @Setter + private String schema; + + @Getter + @Setter + private String vhost; + + + @Override + public String toString() { + return "OnPublishHookParam{" + + "id='" + id + '\'' + + ", app='" + app + '\'' + + ", stream='" + stream + '\'' + + ", ip='" + ip + '\'' + + ", params='" + params + '\'' + + ", port=" + port + + ", schema='" + schema + '\'' + + ", vhost='" + vhost + '\'' + + '}'; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/media/zlm/dto/hook/OnRecordMp4HookParam.java b/src/main/java/com/genersoft/iot/vmp/media/zlm/dto/hook/OnRecordMp4HookParam.java new file mode 100755 index 0000000..deeeff4 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/media/zlm/dto/hook/OnRecordMp4HookParam.java @@ -0,0 +1,124 @@ +package com.genersoft.iot.vmp.media.zlm.dto.hook; + +/** + * zlm hook事件中的on_rtp_server_timeout事件的参数 + * @author lin + */ +public class OnRecordMp4HookParam extends HookParam{ + private String app; + private String stream; + private String file_name; + private String file_path; + private long file_size; + private String folder; + private String url; + private String vhost; + private long start_time; + private double time_len; + private String params; + + public String getApp() { + return app; + } + + public void setApp(String app) { + this.app = app; + } + + public String getStream() { + return stream; + } + + public void setStream(String stream) { + this.stream = stream; + } + + public String getFile_name() { + return file_name; + } + + public void setFile_name(String file_name) { + this.file_name = file_name; + } + + public String getFile_path() { + return file_path; + } + + public void setFile_path(String file_path) { + this.file_path = file_path; + } + + public long getFile_size() { + return file_size; + } + + public void setFile_size(long file_size) { + this.file_size = file_size; + } + + public String getFolder() { + return folder; + } + + public void setFolder(String folder) { + this.folder = folder; + } + + public String getUrl() { + return url; + } + + public void setUrl(String url) { + this.url = url; + } + + public String getVhost() { + return vhost; + } + + public void setVhost(String vhost) { + this.vhost = vhost; + } + + public long getStart_time() { + return start_time; + } + + public void setStart_time(long start_time) { + this.start_time = start_time; + } + + public double getTime_len() { + return time_len; + } + + public void setTime_len(double time_len) { + this.time_len = time_len; + } + + public String getParams() { + return params; + } + + public void setParams(String params) { + this.params = params; + } + + @Override + public String toString() { + return "OnRecordMp4HookParam{" + + "app='" + app + '\'' + + ", stream='" + stream + '\'' + + ", file_name='" + file_name + '\'' + + ", file_path='" + file_path + '\'' + + ", file_size='" + file_size + '\'' + + ", folder='" + folder + '\'' + + ", url='" + url + '\'' + + ", vhost='" + vhost + '\'' + + ", start_time=" + start_time + + ", time_len=" + time_len + + ", params=" + params + + '}'; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/media/zlm/dto/hook/OnRtpServerTimeoutHookParam.java b/src/main/java/com/genersoft/iot/vmp/media/zlm/dto/hook/OnRtpServerTimeoutHookParam.java new file mode 100755 index 0000000..6179ce4 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/media/zlm/dto/hook/OnRtpServerTimeoutHookParam.java @@ -0,0 +1,64 @@ +package com.genersoft.iot.vmp.media.zlm.dto.hook; + +/** + * zlm hook事件中的on_rtp_server_timeout事件的参数 + * @author lin + */ +public class OnRtpServerTimeoutHookParam extends HookParam{ + private int local_port; + private String stream_id; + private int tcpMode; + private boolean re_use_port; + private String ssrc; + + public int getLocal_port() { + return local_port; + } + + public void setLocal_port(int local_port) { + this.local_port = local_port; + } + + public String getStream_id() { + return stream_id; + } + + public void setStream_id(String stream_id) { + this.stream_id = stream_id; + } + + public int getTcpMode() { + return tcpMode; + } + + public void setTcpMode(int tcpMode) { + this.tcpMode = tcpMode; + } + + public boolean isRe_use_port() { + return re_use_port; + } + + public void setRe_use_port(boolean re_use_port) { + this.re_use_port = re_use_port; + } + + public String getSsrc() { + return ssrc; + } + + public void setSsrc(String ssrc) { + this.ssrc = ssrc; + } + + @Override + public String toString() { + return "OnRtpServerTimeoutHookParam{" + + "local_port=" + local_port + + ", stream_id='" + stream_id + '\'' + + ", tcpMode=" + tcpMode + + ", re_use_port=" + re_use_port + + ", ssrc='" + ssrc + '\'' + + '}'; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/media/zlm/dto/hook/OnSendRtpStoppedHookParam.java b/src/main/java/com/genersoft/iot/vmp/media/zlm/dto/hook/OnSendRtpStoppedHookParam.java new file mode 100755 index 0000000..4989b4a --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/media/zlm/dto/hook/OnSendRtpStoppedHookParam.java @@ -0,0 +1,35 @@ +package com.genersoft.iot.vmp.media.zlm.dto.hook; + +/** + * zlm hook事件中的on_send_rtp_stopped事件的参数 + * @author lin + */ +public class OnSendRtpStoppedHookParam extends HookParam{ + private String app; + private String stream; + + + public String getApp() { + return app; + } + + public void setApp(String app) { + this.app = app; + } + + public String getStream() { + return stream; + } + + public void setStream(String stream) { + this.stream = stream; + } + + @Override + public String toString() { + return "OnSendRtpStoppedHookParam{" + + "app='" + app + '\'' + + ", stream='" + stream + '\'' + + '}'; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/media/zlm/dto/hook/OnServerKeepaliveHookParam.java b/src/main/java/com/genersoft/iot/vmp/media/zlm/dto/hook/OnServerKeepaliveHookParam.java new file mode 100755 index 0000000..5439f20 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/media/zlm/dto/hook/OnServerKeepaliveHookParam.java @@ -0,0 +1,27 @@ +package com.genersoft.iot.vmp.media.zlm.dto.hook; + +import com.genersoft.iot.vmp.media.zlm.dto.ServerKeepaliveData; + +/** + * zlm hook事件中的on_play事件的参数 + * @author lin + */ +public class OnServerKeepaliveHookParam extends HookParam{ + + private ServerKeepaliveData data; + + public ServerKeepaliveData getData() { + return data; + } + + public void setData(ServerKeepaliveData data) { + this.data = data; + } + + @Override + public String toString() { + return "OnServerKeepaliveHookParam{" + + "data=" + data + + '}'; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/media/zlm/dto/hook/OnStreamChangedHookParam.java b/src/main/java/com/genersoft/iot/vmp/media/zlm/dto/hook/OnStreamChangedHookParam.java new file mode 100755 index 0000000..5ed378e --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/media/zlm/dto/hook/OnStreamChangedHookParam.java @@ -0,0 +1,220 @@ +package com.genersoft.iot.vmp.media.zlm.dto.hook; + +import com.genersoft.iot.vmp.vmanager.bean.StreamContent; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.util.List; +import java.util.Map; + +/** + * @author lin + */ +@EqualsAndHashCode(callSuper = true) +@Data +public class OnStreamChangedHookParam extends HookParam{ + + /** + * 注册/注销 + */ + private boolean regist; + + /** + * 应用名 + */ + private String app; + + /** + * 流id + */ + private String stream; + + /** + * 推流鉴权Id + */ + private String callId; + + /** + * 观看总人数,包括hls/rtsp/rtmp/http-flv/ws-flv + */ + private int totalReaderCount; + + /** + * 协议 包括hls/rtsp/rtmp/http-flv/ws-flv + */ + private String schema; + + + /** + * 产生源类型, + * unknown = 0, + * rtmp_push=1, + * rtsp_push=2, + * rtp_push=3, + * pull=4, + * ffmpeg_pull=5, + * mp4_vod=6, + * device_chn=7 + */ + private int originType; + + /** + * 客户端和服务器网络信息,可能为null类型 + */ + private OriginSock originSock; + + /** + * 产生源类型的字符串描述 + */ + private String originTypeStr; + + /** + * 产生源的url + */ + private String originUrl; + + /** + * 服务器id + */ + private String severId; + + /** + * GMT unix系统时间戳,单位秒 + */ + private Long createStamp; + + /** + * 存活时间,单位秒 + */ + private Long aliveSecond; + + /** + * 数据产生速度,单位byte/s + */ + private Long bytesSpeed; + + /** + * 音视频轨道 + */ + private List tracks; + + /** + * 音视频轨道 + */ + private String vhost; + + /** + * 额外的参数字符串 + */ + private String params; + + /** + * 额外的参数 + */ + private Map paramMap; + + /** + * 是否是docker部署, docker部署不会自动更新zlm使用的端口,需要自己手动修改 + */ + private boolean docker; + + @Data + public static class MediaTrack { + /** + * 音频通道数 + */ + private int channels; + + /** + * H264 = 0, H265 = 1, AAC = 2, G711A = 3, G711U = 4 + */ + private int codec_id; + + /** + * 编码类型名称 CodecAAC CodecH264 + */ + private String codec_id_name; + + /** + * Video = 0, Audio = 1 + */ + private int codec_type; + + /** + * 轨道是否准备就绪 + */ + private boolean ready; + + /** + * 音频采样位数 + */ + private int sample_bit; + + /** + * 音频采样率 + */ + private int sample_rate; + + /** + * 视频fps + */ + private float fps; + + /** + * 视频高 + */ + private int height; + + /** + * 视频宽 + */ + private int width; + + /** + * 帧数 + */ + private int frames; + + /** + * 关键帧数 + */ + private int key_frames; + + /** + * GOP大小 + */ + private int gop_size; + + /** + * GOP间隔时长(ms) + */ + private int gop_interval_ms; + + /** + * 丢帧率 + */ + private float loss; + } + + @Data + public static class OriginSock{ + private String identifier; + private String local_ip; + private int local_port; + private String peer_ip; + private int peer_port; + + } + + private StreamContent streamInfo; + + @Override + public String toString() { + return "OnStreamChangedHookParam{" + + "regist=" + regist + + ", app='" + app + '\'' + + ", stream='" + stream + '\'' + + ", severId='" + severId + '\'' + + '}'; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/media/zlm/dto/hook/OnStreamNoneReaderHookParam.java b/src/main/java/com/genersoft/iot/vmp/media/zlm/dto/hook/OnStreamNoneReaderHookParam.java new file mode 100755 index 0000000..3b62842 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/media/zlm/dto/hook/OnStreamNoneReaderHookParam.java @@ -0,0 +1,51 @@ +package com.genersoft.iot.vmp.media.zlm.dto.hook; + +public class OnStreamNoneReaderHookParam extends HookParam{ + + private String schema; + private String app; + private String stream; + private String vhost; + + public String getSchema() { + return schema; + } + + public void setSchema(String schema) { + this.schema = schema; + } + + public String getApp() { + return app; + } + + public void setApp(String app) { + this.app = app; + } + + public String getStream() { + return stream; + } + + public void setStream(String stream) { + this.stream = stream; + } + + public String getVhost() { + return vhost; + } + + public void setVhost(String vhost) { + this.vhost = vhost; + } + + @Override + public String toString() { + return "OnStreamNoneReaderHookParam{" + + "schema='" + schema + '\'' + + ", app='" + app + '\'' + + ", stream='" + stream + '\'' + + ", vhost='" + vhost + '\'' + + '}'; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/media/zlm/dto/hook/OnStreamNotFoundHookParam.java b/src/main/java/com/genersoft/iot/vmp/media/zlm/dto/hook/OnStreamNotFoundHookParam.java new file mode 100755 index 0000000..76e6a72 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/media/zlm/dto/hook/OnStreamNotFoundHookParam.java @@ -0,0 +1,95 @@ +package com.genersoft.iot.vmp.media.zlm.dto.hook; + +/** + * zlm hook事件中的on_stream_not_found事件的参数 + * @author lin + */ +public class OnStreamNotFoundHookParam extends HookParam{ + private String id; + private String app; + private String stream; + private String ip; + private String params; + private int port; + private String schema; + private String vhost; + + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getApp() { + return app; + } + + public void setApp(String app) { + this.app = app; + } + + public String getStream() { + return stream; + } + + public void setStream(String stream) { + this.stream = stream; + } + + public String getIp() { + return ip; + } + + public void setIp(String ip) { + this.ip = ip; + } + + public String getParams() { + return params; + } + + public void setParams(String params) { + this.params = params; + } + + public int getPort() { + return port; + } + + public void setPort(int port) { + this.port = port; + } + + public String getSchema() { + return schema; + } + + public void setSchema(String schema) { + this.schema = schema; + } + + public String getVhost() { + return vhost; + } + + public void setVhost(String vhost) { + this.vhost = vhost; + } + + @Override + public String toString() { + return "OnStreamNotFoundHookParam{" + + "id='" + id + '\'' + + ", app='" + app + '\'' + + ", stream='" + stream + '\'' + + ", ip='" + ip + '\'' + + ", params='" + params + '\'' + + ", port=" + port + + ", schema='" + schema + '\'' + + ", vhost='" + vhost + '\'' + + '}'; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/media/zlm/dto/hook/OriginType.java b/src/main/java/com/genersoft/iot/vmp/media/zlm/dto/hook/OriginType.java new file mode 100755 index 0000000..926cf4d --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/media/zlm/dto/hook/OriginType.java @@ -0,0 +1,23 @@ +package com.genersoft.iot.vmp.media.zlm.dto.hook; + +public enum OriginType { + // 不可调整顺序 + UNKNOWN("UNKNOWN"), + RTMP_PUSH("PUSH"), + RTSP_PUSH("PUSH"), + RTP_PUSH("RTP"), + PULL("PULL"), + FFMPEG_PULL("PULL"), + MP4_VOD("MP4_VOD"), + DEVICE_CHN("DEVICE_CHN"), + RTC_PUSH("PUSH"); + + private final String type; + OriginType(String type) { + this.type = type; + } + + public String getType() { + return type; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/media/zlm/event/HookZlmServerKeepaliveEvent.java b/src/main/java/com/genersoft/iot/vmp/media/zlm/event/HookZlmServerKeepaliveEvent.java new file mode 100644 index 0000000..b927062 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/media/zlm/event/HookZlmServerKeepaliveEvent.java @@ -0,0 +1,24 @@ +package com.genersoft.iot.vmp.media.zlm.event; + +import com.genersoft.iot.vmp.media.bean.MediaServer; +import org.springframework.context.ApplicationEvent; + +/** + * zlm 心跳事件 + */ +public class HookZlmServerKeepaliveEvent extends ApplicationEvent { + + public HookZlmServerKeepaliveEvent(Object source) { + super(source); + } + + private MediaServer mediaServerItem; + + public MediaServer getMediaServerItem() { + return mediaServerItem; + } + + public void setMediaServerItem(MediaServer mediaServerItem) { + this.mediaServerItem = mediaServerItem; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/media/zlm/event/HookZlmServerStartEvent.java b/src/main/java/com/genersoft/iot/vmp/media/zlm/event/HookZlmServerStartEvent.java new file mode 100644 index 0000000..e1c28b1 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/media/zlm/event/HookZlmServerStartEvent.java @@ -0,0 +1,24 @@ +package com.genersoft.iot.vmp.media.zlm.event; + +import com.genersoft.iot.vmp.media.bean.MediaServer; +import org.springframework.context.ApplicationEvent; + +/** + * zlm server_start事件 + */ +public class HookZlmServerStartEvent extends ApplicationEvent { + + public HookZlmServerStartEvent(Object source) { + super(source); + } + + private MediaServer mediaServerItem; + + public MediaServer getMediaServerItem() { + return mediaServerItem; + } + + public void setMediaServerItem(MediaServer mediaServerItem) { + this.mediaServerItem = mediaServerItem; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/service/ICloudRecordService.java b/src/main/java/com/genersoft/iot/vmp/service/ICloudRecordService.java new file mode 100755 index 0000000..80e865a --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/service/ICloudRecordService.java @@ -0,0 +1,76 @@ +package com.genersoft.iot.vmp.service; + +import com.alibaba.fastjson2.JSONArray; +import com.genersoft.iot.vmp.common.StreamInfo; +import com.genersoft.iot.vmp.media.bean.MediaServer; +import com.genersoft.iot.vmp.service.bean.CloudRecordItem; +import com.genersoft.iot.vmp.service.bean.DownloadFileInfo; +import com.genersoft.iot.vmp.service.bean.ErrorCallback; +import com.genersoft.iot.vmp.vmanager.cloudRecord.bean.CloudRecordUrl; +import com.github.pagehelper.PageInfo; + +import java.util.List; +import java.util.Set; + +/** + * 云端录像管理 + * @author lin + */ +public interface ICloudRecordService { + + /** + * 分页回去云端录像列表 + */ + PageInfo getList(int page, int count, String query, String app, String stream, String startTime, String endTime, List mediaServerItems, String callId, Boolean ascOrder); + + /** + * 获取所有的日期 + */ + List getDateList(String app, String stream, int year, int month, List mediaServerItems); + + /** + * 添加合并任务 + */ + String addTask(String app, String stream, MediaServer mediaServerItem, String startTime, + String endTime, String callId, String remoteHost, boolean filterMediaServer); + + + /** + * 查询合并任务列表 + */ + JSONArray queryTask(String app, String stream, String callId, String taskId, String mediaServerId, Boolean isEnd, String scheme); + + /** + * 收藏视频,收藏的视频过期不会删除 + */ + int changeCollect(boolean result, String app, String stream, String mediaServerId, String startTime, String endTime, String callId); + + /** + * 添加指定录像收藏 + */ + int changeCollectById(Integer recordId, boolean result); + + /** + * 获取播放地址 + */ + DownloadFileInfo getPlayUrlPath(Integer recordId); + + List getAllList(String query, String app, String stream, String startTime, String endTime, List mediaServerItems, String callId, List ids); + + /** + * 加载录像文件,形成录像流 + */ + void loadMP4FileForDate(String app, String stream, String date, ErrorCallback callback); + + void seekRecord(String mediaServerId,String app, String stream, Double seek, String schema); + + void setRecordSpeed(String mediaServerId, String app, String stream, Integer speed, String schema); + + void deleteFileByIds(Set ids); + + void loadMP4File(String app, String stream, int cloudRecordId, ErrorCallback callback); + + List getUrlListByIds(List ids); + + List getUrlList(String app, String stream, String callId); +} diff --git a/src/main/java/com/genersoft/iot/vmp/service/ILogService.java b/src/main/java/com/genersoft/iot/vmp/service/ILogService.java new file mode 100644 index 0000000..ef6161c --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/service/ILogService.java @@ -0,0 +1,12 @@ +package com.genersoft.iot.vmp.service; + +import com.genersoft.iot.vmp.service.bean.LogFileInfo; + +import java.io.File; +import java.util.List; + +public interface ILogService { + List queryList(String query, String startTime, String endTime); + + File getFileByName(String fileName); +} diff --git a/src/main/java/com/genersoft/iot/vmp/service/IMapService.java b/src/main/java/com/genersoft/iot/vmp/service/IMapService.java new file mode 100644 index 0000000..bc80673 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/service/IMapService.java @@ -0,0 +1,13 @@ +package com.genersoft.iot.vmp.service; + +import com.genersoft.iot.vmp.vmanager.bean.MapConfig; +import com.genersoft.iot.vmp.vmanager.bean.MapModelIcon; + +import java.util.List; + +public interface IMapService { + + List getConfig(); + + List getModelList(); +} diff --git a/src/main/java/com/genersoft/iot/vmp/service/IMediaService.java b/src/main/java/com/genersoft/iot/vmp/service/IMediaService.java new file mode 100755 index 0000000..26f1585 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/service/IMediaService.java @@ -0,0 +1,19 @@ +package com.genersoft.iot.vmp.service; + +import com.genersoft.iot.vmp.media.bean.ResultForOnPublish; +import com.genersoft.iot.vmp.media.bean.MediaServer; + +/** + * 媒体信息业务 + */ +public interface IMediaService { + + /** + * 播放鉴权 + */ + boolean authenticatePlay(String app, String stream, String callId); + + ResultForOnPublish authenticatePublish(MediaServer mediaServer, String app, String stream, String params); + + boolean closeStreamOnNoneReader(String mediaServerId, String app, String stream, String schema); +} diff --git a/src/main/java/com/genersoft/iot/vmp/service/IMobilePositionService.java b/src/main/java/com/genersoft/iot/vmp/service/IMobilePositionService.java new file mode 100644 index 0000000..1643ad7 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/service/IMobilePositionService.java @@ -0,0 +1,21 @@ +package com.genersoft.iot.vmp.service; + + +import com.genersoft.iot.vmp.gb28181.bean.MobilePosition; +import com.genersoft.iot.vmp.gb28181.bean.Platform; + +import java.util.List; + +public interface IMobilePositionService { + + void add(List mobilePositionList); + + void add(MobilePosition mobilePosition); + + List queryMobilePositions(String deviceId, String channelId, String startTime, String endTime); + + List queryEnablePlatformListWithAsMessageChannel(); + + MobilePosition queryLatestPosition(String deviceId); + +} diff --git a/src/main/java/com/genersoft/iot/vmp/service/IReceiveRtpServerService.java b/src/main/java/com/genersoft/iot/vmp/service/IReceiveRtpServerService.java new file mode 100644 index 0000000..caf2b90 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/service/IReceiveRtpServerService.java @@ -0,0 +1,13 @@ +package com.genersoft.iot.vmp.service; + +import com.genersoft.iot.vmp.gb28181.bean.OpenRTPServerResult; +import com.genersoft.iot.vmp.media.bean.MediaServer; +import com.genersoft.iot.vmp.service.bean.ErrorCallback; +import com.genersoft.iot.vmp.service.bean.RTPServerParam; +import com.genersoft.iot.vmp.service.bean.SSRCInfo; + +public interface IReceiveRtpServerService { + SSRCInfo openRTPServer(RTPServerParam rtpServerParam, ErrorCallback callback); + + void closeRTPServer(MediaServer mediaServer, SSRCInfo ssrcInfo); +} diff --git a/src/main/java/com/genersoft/iot/vmp/service/IRecordPlanService.java b/src/main/java/com/genersoft/iot/vmp/service/IRecordPlanService.java new file mode 100644 index 0000000..f3b3491 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/service/IRecordPlanService.java @@ -0,0 +1,31 @@ +package com.genersoft.iot.vmp.service; + +import com.genersoft.iot.vmp.gb28181.bean.CommonGBChannel; +import com.genersoft.iot.vmp.service.bean.RecordPlan; +import com.github.pagehelper.PageInfo; + +import java.util.List; + +public interface IRecordPlanService { + + + RecordPlan get(Integer planId); + + void update(RecordPlan plan); + + void delete(Integer planId); + + PageInfo query(Integer page, Integer count, String query); + + void add(RecordPlan plan); + + void link(List channelIds, Integer planId); + + PageInfo queryChannelList(int page, int count, String query, Integer channelType, Boolean online, Integer planId, Boolean hasLink); + + void linkAll(Integer planId); + + void cleanAll(Integer planId); + + Integer recording(String app, String stream); +} diff --git a/src/main/java/com/genersoft/iot/vmp/service/IRoleService.java b/src/main/java/com/genersoft/iot/vmp/service/IRoleService.java new file mode 100755 index 0000000..d207c6a --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/service/IRoleService.java @@ -0,0 +1,18 @@ +package com.genersoft.iot.vmp.service; + +import com.genersoft.iot.vmp.storager.dao.dto.Role; + +import java.util.List; + +public interface IRoleService { + + Role getRoleById(int id); + + int add(Role role); + + int delete(int id); + + List getAll(); + + int update(Role role); +} diff --git a/src/main/java/com/genersoft/iot/vmp/service/ISendRtpServerService.java b/src/main/java/com/genersoft/iot/vmp/service/ISendRtpServerService.java new file mode 100644 index 0000000..ca8c33d --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/service/ISendRtpServerService.java @@ -0,0 +1,45 @@ +package com.genersoft.iot.vmp.service; + +import com.genersoft.iot.vmp.gb28181.bean.SendRtpInfo; +import com.genersoft.iot.vmp.media.bean.MediaServer; + +import java.util.List; + +public interface ISendRtpServerService { + + SendRtpInfo createSendRtpInfo(MediaServer mediaServer, String ip, Integer port, String ssrc, String requesterId, + String deviceId, Integer channelId, Boolean isTcp, Boolean rtcp); + + SendRtpInfo createSendRtpInfo(MediaServer mediaServer, String ip, Integer port, String ssrc, String platformId, + String app, String stream, Integer channelId, Boolean tcp, Boolean rtcp); + + void update(SendRtpInfo sendRtpItem); + + SendRtpInfo queryByChannelId(Integer channelId, String targetId); + + SendRtpInfo queryByCallId(String callId); + + List queryByStream(String stream); + + SendRtpInfo queryByStream(String stream, String targetId); + + void delete(SendRtpInfo sendRtpInfo); + + void deleteByCallId(String callId); + + void deleteByStream(String Stream, String targetId); + + void deleteByChannel(Integer channelId, String targetId); + + List queryAll(); + + boolean isChannelSendingRTP(Integer channelId); + + List queryForPlatform(String platformId); + + List queryByChannelId(int id); + + void deleteByStream(String stream); + + int getNextPort(MediaServer mediaServer); +} diff --git a/src/main/java/com/genersoft/iot/vmp/service/IUserApiKeyService.java b/src/main/java/com/genersoft/iot/vmp/service/IUserApiKeyService.java new file mode 100644 index 0000000..b3cc580 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/service/IUserApiKeyService.java @@ -0,0 +1,25 @@ +package com.genersoft.iot.vmp.service; + +import com.genersoft.iot.vmp.storager.dao.dto.UserApiKey; +import com.github.pagehelper.PageInfo; + +public interface IUserApiKeyService { + int addApiKey(UserApiKey userApiKey); + + boolean isApiKeyExists(String apiKey); + + PageInfo getUserApiKeys(int page, int count); + + int enable(Integer id); + + int disable(Integer id); + + int remark(Integer id, String remark); + + int delete(Integer id); + + UserApiKey getUserApiKeyById(Integer id); + + int reset(Integer id, String apiKey); + +} diff --git a/src/main/java/com/genersoft/iot/vmp/service/IUserService.java b/src/main/java/com/genersoft/iot/vmp/service/IUserService.java new file mode 100755 index 0000000..1e9b724 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/service/IUserService.java @@ -0,0 +1,31 @@ +package com.genersoft.iot.vmp.service; + +import com.genersoft.iot.vmp.storager.dao.dto.User; +import com.github.pagehelper.PageInfo; + +import java.util.List; + +public interface IUserService { + + User getUser(String username, String password); + + boolean changePassword(int id, String password); + + User getUserById(int id); + + User getUserByUsername(String username); + + int addUser(User user); + + int deleteUser(int id); + + List getAllUsers(); + + int updateUsers(User user); + + boolean checkPushAuthority(String callId, String sign); + + PageInfo getUsers(int page, int count); + + int changePushKey(int id, String pushKey); +} diff --git a/src/main/java/com/genersoft/iot/vmp/service/bean/CloudRecordItem.java b/src/main/java/com/genersoft/iot/vmp/service/bean/CloudRecordItem.java new file mode 100644 index 0000000..01e452a --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/service/bean/CloudRecordItem.java @@ -0,0 +1,119 @@ +package com.genersoft.iot.vmp.service.bean; + +import com.genersoft.iot.vmp.media.event.media.MediaRecordMp4Event; +import com.genersoft.iot.vmp.media.event.media.MediaRecordProcessEvent; +import com.genersoft.iot.vmp.utils.MediaServerUtils; +import lombok.Data; + +import java.util.Map; + +/** + * 云端录像数据 + */ +@Data +public class CloudRecordItem { + /** + * 主键 + */ + private int id; + + /** + * 应用名 + */ + private String app; + + /** + * 流 + */ + private String stream; + + /** + * 健全ID + */ + private String callId; + + /** + * 开始时间 + */ + private long startTime; + + /** + * 结束时间 + */ + private long endTime; + + /** + * ZLM Id + */ + private String mediaServerId; + + /** + * 文件名称 + */ + private String fileName; + + /** + * 文件路径 + */ + private String filePath; + + /** + * 文件夹 + */ + private String folder; + + /** + * 收藏,收藏的文件不移除 + */ + private Boolean collect; + + /** + * 保留,收藏的文件不移除 + */ + private Boolean reserve; + + /** + * 文件大小 + */ + private long fileSize; + + /** + * 文件时长 + */ + private double timeLen; + + /** + * 所属服务ID + */ + private String serverId; + + public static CloudRecordItem getInstance(MediaRecordMp4Event param) { + CloudRecordItem cloudRecordItem = new CloudRecordItem(); + cloudRecordItem.setApp(param.getApp()); + cloudRecordItem.setStream(param.getStream()); + cloudRecordItem.setStartTime(param.getRecordInfo().getStartTime()); + cloudRecordItem.setFileName(param.getRecordInfo().getFileName()); + cloudRecordItem.setFolder(param.getRecordInfo().getFolder()); + cloudRecordItem.setFileSize(param.getRecordInfo().getFileSize()); + cloudRecordItem.setFilePath(param.getRecordInfo().getFilePath()); + cloudRecordItem.setMediaServerId(param.getMediaServer().getId()); + cloudRecordItem.setTimeLen(param.getRecordInfo().getTimeLen()); + cloudRecordItem.setEndTime((param.getRecordInfo().getStartTime() + (long)param.getRecordInfo().getTimeLen())); + Map paramsMap = MediaServerUtils.urlParamToMap(param.getRecordInfo().getParams()); + if (paramsMap.get("callId") != null) { + cloudRecordItem.setCallId(paramsMap.get("callId")); + } + return cloudRecordItem; + } + + public static CloudRecordItem getInstance(MediaRecordProcessEvent event) { + CloudRecordItem cloudRecordItem = new CloudRecordItem(); + cloudRecordItem.setApp(event.getApp()); + cloudRecordItem.setStream(event.getStream()); + cloudRecordItem.setFileName(event.getFileName()); + cloudRecordItem.setMediaServerId(event.getMediaServer().getId()); + cloudRecordItem.setTimeLen(event.getCurrentFileDuration() * 1000); + return cloudRecordItem; + } + +} diff --git a/src/main/java/com/genersoft/iot/vmp/service/bean/DownloadFileInfo.java b/src/main/java/com/genersoft/iot/vmp/service/bean/DownloadFileInfo.java new file mode 100644 index 0000000..c8e7b15 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/service/bean/DownloadFileInfo.java @@ -0,0 +1,13 @@ +package com.genersoft.iot.vmp.service.bean; + +import lombok.Data; + +@Data +public class DownloadFileInfo { + + private String httpPath; + private String httpsPath; + private String httpDomainPath; + private String httpsDomainPath; + +} diff --git a/src/main/java/com/genersoft/iot/vmp/service/bean/ErrorCallback.java b/src/main/java/com/genersoft/iot/vmp/service/bean/ErrorCallback.java new file mode 100755 index 0000000..6211d00 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/service/bean/ErrorCallback.java @@ -0,0 +1,6 @@ +package com.genersoft.iot.vmp.service.bean; + +public interface ErrorCallback { + + void run(int code, String msg, T data); +} diff --git a/src/main/java/com/genersoft/iot/vmp/service/bean/GPSMsgInfo.java b/src/main/java/com/genersoft/iot/vmp/service/bean/GPSMsgInfo.java new file mode 100755 index 0000000..eca5c5d --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/service/bean/GPSMsgInfo.java @@ -0,0 +1,68 @@ +package com.genersoft.iot.vmp.service.bean; + +import com.genersoft.iot.vmp.gb28181.bean.MobilePosition; +import com.genersoft.iot.vmp.utils.DateUtil; +import lombok.Data; + +@Data +public class GPSMsgInfo { + + /** + * 通道国标ID + */ + private String id; + + /** + * 通道ID + */ + private Integer channelId; + + /** + * + */ + private String app; + + /** + * 经度 (必选) + */ + private double lng; + + /** + * 纬度 (必选) + */ + private double lat; + + /** + * 速度,单位:km/h (可选) + */ + private Double speed; + + /** + * 产生通知时间, 时间格式: 2020-01-14T14:32:12 + */ + private String time; + + /** + * 方向,取值为当前摄像头方向与正北方的顺时针夹角,取值范围0°~360°,单位:(°)(可选) + */ + private Double direction; + + /** + * 海拔高度,单位:m(可选) + */ + private Double altitude; + + private boolean stored; + + public static GPSMsgInfo getInstance(MobilePosition mobilePosition) { + GPSMsgInfo gpsMsgInfo = new GPSMsgInfo(); + gpsMsgInfo.setChannelId(mobilePosition.getChannelId()); + gpsMsgInfo.setAltitude(mobilePosition.getAltitude()); + gpsMsgInfo.setLng(mobilePosition.getLongitude()); + gpsMsgInfo.setLat(mobilePosition.getLatitude()); + gpsMsgInfo.setSpeed(mobilePosition.getSpeed()); + gpsMsgInfo.setDirection(mobilePosition.getDirection()); + gpsMsgInfo.setTime(DateUtil.yyyy_MM_dd_HH_mm_ssToISO8601(mobilePosition.getTime())); + return gpsMsgInfo; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/service/bean/InviteErrorCode.java b/src/main/java/com/genersoft/iot/vmp/service/bean/InviteErrorCode.java new file mode 100755 index 0000000..70a84c0 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/service/bean/InviteErrorCode.java @@ -0,0 +1,40 @@ +package com.genersoft.iot.vmp.service.bean; + +/** + * 全局错误码 + */ +public enum InviteErrorCode { + SUCCESS(0, "成功"), + FAIL(-100, "失败"), + ERROR_FOR_SIGNALLING_TIMEOUT(-1, "信令超时"), + ERROR_FOR_STREAM_TIMEOUT(-2, "收流超时"), + ERROR_FOR_RESOURCE_EXHAUSTION(-3, "资源耗尽"), + ERROR_FOR_CATCH_DATA(-4, "缓存数据异常"), + ERROR_FOR_SIGNALLING_ERROR(-5, "收到信令错误"), + ERROR_FOR_STREAM_PARSING_EXCEPTIONS(-6, "流地址解析错误"), + ERROR_FOR_SDP_PARSING_EXCEPTIONS(-7, "SDP信息解析失败"), + ERROR_FOR_SSRC_UNAVAILABLE(-8, "SSRC不可用"), + ERROR_FOR_RESET_SSRC(-9, "重新设置收流信息失败"), + ERROR_FOR_SIP_SENDING_FAILED(-10, "命令发送失败"), + ERROR_FOR_ASSIST_NOT_READY(-11, "没有可用的assist服务"), + ERROR_FOR_PARAMETER_ERROR(-13, "参数异常"), + ERROR_FOR_TCP_ACTIVE_CONNECTION_REFUSED_ERROR(-14, "TCP主动连接失败"), + ERROR_FOR_FINISH(-20, "已结束"), + ; + + private final int code; + private final String msg; + + InviteErrorCode(int code, String msg) { + this.code = code; + this.msg = msg; + } + + public int getCode() { + return code; + } + + public String getMsg() { + return msg; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/service/bean/InviteTimeOutCallback.java b/src/main/java/com/genersoft/iot/vmp/service/bean/InviteTimeOutCallback.java new file mode 100755 index 0000000..e30db5d --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/service/bean/InviteTimeOutCallback.java @@ -0,0 +1,6 @@ +package com.genersoft.iot.vmp.service.bean; + +public interface InviteTimeOutCallback { + + void run(int code, String msg); // code: 0 sip超时, 1 收流超时 +} diff --git a/src/main/java/com/genersoft/iot/vmp/service/bean/LogFileInfo.java b/src/main/java/com/genersoft/iot/vmp/service/bean/LogFileInfo.java new file mode 100644 index 0000000..c73cff3 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/service/bean/LogFileInfo.java @@ -0,0 +1,13 @@ +package com.genersoft.iot.vmp.service.bean; + +import lombok.Data; + +@Data +public class LogFileInfo { + + private String fileName; + private Long fileSize; + private Long startTime; + private Long endTime; + +} diff --git a/src/main/java/com/genersoft/iot/vmp/service/bean/MediaServerLoad.java b/src/main/java/com/genersoft/iot/vmp/service/bean/MediaServerLoad.java new file mode 100755 index 0000000..cb30f67 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/service/bean/MediaServerLoad.java @@ -0,0 +1,50 @@ +package com.genersoft.iot.vmp.service.bean; + +public class MediaServerLoad { + + private String id; + private int push; + private int proxy; + private int gbReceive; + private int gbSend; + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public int getPush() { + return push; + } + + public void setPush(int push) { + this.push = push; + } + + public int getProxy() { + return proxy; + } + + public void setProxy(int proxy) { + this.proxy = proxy; + } + + public int getGbReceive() { + return gbReceive; + } + + public void setGbReceive(int gbReceive) { + this.gbReceive = gbReceive; + } + + public int getGbSend() { + return gbSend; + } + + public void setGbSend(int gbSend) { + this.gbSend = gbSend; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/service/bean/MessageForPushChannel.java b/src/main/java/com/genersoft/iot/vmp/service/bean/MessageForPushChannel.java new file mode 100755 index 0000000..3c8aba5 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/service/bean/MessageForPushChannel.java @@ -0,0 +1,74 @@ +package com.genersoft.iot.vmp.service.bean; + +import lombok.Data; + +/** + * 当上级平台 + * @author lin + */ + +@Data +public class MessageForPushChannel { + /** + * 消息类型 + * 0 流注销 1 流注册 + */ + private int type; + + /** + * 流应用名 + */ + private String app; + + /** + * 流Id + */ + private String stream; + + /** + * 国标ID + */ + private String gbId; + + /** + * 请求的平台国标编号 + */ + private String platFormId; + + /** + * 请求的平台自增ID + */ + private int platFormIndex; + + /** + * 请求平台名称 + */ + private String platFormName; + + /** + * WVP服务ID + */ + private String serverId; + + /** + * 目标流媒体节点ID + */ + private String mediaServerId; + + + + public static MessageForPushChannel getInstance(int type, String app, String stream, String gbId, + String platFormId, String platFormName, String serverId, + String mediaServerId){ + MessageForPushChannel messageForPushChannel = new MessageForPushChannel(); + messageForPushChannel.setType(type); + messageForPushChannel.setGbId(gbId); + messageForPushChannel.setApp(app); + messageForPushChannel.setStream(stream); + messageForPushChannel.setServerId(serverId); + messageForPushChannel.setMediaServerId(mediaServerId); + messageForPushChannel.setPlatFormId(platFormId); + messageForPushChannel.setPlatFormName(platFormName); + return messageForPushChannel; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/service/bean/MessageForPushChannelResponse.java b/src/main/java/com/genersoft/iot/vmp/service/bean/MessageForPushChannelResponse.java new file mode 100755 index 0000000..10d1b43 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/service/bean/MessageForPushChannelResponse.java @@ -0,0 +1,71 @@ +package com.genersoft.iot.vmp.service.bean; + +/** + * 当redis回复推流结果上级平台 + * @author lin + */ +public class MessageForPushChannelResponse { + /** + * 错误玛 + * 0 成功 1 失败 + */ + private int code; + /** + * 错误内容 + */ + private String msg; + + /** + * 流应用名 + */ + private String app; + + /** + * 流Id + */ + private String stream; + + + + public static MessageForPushChannelResponse getInstance(int code, String msg, String app, String stream){ + MessageForPushChannelResponse messageForPushChannel = new MessageForPushChannelResponse(); + messageForPushChannel.setCode(code); + messageForPushChannel.setMsg(msg); + messageForPushChannel.setApp(app); + messageForPushChannel.setStream(stream); + return messageForPushChannel; + } + + + public int getCode() { + return code; + } + + public void setCode(int code) { + this.code = code; + } + + public String getApp() { + return app; + } + + public void setApp(String app) { + this.app = app; + } + + public String getStream() { + return stream; + } + + public void setStream(String stream) { + this.stream = stream; + } + + public String getMsg() { + return msg; + } + + public void setMsg(String msg) { + this.msg = msg; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/service/bean/PlayBackCallback.java b/src/main/java/com/genersoft/iot/vmp/service/bean/PlayBackCallback.java new file mode 100755 index 0000000..33a09bd --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/service/bean/PlayBackCallback.java @@ -0,0 +1,7 @@ +package com.genersoft.iot.vmp.service.bean; + +public interface PlayBackCallback { + + void call(PlayBackResult msg); + +} diff --git a/src/main/java/com/genersoft/iot/vmp/service/bean/PlayBackResult.java b/src/main/java/com/genersoft/iot/vmp/service/bean/PlayBackResult.java new file mode 100755 index 0000000..d7da931 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/service/bean/PlayBackResult.java @@ -0,0 +1,69 @@ +package com.genersoft.iot.vmp.service.bean; + +import com.alibaba.fastjson2.JSONObject; +import com.genersoft.iot.vmp.gb28181.event.SipSubscribe; +import com.genersoft.iot.vmp.media.bean.MediaServer; + +import java.util.EventObject; + + +/** + * @author lin + */ +public class PlayBackResult { + private int code; + + private String msg; + private T data; + private MediaServer mediaServerItem; + private JSONObject response; + private SipSubscribe.EventResult event; + + public int getCode() { + return code; + } + + public void setCode(int code) { + this.code = code; + } + + public T getData() { + return data; + } + + public void setData(T data) { + this.data = data; + } + + public MediaServer getMediaServerItem() { + return mediaServerItem; + } + + public void setMediaServerItem(MediaServer mediaServerItem) { + this.mediaServerItem = mediaServerItem; + } + + public JSONObject getResponse() { + return response; + } + + public void setResponse(JSONObject response) { + this.response = response; + } + + public SipSubscribe.EventResult getEvent() { + return event; + } + + public void setEvent(SipSubscribe.EventResult event) { + this.event = event; + } + + public String getMsg() { + return msg; + } + + public void setMsg(String msg) { + this.msg = msg; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/service/bean/PushStreamStatusChangeFromRedisDto.java b/src/main/java/com/genersoft/iot/vmp/service/bean/PushStreamStatusChangeFromRedisDto.java new file mode 100755 index 0000000..9e9ce35 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/service/bean/PushStreamStatusChangeFromRedisDto.java @@ -0,0 +1,19 @@ +package com.genersoft.iot.vmp.service.bean; + +import lombok.Data; + +import java.util.List; + +/** + * 收到redis通知修改推流通道状态 + * @author lin + */ +@Data +public class PushStreamStatusChangeFromRedisDto { + + private boolean setAllOffline; + + private List onlineStreams; + + private List offlineStreams; +} diff --git a/src/main/java/com/genersoft/iot/vmp/service/bean/RTPServerParam.java b/src/main/java/com/genersoft/iot/vmp/service/bean/RTPServerParam.java new file mode 100644 index 0000000..2baf335 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/service/bean/RTPServerParam.java @@ -0,0 +1,25 @@ +package com.genersoft.iot.vmp.service.bean; + +import com.genersoft.iot.vmp.media.bean.MediaServer; +import lombok.Data; + +@Data +public class RTPServerParam { + + private MediaServer mediaServerItem; + private String streamId; + private String presetSsrc; + private boolean ssrcCheck; + private boolean playback; + private Integer port; + private boolean onlyAuto; + private boolean disableAudio; + private boolean reUsePort; + + /** + * tcp模式,0时为不启用tcp监听,1时为启用tcp监听,2时为tcp主动连接模式 + */ + private Integer tcpMode; + + +} diff --git a/src/main/java/com/genersoft/iot/vmp/service/bean/RecordPlan.java b/src/main/java/com/genersoft/iot/vmp/service/bean/RecordPlan.java new file mode 100644 index 0000000..5333b2c --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/service/bean/RecordPlan.java @@ -0,0 +1,32 @@ +package com.genersoft.iot.vmp.service.bean; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.util.List; + +@Data +@Schema(description = "录制计划") +public class RecordPlan { + + @Schema(description = "计划数据库ID") + private int id; + + @Schema(description = "计划名称") + private String name; + + @Schema(description = "计划关联通道数量") + private int channelCount; + + @Schema(description = "是否开启定时截图") + private Boolean snap; + + @Schema(description = "创建时间") + private String createTime; + + @Schema(description = "更新时间") + private String updateTime; + + @Schema(description = "计划内容") + private List planItemList; +} diff --git a/src/main/java/com/genersoft/iot/vmp/service/bean/RecordPlanItem.java b/src/main/java/com/genersoft/iot/vmp/service/bean/RecordPlanItem.java new file mode 100644 index 0000000..31fa321 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/service/bean/RecordPlanItem.java @@ -0,0 +1,25 @@ +package com.genersoft.iot.vmp.service.bean; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Data +@Schema(description = "录制计划项") +public class RecordPlanItem { + + @Schema(description = "计划项数据库ID") + private int id; + + @Schema(description = "计划开始时间的序号, 从0点开始,每半个小时增加1") + private Integer start; + + @Schema(description = "计划结束时间的序号, 从0点开始,每半个小时增加1") + private Integer stop; + + @Schema(description = "计划周几执行") + private Integer weekDay; + + @Schema(description = "所属计划ID") + private Integer planId; + +} diff --git a/src/main/java/com/genersoft/iot/vmp/service/bean/RequestPushStreamMsg.java b/src/main/java/com/genersoft/iot/vmp/service/bean/RequestPushStreamMsg.java new file mode 100755 index 0000000..84ee7ba --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/service/bean/RequestPushStreamMsg.java @@ -0,0 +1,188 @@ +package com.genersoft.iot.vmp.service.bean; + +import com.genersoft.iot.vmp.gb28181.bean.SendRtpInfo; + +/** + * redis消息:请求下级推送流信息 + * @author lin + */ +public class RequestPushStreamMsg { + + + /** + * 下级服务ID + */ + private String mediaServerId; + + /** + * 流ID + */ + private String app; + + /** + * 应用名 + */ + private String stream; + + /** + * 目标IP + */ + private String ip; + + /** + * 目标端口 + */ + private int port; + + /** + * ssrc + */ + private String ssrc; + + /** + * 是否使用TCP方式 + */ + private boolean tcp; + + /** + * 本地使用的端口 + */ + private int srcPort; + + /** + * 发送时,rtp的pt(uint8_t),不传时默认为96 + */ + private int pt; + + /** + * 发送时,rtp的负载类型。为true时,负载为ps;为false时,为es; + */ + private boolean ps; + + /** + * 是否只有音频 + */ + private boolean onlyAudio; + + + public static RequestPushStreamMsg getInstance(String mediaServerId, String app, String stream, String ip, int port, String ssrc, + boolean tcp, int srcPort, int pt, boolean ps, boolean onlyAudio) { + RequestPushStreamMsg requestPushStreamMsg = new RequestPushStreamMsg(); + requestPushStreamMsg.setMediaServerId(mediaServerId); + requestPushStreamMsg.setApp(app); + requestPushStreamMsg.setStream(stream); + requestPushStreamMsg.setIp(ip); + requestPushStreamMsg.setPort(port); + requestPushStreamMsg.setSsrc(ssrc); + requestPushStreamMsg.setTcp(tcp); + requestPushStreamMsg.setSrcPort(srcPort); + requestPushStreamMsg.setPt(pt); + requestPushStreamMsg.setPs(ps); + requestPushStreamMsg.setOnlyAudio(onlyAudio); + return requestPushStreamMsg; + } + + public static RequestPushStreamMsg getInstance(SendRtpInfo sendRtpItem) { + RequestPushStreamMsg requestPushStreamMsg = new RequestPushStreamMsg(); + requestPushStreamMsg.setMediaServerId(sendRtpItem.getMediaServerId()); + requestPushStreamMsg.setApp(sendRtpItem.getApp()); + requestPushStreamMsg.setStream(sendRtpItem.getStream()); + requestPushStreamMsg.setIp(sendRtpItem.getIp()); + requestPushStreamMsg.setPort(sendRtpItem.getPort()); + requestPushStreamMsg.setSsrc(sendRtpItem.getSsrc()); + requestPushStreamMsg.setTcp(sendRtpItem.isTcp()); + requestPushStreamMsg.setSrcPort(sendRtpItem.getLocalPort()); + requestPushStreamMsg.setPt(sendRtpItem.getPt()); + requestPushStreamMsg.setPs(sendRtpItem.isUsePs()); + requestPushStreamMsg.setOnlyAudio(sendRtpItem.isOnlyAudio()); + return requestPushStreamMsg; + } + + public String getMediaServerId() { + return mediaServerId; + } + + public void setMediaServerId(String mediaServerId) { + this.mediaServerId = mediaServerId; + } + + public String getApp() { + return app; + } + + public void setApp(String app) { + this.app = app; + } + + public String getStream() { + return stream; + } + + public void setStream(String stream) { + this.stream = stream; + } + + public String getIp() { + return ip; + } + + public void setIp(String ip) { + this.ip = ip; + } + + public int getPort() { + return port; + } + + public void setPort(int port) { + this.port = port; + } + + public String getSsrc() { + return ssrc; + } + + public void setSsrc(String ssrc) { + this.ssrc = ssrc; + } + + public boolean isTcp() { + return tcp; + } + + public void setTcp(boolean tcp) { + this.tcp = tcp; + } + + public int getSrcPort() { + return srcPort; + } + + public void setSrcPort(int srcPort) { + this.srcPort = srcPort; + } + + public int getPt() { + return pt; + } + + public void setPt(int pt) { + this.pt = pt; + } + + public boolean isPs() { + return ps; + } + + public void setPs(boolean ps) { + this.ps = ps; + } + + public boolean isOnlyAudio() { + return onlyAudio; + } + + public void setOnlyAudio(boolean onlyAudio) { + this.onlyAudio = onlyAudio; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/service/bean/RequestSendItemMsg.java b/src/main/java/com/genersoft/iot/vmp/service/bean/RequestSendItemMsg.java new file mode 100755 index 0000000..41c16e2 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/service/bean/RequestSendItemMsg.java @@ -0,0 +1,188 @@ +package com.genersoft.iot.vmp.service.bean; + +/** + * redis消息:请求下级回复推送信息 + * @author lin + */ +public class RequestSendItemMsg { + + /** + * 下级服务ID + */ + private String serverId; + + /** + * 下级服务ID + */ + private String mediaServerId; + + /** + * 流ID + */ + private String app; + + /** + * 应用名 + */ + private String stream; + + /** + * 目标IP + */ + private String ip; + + /** + * 目标端口 + */ + private int port; + + /** + * ssrc + */ + private String ssrc; + + /** + * 平台国标编号 + */ + private String platformId; + + /** + * 平台名称 + */ + private String platformName; + + /** + * 通道ID + */ + private String channelId; + + + /** + * 是否使用TCP + */ + private Boolean isTcp; + + + /** + * 是否使用TCP + */ + private Boolean rtcp; + + + + + public static RequestSendItemMsg getInstance(String serverId, String mediaServerId, String app, String stream, String ip, int port, + String ssrc, String platformId, String channelId, Boolean isTcp, Boolean rtcp, String platformName) { + RequestSendItemMsg requestSendItemMsg = new RequestSendItemMsg(); + requestSendItemMsg.setServerId(serverId); + requestSendItemMsg.setMediaServerId(mediaServerId); + requestSendItemMsg.setApp(app); + requestSendItemMsg.setStream(stream); + requestSendItemMsg.setIp(ip); + requestSendItemMsg.setPort(port); + requestSendItemMsg.setSsrc(ssrc); + requestSendItemMsg.setPlatformId(platformId); + requestSendItemMsg.setPlatformName(platformName); + requestSendItemMsg.setChannelId(channelId); + requestSendItemMsg.setTcp(isTcp); + requestSendItemMsg.setRtcp(rtcp); + + return requestSendItemMsg; + } + + public String getServerId() { + return serverId; + } + + public void setServerId(String serverId) { + this.serverId = serverId; + } + + public String getMediaServerId() { + return mediaServerId; + } + + public void setMediaServerId(String mediaServerId) { + this.mediaServerId = mediaServerId; + } + + public String getApp() { + return app; + } + + public void setApp(String app) { + this.app = app; + } + + public String getStream() { + return stream; + } + + public void setStream(String stream) { + this.stream = stream; + } + + public String getIp() { + return ip; + } + + public void setIp(String ip) { + this.ip = ip; + } + + public int getPort() { + return port; + } + + public void setPort(int port) { + this.port = port; + } + + public String getSsrc() { + return ssrc; + } + + public void setSsrc(String ssrc) { + this.ssrc = ssrc; + } + + public String getPlatformId() { + return platformId; + } + + public void setPlatformId(String platformId) { + this.platformId = platformId; + } + + public String getPlatformName() { + return platformName; + } + + public void setPlatformName(String platformName) { + this.platformName = platformName; + } + + public String getChannelId() { + return channelId; + } + + public void setChannelId(String channelId) { + this.channelId = channelId; + } + + public Boolean getTcp() { + return isTcp; + } + + public void setTcp(Boolean tcp) { + isTcp = tcp; + } + + public Boolean getRtcp() { + return rtcp; + } + + public void setRtcp(Boolean rtcp) { + this.rtcp = rtcp; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/service/bean/RequestStopPushStreamMsg.java b/src/main/java/com/genersoft/iot/vmp/service/bean/RequestStopPushStreamMsg.java new file mode 100755 index 0000000..a63d916 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/service/bean/RequestStopPushStreamMsg.java @@ -0,0 +1,49 @@ +package com.genersoft.iot.vmp.service.bean; + +import com.genersoft.iot.vmp.gb28181.bean.SendRtpInfo; + + +public class RequestStopPushStreamMsg { + + + private SendRtpInfo sendRtpItem; + + + private String platformName; + + + private int platFormIndex; + + public SendRtpInfo getSendRtpItem() { + return sendRtpItem; + } + + public void setSendRtpItem(SendRtpInfo sendRtpItem) { + this.sendRtpItem = sendRtpItem; + } + + public String getPlatformName() { + return platformName; + } + + public void setPlatformName(String platformName) { + this.platformName = platformName; + } + + + public int getPlatFormIndex() { + return platFormIndex; + } + + public void setPlatFormIndex(int platFormIndex) { + this.platFormIndex = platFormIndex; + } + + public static RequestStopPushStreamMsg getInstance(SendRtpInfo sendRtpItem, String platformName, int platFormIndex) { + RequestStopPushStreamMsg streamMsg = new RequestStopPushStreamMsg(); + streamMsg.setSendRtpItem(sendRtpItem); + streamMsg.setPlatformName(platformName); + streamMsg.setPlatFormIndex(platFormIndex); + return streamMsg; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/service/bean/ResponseSendItemMsg.java b/src/main/java/com/genersoft/iot/vmp/service/bean/ResponseSendItemMsg.java new file mode 100755 index 0000000..8680099 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/service/bean/ResponseSendItemMsg.java @@ -0,0 +1,31 @@ +package com.genersoft.iot.vmp.service.bean; + +import com.genersoft.iot.vmp.gb28181.bean.SendRtpInfo; +import com.genersoft.iot.vmp.media.bean.MediaServer; + +/** + * redis消息:下级回复推送信息 + * @author lin + */ +public class ResponseSendItemMsg { + + private SendRtpInfo sendRtpItem; + + private MediaServer mediaServerItem; + + public SendRtpInfo getSendRtpItem() { + return sendRtpItem; + } + + public void setSendRtpItem(SendRtpInfo sendRtpItem) { + this.sendRtpItem = sendRtpItem; + } + + public MediaServer getMediaServerItem() { + return mediaServerItem; + } + + public void setMediaServerItem(MediaServer mediaServerItem) { + this.mediaServerItem = mediaServerItem; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/service/bean/SSRCInfo.java b/src/main/java/com/genersoft/iot/vmp/service/bean/SSRCInfo.java new file mode 100755 index 0000000..c35ceb5 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/service/bean/SSRCInfo.java @@ -0,0 +1,21 @@ +package com.genersoft.iot.vmp.service.bean; + +import lombok.Data; + +@Data +public class SSRCInfo { + + private int port; + private String ssrc; + private String app; + private String Stream; + private String timeOutTaskKey; + + public SSRCInfo(int port, String ssrc, String app, String stream, String timeOutTaskKey) { + this.port = port; + this.ssrc = ssrc; + this.app = app; + this.Stream = stream; + this.timeOutTaskKey = timeOutTaskKey; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/service/bean/StreamPushItemFromRedis.java b/src/main/java/com/genersoft/iot/vmp/service/bean/StreamPushItemFromRedis.java new file mode 100755 index 0000000..ff32d79 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/service/bean/StreamPushItemFromRedis.java @@ -0,0 +1,34 @@ +package com.genersoft.iot.vmp.service.bean; + + +public class StreamPushItemFromRedis { + private String app; + private String stream; + private long timeStamp; + + public String getApp() { + return app; + } + + public void setApp(String app) { + this.app = app; + } + + public String getStream() { + return stream; + } + + public void setStream(String stream) { + this.stream = stream; + } + + public long getTimeStamp() { + return timeStamp; + } + + public void setTimeStamp(long timeStamp) { + this.timeStamp = timeStamp; + } +} + + diff --git a/src/main/java/com/genersoft/iot/vmp/service/bean/ThirdPartyGB.java b/src/main/java/com/genersoft/iot/vmp/service/bean/ThirdPartyGB.java new file mode 100755 index 0000000..9d6b06a --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/service/bean/ThirdPartyGB.java @@ -0,0 +1,23 @@ +package com.genersoft.iot.vmp.service.bean; + +public class ThirdPartyGB { + + private String name; + private String nationalStandardNo; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getNationalStandardNo() { + return nationalStandardNo; + } + + public void setNationalStandardNo(String nationalStandardNo) { + this.nationalStandardNo = nationalStandardNo; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/service/bean/WvpRedisMsg.java b/src/main/java/com/genersoft/iot/vmp/service/bean/WvpRedisMsg.java new file mode 100755 index 0000000..74890be --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/service/bean/WvpRedisMsg.java @@ -0,0 +1,116 @@ +package com.genersoft.iot.vmp.service.bean; + +/** + * @author lin + */ +public class WvpRedisMsg { + + public static WvpRedisMsg getInstance(String fromId, String toId, String type, String cmd, String serial, String content){ + WvpRedisMsg wvpRedisMsg = new WvpRedisMsg(); + wvpRedisMsg.setFromId(fromId); + wvpRedisMsg.setToId(toId); + wvpRedisMsg.setType(type); + wvpRedisMsg.setCmd(cmd); + wvpRedisMsg.setSerial(serial); + wvpRedisMsg.setContent(content); + return wvpRedisMsg; + } + + private String fromId; + + private String toId; + /** + * req 请求, res 回复 + */ + private String type; + private String cmd; + + /** + * 消息的ID + */ + private String serial; + private String content; + + private final static String requestTag = "req"; + private final static String responseTag = "res"; + + public static WvpRedisMsg getRequestInstance(String fromId, String toId, String cmd, String serial, String content) { + WvpRedisMsg wvpRedisMsg = new WvpRedisMsg(); + wvpRedisMsg.setType(requestTag); + wvpRedisMsg.setFromId(fromId); + wvpRedisMsg.setToId(toId); + wvpRedisMsg.setCmd(cmd); + wvpRedisMsg.setSerial(serial); + wvpRedisMsg.setContent(content); + return wvpRedisMsg; + } + + public static WvpRedisMsg getResponseInstance() { + WvpRedisMsg wvpRedisMsg = new WvpRedisMsg(); + wvpRedisMsg.setType(responseTag); + return wvpRedisMsg; + } + + public static WvpRedisMsg getResponseInstance(String fromId, String toId, String cmd, String serial, String content) { + WvpRedisMsg wvpRedisMsg = new WvpRedisMsg(); + wvpRedisMsg.setType(responseTag); + wvpRedisMsg.setFromId(fromId); + wvpRedisMsg.setToId(toId); + wvpRedisMsg.setCmd(cmd); + wvpRedisMsg.setSerial(serial); + wvpRedisMsg.setContent(content); + return wvpRedisMsg; + } + + public static boolean isRequest(WvpRedisMsg wvpRedisMsg) { + return requestTag.equals(wvpRedisMsg.getType()); + } + + public String getSerial() { + return serial; + } + + public void setSerial(String serial) { + this.serial = serial; + } + + public String getFromId() { + return fromId; + } + + public void setFromId(String fromId) { + this.fromId = fromId; + } + + public String getToId() { + return toId; + } + + public void setToId(String toId) { + this.toId = toId; + } + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + public String getCmd() { + return cmd; + } + + public void setCmd(String cmd) { + this.cmd = cmd; + } + + public String getContent() { + return content; + } + + public void setContent(String content) { + this.content = content; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/service/bean/WvpRedisMsgCmd.java b/src/main/java/com/genersoft/iot/vmp/service/bean/WvpRedisMsgCmd.java new file mode 100755 index 0000000..e9ee4cb --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/service/bean/WvpRedisMsgCmd.java @@ -0,0 +1,22 @@ +package com.genersoft.iot.vmp.service.bean; + +/** + * @author lin + */ + +public class WvpRedisMsgCmd { + + /** + * 请求获取推流信息 + */ + public static final String GET_SEND_ITEM = "GetSendItem"; + /** + * 请求推流的请求 + */ + public static final String REQUEST_PUSH_STREAM = "RequestPushStream"; + /** + * 停止推流的请求 + */ + public static final String REQUEST_STOP_PUSH_STREAM = "RequestStopPushStream"; + +} diff --git a/src/main/java/com/genersoft/iot/vmp/service/impl/CloudRecordServiceImpl.java b/src/main/java/com/genersoft/iot/vmp/service/impl/CloudRecordServiceImpl.java new file mode 100644 index 0000000..4dc5df8 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/service/impl/CloudRecordServiceImpl.java @@ -0,0 +1,431 @@ +package com.genersoft.iot.vmp.service.impl; + +import com.alibaba.fastjson2.JSONArray; +import com.alibaba.fastjson2.JSONObject; +import com.genersoft.iot.vmp.common.StreamInfo; +import com.genersoft.iot.vmp.conf.UserSetting; +import com.genersoft.iot.vmp.conf.exception.ControllerException; +import com.genersoft.iot.vmp.media.bean.MediaServer; +import com.genersoft.iot.vmp.media.bean.RecordInfo; +import com.genersoft.iot.vmp.media.event.media.MediaRecordMp4Event; +import com.genersoft.iot.vmp.media.service.IMediaServerService; +import com.genersoft.iot.vmp.media.zlm.AssistRESTfulUtils; +import com.genersoft.iot.vmp.media.zlm.dto.StreamAuthorityInfo; +import com.genersoft.iot.vmp.service.ICloudRecordService; +import com.genersoft.iot.vmp.service.bean.CloudRecordItem; +import com.genersoft.iot.vmp.service.bean.DownloadFileInfo; +import com.genersoft.iot.vmp.service.bean.ErrorCallback; +import com.genersoft.iot.vmp.service.redisMsg.IRedisRpcPlayService; +import com.genersoft.iot.vmp.storager.IRedisCatchStorage; +import com.genersoft.iot.vmp.storager.dao.CloudRecordServiceMapper; +import com.genersoft.iot.vmp.utils.DateUtil; +import com.genersoft.iot.vmp.vmanager.bean.ErrorCode; +import com.genersoft.iot.vmp.vmanager.cloudRecord.bean.CloudRecordUrl; +import com.github.pagehelper.PageHelper; +import com.github.pagehelper.PageInfo; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.ObjectUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.event.EventListener; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Service; +import org.springframework.util.Assert; + +import java.io.File; +import java.time.LocalDate; +import java.time.ZoneOffset; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +@Slf4j +@Service +public class CloudRecordServiceImpl implements ICloudRecordService { + + @Autowired + private CloudRecordServiceMapper cloudRecordServiceMapper; + + @Autowired + private IMediaServerService mediaServerService; + + @Autowired + private IRedisCatchStorage redisCatchStorage; + + @Autowired + private AssistRESTfulUtils assistRESTfulUtils; + + @Autowired + private UserSetting userSetting; + + @Autowired + private IRedisRpcPlayService redisRpcPlayService; + + @Override + public PageInfo getList(int page, int count, String query, String app, String stream, String startTime, + String endTime, List mediaServerItems, String callId, Boolean ascOrder) { + // 开始时间和结束时间在数据库中都是以秒为单位的 + Long startTimeStamp = null; + Long endTimeStamp = null; + if (startTime != null ) { + if (!DateUtil.verification(startTime, DateUtil.formatter)) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "开始时间格式错误,正确格式为: " + DateUtil.formatter); + } + startTimeStamp = DateUtil.yyyy_MM_dd_HH_mm_ssToTimestampMs(startTime); + + } + if (endTime != null ) { + if (!DateUtil.verification(endTime, DateUtil.formatter)) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "结束时间格式错误,正确格式为: " + DateUtil.formatter); + } + endTimeStamp = DateUtil.yyyy_MM_dd_HH_mm_ssToTimestampMs(endTime); + + } + PageHelper.startPage(page, count); + if (query != null) { + query = query.replaceAll("/", "//") + .replaceAll("%", "/%") + .replaceAll("_", "/_"); + } + List all = cloudRecordServiceMapper.getList(query, app, stream, startTimeStamp, endTimeStamp, + callId, mediaServerItems, null, ascOrder); + return new PageInfo<>(all); + } + + @Override + public List getDateList(String app, String stream, int year, int month, List mediaServerItems) { + LocalDate startDate = LocalDate.of(year, month, 1); + LocalDate endDate; + if (month == 12) { + endDate = LocalDate.of(year + 1, 1, 1); + }else { + endDate = LocalDate.of(year, month + 1, 1); + } + long startTimeStamp = startDate.atStartOfDay().toInstant(ZoneOffset.ofHours(8)).toEpochMilli(); + long endTimeStamp = endDate.atStartOfDay().toInstant(ZoneOffset.ofHours(8)).toEpochMilli(); + List cloudRecordItemList = cloudRecordServiceMapper.getList(null, app, stream, startTimeStamp, + endTimeStamp, null, mediaServerItems, null, null); + if (cloudRecordItemList.isEmpty()) { + return new ArrayList<>(); + } + Set resultSet = new HashSet<>(); + cloudRecordItemList.stream().forEach(cloudRecordItem -> { + String date = DateUtil.timestampTo_yyyy_MM_dd(cloudRecordItem.getStartTime()); + resultSet.add(date); + }); + return new ArrayList<>(resultSet); + } + + @Async("taskExecutor") + @EventListener + public void onApplicationEvent(MediaRecordMp4Event event) { + CloudRecordItem cloudRecordItem = CloudRecordItem.getInstance(event); + cloudRecordItem.setServerId(userSetting.getServerId()); + if (ObjectUtils.isEmpty(cloudRecordItem.getCallId())) { + StreamAuthorityInfo streamAuthorityInfo = redisCatchStorage.getStreamAuthorityInfo(event.getApp(), event.getStream()); + if (streamAuthorityInfo != null) { + cloudRecordItem.setCallId(streamAuthorityInfo.getCallId()); + } + } + log.info("[添加录像记录] {}/{}, callId: {}, 内容:{}", event.getApp(), event.getStream(), cloudRecordItem.getCallId(), event.getRecordInfo()); + cloudRecordServiceMapper.add(cloudRecordItem); + } + + @Override + public String addTask(String app, String stream, MediaServer mediaServerItem, String startTime, String endTime, + String callId, String remoteHost, boolean filterMediaServer) { + // 参数校验 + Assert.notNull(app,"应用名为NULL"); + Assert.notNull(stream,"流ID为NULL"); + if (mediaServerItem.getRecordAssistPort() == 0) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "为配置Assist服务"); + } + Long startTimeStamp = null; + Long endTimeStamp = null; + if (startTime != null) { + startTimeStamp = DateUtil.yyyy_MM_dd_HH_mm_ssToTimestamp(startTime); + } + if (endTime != null) { + endTimeStamp = DateUtil.yyyy_MM_dd_HH_mm_ssToTimestamp(endTime); + } + + List mediaServers = new ArrayList<>(); + mediaServers.add(mediaServerItem); + // 检索相关的录像文件 + List filePathList = cloudRecordServiceMapper.queryRecordFilePathList(app, stream, startTimeStamp, + endTimeStamp, callId, filterMediaServer ? mediaServers : null); + if (filePathList == null || filePathList.isEmpty()) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "未检索到视频文件"); + } + JSONObject result = assistRESTfulUtils.addTask(mediaServerItem, app, stream, startTime, endTime, callId, filePathList, remoteHost); + if (result.getInteger("code") != 0) { + throw new ControllerException(result.getInteger("code"), result.getString("msg")); + } + return result.getString("data"); + } + + @Override + public JSONArray queryTask(String app, String stream, String callId, String taskId, String mediaServerId, + Boolean isEnd, String scheme) { + MediaServer mediaServerItem = null; + if (mediaServerId == null) { + mediaServerItem = mediaServerService.getDefaultMediaServer(); + }else { + mediaServerItem = mediaServerService.getOne(mediaServerId); + } + if (mediaServerItem == null) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "未找到可用的流媒体"); + } + + JSONObject result = assistRESTfulUtils.queryTaskList(mediaServerItem, app, stream, callId, taskId, isEnd, scheme); + if (result == null || result.getInteger("code") != 0) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), result == null ? "查询任务列表失败" : result.getString("msg")); + } + return result.getJSONArray("data"); + } + + @Override + public int changeCollect(boolean result, String app, String stream, String mediaServerId, String startTime, String endTime, String callId) { + // 开始时间和结束时间在数据库中都是以秒为单位的 + Long startTimeStamp = null; + Long endTimeStamp = null; + if (startTime != null ) { + if (!DateUtil.verification(startTime, DateUtil.formatter)) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "开始时间格式错误,正确格式为: " + DateUtil.formatter); + } + startTimeStamp = DateUtil.yyyy_MM_dd_HH_mm_ssToTimestamp(startTime); + + } + if (endTime != null ) { + if (!DateUtil.verification(endTime, DateUtil.formatter)) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "结束时间格式错误,正确格式为: " + DateUtil.formatter); + } + endTimeStamp = DateUtil.yyyy_MM_dd_HH_mm_ssToTimestamp(endTime); + + } + + List mediaServerItems; + if (!ObjectUtils.isEmpty(mediaServerId)) { + mediaServerItems = new ArrayList<>(); + MediaServer mediaServerItem = mediaServerService.getOne(mediaServerId); + if (mediaServerItem == null) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "未找到流媒体: " + mediaServerId); + } + mediaServerItems.add(mediaServerItem); + } else { + mediaServerItems = null; + } + + List all = cloudRecordServiceMapper.getList(null, app, stream, startTimeStamp, endTimeStamp, + callId, mediaServerItems, null, null); + if (all.isEmpty()) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "未找到待收藏的视频"); + } + int limitCount = 50; + int resultCount = 0; + if (all.size() > limitCount) { + for (int i = 0; i < all.size(); i += limitCount) { + int toIndex = i + limitCount; + if (i + limitCount > all.size()) { + toIndex = all.size(); + } + resultCount += cloudRecordServiceMapper.updateCollectList(result, all.subList(i, toIndex)); + + } + }else { + resultCount = cloudRecordServiceMapper.updateCollectList(result, all); + } + return resultCount; + } + + @Override + public int changeCollectById(Integer recordId, boolean result) { + return cloudRecordServiceMapper.changeCollectById(result, recordId); + } + + @Override + public DownloadFileInfo getPlayUrlPath(Integer recordId) { + CloudRecordItem recordItem = cloudRecordServiceMapper.queryOne(recordId); + if (recordItem == null) { + throw new ControllerException(ErrorCode.ERROR400.getCode(), "资源不存在"); + } + if (!userSetting.getServerId().equals(recordItem.getServerId())) { + return redisRpcPlayService.getRecordPlayUrl(recordItem.getServerId(), recordId); + } + + MediaServer mediaServer = mediaServerService.getOne(recordItem.getMediaServerId()); + + return mediaServerService.getDownloadFilePath(mediaServer, RecordInfo.getInstance(recordItem)); + } + + @Override + public List getAllList(String query, String app, String stream, String startTime, String endTime, List mediaServerItems, String callId, List ids) { + // 开始时间和结束时间在数据库中都是以秒为单位的 + Long startTimeStamp = null; + Long endTimeStamp = null; + if (startTime != null ) { + if (!DateUtil.verification(startTime, DateUtil.formatter)) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "开始时间格式错误,正确格式为: " + DateUtil.formatter); + } + startTimeStamp = DateUtil.yyyy_MM_dd_HH_mm_ssToTimestampMs(startTime); + + } + if (endTime != null ) { + if (!DateUtil.verification(endTime, DateUtil.formatter)) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "结束时间格式错误,正确格式为: " + DateUtil.formatter); + } + endTimeStamp = DateUtil.yyyy_MM_dd_HH_mm_ssToTimestampMs(endTime); + + } + return cloudRecordServiceMapper.getList(query, app, stream, startTimeStamp, endTimeStamp, + callId, mediaServerItems, ids, null); + } + + @Override + public void loadMP4File(String app, String stream, int cloudRecordId, ErrorCallback callback) { + + CloudRecordItem recordItem = cloudRecordServiceMapper.queryOne(cloudRecordId); + if (recordItem == null) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "无录像"); + } + String mediaServerId = recordItem.getMediaServerId(); + MediaServer mediaServer = mediaServerService.getOne(mediaServerId); + if (mediaServer == null) { + log.warn("[云端录像] 播放 未找到录制的流媒体,将自动选择低负载流媒体使用"); + mediaServer = mediaServerService.getMediaServerForMinimumLoad(null); + } + if (mediaServer == null) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "无可用流媒体"); + } + String fileName = recordItem.getFileName().substring(0 , recordItem.getFileName().indexOf(".")); + String filePath = recordItem.getFilePath(); +// if (filePath != null) { +// fileName = filePath.substring(0, filePath.lastIndexOf("/")); +// } + mediaServerService.loadMP4File(mediaServer, app, stream, filePath, fileName, ((code, msg, streamInfo) -> { + if (code == ErrorCode.SUCCESS.getCode()) { + streamInfo.setDuration(recordItem.getTimeLen()); + } + callback.run(code, msg, streamInfo); + })); + } + + @Override + public void loadMP4FileForDate(String app, String stream, String date, ErrorCallback callback) { + long startTimestamp = DateUtil.yyyy_MM_dd_HH_mm_ssToTimestampMs(date + " 00:00:00"); + long endTimestamp = startTimestamp + 24 * 60 * 60 * 1000; + + List recordItemList = cloudRecordServiceMapper.getList(null, app, stream, startTimestamp, endTimestamp, null, null, null, false); + if (recordItemList.isEmpty()) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "此时间无录像"); + } + String mediaServerId = recordItemList.get(0).getMediaServerId(); + MediaServer mediaServer = mediaServerService.getOne(mediaServerId); + if (mediaServer == null) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "媒体节点不存在: " + mediaServerId); + } + String dateDir = null; + String filePath = recordItemList.get(0).getFilePath(); + if (filePath != null) { + dateDir = filePath.substring(0, filePath.lastIndexOf("/")); + } + mediaServerService.loadMP4FileForDate(mediaServer, app, stream, date, dateDir, callback); + + } + + @Override + public void seekRecord(String mediaServerId,String app, String stream, Double seek, String schema) { + MediaServer mediaServer = mediaServerService.getOne(mediaServerId); + if (mediaServer == null) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "媒体节点不存在: " + mediaServerId); + } + mediaServerService.seekRecordStamp(mediaServer, app, stream, seek, schema); + } + + @Override + public void setRecordSpeed(String mediaServerId, String app, String stream, Integer speed, String schema) { + MediaServer mediaServer = mediaServerService.getOne(mediaServerId); + if (mediaServer == null) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "媒体节点不存在: " + mediaServerId); + } + mediaServerService.setRecordSpeed(mediaServer, app, stream, speed, schema); + } + + @Override + public void deleteFileByIds(Set ids) { + log.info("[删除录像文件] ids: {}", ids.toArray()); + List cloudRecordItemList = cloudRecordServiceMapper.queryRecordByIds(ids); + if (cloudRecordItemList.isEmpty()) { + return; + } + List cloudRecordItemIdListForDelete = new ArrayList<>(); + StringBuilder stringBuilder = new StringBuilder(); + for (CloudRecordItem cloudRecordItem : cloudRecordItemList) { + String date = new File(cloudRecordItem.getFilePath()).getParentFile().getName(); + MediaServer mediaServer = mediaServerService.getOne(cloudRecordItem.getMediaServerId()); + try { + boolean deleteResult = mediaServerService.deleteRecordDirectory(mediaServer, cloudRecordItem.getApp(), + cloudRecordItem.getStream(), date, cloudRecordItem.getFileName()); + if (deleteResult) { + log.warn("[录像文件] 删除磁盘文件成功: {}", cloudRecordItem.getFilePath()); + cloudRecordItemIdListForDelete.add(cloudRecordItem); + } + }catch (ControllerException e) { + if (stringBuilder.length() > 0) { + stringBuilder.append(", "); + } + stringBuilder.append(cloudRecordItem.getFileName()); + } + + } + if (!cloudRecordItemIdListForDelete.isEmpty()) { + cloudRecordServiceMapper.deleteList(cloudRecordItemIdListForDelete); + } + if (stringBuilder.length() > 0) { + stringBuilder.append(" 删除失败"); + throw new ControllerException(ErrorCode.ERROR100.getCode(), stringBuilder.toString()); + } + } + + @Override + public List getUrlListByIds(List ids) { + List cloudRecordItems = cloudRecordServiceMapper.queryRecordByIds(ids); + if (cloudRecordItems.isEmpty()) { + return List.of(); + } + return getCloudRecordUrl(cloudRecordItems); + } + + @Override + public List getUrlList(String app, String stream, String callId) { + List cloudRecordItems = cloudRecordServiceMapper.queryRecordByAppStreamAndCallId(app, stream, callId); + if (cloudRecordItems.isEmpty()) { + return List.of(); + } + return getCloudRecordUrl(cloudRecordItems); + } + + private List getCloudRecordUrl(List cloudRecordItems) { + if (cloudRecordItems.isEmpty()) { + return List.of(); + } + List resultList = new ArrayList<>(); + for (CloudRecordItem cloudRecordItem : cloudRecordItems) { + CloudRecordUrl cloudRecordUrl = new CloudRecordUrl(); + cloudRecordUrl.setId(cloudRecordItem.getId()); + cloudRecordUrl.setFileName(cloudRecordItem.getStartTime() + ".mp4"); + cloudRecordUrl.setFilePath(cloudRecordItem.getFilePath()); + if (!userSetting.getServerId().equals(cloudRecordItem.getServerId())) { + cloudRecordUrl.setDownloadUrl(redisRpcPlayService.getRecordPlayUrl(cloudRecordItem.getServerId(), cloudRecordItem.getId()).getHttpPath()); + }else { + MediaServer mediaServer = mediaServerService.getOne(cloudRecordItem.getMediaServerId()); + mediaServer.setStreamIp(mediaServer.getIp()); + DownloadFileInfo downloadFilePath = mediaServerService.getDownloadFilePath(mediaServer, RecordInfo.getInstance(cloudRecordItem)); + cloudRecordUrl.setDownloadUrl(downloadFilePath.getHttpPath()); + } + resultList.add(cloudRecordUrl); + } + + return resultList; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/service/impl/LogServiceImpl.java b/src/main/java/com/genersoft/iot/vmp/service/impl/LogServiceImpl.java new file mode 100644 index 0000000..47ca3ac --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/service/impl/LogServiceImpl.java @@ -0,0 +1,110 @@ +package com.genersoft.iot.vmp.service.impl; + +import ch.qos.logback.classic.Logger; +import ch.qos.logback.core.rolling.RollingFileAppender; +import com.genersoft.iot.vmp.conf.exception.ControllerException; +import com.genersoft.iot.vmp.service.ILogService; +import com.genersoft.iot.vmp.service.bean.LogFileInfo; +import com.genersoft.iot.vmp.utils.DateUtil; +import com.genersoft.iot.vmp.vmanager.bean.ErrorCode; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.io.input.ReversedLinesFileReader; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Service; + +import java.io.*; +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.List; + +@Service +@Slf4j +public class LogServiceImpl implements ILogService { + + @Override + public List queryList(String query, String startTime, String endTime) { + File logFile = getLogDir(); + if (logFile == null || !logFile.exists()) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "获取日志文件目录失败"); + } + File[] files = logFile.listFiles(); + List result = new ArrayList<>(); + if (files == null || files.length == 0) { + return result; + } + + // 读取文件创建时间作为开始时间,修改时间为结束时间 + Long startTimestamp = null; + if (startTime != null) { + startTimestamp = DateUtil.yyyy_MM_dd_HH_mm_ssToTimestampMs(startTime); + } + Long endTimestamp = null; + if (endTime != null) { + endTimestamp = DateUtil.yyyy_MM_dd_HH_mm_ssToTimestampMs(endTime); + } + for (File file : files) { + LogFileInfo logFileInfo = new LogFileInfo(); + logFileInfo.setFileName(file.getName()); + logFileInfo.setFileSize(file.length()); + if (query != null && !file.getName().contains(query)) { + continue; + } + try { + Long[] fileAttributes = getFileAttributes(file); + if (fileAttributes == null) { + continue; + } + long startTimestampForFile = fileAttributes[0]; + long endTimestampForFile = fileAttributes[1]; + logFileInfo.setStartTime(startTimestampForFile); + logFileInfo.setEndTime(endTimestampForFile); + if (startTimestamp != null && startTimestamp > startTimestampForFile) { + continue; + } + if (endTimestamp != null && endTimestamp < endTimestampForFile) { + continue; + } + } catch (IOException e) { + log.error("[读取日志文件列表] 获取创建时间和修改时间失败", e); + continue; + } + result.add(logFileInfo); + + } + result.sort((o1, o2) -> o2.getStartTime().compareTo(o1.getStartTime())); + return result; + } + + private File getLogDir() { + Logger logger = (Logger) LoggerFactory.getLogger("root"); + RollingFileAppender rollingFileAppender = (RollingFileAppender) logger.getAppender("RollingFile"); + File rollingFile = new File(rollingFileAppender.getFile()); + return rollingFile.getParentFile(); + } + + Long[] getFileAttributes(File file) throws IOException { + BufferedReader bufferedReader = new BufferedReader(new FileReader(file)); + String startLine = bufferedReader.readLine(); + if (startLine== null) { + return null; + } + String startTime = startLine.substring(0, 19); + + // 最后一行的开头不一定是时间 +// String lastLine = ""; +// try (ReversedLinesFileReader reversedLinesReader = new ReversedLinesFileReader(file, Charset.defaultCharset())) { +// lastLine = reversedLinesReader.readLine(); +// } catch (Exception e) { +// log.error("file read error, msg:{}", e.getMessage(), e); +// } +// String endTime = lastLine.substring(0, 19); + return new Long[]{DateUtil.yyyy_MM_dd_HH_mm_ssToTimestampMs(startTime), file.lastModified()}; + } + + @Override + public File getFileByName(String fileName) { + File logDir = getLogDir(); + + return new File(logDir, fileName); + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/service/impl/MediaServiceImpl.java b/src/main/java/com/genersoft/iot/vmp/service/impl/MediaServiceImpl.java new file mode 100755 index 0000000..8337425 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/service/impl/MediaServiceImpl.java @@ -0,0 +1,300 @@ +package com.genersoft.iot.vmp.service.impl; + +import com.genersoft.iot.vmp.common.InviteInfo; +import com.genersoft.iot.vmp.common.InviteSessionStatus; +import com.genersoft.iot.vmp.common.InviteSessionType; +import com.genersoft.iot.vmp.common.VideoManagerConstants; +import com.genersoft.iot.vmp.conf.UserSetting; +import com.genersoft.iot.vmp.conf.exception.ControllerException; +import com.genersoft.iot.vmp.gb28181.bean.DeviceChannel; +import com.genersoft.iot.vmp.gb28181.bean.SsrcTransaction; +import com.genersoft.iot.vmp.gb28181.service.IDeviceChannelService; +import com.genersoft.iot.vmp.gb28181.service.IInviteStreamService; +import com.genersoft.iot.vmp.gb28181.session.SipInviteSessionManager; +import com.genersoft.iot.vmp.jt1078.bean.JTMediaStreamType; +import com.genersoft.iot.vmp.jt1078.service.Ijt1078PlayService; +import com.genersoft.iot.vmp.jt1078.service.Ijt1078Service; +import com.genersoft.iot.vmp.media.bean.MediaServer; +import com.genersoft.iot.vmp.media.bean.ResultForOnPublish; +import com.genersoft.iot.vmp.media.zlm.dto.StreamAuthorityInfo; +import com.genersoft.iot.vmp.service.IMediaService; +import com.genersoft.iot.vmp.service.IRecordPlanService; +import com.genersoft.iot.vmp.service.ISendRtpServerService; +import com.genersoft.iot.vmp.service.IUserService; +import com.genersoft.iot.vmp.storager.IRedisCatchStorage; +import com.genersoft.iot.vmp.streamProxy.bean.StreamProxy; +import com.genersoft.iot.vmp.streamProxy.service.IStreamProxyService; +import com.genersoft.iot.vmp.utils.DateUtil; +import com.genersoft.iot.vmp.utils.MediaServerUtils; +import com.genersoft.iot.vmp.vmanager.bean.ErrorCode; +import com.genersoft.iot.vmp.vmanager.bean.OtherPsSendInfo; +import com.genersoft.iot.vmp.vmanager.bean.OtherRtpSendInfo; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Service; + +import java.util.Map; + +@Slf4j +@Service +public class MediaServiceImpl implements IMediaService { + + @Autowired + private IRedisCatchStorage redisCatchStorage; + + @Autowired + private IStreamProxyService streamProxyService; + + @Autowired + private UserSetting userSetting; + + @Autowired + private RedisTemplate redisTemplate; + + @Autowired + private IUserService userService; + + @Autowired + private IInviteStreamService inviteStreamService; + + @Autowired + private IDeviceChannelService deviceChannelService; + + @Autowired + private SipInviteSessionManager sessionManager; + + @Autowired + private Ijt1078Service ijt1078Service; + + @Autowired + private Ijt1078PlayService jt1078PlayService; + + @Autowired + private ISendRtpServerService sendRtpServerService; + + + @Autowired + private IRecordPlanService recordPlanService; + + @Override + public boolean authenticatePlay(String app, String stream, String callId) { + if (app == null || stream == null) { + return false; + } + if ("rtp".equals(app)) { + return true; + } + StreamAuthorityInfo streamAuthorityInfo = redisCatchStorage.getStreamAuthorityInfo(app, stream); + if (streamAuthorityInfo == null || streamAuthorityInfo.getCallId() == null) { + return true; + } + return streamAuthorityInfo.getCallId().equals(callId); + } + + @Override + public ResultForOnPublish authenticatePublish(MediaServer mediaServer, String app, String stream, String params) { + // 推流鉴权的处理 + if (!"rtp".equals(app) && !"1078".equals(app) ) { + if ("talk".equals(app) && stream.endsWith("_talk")) { + ResultForOnPublish result = new ResultForOnPublish(); + result.setEnable_mp4(false); + result.setEnable_audio(true); + return result; + } + if ("jt_talk".equals(app) && stream.endsWith("_talk")) { + ResultForOnPublish result = new ResultForOnPublish(); + result.setEnable_mp4(false); + result.setEnable_audio(true); + return result; + } + if ("mp4_record".equals(app) ) { + ResultForOnPublish result = new ResultForOnPublish(); + result.setEnable_mp4(false); + result.setEnable_audio(true); + return result; + } + StreamProxy streamProxyItem = streamProxyService.getStreamProxyByAppAndStream(app, stream); + if (streamProxyItem != null) { + ResultForOnPublish result = new ResultForOnPublish(); + result.setEnable_audio(streamProxyItem.isEnableAudio()); + result.setEnable_mp4(streamProxyItem.isEnableMp4()); + return result; + } + if (userSetting.getPushAuthority()) { + // 对于推流进行鉴权 + Map paramMap = MediaServerUtils.urlParamToMap(params); + // 推流鉴权 + if (params == null) { + log.info("推流鉴权失败: 缺少必要参数:sign=md5(user表的pushKey)"); + throw new ControllerException(ErrorCode.ERROR401.getCode(), "Unauthorized"); + } + + String sign = paramMap.get("sign"); + if (sign == null) { + log.info("推流鉴权失败: 缺少必要参数:sign=md5(user表的pushKey)"); + throw new ControllerException(ErrorCode.ERROR401.getCode(), "Unauthorized"); + } + // 推流自定义播放鉴权码 + String callId = paramMap.get("callId"); + // 鉴权配置 + boolean hasAuthority = userService.checkPushAuthority(callId, sign); + if (!hasAuthority) { + log.info("推流鉴权失败: sign 无权限: callId={}. sign={}", callId, sign); + throw new ControllerException(ErrorCode.ERROR401.getCode(), "Unauthorized"); + } + StreamAuthorityInfo streamAuthorityInfo = StreamAuthorityInfo.getInstanceByHook(app, stream, mediaServer.getId()); + streamAuthorityInfo.setCallId(callId); + streamAuthorityInfo.setSign(sign); + // 鉴权通过 + redisCatchStorage.updateStreamAuthorityInfo(app, stream, streamAuthorityInfo); + } + } + + + ResultForOnPublish result = new ResultForOnPublish(); + result.setEnable_audio(true); + + // 国标流 + if ("rtp".equals(app)) { + + InviteInfo inviteInfo = inviteStreamService.getInviteInfoByStream(null, stream); + + if (inviteInfo != null) { + result.setEnable_mp4(inviteInfo.getRecord()); + }else { + result.setEnable_mp4(userSetting.getRecordSip()); + } + + // 单端口模式下修改流 ID + if (!mediaServer.isRtpEnable() && inviteInfo == null) { + String ssrc = String.format("%010d", Long.parseLong(stream, 16)); + inviteInfo = inviteStreamService.getInviteInfoBySSRC(ssrc); + if (inviteInfo != null) { + result.setStream_replace(inviteInfo.getStream()); + log.info("[HOOK]推流鉴权 stream: {} 替换为 {}", stream, inviteInfo.getStream()); + stream = inviteInfo.getStream(); + } + } + + // 设置音频信息及录制信息 + SsrcTransaction ssrcTransaction = sessionManager.getSsrcTransactionByStream(app, stream); + if (ssrcTransaction != null ) { + + // 为录制国标模拟一个鉴权信息, 方便后续写入录像文件时使用 + StreamAuthorityInfo streamAuthorityInfo = StreamAuthorityInfo.getInstanceByHook(app, stream, mediaServer.getId()); + streamAuthorityInfo.setApp(app); + streamAuthorityInfo.setStream(ssrcTransaction.getStream()); + streamAuthorityInfo.setCallId(ssrcTransaction.getSipTransactionInfo().getCallId()); + + redisCatchStorage.updateStreamAuthorityInfo(app, ssrcTransaction.getStream(), streamAuthorityInfo); + + String deviceId = ssrcTransaction.getDeviceId(); + Integer channelId = ssrcTransaction.getChannelId(); + DeviceChannel deviceChannel = deviceChannelService.getOneForSourceById(channelId); + if (deviceChannel != null) { + result.setEnable_audio(deviceChannel.isHasAudio()); + } + // 如果是录像下载就设置视频间隔十秒 + if (ssrcTransaction.getType() == InviteSessionType.DOWNLOAD) { + // 获取录像的总时长,然后设置为这个视频的时长 + InviteInfo inviteInfoForDownload = inviteStreamService.getInviteInfo(InviteSessionType.DOWNLOAD, channelId, stream); + if (inviteInfoForDownload != null) { + String startTime = inviteInfoForDownload.getStartTime(); + String endTime = inviteInfoForDownload.getEndTime(); + long difference = DateUtil.getDifference(startTime, endTime) / 1000; + result.setMp4_max_second((int) difference); + result.setEnable_mp4(true); + // 设置为2保证得到的mp4的时长是正常的 + result.setModify_stamp(2); + } + } + // 如果是talk对讲,则默认获取声音 + if (ssrcTransaction.getType() == InviteSessionType.TALK) { + result.setEnable_audio(true); + } + } + } else if (app.equals("broadcast")) { + result.setEnable_audio(true); + result.setEnable_mp4(userSetting.getRecordSip()); + } else if (app.equals("talk")) { + result.setEnable_audio(true); + result.setEnable_mp4(userSetting.getRecordSip()); + }else { + result.setEnable_mp4(userSetting.getRecordPushLive()); + } + if (app.equalsIgnoreCase("rtp")) { + String receiveKey = VideoManagerConstants.WVP_OTHER_RECEIVE_RTP_INFO + userSetting.getServerId() + "_" + stream; + OtherRtpSendInfo otherRtpSendInfo = (OtherRtpSendInfo) redisTemplate.opsForValue().get(receiveKey); + + String receiveKeyForPS = VideoManagerConstants.WVP_OTHER_RECEIVE_PS_INFO + userSetting.getServerId() + "_" + stream; + OtherPsSendInfo otherPsSendInfo = (OtherPsSendInfo) redisTemplate.opsForValue().get(receiveKeyForPS); + if (otherRtpSendInfo != null || otherPsSendInfo != null) { + result.setEnable_mp4(true); + } + } + return result; + } + + @Override + public boolean closeStreamOnNoneReader(String mediaServerId, String app, String stream, String schema) { + boolean result = false; + if (recordPlanService.recording(app, stream) != null) { + return false; + } + // 国标类型的流 + if ("rtp".equals(app)) { + result = userSetting.getStreamOnDemand(); + // 国标流, 点播/录像回放/录像下载 + InviteInfo inviteInfo = inviteStreamService.getInviteInfoByStream(null, stream); + if (inviteInfo != null) { + if (inviteInfo.getStatus() == InviteSessionStatus.ok){ + // 录像下载 + if (inviteInfo.getType() == InviteSessionType.DOWNLOAD) { + return false; + } + DeviceChannel deviceChannel = deviceChannelService.getOneForSourceById(inviteInfo.getChannelId()); + if (deviceChannel == null) { + return false; + } + } + return result; + } + }else if ("1078".equals(app)) { + // 判断是否是1078播放类型 + JTMediaStreamType jtMediaStreamType = ijt1078Service.checkStreamFromJt(stream); + if (jtMediaStreamType != null) { + String[] streamParamArray = stream.split("_"); + if (jtMediaStreamType.equals(JTMediaStreamType.PLAY)) { + jt1078PlayService.stopPlay(streamParamArray[0], Integer.parseInt(streamParamArray[1])); + }else if (jtMediaStreamType.equals(JTMediaStreamType.PLAYBACK)) { + jt1078PlayService.stopPlayback(streamParamArray[0], Integer.parseInt(streamParamArray[1])); + } + }else { + return false; + } + }else if ("talk".equals(app) || "broadcast".equals(app)) { + return false; + } else if ("mp4_record".equals(app)) { + return true; + } else { + // 非国标流 推流/拉流代理 + // 拉流代理 + StreamProxy streamProxy = streamProxyService.getStreamProxyByAppAndStream(app, stream); + if (streamProxy != null) { + if (streamProxy.isEnableDisableNoneReader()) { + // 无人观看停用 + // 修改数据 + streamProxyService.stopByAppAndStream(app, stream); + return true; + } else { + // 无人观看不做处理 + return false; + } + }else { + return false; + } + } + return result; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/service/impl/MobilePositionServiceImpl.java b/src/main/java/com/genersoft/iot/vmp/service/impl/MobilePositionServiceImpl.java new file mode 100644 index 0000000..54771f3 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/service/impl/MobilePositionServiceImpl.java @@ -0,0 +1,146 @@ +package com.genersoft.iot.vmp.service.impl; + +import com.genersoft.iot.vmp.conf.UserSetting; +import com.genersoft.iot.vmp.gb28181.bean.Device; +import com.genersoft.iot.vmp.gb28181.bean.DeviceChannel; +import com.genersoft.iot.vmp.gb28181.bean.MobilePosition; +import com.genersoft.iot.vmp.gb28181.bean.Platform; +import com.genersoft.iot.vmp.gb28181.dao.DeviceChannelMapper; +import com.genersoft.iot.vmp.gb28181.dao.DeviceMapper; +import com.genersoft.iot.vmp.gb28181.dao.DeviceMobilePositionMapper; +import com.genersoft.iot.vmp.gb28181.dao.PlatformMapper; +import com.genersoft.iot.vmp.gb28181.utils.Coordtransform; +import com.genersoft.iot.vmp.service.IMobilePositionService; +import com.genersoft.iot.vmp.utils.DateUtil; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@Slf4j +@Service +public class MobilePositionServiceImpl implements IMobilePositionService { + + @Autowired + private DeviceMapper deviceMapper; + + @Autowired + private DeviceChannelMapper channelMapper; + + @Autowired + private DeviceMobilePositionMapper mobilePositionMapper; + + @Autowired + private UserSetting userSetting; + + + @Autowired + private PlatformMapper platformMapper; + + @Autowired + private RedisTemplate redisTemplate; + + private final String REDIS_MOBILE_POSITION_LIST = "redis_mobile_position_list"; + + @Override + public void add(MobilePosition mobilePosition) { + List list = new ArrayList<>(); + list.add(mobilePosition); + add(list); + } + + @Override + public void add(List mobilePositionList) { + redisTemplate.opsForList().leftPushAll(REDIS_MOBILE_POSITION_LIST, mobilePositionList); + } + + private List get(int length) { + Long size = redisTemplate.opsForList().size(REDIS_MOBILE_POSITION_LIST); + if (size == null || size == 0) { + return new ArrayList<>(); + } + return redisTemplate.opsForList().rightPop(REDIS_MOBILE_POSITION_LIST, Math.min(length, size)); + } + + + + /** + * 查询移动位置轨迹 + */ + @Override + public synchronized List queryMobilePositions(String deviceId, String channelId, String startTime, String endTime) { + return mobilePositionMapper.queryPositionByDeviceIdAndTime(deviceId, channelId, startTime, endTime); + } + + @Override + public List queryEnablePlatformListWithAsMessageChannel() { + return platformMapper.queryEnablePlatformListWithAsMessageChannel(); + } + + /** + * 查询最新移动位置 + * @param deviceId + */ + @Override + public MobilePosition queryLatestPosition(String deviceId) { + return mobilePositionMapper.queryLatestPositionByDevice(deviceId); + } + + @Scheduled(fixedDelay = 1000) + @Transactional + public void executeTaskQueue() { + int countLimit = 3000; + List mobilePositions = get(countLimit); + if (mobilePositions == null || mobilePositions.isEmpty()) { + return; + } + if (userSetting.getSavePositionHistory()) { + mobilePositionMapper.batchadd(mobilePositions); + } + log.info("[移动位置订阅]更新通道位置: {}", mobilePositions.size()); + Map> updateChannelMap = new HashMap<>(); + for (MobilePosition mobilePosition : mobilePositions) { + DeviceChannel deviceChannel = new DeviceChannel(); + deviceChannel.setId(mobilePosition.getChannelId()); + deviceChannel.setDeviceId(mobilePosition.getDeviceId()); + deviceChannel.setLongitude(mobilePosition.getLongitude()); + deviceChannel.setLatitude(mobilePosition.getLatitude()); + deviceChannel.setGpsTime(mobilePosition.getTime()); + deviceChannel.setUpdateTime(DateUtil.getNow()); + if (mobilePosition.getLongitude() > 0 || mobilePosition.getLatitude() > 0) { + Double[] wgs84Position = Coordtransform.GCJ02ToWGS84(mobilePosition.getLongitude(), mobilePosition.getLatitude()); + deviceChannel.setGbLongitude(wgs84Position[0]); + deviceChannel.setGbLatitude(wgs84Position[1]); + } + if (!updateChannelMap.containsKey(mobilePosition.getDeviceId())) { + updateChannelMap.put(mobilePosition.getDeviceId(), new HashMap<>()); + } + updateChannelMap.get(mobilePosition.getDeviceId()).put(mobilePosition.getChannelId(), deviceChannel); + } + List deviceIds = new ArrayList<>(updateChannelMap.keySet()); + if (deviceIds.isEmpty()) { + log.info("[移动位置订阅]为查询到对应的设备,消息已经忽略"); + return; + } + List deviceList = deviceMapper.queryByDeviceIds(deviceIds); + for (Device device : deviceList) { + Map channelMap = updateChannelMap.get(device.getDeviceId()); + if (device.getGeoCoordSys().equalsIgnoreCase("GCJ02")) { + channelMap.values().forEach(channel -> { + Double[] wgs84Position = Coordtransform.GCJ02ToWGS84(channel.getLongitude(), channel.getLatitude()); + channel.setGbLongitude(wgs84Position[0]); + channel.setGbLatitude(wgs84Position[1]); + }); + } + channelMapper.batchUpdatePosition(new ArrayList<>(channelMap.values())); + } + } + +} diff --git a/src/main/java/com/genersoft/iot/vmp/service/impl/RecordPlanServiceImpl.java b/src/main/java/com/genersoft/iot/vmp/service/impl/RecordPlanServiceImpl.java new file mode 100644 index 0000000..05c4d5f --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/service/impl/RecordPlanServiceImpl.java @@ -0,0 +1,288 @@ +package com.genersoft.iot.vmp.service.impl; + +import com.genersoft.iot.vmp.common.StreamInfo; +import com.genersoft.iot.vmp.conf.exception.ControllerException; +import com.genersoft.iot.vmp.gb28181.bean.CommonGBChannel; +import com.genersoft.iot.vmp.gb28181.dao.CommonGBChannelMapper; +import com.genersoft.iot.vmp.gb28181.service.IGbChannelPlayService; +import com.genersoft.iot.vmp.media.bean.MediaInfo; +import com.genersoft.iot.vmp.media.event.media.MediaDepartureEvent; +import com.genersoft.iot.vmp.media.service.IMediaServerService; +import com.genersoft.iot.vmp.service.IRecordPlanService; +import com.genersoft.iot.vmp.service.bean.InviteErrorCode; +import com.genersoft.iot.vmp.service.bean.RecordPlan; +import com.genersoft.iot.vmp.service.bean.RecordPlanItem; +import com.genersoft.iot.vmp.storager.dao.RecordPlanMapper; +import com.genersoft.iot.vmp.utils.DateUtil; +import com.genersoft.iot.vmp.vmanager.bean.ErrorCode; +import com.github.pagehelper.PageHelper; +import com.github.pagehelper.PageInfo; +import com.google.common.base.Joiner; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.event.EventListener; +import org.springframework.scheduling.annotation.Async; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; +import java.util.*; +import java.util.concurrent.TimeUnit; + +@Service +@Slf4j +public class RecordPlanServiceImpl implements IRecordPlanService { + + @Autowired + private RecordPlanMapper recordPlanMapper; + + @Autowired + private CommonGBChannelMapper channelMapper; + + @Autowired + private IGbChannelPlayService channelPlayService; + + @Autowired + private IMediaServerService mediaServerService; + + + + /** + * 流离开的处理 + */ + @Async("taskExecutor") + @EventListener + public void onApplicationEvent(MediaDepartureEvent event) { + // 流断开,检查是否还处于录像状态, 如果是则继续录像 + Integer channelId = recording(event.getApp(), event.getStream()); + if(channelId == null) { + return; + } + // 重新拉起 + CommonGBChannel channel = channelMapper.queryById(channelId); + if (channel == null) { + log.warn("[录制计划] 流离开时拉起需要录像的流时, 发现通道不存在, id: {}", channelId); + return; + } + // 开启点播, + channelPlayService.play(channel, null, true, ((code, msg, streamInfo) -> { + if (code == InviteErrorCode.SUCCESS.getCode() && streamInfo != null) { + log.info("[录像] 流离开时拉起需要录像的流, 开启成功, 通道ID: {}", channel.getGbId()); + recordStreamMap.put(channel.getGbId(), streamInfo); + } else { + recordStreamMap.remove(channelId); + log.info("[录像] 流离开时拉起需要录像的流, 开启失败, 十分钟后重试, 通道ID: {}", channel.getGbId()); + } + })); + } + + Map recordStreamMap = new HashMap<>(); + + @Scheduled(fixedRate = 1, timeUnit = TimeUnit.MINUTES) + public void execution() { + // 查询现在需要录像的通道Id + List startChannelIdList = queryCurrentChannelRecord(); + + if (startChannelIdList.isEmpty()) { + // 当前没有录像任务, 如果存在旧的正在录像的就移除 + if(!recordStreamMap.isEmpty()) { + Set recordStreamSet = new HashSet<>(recordStreamMap.keySet()); + stopStreams(recordStreamSet, recordStreamMap); + recordStreamMap.clear(); + } + }else { + // 当前存在录像任务, 获取正在录像中存在但是当前录制列表不存在的内容,进行停止; 获取正在录像中没有但是当前需录制的列表中存在的进行开启. + Set recordStreamSet = new HashSet<>(recordStreamMap.keySet()); + startChannelIdList.forEach(recordStreamSet::remove); + if (!recordStreamSet.isEmpty()) { + // 正在录像中存在但是当前录制列表不存在的内容,进行停止; + stopStreams(recordStreamSet, recordStreamMap); + } + + // 移除startChannelIdList中已经在录像的部分, 剩下的都是需要新添加的(正在录像中没有但是当前需录制的列表中存在的进行开启) + recordStreamMap.keySet().forEach(startChannelIdList::remove); + if (!startChannelIdList.isEmpty()) { + // 获取所有的关联的通道 + List channelList = channelMapper.queryByIds(startChannelIdList); + if (!channelList.isEmpty()) { + // 查找是否已经开启录像, 如果没有则开启录像 + for (CommonGBChannel channel : channelList) { + // 开启点播, + channelPlayService.play(channel, null, true, ((code, msg, streamInfo) -> { + if (code == InviteErrorCode.SUCCESS.getCode() && streamInfo != null) { + log.info("[录像] 开启成功, 通道ID: {}", channel.getGbId()); + recordStreamMap.put(channel.getGbId(), streamInfo); + } else { + log.info("[录像] 开启失败, 十分钟后重试, 通道ID: {}", channel.getGbId()); + } + })); + } + } else { + log.error("[录制计划] 数据异常, 这些关联的通道已经不存在了: {}", Joiner.on(",").join(startChannelIdList)); + } + } + } + } + + /** + * 获取当前时间段应该录像的通道Id列表 + */ + private List queryCurrentChannelRecord(){ + // 获取当前时间在一周内的序号, 数据库存储的从第几个30分钟开始, 0-47, 包括首尾 + LocalDateTime now = LocalDateTime.now(); + int week = now.getDayOfWeek().getValue(); + int index = now.getHour() * 60 + now.getMinute(); + + // 查询现在需要录像的通道Id + return recordPlanMapper.queryRecordIng(week, index); + } + + private void stopStreams(Collection channelIds, Map recordStreamMap) { + for (Integer channelId : channelIds) { + try { + StreamInfo streamInfo = recordStreamMap.get(channelId); + if (streamInfo == null) { + continue; + } + // 查看是否有人观看,存在则不做处理,等待后续自然处理,如果无人观看,则关闭该流 + MediaInfo mediaInfo = mediaServerService.getMediaInfo(streamInfo.getMediaServer(), streamInfo.getApp(), streamInfo.getStream()); + if (mediaInfo.getReaderCount() == null || mediaInfo.getReaderCount() == 0) { + mediaServerService.closeStreams(streamInfo.getMediaServer(), streamInfo.getApp(), streamInfo.getStream()); + log.info("[录制计划] 停止, 通道ID: {}", channelId); + } + }catch (Exception e) { + log.error("[录制计划] 停止时异常", e); + }finally { + recordStreamMap.remove(channelId); + } + } + } + + @Override + public Integer recording(String app, String stream) { + for (Integer channelId : recordStreamMap.keySet()) { + StreamInfo streamInfo = recordStreamMap.get(channelId); + if (streamInfo != null && streamInfo.getApp().equals(app) && streamInfo.getStream().equals(stream)) { + return channelId; + } + } + return null; + } + + @Override + @Transactional + public void add(RecordPlan plan) { + plan.setCreateTime(DateUtil.getNow()); + plan.setUpdateTime(DateUtil.getNow()); + recordPlanMapper.add(plan); + if (plan.getId() > 0 && !plan.getPlanItemList().isEmpty()) { + for (RecordPlanItem recordPlanItem : plan.getPlanItemList()) { + recordPlanItem.setPlanId(plan.getId()); + } + recordPlanMapper.batchAddItem(plan.getId(), plan.getPlanItemList()); + } + // TODO 更新录像队列 + } + + @Override + public RecordPlan get(Integer planId) { + RecordPlan recordPlan = recordPlanMapper.get(planId); + if (recordPlan == null) { + return null; + } + List recordPlanItemList = recordPlanMapper.getItemList(planId); + if (!recordPlanItemList.isEmpty()) { + recordPlan.setPlanItemList(recordPlanItemList); + } + return recordPlan; + } + + @Override + @Transactional + public void update(RecordPlan plan) { + plan.setUpdateTime(DateUtil.getNow()); + recordPlanMapper.update(plan); + recordPlanMapper.cleanItems(plan.getId()); + if (plan.getPlanItemList() != null && !plan.getPlanItemList().isEmpty()){ + List planItemList = new ArrayList<>(); + for (RecordPlanItem recordPlanItem : plan.getPlanItemList()) { + if (recordPlanItem.getStart() == null || recordPlanItem.getStop() == null || recordPlanItem.getWeekDay() == null){ + continue; + } + if (recordPlanItem.getPlanId() == null) { + recordPlanItem.setPlanId(plan.getId()); + } + planItemList.add(recordPlanItem); + } + if(!planItemList.isEmpty()) { + recordPlanMapper.batchAddItem(plan.getId(), planItemList); + } + } + // TODO 更新录像队列 + + } + + @Override + @Transactional + public void delete(Integer planId) { + RecordPlan recordPlan = recordPlanMapper.get(planId); + if (recordPlan == null) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "录制计划不存在"); + } + // 清理关联的通道 + channelMapper.removeRecordPlanByPlanId(recordPlan.getId()); + recordPlanMapper.cleanItems(planId); + recordPlanMapper.delete(planId); + // TODO 更新录像队列 + } + + @Override + public PageInfo query(Integer page, Integer count, String query) { + PageHelper.startPage(page, count); + if (query != null) { + query = query.replaceAll("/", "//") + .replaceAll("%", "/%") + .replaceAll("_", "/_"); + } + List all = recordPlanMapper.query(query); + return new PageInfo<>(all); + } + + @Override + public void link(List channelIds, Integer planId) { + if (channelIds == null || channelIds.isEmpty()) { + log.info("[录制计划] 关联/移除关联时, 通道编号必须存在"); + throw new ControllerException(ErrorCode.ERROR100.getCode(), "通道编号必须存在"); + } + if (planId == null) { + channelMapper.removeRecordPlan(channelIds); + }else { + channelMapper.addRecordPlan(channelIds, planId); + } + // 查看当前的待录制列表是否变化,如果变化,则调用录制计划马上开始录制 + execution(); + } + + @Override + public PageInfo queryChannelList(int page, int count, String query, Integer dataType, Boolean online, Integer planId, Boolean hasLink) { + PageHelper.startPage(page, count); + if (query != null) { + query = query.replaceAll("/", "//") + .replaceAll("%", "/%") + .replaceAll("_", "/_"); + } + List all = channelMapper.queryForRecordPlanForWebList(planId, query, dataType, online, hasLink); + return new PageInfo<>(all); + } + + @Override + public void linkAll(Integer planId) { + channelMapper.addRecordPlanForAll(planId); + } + + @Override + public void cleanAll(Integer planId) { + channelMapper.removeRecordPlanByPlanId(planId); + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/service/impl/RoleServerImpl.java b/src/main/java/com/genersoft/iot/vmp/service/impl/RoleServerImpl.java new file mode 100755 index 0000000..d31bbce --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/service/impl/RoleServerImpl.java @@ -0,0 +1,41 @@ +package com.genersoft.iot.vmp.service.impl; + +import com.genersoft.iot.vmp.service.IRoleService; +import com.genersoft.iot.vmp.storager.dao.RoleMapper; +import com.genersoft.iot.vmp.storager.dao.dto.Role; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +public class RoleServerImpl implements IRoleService { + + @Autowired + private RoleMapper roleMapper; + + @Override + public Role getRoleById(int id) { + return roleMapper.selectById(id); + } + + @Override + public int add(Role role) { + return roleMapper.add(role); + } + + @Override + public int delete(int id) { + return roleMapper.delete(id); + } + + @Override + public List getAll() { + return roleMapper.selectAll(); + } + + @Override + public int update(Role role) { + return roleMapper.update(role); + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/service/impl/RtpServerServiceImpl.java b/src/main/java/com/genersoft/iot/vmp/service/impl/RtpServerServiceImpl.java new file mode 100644 index 0000000..8609926 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/service/impl/RtpServerServiceImpl.java @@ -0,0 +1,162 @@ +package com.genersoft.iot.vmp.service.impl; + +import com.genersoft.iot.vmp.conf.DynamicTask; +import com.genersoft.iot.vmp.conf.UserSetting; +import com.genersoft.iot.vmp.gb28181.bean.OpenRTPServerResult; +import com.genersoft.iot.vmp.gb28181.session.SSRCFactory; +import com.genersoft.iot.vmp.gb28181.session.SipInviteSessionManager; +import com.genersoft.iot.vmp.media.bean.MediaServer; +import com.genersoft.iot.vmp.media.event.hook.Hook; +import com.genersoft.iot.vmp.media.event.hook.HookSubscribe; +import com.genersoft.iot.vmp.media.event.hook.HookType; +import com.genersoft.iot.vmp.media.event.media.MediaArrivalEvent; +import com.genersoft.iot.vmp.media.event.media.MediaDepartureEvent; +import com.genersoft.iot.vmp.media.service.IMediaServerService; +import com.genersoft.iot.vmp.service.IReceiveRtpServerService; +import com.genersoft.iot.vmp.service.bean.ErrorCallback; +import com.genersoft.iot.vmp.service.bean.InviteErrorCode; +import com.genersoft.iot.vmp.service.bean.RTPServerParam; +import com.genersoft.iot.vmp.service.bean.SSRCInfo; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.event.EventListener; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Service; + +import java.util.UUID; + +@Slf4j +@Service +public class RtpServerServiceImpl implements IReceiveRtpServerService { + + @Autowired + private IMediaServerService mediaServerService; + + @Autowired + private DynamicTask dynamicTask; + + @Autowired + private SSRCFactory ssrcFactory; + + @Autowired + private UserSetting userSetting; + + @Autowired + private HookSubscribe subscribe; + + @Autowired + private SipInviteSessionManager sessionManager; + + /** + * 流到来的处理 + */ + @Async("taskExecutor") + @org.springframework.context.event.EventListener + public void onApplicationEvent(MediaArrivalEvent event) { + + } + + /** + * 流离开的处理 + */ + @Async("taskExecutor") + @EventListener + public void onApplicationEvent(MediaDepartureEvent event) { + + } + + @Override + public SSRCInfo openRTPServer(RTPServerParam rtpServerParam, ErrorCallback callback) { + if (callback == null) { + log.warn("[开启RTP收流] 失败,回调为NULL"); + return null; + } + if (rtpServerParam.getMediaServerItem() == null) { + log.warn("[开启RTP收流] 失败,媒体节点为NULL"); + return null; + } + + // 获取mediaServer可用的ssrc + final String ssrc; + if (rtpServerParam.getPresetSsrc() != null) { + ssrc = rtpServerParam.getPresetSsrc(); + }else { + if (rtpServerParam.isPlayback()) { + ssrc = ssrcFactory.getPlayBackSsrc(rtpServerParam.getMediaServerItem().getId()); + }else { + ssrc = ssrcFactory.getPlaySsrc(rtpServerParam.getMediaServerItem().getId()); + } + } + final String streamId; + if (rtpServerParam.getStreamId() == null) { + streamId = String.format("%08x", Long.parseLong(ssrc)).toUpperCase(); + }else { + streamId = rtpServerParam.getStreamId(); + } + if (rtpServerParam.isSsrcCheck() && rtpServerParam.getTcpMode() > 0) { + // 目前zlm不支持 tcp模式更新ssrc,暂时关闭ssrc校验 + log.warn("[openRTPServer] 平台对接时下级可能自定义ssrc,但是tcp模式zlm收流目前无法更新ssrc,可能收流超时,此时请使用udp收流或者关闭ssrc校验"); + } + int rtpServerPort; + if (rtpServerParam.getMediaServerItem().isRtpEnable()) { + rtpServerPort = mediaServerService.createRTPServer(rtpServerParam.getMediaServerItem(), streamId, + rtpServerParam.isSsrcCheck() ? Long.parseLong(ssrc) : 0, rtpServerParam.getPort(), rtpServerParam.isOnlyAuto(), + rtpServerParam.isDisableAudio(), rtpServerParam.isReUsePort(), rtpServerParam.getTcpMode()); + } else { + rtpServerPort = rtpServerParam.getMediaServerItem().getRtpProxyPort(); + } + if (rtpServerPort == 0) { + callback.run(InviteErrorCode.ERROR_FOR_RESOURCE_EXHAUSTION.getCode(), "开启RTPServer失败", null); + // 释放ssrc + if (rtpServerParam.getPresetSsrc() == null) { + ssrcFactory.releaseSsrc(rtpServerParam.getMediaServerItem().getId(), ssrc); + } + return null; + } + + // 设置流超时的定时任务 + String timeOutTaskKey = UUID.randomUUID().toString(); + + SSRCInfo ssrcInfo = new SSRCInfo(rtpServerPort, ssrc, "rtp", streamId, timeOutTaskKey); + OpenRTPServerResult openRTPServerResult = new OpenRTPServerResult(); + openRTPServerResult.setSsrcInfo(ssrcInfo); + + Hook rtpHook = Hook.getInstance(HookType.on_media_arrival, ssrcInfo.getApp(), streamId, rtpServerParam.getMediaServerItem().getId()); + dynamicTask.startDelay(timeOutTaskKey, () -> { + // 收流超时 + // 释放ssrc + if (rtpServerParam.getPresetSsrc() == null) { + ssrcFactory.releaseSsrc(rtpServerParam.getMediaServerItem().getId(), ssrc); + } + // 关闭收流端口 + mediaServerService.closeRTPServer(rtpServerParam.getMediaServerItem(), streamId); + subscribe.removeSubscribe(rtpHook); + callback.run(InviteErrorCode.ERROR_FOR_STREAM_TIMEOUT.getCode(), InviteErrorCode.ERROR_FOR_STREAM_TIMEOUT.getMsg(), openRTPServerResult); + }, userSetting.getPlayTimeout()); + // 开启流到来的监听 + subscribe.addSubscribe(rtpHook, (hookData) -> { + dynamicTask.stop(timeOutTaskKey); + // hook响应 + openRTPServerResult.setHookData(hookData); + callback.run(InviteErrorCode.SUCCESS.getCode(), InviteErrorCode.SUCCESS.getMsg(), openRTPServerResult); + subscribe.removeSubscribe(rtpHook); + }); + + return ssrcInfo; + } + + @Override + public void closeRTPServer(MediaServer mediaServer, SSRCInfo ssrcInfo) { + if (mediaServer == null) { + return; + } + if (ssrcInfo.getTimeOutTaskKey() != null) { + dynamicTask.stop(ssrcInfo.getTimeOutTaskKey()); + } + if (ssrcInfo.getSsrc() != null) { + // 释放ssrc + ssrcFactory.releaseSsrc(mediaServer.getId(), ssrcInfo.getSsrc()); + } + mediaServerService.closeRTPServer(mediaServer, ssrcInfo.getStream()); + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/service/impl/SendRtpServerServiceImpl.java b/src/main/java/com/genersoft/iot/vmp/service/impl/SendRtpServerServiceImpl.java new file mode 100644 index 0000000..053ff9e --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/service/impl/SendRtpServerServiceImpl.java @@ -0,0 +1,256 @@ +package com.genersoft.iot.vmp.service.impl; + +import com.genersoft.iot.vmp.common.VideoManagerConstants; +import com.genersoft.iot.vmp.conf.UserSetting; +import com.genersoft.iot.vmp.gb28181.bean.PlayException; +import com.genersoft.iot.vmp.gb28181.bean.SendRtpInfo; +import com.genersoft.iot.vmp.media.bean.MediaServer; +import com.genersoft.iot.vmp.service.ISendRtpServerService; +import com.genersoft.iot.vmp.utils.JsonUtil; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.math.NumberUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.support.atomic.RedisAtomicInteger; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +@Service +@Slf4j +public class SendRtpServerServiceImpl implements ISendRtpServerService { + + @Autowired + private UserSetting userSetting; + + @Autowired + private RedisTemplate redisTemplate; + + + @Override + public SendRtpInfo createSendRtpInfo(MediaServer mediaServer, String ip, Integer port, String ssrc, String requesterId, + String deviceId, Integer channelId, Boolean isTcp, Boolean rtcp) { + int localPort = getNextPort(mediaServer); + if (localPort <= 0) { + return null; + } + return SendRtpInfo.getInstance(localPort, mediaServer, ip, port, ssrc, deviceId, null, channelId, + isTcp, rtcp, userSetting.getServerId()); + } + + @Override + public SendRtpInfo createSendRtpInfo(MediaServer mediaServer, String ip, Integer port, String ssrc, String platformId, + String app, String stream, Integer channelId, Boolean tcp, Boolean rtcp){ + + int localPort = getNextPort(mediaServer); + if (localPort <= 0) { + throw new PlayException(javax.sip.message.Response.SERVER_INTERNAL_ERROR, "server internal error"); + } + SendRtpInfo sendRtpInfo = SendRtpInfo.getInstance(localPort, mediaServer, ip, port, ssrc, null, platformId, channelId, + tcp, rtcp, userSetting.getServerId()); + if (sendRtpInfo == null) { + return null; + } + sendRtpInfo.setApp(app); + sendRtpInfo.setStream(stream); + return sendRtpInfo; + } + + @Override + public void update(SendRtpInfo sendRtpItem) { + redisTemplate.opsForHash().put(VideoManagerConstants.SEND_RTP_INFO_CALLID, sendRtpItem.getCallId(), sendRtpItem); + redisTemplate.opsForHash().put(VideoManagerConstants.SEND_RTP_INFO_STREAM + sendRtpItem.getStream(), sendRtpItem.getTargetId(), sendRtpItem); + redisTemplate.opsForHash().put(VideoManagerConstants.SEND_RTP_INFO_CHANNEL + sendRtpItem.getChannelId(), sendRtpItem.getTargetId(), sendRtpItem); + } + + @Override + public SendRtpInfo queryByChannelId(Integer channelId, String targetId) { + String key = VideoManagerConstants.SEND_RTP_INFO_CHANNEL + channelId; + return JsonUtil.redisHashJsonToObject(redisTemplate, key, targetId, SendRtpInfo.class); + } + + @Override + public SendRtpInfo queryByCallId(String callId) { + String key = VideoManagerConstants.SEND_RTP_INFO_CALLID; + return (SendRtpInfo)redisTemplate.opsForHash().get(key, callId); + } + + @Override + public SendRtpInfo queryByStream(String stream, String targetId) { + String key = VideoManagerConstants.SEND_RTP_INFO_STREAM + stream; + return JsonUtil.redisHashJsonToObject(redisTemplate, key, targetId, SendRtpInfo.class); + } + + @Override + public List queryByStream(String stream) { + String key = VideoManagerConstants.SEND_RTP_INFO_STREAM + stream; + List values = redisTemplate.opsForHash().values(key); + List result= new ArrayList<>(); + for (Object o : values) { + result.add((SendRtpInfo) o); + } + + return result; + } + + /** + * 删除RTP推送信息缓存 + */ + @Override + public void delete(SendRtpInfo sendRtpInfo) { + if (sendRtpInfo == null) { + return; + } + redisTemplate.opsForHash().delete(VideoManagerConstants.SEND_RTP_INFO_CALLID, sendRtpInfo.getCallId()); + redisTemplate.opsForHash().delete(VideoManagerConstants.SEND_RTP_INFO_STREAM + sendRtpInfo.getStream(), sendRtpInfo.getTargetId()); + redisTemplate.opsForHash().delete(VideoManagerConstants.SEND_RTP_INFO_CHANNEL + sendRtpInfo.getChannelId(), sendRtpInfo.getTargetId()); + } + @Override + public void deleteByCallId(String callId) { + SendRtpInfo sendRtpInfo = queryByCallId(callId); + if (sendRtpInfo == null) { + return; + } + delete(sendRtpInfo); + } + @Override + public void deleteByStream(String stream, String targetId) { + SendRtpInfo sendRtpInfo = queryByStream(stream, targetId); + if (sendRtpInfo == null) { + return; + } + delete(sendRtpInfo); + } + + @Override + public void deleteByStream(String stream) { + List sendRtpInfos = queryByStream(stream); + for (SendRtpInfo sendRtpInfo : sendRtpInfos) { + delete(sendRtpInfo); + } + } + + @Override + public void deleteByChannel(Integer channelId, String targetId) { + SendRtpInfo sendRtpInfo = queryByChannelId(channelId, targetId); + if (sendRtpInfo == null) { + return; + } + delete(sendRtpInfo); + } + + @Override + public List queryByChannelId(int channelId) { + String key = VideoManagerConstants.SEND_RTP_INFO_CHANNEL + channelId; + List values = redisTemplate.opsForHash().values(key); + List result= new ArrayList<>(); + for (Object o : values) { + result.add((SendRtpInfo) o); + } + return result; + } + + @Override + public List queryAll() { + String key = VideoManagerConstants.SEND_RTP_INFO_CALLID; + List values = redisTemplate.opsForHash().values(key); + List result= new ArrayList<>(); + for (Object o : values) { + result.add((SendRtpInfo) o); + } + return result; + } + + /** + * 查询某个通道是否存在上级点播(RTP推送) + */ + @Override + public boolean isChannelSendingRTP(Integer channelId) { + List sendRtpInfoList = queryByChannelId(channelId); + return !sendRtpInfoList.isEmpty(); + } + + @Override + public List queryForPlatform(String platformId) { + List sendRtpInfos = queryAll(); + if (!sendRtpInfos.isEmpty()) { + sendRtpInfos.removeIf(sendRtpInfo -> !sendRtpInfo.isSendToPlatform() || !sendRtpInfo.getTargetId().equals(platformId)); + } + return sendRtpInfos; + } + + private Set getAllSendRtpPort() { + String key = VideoManagerConstants.SEND_RTP_INFO_CALLID; + List values = redisTemplate.opsForHash().values(key); + Set result = new HashSet<>(); + for (Object value : values) { + SendRtpInfo sendRtpInfo = (SendRtpInfo) value; + result.add(sendRtpInfo.getPort()); + } + return result; + } + + + @Override + public synchronized int getNextPort(MediaServer mediaServer) { + if (mediaServer == null) { + log.warn("[发送端口管理] 参数错误,mediaServer为NULL"); + return -1; + } + String sendIndexKey = VideoManagerConstants.SEND_RTP_PORT + userSetting.getServerId() + ":" + mediaServer.getId(); + Set sendRtpSet = getAllSendRtpPort(); + String sendRtpPortRange = mediaServer.getSendRtpPortRange(); + int startPort; + int endPort; + if (sendRtpPortRange != null) { + String[] portArray = sendRtpPortRange.split(","); + if (portArray.length != 2 || !NumberUtils.isParsable(portArray[0]) || !NumberUtils.isParsable(portArray[1])) { + log.warn("{}发送端口配置格式错误,自动使用50000-60000作为端口范围", mediaServer.getId()); + startPort = 50000; + endPort = 60000; + }else { + if ( Integer.parseInt(portArray[1]) - Integer.parseInt(portArray[0]) < 1) { + log.warn("{}发送端口配置错误,结束端口至少比开始端口大一,自动使用50000-60000作为端口范围", mediaServer.getId()); + startPort = 50000; + endPort = 60000; + }else { + startPort = Integer.parseInt(portArray[0]); + endPort = Integer.parseInt(portArray[1]); + } + } + }else { + log.warn("{}未设置发送端口默认值,自动使用50000-60000作为端口范围", mediaServer.getId()); + startPort = 50000; + endPort = 60000; + } + if (redisTemplate == null || redisTemplate.getConnectionFactory() == null) { + log.warn("{}获取redis连接信息失败", mediaServer.getId()); + return -1; + } + RedisAtomicInteger redisAtomicInteger = new RedisAtomicInteger(sendIndexKey , redisTemplate.getConnectionFactory()); + if (redisAtomicInteger.get() < startPort) { + redisAtomicInteger.set(startPort); + return startPort; + }else { + for (int i = 0; i < endPort - startPort; i++) { + int port = redisAtomicInteger.getAndIncrement(); + if (port > endPort) { + redisAtomicInteger.set(startPort); + if (sendRtpSet.contains(startPort)) { + continue; + }else { + return startPort; + } + } + if (!sendRtpSet.contains(port)) { + return port; + } + } + } + log.warn("{}获取发送端口失败, 无可用端口", mediaServer.getId()); + return -1; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/service/impl/UserApiKeyServiceImpl.java b/src/main/java/com/genersoft/iot/vmp/service/impl/UserApiKeyServiceImpl.java new file mode 100644 index 0000000..8c552b1 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/service/impl/UserApiKeyServiceImpl.java @@ -0,0 +1,78 @@ +package com.genersoft.iot.vmp.service.impl; + +import com.genersoft.iot.vmp.service.IUserApiKeyService; +import com.genersoft.iot.vmp.storager.dao.UserApiKeyMapper; +import com.genersoft.iot.vmp.storager.dao.dto.UserApiKey; +import com.github.pagehelper.PageHelper; +import com.github.pagehelper.PageInfo; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.cache.annotation.CacheEvict; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +public class UserApiKeyServiceImpl implements IUserApiKeyService { + + @Autowired + UserApiKeyMapper userApiKeyMapper; + + @Autowired + private RedisTemplate redisTemplate; + + @Override + public int addApiKey(UserApiKey userApiKey) { + return userApiKeyMapper.add(userApiKey); + } + + @Override + public boolean isApiKeyExists(String apiKey) { + return userApiKeyMapper.isApiKeyExists(apiKey); + } + + @Override + public PageInfo getUserApiKeys(int page, int count) { + PageHelper.startPage(page, count); + List userApiKeys = userApiKeyMapper.getUserApiKeys(); + return new PageInfo<>(userApiKeys); + } + + @Cacheable(cacheNames = "userApiKey", key = "#id", sync = true) + @Override + public UserApiKey getUserApiKeyById(Integer id) { + return userApiKeyMapper.selectById(id); + } + + @CacheEvict(cacheNames = "userApiKey", key = "#id") + @Override + public int enable(Integer id) { + return userApiKeyMapper.enable(id); + } + + @CacheEvict(cacheNames = "userApiKey", key = "#id") + @Override + public int disable(Integer id) { + return userApiKeyMapper.disable(id); + } + + @CacheEvict(cacheNames = "userApiKey", key = "#id") + @Override + public int remark(Integer id, String remark) { + return userApiKeyMapper.remark(id, remark); + } + + @CacheEvict(cacheNames = "userApiKey", key = "#id") + @Override + public int delete(Integer id) { + return userApiKeyMapper.delete(id); + } + + @CacheEvict(cacheNames = "userApiKey", key = "#id") + @Override + public int reset(Integer id, String apiKey) { + return userApiKeyMapper.apiKey(id, apiKey); + } + +} diff --git a/src/main/java/com/genersoft/iot/vmp/service/impl/UserServiceImpl.java b/src/main/java/com/genersoft/iot/vmp/service/impl/UserServiceImpl.java new file mode 100755 index 0000000..cf0bea2 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/service/impl/UserServiceImpl.java @@ -0,0 +1,97 @@ +package com.genersoft.iot.vmp.service.impl; + +import com.genersoft.iot.vmp.service.IUserService; +import com.genersoft.iot.vmp.storager.dao.UserMapper; +import com.genersoft.iot.vmp.storager.dao.dto.User; +import com.github.pagehelper.PageHelper; +import com.github.pagehelper.PageInfo; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.util.DigestUtils; + +import java.util.List; + +@Service +public class UserServiceImpl implements IUserService { + + @Autowired + private UserMapper userMapper; + + @Override + public User getUser(String username, String password) { + return userMapper.select(username, password); + } + + @Override + public boolean changePassword(int id, String password) { + User user = userMapper.selectById(id); + user.setPassword(password); + return userMapper.update(user) > 0; + } + + @Override + public User getUserById(int id) { + return userMapper.selectById(id); + } + + @Override + public User getUserByUsername(String username) { + return userMapper.getUserByUsername(username); + } + + @Override + public int addUser(User user) { + User userByUsername = userMapper.getUserByUsername(user.getUsername()); + if (userByUsername != null) { + return 0; + } + return userMapper.add(user); + } + @Override + public int deleteUser(int id) { + return userMapper.delete(id); + } + + @Override + public List getAllUsers() { + return userMapper.selectAll(); + } + + @Override + public int updateUsers(User user) { + return userMapper.update(user); + } + + + @Override + public boolean checkPushAuthority(String callId, String sign) { + + List users = userMapper.getUsers(); + if (users.size() == 0) { + return false; + } + for (User user : users) { + if (user.getPushKey() == null) { + continue; + } + String checkStr = callId == null? user.getPushKey():(callId + "_" + user.getPushKey()) ; + String checkSign = DigestUtils.md5DigestAsHex(checkStr.getBytes()); + if (checkSign.equals(sign)) { + return true; + } + } + return false; + } + + @Override + public PageInfo getUsers(int page, int count) { + PageHelper.startPage(page, count); + List users = userMapper.getUsers(); + return new PageInfo<>(users); + } + + @Override + public int changePushKey(int id, String pushKey) { + return userMapper.changePushKey(id,pushKey); + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/service/redisMsg/IRedisRpcPlayService.java b/src/main/java/com/genersoft/iot/vmp/service/redisMsg/IRedisRpcPlayService.java new file mode 100644 index 0000000..69ed485 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/service/redisMsg/IRedisRpcPlayService.java @@ -0,0 +1,38 @@ +package com.genersoft.iot.vmp.service.redisMsg; + +import com.genersoft.iot.vmp.common.InviteSessionType; +import com.genersoft.iot.vmp.common.StreamInfo; +import com.genersoft.iot.vmp.gb28181.bean.RecordInfo; +import com.genersoft.iot.vmp.service.bean.DownloadFileInfo; +import com.genersoft.iot.vmp.service.bean.ErrorCallback; +import com.genersoft.iot.vmp.vmanager.bean.AudioBroadcastResult; + +public interface IRedisRpcPlayService { + + + void play(String serverId, Integer channelId, ErrorCallback callback); + + void stop(String serverId, InviteSessionType type, int channelId, String stream); + + void playback(String serverId, Integer channelId, String startTime, String endTime, ErrorCallback callback); + + void playbackPause(String serverId, String streamId); + + void playbackResume(String serverId, String streamId); + + void download(String serverId, Integer channelId, String startTime, String endTime, int downloadSpeed, ErrorCallback callback); + + void queryRecordInfo(String serverId, Integer channelId, String startTime, String endTime, ErrorCallback callback); + + String frontEndCommand(String serverId, Integer channelId, int cmdCode, int parameter1, int parameter2, int combindCode2); + + void playPush(String serverId, Integer id, ErrorCallback callback); + + void playProxy(String serverId, int id, ErrorCallback callback); + + void stopProxy(String serverId, int id); + + DownloadFileInfo getRecordPlayUrl(String serverId, Integer recordId); + + AudioBroadcastResult audioBroadcast(String serverId, String deviceId, String channelDeviceId, Boolean broadcastMode); +} diff --git a/src/main/java/com/genersoft/iot/vmp/service/redisMsg/IRedisRpcService.java b/src/main/java/com/genersoft/iot/vmp/service/redisMsg/IRedisRpcService.java new file mode 100644 index 0000000..dec5fcf --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/service/redisMsg/IRedisRpcService.java @@ -0,0 +1,69 @@ +package com.genersoft.iot.vmp.service.redisMsg; + +import com.genersoft.iot.vmp.common.CommonCallback; +import com.genersoft.iot.vmp.common.StreamInfo; +import com.genersoft.iot.vmp.gb28181.bean.*; +import com.genersoft.iot.vmp.gb28181.event.subscribe.catalog.CatalogEvent; +import com.genersoft.iot.vmp.vmanager.bean.WVPResult; + +import java.util.List; + +public interface IRedisRpcService { + + SendRtpInfo getSendRtpItem(String callId); + + WVPResult startSendRtp(String callId, SendRtpInfo sendRtpItem); + + WVPResult stopSendRtp(String callId); + + long waitePushStreamOnline(SendRtpInfo sendRtpItem, CommonCallback callback); + + void stopWaitePushStreamOnline(SendRtpInfo sendRtpItem); + + void rtpSendStopped(String callId); + + void removeCallback(long key); + + long onStreamOnlineEvent(String app, String stream, CommonCallback callback); + void unPushStreamOnlineEvent(String app, String stream); + + void subscribeCatalog(int id, int cycle); + + void subscribeMobilePosition(int id, int cycle, int interval); + + boolean updatePlatform(String serverId, Platform platform); + + void catalogEventPublish(String serverId, CatalogEvent catalogEvent); + + WVPResult devicesSync(String serverId, String deviceId); + + SyncStatus getChannelSyncStatus(String serverId, String deviceId); + + WVPResult deviceBasicConfig(String serverId, Device device, BasicParam basicParam); + + WVPResult deviceConfigQuery(String serverId, Device device, String channelId, String configType); + + void teleboot(String serverId, Device device); + + WVPResult recordControl(String serverId, Device device, String channelId, String recordCmdStr); + + WVPResult guard(String serverId, Device device, String guardCmdStr); + + WVPResult resetAlarm(String serverId, Device device, String channelId, String alarmMethod, String alarmType); + + void iFrame(String serverId, Device device, String channelId); + + WVPResult homePosition(String serverId, Device device, String channelId, Boolean enabled, Integer resetTime, Integer presetIndex); + + void dragZoomIn(String serverId, Device device, String channelId, int length, int width, int midpointx, int midpointy, int lengthx, int lengthy); + + void dragZoomOut(String serverId, Device device, String channelId, int length, int width, int midpointx, int midpointy, int lengthx, int lengthy); + + WVPResult deviceStatus(String serverId, Device device); + + WVPResult alarm(String serverId, Device device, String startPriority, String endPriority, String alarmMethod, String alarmType, String startTime, String endTime); + + WVPResult deviceInfo(String serverId, Device device); + + WVPResult> queryPreset(String serverId, Device device, String channelId); +} diff --git a/src/main/java/com/genersoft/iot/vmp/service/redisMsg/RedisAlarmMsgListener.java b/src/main/java/com/genersoft/iot/vmp/service/redisMsg/RedisAlarmMsgListener.java new file mode 100755 index 0000000..d9453d2 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/service/redisMsg/RedisAlarmMsgListener.java @@ -0,0 +1,180 @@ +package com.genersoft.iot.vmp.service.redisMsg; + +import com.alibaba.fastjson2.JSON; +import com.genersoft.iot.vmp.conf.UserSetting; +import com.genersoft.iot.vmp.gb28181.bean.AlarmChannelMessage; +import com.genersoft.iot.vmp.gb28181.bean.Device; +import com.genersoft.iot.vmp.gb28181.bean.DeviceAlarm; +import com.genersoft.iot.vmp.gb28181.bean.Platform; +import com.genersoft.iot.vmp.gb28181.service.IDeviceChannelService; +import com.genersoft.iot.vmp.gb28181.service.IDeviceService; +import com.genersoft.iot.vmp.gb28181.service.IPlatformChannelService; +import com.genersoft.iot.vmp.gb28181.service.IPlatformService; +import com.genersoft.iot.vmp.gb28181.transmit.cmd.ISIPCommander; +import com.genersoft.iot.vmp.gb28181.transmit.cmd.ISIPCommanderForPlatform; +import com.genersoft.iot.vmp.service.IMobilePositionService; +import com.genersoft.iot.vmp.utils.DateUtil; +import jakarta.validation.constraints.NotNull; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.connection.Message; +import org.springframework.data.redis.connection.MessageListener; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; +import org.springframework.util.ObjectUtils; + +import javax.sip.InvalidArgumentException; +import javax.sip.SipException; +import java.text.ParseException; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ConcurrentLinkedQueue; + +/** + * 监听 SUBSCRIBE alarm_receive + * 发布 PUBLISH alarm_receive '{ "gbId": "", "alarmSn": 1, "alarmType": "111", "alarmDescription": "222", }' + */ +@Slf4j +@Component +public class RedisAlarmMsgListener implements MessageListener { + + @Autowired + private ISIPCommander commander; + + @Autowired + private ISIPCommanderForPlatform commanderForPlatform; + + @Autowired + private IDeviceService deviceService; + + @Autowired + private IDeviceChannelService channelService; + + @Autowired + private IMobilePositionService mobilePositionService; + + @Autowired + private IPlatformService platformService; + + @Autowired + private IPlatformChannelService platformChannelService; + + private final ConcurrentLinkedQueue taskQueue = new ConcurrentLinkedQueue<>(); + + @Autowired + private UserSetting userSetting; + + @Override + public void onMessage(@NotNull Message message, byte[] bytes) { + log.info("[REDIS: ALARM]: {}", new String(message.getBody())); + taskQueue.offer(message); + } + + @Scheduled(fixedDelay = 100) + public void executeTaskQueue() { + if (taskQueue.isEmpty()) { + return; + } + List messageDataList = new ArrayList<>(); + int size = taskQueue.size(); + for (int i = 0; i < size; i++) { + Message msg = taskQueue.poll(); + if (msg != null) { + messageDataList.add(msg); + } + } + if (messageDataList.isEmpty()) { + return; + } + for (Message msg : messageDataList) { + try { + AlarmChannelMessage alarmChannelMessage = JSON.parseObject(msg.getBody(), AlarmChannelMessage.class); + if (alarmChannelMessage == null) { + log.warn("[REDIS的ALARM通知]消息解析失败"); + continue; + } + String chanelId = alarmChannelMessage.getGbId(); + + DeviceAlarm deviceAlarm = new DeviceAlarm(); + deviceAlarm.setCreateTime(DateUtil.getNow()); + deviceAlarm.setChannelId(chanelId); + deviceAlarm.setAlarmDescription(alarmChannelMessage.getAlarmDescription()); + deviceAlarm.setAlarmMethod("" + alarmChannelMessage.getAlarmSn()); + deviceAlarm.setAlarmType("" + alarmChannelMessage.getAlarmType()); + deviceAlarm.setAlarmPriority("1"); + deviceAlarm.setAlarmTime(DateUtil.getNow()); + deviceAlarm.setLongitude(0); + deviceAlarm.setLatitude(0); + + if (ObjectUtils.isEmpty(chanelId)) { + if (userSetting.getSendToPlatformsWhenIdLost()) { + // 发送给所有的上级 + List parentPlatforms = platformService.queryEnablePlatformList(userSetting.getServerId()); + if (!parentPlatforms.isEmpty()) { + for (Platform parentPlatform : parentPlatforms) { + try { + deviceAlarm.setChannelId(parentPlatform.getDeviceGBId()); + commanderForPlatform.sendAlarmMessage(parentPlatform, deviceAlarm); + } catch (SipException | InvalidArgumentException | ParseException e) { + log.error("[命令发送失败] 国标级联 发送报警: {}", e.getMessage()); + } + } + } + } else { + // 获取开启了消息推送的设备和平台 + List parentPlatforms = mobilePositionService.queryEnablePlatformListWithAsMessageChannel(); + if (!parentPlatforms.isEmpty()) { + for (Platform parentPlatform : parentPlatforms) { + try { + deviceAlarm.setChannelId(parentPlatform.getDeviceGBId()); + commanderForPlatform.sendAlarmMessage(parentPlatform, deviceAlarm); + } catch (SipException | InvalidArgumentException | ParseException e) { + log.error("[命令发送失败] 国标级联 发送报警: {}", e.getMessage()); + } + } + } + } + // 获取开启了消息推送的设备和平台 + List devices = channelService.queryDeviceWithAsMessageChannel(); + if (!devices.isEmpty()) { + for (Device device : devices) { + try { + deviceAlarm.setChannelId(device.getDeviceId()); + commander.sendAlarmMessage(device, deviceAlarm); + } catch (InvalidArgumentException | SipException | ParseException e) { + log.error("[命令发送失败] 发送报警: {}", e.getMessage()); + } + } + } + } else { + // 获取该通道ID是属于设备还是对应的上级平台 + Device device = deviceService.getDeviceBySourceChannelDeviceId(chanelId); + List platforms = platformChannelService.queryByPlatformBySharChannelId(chanelId); + if (device != null && device.getServerId().equals(userSetting.getServerId()) && (platforms == null || platforms.isEmpty())) { + try { + commander.sendAlarmMessage(device, deviceAlarm); + } catch (InvalidArgumentException | SipException | ParseException e) { + log.error("[命令发送失败] 发送报警: {}", e.getMessage()); + } + } else if (device == null && (platforms != null && !platforms.isEmpty() )) { + for (Platform platform : platforms) { + if (platform.getServerId().equals(userSetting.getServerId())) { + try { + commanderForPlatform.sendAlarmMessage(platform, deviceAlarm); + } catch (InvalidArgumentException | SipException | ParseException e) { + log.error("[命令发送失败] 发送报警: {}", e.getMessage()); + } + } + } + } else { + log.warn("[REDIS的ALARM通知] 未查询到" + chanelId + "所属的平台或设备"); + } + } + } catch (Exception e) { + log.error("未处理的异常 ", e); + log.warn("[REDIS的ALARM通知] 发现未处理的异常, {}", e.getMessage()); + } + } + } +} + diff --git a/src/main/java/com/genersoft/iot/vmp/service/redisMsg/RedisCloseStreamMsgListener.java b/src/main/java/com/genersoft/iot/vmp/service/redisMsg/RedisCloseStreamMsgListener.java new file mode 100755 index 0000000..f0ba942 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/service/redisMsg/RedisCloseStreamMsgListener.java @@ -0,0 +1,66 @@ +package com.genersoft.iot.vmp.service.redisMsg; + +import com.alibaba.fastjson2.JSON; +import com.alibaba.fastjson2.JSONObject; +import com.genersoft.iot.vmp.streamPush.service.IStreamPushService; +import lombok.extern.slf4j.Slf4j; +import org.jetbrains.annotations.NotNull; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.connection.Message; +import org.springframework.data.redis.connection.MessageListener; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ConcurrentLinkedQueue; + +/** + * 接收来自redis的关闭流更新通知 + * 消息举例: PUBLISH VM_MSG_STREAM_PUSH_CLOSE "{'app': 'live', 'stream': 'stream'}" + * @author lin + */ +@Slf4j +@Component +public class RedisCloseStreamMsgListener implements MessageListener { + + @Autowired + private IStreamPushService pushService; + + private final ConcurrentLinkedQueue taskQueue = new ConcurrentLinkedQueue<>(); + + @Override + public void onMessage(@NotNull Message message, byte[] bytes) { + log.info("[REDIS: 关闭流]: {}", new String(message.getBody())); + taskQueue.offer(message); + } + + @Scheduled(fixedDelay = 100) + public void executeTaskQueue() { + if (taskQueue.isEmpty()) { + return; + } + List messageDataList = new ArrayList<>(); + int size = taskQueue.size(); + for (int i = 0; i < size; i++) { + Message msg = taskQueue.poll(); + if (msg != null) { + messageDataList.add(msg); + } + } + if (messageDataList.isEmpty()) { + return; + } + for (Message msg : messageDataList) { + try { + JSONObject jsonObject = JSON.parseObject(msg.getBody()); + String app = jsonObject.getString("app"); + String stream = jsonObject.getString("stream"); + pushService.stopByAppAndStream(app, stream); + }catch (Exception e) { + log.warn("[REDIS的关闭推流通知] 发现未处理的异常, \r\n{}", JSON.toJSONString(msg)); + log.error("[REDIS的关闭推流通知] 异常内容: ", e); + } + } + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/service/redisMsg/RedisGpsMsgListener.java b/src/main/java/com/genersoft/iot/vmp/service/redisMsg/RedisGpsMsgListener.java new file mode 100755 index 0000000..ad6bfe5 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/service/redisMsg/RedisGpsMsgListener.java @@ -0,0 +1,101 @@ +package com.genersoft.iot.vmp.service.redisMsg; + +import com.alibaba.fastjson2.JSON; +import com.genersoft.iot.vmp.gb28181.service.IGbChannelService; +import com.genersoft.iot.vmp.service.bean.GPSMsgInfo; +import com.genersoft.iot.vmp.storager.IRedisCatchStorage; +import com.genersoft.iot.vmp.streamPush.service.IStreamPushService; +import com.genersoft.iot.vmp.utils.DateUtil; +import lombok.extern.slf4j.Slf4j; +import org.jetbrains.annotations.NotNull; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.connection.Message; +import org.springframework.data.redis.connection.MessageListener; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +/** + * 接收来自redis的GPS更新通知 + * + * @author lin + * 监听: SUBSCRIBE VM_MSG_GPS + * 发布 PUBLISH VM_MSG_GPS '{"messageId":"1727228507555","id":"24212345671381000047","lng":116.30307666666667,"lat":40.03295833333333,"time":"2024-09-25T09:41:47","direction":"56.0","speed":0.0,"altitude":60.0,"unitNo":"100000000","memberNo":"10000047"}' + */ +@Slf4j +@Component +public class RedisGpsMsgListener implements MessageListener { + + @Autowired + private IRedisCatchStorage redisCatchStorage; + + @Autowired + private IStreamPushService streamPushService; + + @Autowired + private IGbChannelService channelService; + + private final ConcurrentLinkedQueue taskQueue = new ConcurrentLinkedQueue<>(); + + + @Override + public void onMessage(@NotNull Message message, byte[] bytes) { + log.debug("[REDIS: GPS]: {}", new String(message.getBody())); + taskQueue.offer(message); + } + + @Scheduled(fixedDelay = 200, timeUnit = TimeUnit.MILLISECONDS) //每400毫秒执行一次 + public void executeTaskQueue() { + if (taskQueue.isEmpty()) { + return; + } + List messageDataList = new ArrayList<>(); + int size = taskQueue.size(); + for (int i = 0; i < size; i++) { + Message msg = taskQueue.poll(); + if (msg != null) { + messageDataList.add(msg); + } + } + if (messageDataList.isEmpty()) { + return; + } + for (Message msg : messageDataList) { + try { + GPSMsgInfo gpsMsgInfo = JSON.parseObject(msg.getBody(), GPSMsgInfo.class); + gpsMsgInfo.setStored(false); + gpsMsgInfo.setTime(DateUtil.ISO8601Toyyyy_MM_dd_HH_mm_ss(gpsMsgInfo.getTime())); + log.info("[REDIS的位置变化通知], {}", JSON.toJSONString(gpsMsgInfo)); + // 只是放入redis缓存起来 + redisCatchStorage.updateGpsMsgInfo(gpsMsgInfo); + } catch (Exception e) { + log.warn("[REDIS的位置变化通知] 发现未处理的异常, \r\n{}", JSON.toJSONString(msg)); + log.error("[REDIS的位置变化通知] 异常内容: ", e); + } + } + } + + /** + * 定时将经纬度更新到数据库 + */ + @Scheduled(fixedDelay = 2, timeUnit = TimeUnit.SECONDS) //每2秒执行一次 + public void execute() { + // 需要查询到 + List gpsMsgInfoList = redisCatchStorage.getAllGpsMsgInfo(); + if (!gpsMsgInfoList.isEmpty()) { + gpsMsgInfoList = gpsMsgInfoList.stream().filter(gpsMsgInfo -> !gpsMsgInfo.isStored()).collect(Collectors.toList());; + if (!gpsMsgInfoList.isEmpty()) { + channelService.updateGPSFromGPSMsgInfo(gpsMsgInfoList); + for (GPSMsgInfo msgInfo : gpsMsgInfoList) { + msgInfo.setStored(true); + redisCatchStorage.updateGpsMsgInfo(msgInfo); + } + } + } + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/service/redisMsg/RedisGroupChangeListener.java b/src/main/java/com/genersoft/iot/vmp/service/redisMsg/RedisGroupChangeListener.java new file mode 100755 index 0000000..d6e8e4a --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/service/redisMsg/RedisGroupChangeListener.java @@ -0,0 +1,264 @@ +package com.genersoft.iot.vmp.service.redisMsg; + +import com.alibaba.fastjson2.JSON; +import com.genersoft.iot.vmp.common.VideoManagerConstants; +import com.genersoft.iot.vmp.conf.SipConfig; +import com.genersoft.iot.vmp.conf.UserSetting; +import com.genersoft.iot.vmp.gb28181.bean.Group; +import com.genersoft.iot.vmp.gb28181.bean.RedisGroupMessage; +import com.genersoft.iot.vmp.gb28181.service.IGroupService; +import com.genersoft.iot.vmp.streamPush.service.IStreamPushService; +import com.genersoft.iot.vmp.utils.DateUtil; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.RandomStringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.connection.Message; +import org.springframework.data.redis.connection.MessageListener; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; +import org.springframework.util.ObjectUtils; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ConcurrentLinkedQueue; + +/** + * @Auther: JiangFeng + * @Date: 2022/8/16 11:32 + * @Description: 接收redis发送的推流设备列表更新通知 + * 监听: SUBSCRIBE VM_MSG_GROUP_LIST_CHANGE + * 发布 PUBLISH VM_MSG_GROUP_LIST_CHANGE '[{"groupName":"测试域修改新","topGroupGAlias":3,"messageType":"update","groupAlias":3}]' + */ +@Slf4j +@Component +public class RedisGroupChangeListener implements MessageListener { + + @Resource + private IGroupService groupService; + + @Resource + private IStreamPushService streamPushService; + + @Autowired + private UserSetting userSetting; + + @Autowired + private SipConfig sipConfig; + + private final ConcurrentLinkedQueue taskQueue = new ConcurrentLinkedQueue<>(); + + @Override + public void onMessage(Message message, byte[] bytes) { + log.info("[REDIS-分组信息改变] key: {}, : {}", VideoManagerConstants.VM_MSG_GROUP_LIST_CHANGE, new String(message.getBody())); + taskQueue.offer(message); + } + + @Scheduled(fixedDelay = 100) + public void executeTaskQueue() { + if (taskQueue.isEmpty()) { + return; + } + List messageDataList = new ArrayList<>(); + int size = taskQueue.size(); + for (int i = 0; i < size; i++) { + Message msg = taskQueue.poll(); + if (msg != null) { + messageDataList.add(msg); + } + } + if (messageDataList.isEmpty()) { + return; + } + for (Message msg : messageDataList) { + try { + List groupMessages = JSON.parseArray(new String(msg.getBody()), RedisGroupMessage.class); + for (int i = 0; i < groupMessages.size(); i++) { + RedisGroupMessage groupMessage = groupMessages.get(i); + log.info("[REDIS消息-分组信息更新] {}", groupMessage.toString()); + switch (groupMessage.getMessageType()){ + case "add": + if (!userSetting.isUseAliasForGroupSync()) { + if (groupMessage.getGroupGbId() == null) { + log.info("[REDIS消息-分组信息新增] 分组编号未设置,{}", groupMessage.toString()); + continue; + } + Group group = groupService.queryGroupByDeviceId(groupMessage.getGroupGbId()); + if (group != null) { + log.info("[REDIS消息-分组信息新增] 失败 {},编号已经存在", groupMessage.getGroupGbId()); + continue; + } + if (ObjectUtils.isEmpty(groupMessage.getGroupName()) + || ObjectUtils.isEmpty(groupMessage.getTopGroupGbId()) ){ + log.info("[REDIS消息-分组信息新增] 消息关键字段缺失, {}", groupMessage.toString()); + continue; + } + group = new Group(); + group.setDeviceId(groupMessage.getGroupGbId()); + group.setAlias(groupMessage.getGroupAlias()); + group.setParentDeviceId(groupMessage.getParentGroupGbId()); + group.setBusinessGroup(groupMessage.getTopGroupGbId()); + group.setCreateTime(DateUtil.getNow()); + group.setUpdateTime(DateUtil.getNow()); + groupService.add(group); + + }else { + // 此处使用别名作为判断依据,别名此处常常是分组在第三方系统里的唯一ID + if (groupMessage.getGroupAlias() == null || ObjectUtils.isEmpty(groupMessage.getGroupName()) + || ObjectUtils.isEmpty(groupMessage.getTopGroupGAlias())) { + log.info("[REDIS消息-分组信息新增] 消息关键字段缺失, {}", groupMessage.toString()); + continue; + } + Group group = groupService.queryGroupByAlias(groupMessage.getGroupAlias()); + if (group != null) { + log.info("[REDIS消息-分组信息新增] 失败 {},别名已经存在", groupMessage.getGroupGbId()); + continue; + } + group = new Group(); + boolean isTop = groupMessage.getTopGroupGAlias().equals(groupMessage.getGroupAlias()); + String deviceId = buildGroupDeviceId(isTop); + group.setDeviceId(deviceId); + group.setAlias(groupMessage.getGroupAlias()); + group.setName(groupMessage.getGroupName()); + if (!isTop) { + if (ObjectUtils.isEmpty(groupMessage.getTopGroupGAlias()) ) { + log.info("[REDIS消息-分组信息新增] 消息缺失业务分组别名或者父节点别名, {}", groupMessage.toString()); + continue; + } + + Group topGroup = groupService.queryGroupByAlias(groupMessage.getTopGroupGAlias()); + if (topGroup == null) { + log.info("[REDIS消息-分组信息新增] 业务分组信息未入库, {}", groupMessage.toString()); + continue; + } + group.setBusinessGroup(topGroup.getDeviceId()); + group.setParentId(topGroup.getId()); + } + if (groupMessage.getParentGAlias() != null) { + Group parentGroup = groupService.queryGroupByAlias(groupMessage.getParentGAlias()); + if (parentGroup == null) { + log.info("[REDIS消息-分组信息新增] 虚拟组织父节点信息未入库, {}", groupMessage.toString()); + continue; + } + group.setParentId(parentGroup.getId()); + group.setParentDeviceId(parentGroup.getDeviceId()); + } + group.setCreateTime(DateUtil.getNow()); + group.setUpdateTime(DateUtil.getNow()); + groupService.add(group); + } + + break; + case "update": + if (!userSetting.isUseAliasForGroupSync()) { + if (groupMessage.getGroupGbId() == null) { + log.info("[REDIS消息-分组信息更新] 分组编号未设置,{}", groupMessage.toString()); + continue; + } + Group group = groupService.queryGroupByDeviceId(groupMessage.getGroupGbId()); + if (group == null) { + log.info("[REDIS消息-分组信息更新] 失败 {},编号不存在", groupMessage.getGroupGbId()); + continue; + } + group.setDeviceId(groupMessage.getGroupGbId()); + group.setAlias(groupMessage.getGroupAlias()); + group.setParentDeviceId(groupMessage.getParentGroupGbId()); + group.setBusinessGroup(groupMessage.getTopGroupGbId()); + group.setUpdateTime(DateUtil.getNow()); + groupService.update(group); + }else { + // 此处使用别名作为判断依据,别名此处常常是分组在第三方系统里的唯一ID + if (groupMessage.getGroupAlias() == null) { + log.info("[REDIS消息-分组信息更新] 消息关键字段缺失, {}", groupMessage.toString()); + continue; + } + Group group = groupService.queryGroupByAlias(groupMessage.getGroupAlias()); + if (group == null ) { + log.info("[REDIS消息-分组信息更新] 失败 {},别名不存在", groupMessage.getGroupAlias()); + continue; + } + group.setName(groupMessage.getGroupName()); + group.setUpdateTime(DateUtil.getNow()); + if (groupMessage.getParentGAlias() != null) { + Group parentGroup = groupService.queryGroupByAlias(groupMessage.getParentGAlias()); + if (parentGroup == null) { + log.info("[REDIS消息-分组信息更新] 虚拟组织父节点信息未入库, {}", groupMessage.toString()); + continue; + } + group.setParentId(parentGroup.getId()); + group.setParentDeviceId(parentGroup.getDeviceId()); + }else { + Group businessGroup = groupService.queryGroupByDeviceId(group.getBusinessGroup()); + if (businessGroup == null ) { + log.info("[REDIS消息-分组信息更新] 失败 {},业务分组不存在", groupMessage.getGroupAlias()); + continue; + } + group.setParentId(businessGroup.getId()); + group.setParentDeviceId(null); + } + groupService.update(group); + } + break; + case "delete": + if (!userSetting.isUseAliasForGroupSync()) { + if (groupMessage.getGroupGbId() == null) { + log.info("[REDIS消息-分组信息删除] 分组编号未设置,{}", groupMessage.toString()); + continue; + } + Group group = groupService.queryGroupByDeviceId(groupMessage.getGroupGbId()); + if (group == null) { + log.info("[REDIS消息-分组信息删除] 失败 {},编号不存在", groupMessage.getGroupGbId()); + continue; + } + groupService.delete(group.getId()); + }else { + // 此处使用别名作为判断依据,别名此处常常是分组在第三方系统里的唯一ID + if (groupMessage.getGroupAlias() == null) { + log.info("[REDIS消息-分组信息删除] 消息关键字段缺失, {}", groupMessage.toString()); + continue; + } + Group group = groupService.queryGroupByAlias(groupMessage.getGroupAlias()); + if (group == null) { + log.info("[REDIS消息-分组信息删除] 失败 {},别名不存在", groupMessage.getGroupAlias()); + continue; + } + groupService.delete(group.getId()); + } + break; + default: + log.info("[REDIS消息-分组信息改变] 未识别的消息类型 {},目前支持的消息类型为 add、update、delete", groupMessage.getMessageType()); + } + } + + } catch (Exception e) { + log.warn("[REDIS消息-业务分组同步回复] 发现未处理的异常, \r\n{}", new String(msg.getBody())); + log.error("[REDIS消息-业务分组同步回复] 异常内容: ", e); + } + } + + } + + /** + * 生成分组国标编号 + */ + private String buildGroupDeviceId(boolean isTop) { + try { + String deviceTemplate = userSetting.getGroupSyncDeviceTemplate(); + if (ObjectUtils.isEmpty(deviceTemplate) || !deviceTemplate.contains("%s")) { + String domain = sipConfig.getDomain(); + if (domain.length() != 10) { + domain = sipConfig.getId().substring(0, 10); + } + deviceTemplate = domain + "%s0%s"; + } + String codeType = "216"; + if (isTop) { + codeType = "215"; + } + return String.format(deviceTemplate, codeType, RandomStringUtils.secureStrong().next(6, false, true)); + }catch (Exception e) { + log.error("[REDIS消息-业务分组同步回复] 构建新的分组编号失败", e); + return null; + } + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/service/redisMsg/RedisGroupMsgListener.java b/src/main/java/com/genersoft/iot/vmp/service/redisMsg/RedisGroupMsgListener.java new file mode 100755 index 0000000..bfa4a5d --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/service/redisMsg/RedisGroupMsgListener.java @@ -0,0 +1,200 @@ +package com.genersoft.iot.vmp.service.redisMsg; + +import com.alibaba.fastjson2.JSON; +import com.genersoft.iot.vmp.common.VideoManagerConstants; +import com.genersoft.iot.vmp.conf.SipConfig; +import com.genersoft.iot.vmp.conf.UserSetting; +import com.genersoft.iot.vmp.conf.exception.ControllerException; +import com.genersoft.iot.vmp.gb28181.bean.Group; +import com.genersoft.iot.vmp.gb28181.bean.RedisGroupMessage; +import com.genersoft.iot.vmp.gb28181.service.IGroupService; +import com.genersoft.iot.vmp.streamPush.service.IStreamPushService; +import com.genersoft.iot.vmp.utils.DateUtil; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.RandomStringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.connection.Message; +import org.springframework.data.redis.connection.MessageListener; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; +import org.springframework.util.ObjectUtils; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ConcurrentLinkedQueue; + +/** + * 接收redis发送的推流设备列表更新通知 + * 监听: SUBSCRIBE VM_MSG_GROUP_LIST_RESPONSE + * 发布 PUBLISH VM_MSG_GROUP_LIST_RESPONSE '[{"groupName":"研发AAS","topGroupGAlias":"6","groupAlias":"6"},{"groupName":"测试AAS","topGroupGAlias":"5","groupAlias":"5"},{"groupName":"研发2","topGroupGAlias":"4","groupAlias":"4"},{"groupName":"啊实打实的","topGroupGAlias":"4","groupAlias":"100000009"},{"groupName":"测试域","topGroupGAlias":"3","groupAlias":"3"},{"groupName":"结构1","topGroupGAlias":"3","groupAlias":"100000000"},{"groupName":"结构1-1","topGroupGAlias":"3","parentGAlias":"100000000","groupAlias":"100000001"},{"groupName":"结构2-2","topGroupGAlias":"3","groupAlias":"100000002"},{"groupName":"结构1-2","topGroupGAlias":"3","parentGAlias":"100000001","groupAlias":"100000003"},{"groupName":"结构1-3","topGroupGAlias":"3","parentGAlias":"100000003","groupAlias":"100000004"},{"groupName":"研发1","topGroupGAlias":"3","groupAlias":"100000005"},{"groupName":"研发3","topGroupGAlias":"3","parentGAlias":"100000005","groupAlias":"100000006"},{"groupName":"测试42","topGroupGAlias":"3","parentGAlias":"100000000","groupAlias":"100000010"},{"groupName":"测试2","topGroupGAlias":"3","parentGAlias":"100000000","groupAlias":"100000011"},{"groupName":"测试3","topGroupGAlias":"3","parentGAlias":"100000000","groupAlias":"100000007"},{"groupName":"测试4","topGroupGAlias":"3","parentGAlias":"100000007","groupAlias":"100000008"}]' + */ +@Slf4j +@Component +public class RedisGroupMsgListener implements MessageListener { + + @Resource + private IGroupService groupService; + + @Resource + private IStreamPushService streamPushService; + + @Autowired + private UserSetting userSetting; + + @Autowired + private SipConfig sipConfig; + + private final ConcurrentLinkedQueue taskQueue = new ConcurrentLinkedQueue<>(); + + @Override + public void onMessage(Message message, byte[] bytes) { + log.info("[REDIS: 业务分组同步回复] key: {}, : {}", VideoManagerConstants.VM_MSG_GROUP_LIST_RESPONSE, new String(message.getBody())); + taskQueue.offer(message); + } + + @Scheduled(fixedDelay = 100) + public void executeTaskQueue() { + if (taskQueue.isEmpty()) { + return; + } + List messageDataList = new ArrayList<>(); + int size = taskQueue.size(); + for (int i = 0; i < size; i++) { + Message msg = taskQueue.poll(); + if (msg != null) { + messageDataList.add(msg); + } + } + if (messageDataList.isEmpty()) { + return; + } + if (userSetting.isUseAliasForGroupSync()) { + log.info("[REDIS消息-业务分组同步回复] 使用别名作为唯一ID解析分组消息"); + } + for (Message msg : messageDataList) { + try { + List groupMessages = JSON.parseArray(new String(msg.getBody()), RedisGroupMessage.class); + for (int i = 0; i < groupMessages.size(); i++) { + RedisGroupMessage groupMessage = groupMessages.get(i); + log.info("[REDIS消息-业务分组同步回复] {}", groupMessage.toString()); + if (!userSetting.isUseAliasForGroupSync()) { + if (groupMessage.getGroupGbId() == null) { + log.warn("[REDIS消息-业务分组同步回复] 分组编号未设置,{}", groupMessage.toString()); + continue; + } + Group group = groupService.queryGroupByDeviceId(groupMessage.getGroupGbId()); + if (group == null ) { + if (ObjectUtils.isEmpty(groupMessage.getGroupName()) + || ObjectUtils.isEmpty(groupMessage.getTopGroupGbId()) ){ + log.info("[REDIS消息-业务分组同步回复] 消息关键字段缺失, {}", groupMessage.toString()); + continue; + } + group = new Group(); + group.setDeviceId(groupMessage.getGroupGbId()); + group.setAlias(groupMessage.getGroupAlias()); + group.setParentDeviceId(groupMessage.getParentGroupGbId()); + group.setBusinessGroup(groupMessage.getTopGroupGbId()); + group.setCreateTime(DateUtil.getNow()); + group.setUpdateTime(DateUtil.getNow()); + groupService.add(group); + + }else { + group.setDeviceId(groupMessage.getGroupGbId()); + group.setAlias(groupMessage.getGroupAlias()); + group.setParentDeviceId(groupMessage.getParentGroupGbId()); + group.setBusinessGroup(groupMessage.getTopGroupGbId()); + group.setUpdateTime(DateUtil.getNow()); + groupService.update(group); + } + }else { + // 此处使用别名作为判断依据,别名此处常常是分组在第三方系统里的唯一ID + if (groupMessage.getGroupAlias() == null || ObjectUtils.isEmpty(groupMessage.getGroupName()) + || ObjectUtils.isEmpty(groupMessage.getTopGroupGAlias())) { + log.info("[REDIS消息-业务分组同步回复] 消息关键字段缺失, {}", groupMessage.toString()); + continue; + } + boolean isTop = groupMessage.getTopGroupGAlias().equals(groupMessage.getGroupAlias()); + Group group = groupService.queryGroupByAlias(groupMessage.getGroupAlias()); + if (group == null ) { + group = new Group(); + String deviceId = buildGroupDeviceId(isTop); + group.setDeviceId(deviceId); + group.setAlias(groupMessage.getGroupAlias()); + group.setName(groupMessage.getGroupName()); + group.setCreateTime(DateUtil.getNow()); + } + + if (!isTop) { + if (ObjectUtils.isEmpty(groupMessage.getTopGroupGAlias())) { + log.info("[REDIS消息-业务分组同步回复] 消息缺失业务分组别名, {}", groupMessage.toString()); + continue; + } + + Group topGroup = groupService.queryGroupByAlias(groupMessage.getTopGroupGAlias()); + if (topGroup == null) { + log.info("[REDIS消息-业务分组同步回复] 业务分组信息未入库, {}", groupMessage.toString()); + continue; + } + group.setBusinessGroup(topGroup.getDeviceId()); + if (groupMessage.getParentGAlias() != null) { + Group parentGroup = groupService.queryGroupByAlias(groupMessage.getParentGAlias()); + if (parentGroup == null) { + log.info("[REDIS消息-业务分组同步回复] 虚拟组织父节点信息未入库, {}", groupMessage.toString()); + continue; + } + group.setParentId(parentGroup.getId()); + group.setParentDeviceId(parentGroup.getDeviceId()); + }else { + group.setParentId(topGroup.getId()); + group.setParentDeviceId(null); + } + }else { + group.setParentId(null); + group.setParentDeviceId(null); + } + + group.setUpdateTime(DateUtil.getNow()); + if (group.getId() > 0) { + groupService.update(group); + }else { + groupService.add(group); + } + + } + } + + } catch (ControllerException e) { + log.warn("[REDIS消息-业务分组同步回复] 失败, \r\n{}", e.getMsg()); + }catch (Exception e) { + log.warn("[REDIS消息-业务分组同步回复] 发现未处理的异常, \r\n{}", new String(msg.getBody())); + log.error("[REDIS消息-业务分组同步回复] 异常内容: ", e); + } + } + + } + + /** + * 生成分组国标编号 + */ + private String buildGroupDeviceId(boolean isTop) { + try { + String deviceTemplate = userSetting.getGroupSyncDeviceTemplate(); + if (ObjectUtils.isEmpty(deviceTemplate) || !deviceTemplate.contains("%s")) { + String domain = sipConfig.getDomain(); + if (domain.length() != 10) { + domain = sipConfig.getId().substring(0, 10); + } + deviceTemplate = domain + "%s0%s"; + } + String codeType = "216"; + if (isTop) { + codeType = "215"; + } + return String.format(deviceTemplate, codeType, RandomStringUtils.secureStrong().next(6, false, true)); + }catch (Exception e) { + log.error("[REDIS消息-业务分组同步回复] 构建新的分组编号失败", e); + return null; + } + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/service/redisMsg/RedisPushStreamListMsgListener.java b/src/main/java/com/genersoft/iot/vmp/service/redisMsg/RedisPushStreamListMsgListener.java new file mode 100755 index 0000000..954aa37 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/service/redisMsg/RedisPushStreamListMsgListener.java @@ -0,0 +1,130 @@ +package com.genersoft.iot.vmp.service.redisMsg; + +import com.alibaba.fastjson2.JSON; +import com.alibaba.fastjson2.JSONObject; +import com.genersoft.iot.vmp.media.service.IMediaServerService; +import com.genersoft.iot.vmp.streamPush.bean.RedisPushStreamMessage; +import com.genersoft.iot.vmp.streamPush.bean.StreamPush; +import com.genersoft.iot.vmp.streamPush.service.IStreamPushService; +import com.genersoft.iot.vmp.utils.DateUtil; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.redis.connection.Message; +import org.springframework.data.redis.connection.MessageListener; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +import jakarta.annotation.Resource; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentLinkedQueue; + +/** + * @Auther: JiangFeng + * @Date: 2022/8/16 11:32 + * @Description: 接收redis发送的推流设备列表更新通知 + * 监听: SUBSCRIBE VM_MSG_PUSH_STREAM_LIST_CHANGE + * 发布 PUBLISH VM_MSG_PUSH_STREAM_LIST_CHANGE '[{"app":1000,"stream":10000000,"gbId":"12345678901234567890","name":"A6","status":false},{"app":1000,"stream":10000021,"gbId":"24212345671381000021","name":"终端9273","status":false},{"app":1000,"stream":10000022,"gbId":"24212345671381000022","name":"终端9434","status":true},{"app":1000,"stream":10000025,"gbId":"24212345671381000025","name":"华为M10","status":false},{"app":1000,"stream":10000051,"gbId":"11111111111381111122","name":"终端9720","status":false}]' + */ +@Slf4j +@Component +public class RedisPushStreamListMsgListener implements MessageListener { + + @Resource + private IMediaServerService mediaServerService; + + @Resource + private IStreamPushService streamPushService; + + private final ConcurrentLinkedQueue taskQueue = new ConcurrentLinkedQueue<>(); + + @Override + public void onMessage(Message message, byte[] bytes) { + log.info("[REDIS: 推流设备列表更新]: {}", new String(message.getBody())); + taskQueue.offer(message); + } + + @Scheduled(fixedDelay = 100) + public void executeTaskQueue() { + if (taskQueue.isEmpty()) { + return; + } + List messageDataList = new ArrayList<>(); + int size = taskQueue.size(); + for (int i = 0; i < size; i++) { + Message msg = taskQueue.poll(); + if (msg != null) { + messageDataList.add(msg); + } + } + if (messageDataList.isEmpty()) { + return; + } + for (Message msg : messageDataList) { + try { + List streamPushItems = JSON.parseArray(new String(msg.getBody()), RedisPushStreamMessage.class); + //查询全部的app+stream 用于判断是添加还是修改 + Map allAppAndStream = streamPushService.getAllAppAndStreamMap(); + Map allGBId = streamPushService.getAllGBId(); + + // 用于存储更具APP+Stream过滤后的数据,可以直接存入stream_push表与gb_stream表 + List streamPushItemForSave = new ArrayList<>(); + List streamPushItemForUpdate = new ArrayList<>(); + for (RedisPushStreamMessage pushStreamMessage : streamPushItems) { + String app = pushStreamMessage.getApp(); + String stream = pushStreamMessage.getStream(); + boolean contains = allAppAndStream.containsKey(app + stream); + //不存在就添加 + if (!contains) { + if (allGBId.containsKey(pushStreamMessage.getGbId())) { + StreamPush streamPushInDb = allGBId.get(pushStreamMessage.getGbId()); + log.warn("[REDIS消息-推流设备列表更新-INSERT] 国标编号重复: {}, 已分配给{}/{}", + streamPushInDb.getGbDeviceId(), streamPushInDb.getApp(), streamPushInDb.getStream()); + continue; + } + StreamPush streamPush = pushStreamMessage.buildstreamPush(); + streamPush.setCreateTime(DateUtil.getNow()); + streamPush.setUpdateTime(DateUtil.getNow()); + streamPush.setMediaServerId(mediaServerService.getDefaultMediaServer().getId()); + streamPushItemForSave.add(streamPush); + allGBId.put(streamPush.getGbDeviceId(), streamPush); + } else { + StreamPush streamPushForGbDeviceId = allGBId.get(pushStreamMessage.getGbId()); + if (streamPushForGbDeviceId != null + && (!streamPushForGbDeviceId.getApp().equals(pushStreamMessage.getApp()) + || !streamPushForGbDeviceId.getStream().equals(pushStreamMessage.getStream()))) { + StreamPush streamPushInDb = allGBId.get(pushStreamMessage.getGbId()); + log.warn("[REDIS消息-推流设备列表更新-UPDATE] 国标编号重复: {}, 已分配给{}/{}", + pushStreamMessage.getGbId(), streamPushInDb.getApp(), streamPushInDb.getStream()); + continue; + } + StreamPush streamPush = allAppAndStream.get(app + stream); + streamPush.setUpdateTime(DateUtil.getNow()); + streamPush.setGbDeviceId(pushStreamMessage.getGbId()); + streamPush.setGbName(pushStreamMessage.getName()); + if (pushStreamMessage.getStatus() != null) { + streamPush.setGbStatus(pushStreamMessage.getStatus() ? "ON" : "OFF"); + } + //存在就只修改 name和gbId + streamPushItemForUpdate.add(streamPush); + } + } + if (!streamPushItemForSave.isEmpty()) { + log.info("添加{}条", streamPushItemForSave.size()); + log.info(JSONObject.toJSONString(streamPushItemForSave)); + streamPushService.batchAdd(streamPushItemForSave); + + } + if (!streamPushItemForUpdate.isEmpty()) { + log.info("修改{}条", streamPushItemForUpdate.size()); + log.info(JSONObject.toJSONString(streamPushItemForUpdate)); + streamPushService.batchUpdate(streamPushItemForUpdate); + } + } catch (Exception e) { + log.warn("[REDIS消息-推流设备列表更新] 发现未处理的异常, \r\n{}", new String(msg.getBody())); + log.error("[REDIS消息-推流设备列表更新] 异常内容: ", e); + } + } + + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/service/redisMsg/RedisPushStreamResponseListener.java b/src/main/java/com/genersoft/iot/vmp/service/redisMsg/RedisPushStreamResponseListener.java new file mode 100644 index 0000000..f8a78dc --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/service/redisMsg/RedisPushStreamResponseListener.java @@ -0,0 +1,90 @@ +package com.genersoft.iot.vmp.service.redisMsg; + +import com.alibaba.fastjson2.JSON; +import com.genersoft.iot.vmp.service.bean.MessageForPushChannelResponse; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.data.redis.connection.Message; +import org.springframework.data.redis.connection.MessageListener; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; +import org.springframework.stereotype.Component; +import org.springframework.util.ObjectUtils; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentLinkedQueue; + +/** + * 接收redis返回的推流结果 + * + * @author lin + * PUBLISH VM_MSG_STREAM_PUSH_RESPONSE '{"code":0,"msg":"失败","app":"1000","stream":"10000022"}' + */ +@Slf4j +@Component +public class RedisPushStreamResponseListener implements MessageListener { + + private ConcurrentLinkedQueue taskQueue = new ConcurrentLinkedQueue<>(); + + @Qualifier("taskExecutor") + @Autowired + private ThreadPoolTaskExecutor taskExecutor; + + private final Map responseEvents = new ConcurrentHashMap<>(); + + public interface PushStreamResponseEvent { + void run(MessageForPushChannelResponse response); + } + + @Override + public void onMessage(Message message, byte[] bytes) { + log.info("[REDIS: 推流结果]: {}", new String(message.getBody())); + taskQueue.offer(message); + } + + @Scheduled(fixedDelay = 100) + public void executeTaskQueue() { + if (taskQueue.isEmpty()) { + return; + } + List messageDataList = new ArrayList<>(); + int size = taskQueue.size(); + for (int i = 0; i < size; i++) { + Message msg = taskQueue.poll(); + if (msg != null) { + messageDataList.add(msg); + } + } + if (messageDataList.isEmpty()) { + return; + } + for (Message msg : messageDataList) { + try { + MessageForPushChannelResponse response = JSON.parseObject(new String(msg.getBody()), MessageForPushChannelResponse.class); + if (response == null || ObjectUtils.isEmpty(response.getApp()) || ObjectUtils.isEmpty(response.getStream())) { + log.info("[REDIS消息-请求推流结果]:参数不全"); + continue; + } + // 查看正在等待的invite消息 + if (responseEvents.get(response.getApp() + response.getStream()) != null) { + responseEvents.get(response.getApp() + response.getStream()).run(response); + } + } catch (Exception e) { + log.warn("[REDIS消息-请求推流结果] 发现未处理的异常, \r\n{}", JSON.toJSONString(msg)); + log.error("[REDIS消息-请求推流结果] 异常内容: ", e); + } + } + } + + public void addEvent(String app, String stream, PushStreamResponseEvent callback) { + responseEvents.put(app + stream, callback); + } + + public void removeEvent(String app, String stream) { + responseEvents.remove(app + stream); + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/service/redisMsg/RedisPushStreamStatusMsgListener.java b/src/main/java/com/genersoft/iot/vmp/service/redisMsg/RedisPushStreamStatusMsgListener.java new file mode 100755 index 0000000..e3d4e96 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/service/redisMsg/RedisPushStreamStatusMsgListener.java @@ -0,0 +1,122 @@ +package com.genersoft.iot.vmp.service.redisMsg; + +import com.alibaba.fastjson2.JSON; +import com.genersoft.iot.vmp.common.VideoManagerConstants; +import com.genersoft.iot.vmp.conf.DynamicTask; +import com.genersoft.iot.vmp.conf.UserSetting; +import com.genersoft.iot.vmp.service.bean.PushStreamStatusChangeFromRedisDto; +import com.genersoft.iot.vmp.storager.IRedisCatchStorage; +import com.genersoft.iot.vmp.streamPush.service.IStreamPushService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.ApplicationArguments; +import org.springframework.boot.ApplicationRunner; +import org.springframework.data.redis.connection.Message; +import org.springframework.data.redis.connection.MessageListener; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ConcurrentLinkedQueue; + + +/** + * 接收redis发送的推流设备上线下线通知 + * + * @author lin + * 发送 PUBLISH VM_MSG_PUSH_STREAM_STATUS_CHANGE '{"setAllOffline":false,"offlineStreams":[{"app":"1000","stream":"10000022","timeStamp":1726729716551}]}' + * 订阅 SUBSCRIBE VM_MSG_PUSH_STREAM_STATUS_CHANGE + */ +@Slf4j +@Component +public class RedisPushStreamStatusMsgListener implements MessageListener, ApplicationRunner { + + @Autowired + private IRedisCatchStorage redisCatchStorage; + + @Autowired + private IStreamPushService streamPushService; + + @Autowired + private DynamicTask dynamicTask; + + @Autowired + private UserSetting userSetting; + + private final ConcurrentLinkedQueue taskQueue = new ConcurrentLinkedQueue<>(); + + @Override + public void onMessage(Message message, byte[] bytes) { + log.info("[REDIS: 推流设备状态变化]: {}", new String(message.getBody())); + taskQueue.offer(message); + } + + @Scheduled(fixedDelay = 100) + public void executeTaskQueue() { + if (taskQueue.isEmpty()) { + return; + } + List messageDataList = new ArrayList<>(); + int size = taskQueue.size(); + for (int i = 0; i < size; i++) { + Message msg = taskQueue.poll(); + if (msg != null) { + messageDataList.add(msg); + } + } + if (messageDataList.isEmpty()) { + return; + } + for (Message msg : messageDataList) { + try { + PushStreamStatusChangeFromRedisDto streamStatusMessage = JSON.parseObject(msg.getBody(), PushStreamStatusChangeFromRedisDto.class); + if (streamStatusMessage == null) { + log.warn("[REDIS消息]推流设备状态变化消息解析失败"); + continue; + } + // 取消定时任务 + dynamicTask.stop(VideoManagerConstants.VM_MSG_GET_ALL_ONLINE_REQUESTED); + if (streamStatusMessage.isSetAllOffline()) { + // 所有设备离线 + streamPushService.allOffline(); + } + if (streamStatusMessage.getOfflineStreams() != null + && !streamStatusMessage.getOfflineStreams().isEmpty()) { + // 更新部分设备离线 + log.info("[REDIS: 推流设备状态变化] 更新部分设备离线: {}个", streamStatusMessage.getOfflineStreams().size()); + streamPushService.offline(streamStatusMessage.getOfflineStreams()); + } + if (streamStatusMessage.getOnlineStreams() != null && + !streamStatusMessage.getOnlineStreams().isEmpty()) { + // 更新部分设备上线 + log.info("[REDIS: 推流设备状态变化] 更新部分设备上线: {}个", streamStatusMessage.getOnlineStreams().size()); + streamPushService.online(streamStatusMessage.getOnlineStreams()); + } + } catch (Exception e) { + log.warn("[REDIS消息-推流设备状态变化] 发现未处理的异常, \r\n{}", JSON.parseObject(msg.getBody())); + log.error("[REDIS消息-推流设备状态变化] 异常内容: ", e); + } + } + } + + @Override + public void run(ApplicationArguments args) throws Exception { + if (userSetting.getUsePushingAsStatus()) { + return; + } + // 查询是否存在推流设备,没有则不发送 + List allAppAndStream = streamPushService.getAllAppAndStream(); + if (allAppAndStream == null || allAppAndStream.isEmpty()) { + return; + } + // 启动时设置所有推流通道离线,发起查询请求 + redisCatchStorage.sendStreamPushRequestedMsgForStatus(); + dynamicTask.startDelay(VideoManagerConstants.VM_MSG_GET_ALL_ONLINE_REQUESTED, () -> { + log.info("[REDIS消息]未收到redis回复推流设备状态,执行推流设备离线"); + // 五秒收不到请求就设置通道离线,然后通知上级离线 + streamPushService.allOffline(); + }, 5000); + } + +} diff --git a/src/main/java/com/genersoft/iot/vmp/service/redisMsg/control/RedisRpcChannelPlayController.java b/src/main/java/com/genersoft/iot/vmp/service/redisMsg/control/RedisRpcChannelPlayController.java new file mode 100644 index 0000000..acd8e81 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/service/redisMsg/control/RedisRpcChannelPlayController.java @@ -0,0 +1,345 @@ +package com.genersoft.iot.vmp.service.redisMsg.control; + +import com.alibaba.fastjson2.JSONObject; +import com.genersoft.iot.vmp.common.InviteSessionType; +import com.genersoft.iot.vmp.conf.UserSetting; +import com.genersoft.iot.vmp.conf.exception.ControllerException; +import com.genersoft.iot.vmp.conf.redis.RedisRpcConfig; +import com.genersoft.iot.vmp.conf.redis.bean.RedisRpcMessage; +import com.genersoft.iot.vmp.conf.redis.bean.RedisRpcRequest; +import com.genersoft.iot.vmp.conf.redis.bean.RedisRpcResponse; +import com.genersoft.iot.vmp.gb28181.bean.CommonGBChannel; +import com.genersoft.iot.vmp.gb28181.bean.InviteMessageInfo; +import com.genersoft.iot.vmp.gb28181.service.IGbChannelPlayService; +import com.genersoft.iot.vmp.gb28181.service.IGbChannelService; +import com.genersoft.iot.vmp.gb28181.service.IPTZService; +import com.genersoft.iot.vmp.service.bean.InviteErrorCode; +import com.genersoft.iot.vmp.service.redisMsg.dto.RedisRpcController; +import com.genersoft.iot.vmp.service.redisMsg.dto.RedisRpcMapping; +import com.genersoft.iot.vmp.service.redisMsg.dto.RpcController; +import com.genersoft.iot.vmp.utils.DateUtil; +import com.genersoft.iot.vmp.vmanager.bean.ErrorCode; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Component; + +import javax.sip.message.Response; + +@Component +@Slf4j +@RedisRpcController("channel") +public class RedisRpcChannelPlayController extends RpcController { + + @Autowired + private UserSetting userSetting; + + @Autowired + private RedisTemplate redisTemplate; + + @Autowired + private IGbChannelService channelService; + + @Autowired + private IGbChannelPlayService channelPlayService; + + @Autowired + private IPTZService iptzService; + + private void sendResponse(RedisRpcResponse response){ + log.info("[redis-rpc] >> {}", response); + response.setToId(userSetting.getServerId()); + RedisRpcMessage message = new RedisRpcMessage(); + message.setResponse(response); + redisTemplate.convertAndSend(RedisRpcConfig.REDIS_REQUEST_CHANNEL_KEY, message); + } + + + /** + * 点播国标设备 + */ + @RedisRpcMapping("play") + public RedisRpcResponse playChannel(RedisRpcRequest request) { + int channelId = Integer.parseInt(request.getParam().toString()); + RedisRpcResponse response = request.getResponse(); + + if (channelId <= 0) { + response.setStatusCode(ErrorCode.ERROR400.getCode()); + response.setBody("param error"); + return response; + } + // 获取对应的设备和通道信息 + CommonGBChannel channel = channelService.getOne(channelId); + if (channel == null) { + response.setStatusCode(ErrorCode.ERROR400.getCode()); + response.setBody("param error"); + return response; + } + + InviteMessageInfo inviteInfo = new InviteMessageInfo(); + inviteInfo.setSessionName("Play"); + channelPlayService.startInvite(channel, inviteInfo, null, (code, msg, data) ->{ + if (code == InviteErrorCode.SUCCESS.getCode()) { + response.setStatusCode(ErrorCode.SUCCESS.getCode()); + response.setBody(data); + }else { + response.setStatusCode(code); + } + // 手动发送结果 + sendResponse(response); + }); + return null; + } + + + /** + * 点播国标设备 + */ + @RedisRpcMapping("queryRecordInfo") + public RedisRpcResponse queryRecordInfo(RedisRpcRequest request) { + JSONObject paramJson = JSONObject.parseObject(request.getParam().toString()); + int channelId = paramJson.getIntValue("channelId"); + String startTime = paramJson.getString("startTime"); + String endTime = paramJson.getString("endTime"); + RedisRpcResponse response = request.getResponse(); + + if (channelId <= 0) { + response.setStatusCode(ErrorCode.ERROR400.getCode()); + response.setBody("param error"); + return response; + } + // 获取对应的设备和通道信息 + CommonGBChannel channel = channelService.getOne(channelId); + if (channel == null) { + response.setStatusCode(ErrorCode.ERROR400.getCode()); + response.setBody("param error"); + return response; + } + + try { + channelPlayService.queryRecord(channel, startTime, endTime, (code, msg, data) ->{ + if (code == InviteErrorCode.SUCCESS.getCode()) { + response.setStatusCode(code); + response.setBody(data); + }else { + response.setStatusCode(code); + } + // 手动发送结果 + sendResponse(response); + }); + }catch (ControllerException e) { + response.setStatusCode(ErrorCode.ERROR100.getCode()); + response.setBody(e.getMessage()); + } + + return null; + } + + /** + * 暂停录像回放 + */ + @RedisRpcMapping("playbackPause") + public RedisRpcResponse playbackPause(RedisRpcRequest request) { + String streamId = request.getParam().toString(); + RedisRpcResponse response = request.getResponse(); + + if (streamId == null) { + response.setStatusCode(ErrorCode.ERROR400.getCode()); + response.setBody("param error"); + return response; + } + + try { +// channelPlayService.playbackPause(streamId); + response.setStatusCode(ErrorCode.SUCCESS.getCode()); + }catch (ControllerException e) { + response.setStatusCode(ErrorCode.ERROR100.getCode()); + response.setBody(e.getMessage()); + } + + return response; + } + + /** + * 恢复录像回放 + */ + @RedisRpcMapping("playbackResume") + public RedisRpcResponse playbackResume(RedisRpcRequest request) { + String streamId = request.getParam().toString(); + RedisRpcResponse response = request.getResponse(); + + if (streamId == null) { + response.setStatusCode(ErrorCode.ERROR400.getCode()); + response.setBody("param error"); + return response; + } + + try { +// channelPlayService.playbackResume(streamId); + response.setStatusCode(ErrorCode.SUCCESS.getCode()); + }catch (ControllerException e) { + response.setStatusCode(ErrorCode.ERROR100.getCode()); + response.setBody(e.getMessage()); + } + + return response; + } + + + /** + * 停止点播国标设备 + */ + @RedisRpcMapping("stop") + public RedisRpcResponse stop(RedisRpcRequest request) { + JSONObject jsonObject = JSONObject.parseObject(request.getParam().toString()); + + RedisRpcResponse response = request.getResponse(); + + Integer channelId = jsonObject.getIntValue("channelId"); + if (channelId == null || channelId <= 0) { + response.setStatusCode(ErrorCode.ERROR400.getCode()); + response.setBody("param error"); + return response; + } + + String stream = jsonObject.getString("stream"); + InviteSessionType type = jsonObject.getObject("inviteSessionType", InviteSessionType.class); + + // 获取对应的设备和通道信息 + CommonGBChannel channel = channelService.getOne(channelId); + if (channel == null) { + response.setStatusCode(ErrorCode.ERROR400.getCode()); + response.setBody("param error"); + return response; + } + try { + channelPlayService.stopInvite(type, channel, stream); + response.setStatusCode(ErrorCode.SUCCESS.getCode()); + }catch (Exception e){ + response.setStatusCode(Response.SERVER_INTERNAL_ERROR); + response.setBody(e.getMessage()); + } + return response; + } + + /** + * 录像回放国标设备 + */ + @RedisRpcMapping("playback") + public RedisRpcResponse playbackChannel(RedisRpcRequest request) { + JSONObject paramJson = JSONObject.parseObject(request.getParam().toString()); + int channelId = paramJson.getIntValue("channelId"); + String startTime = paramJson.getString("startTime"); + String endTime = paramJson.getString("endTime"); + RedisRpcResponse response = request.getResponse(); + + if (channelId <= 0) { + response.setStatusCode(ErrorCode.ERROR400.getCode()); + response.setBody("param error"); + return response; + } + // 获取对应的设备和通道信息 + CommonGBChannel channel = channelService.getOne(channelId); + if (channel == null) { + response.setStatusCode(ErrorCode.ERROR400.getCode()); + response.setBody("param error"); + return response; + } + + InviteMessageInfo inviteInfo = new InviteMessageInfo(); + inviteInfo.setSessionName("Playback"); + inviteInfo.setStartTime(DateUtil.yyyy_MM_dd_HH_mm_ssToTimestamp(startTime)); + inviteInfo.setStopTime(DateUtil.yyyy_MM_dd_HH_mm_ssToTimestamp(endTime)); + channelPlayService.startInvite(channel, inviteInfo, null, (code, msg, data) ->{ + if (code == InviteErrorCode.SUCCESS.getCode()) { + response.setStatusCode(ErrorCode.SUCCESS.getCode()); + response.setBody(data); + }else { + response.setStatusCode(code); + } + // 手动发送结果 + sendResponse(response); + }); + return null; + } + + /** + * 录像回放国标设备 + */ + @RedisRpcMapping("download") + public RedisRpcResponse downloadChannel(RedisRpcRequest request) { + JSONObject paramJson = JSONObject.parseObject(request.getParam().toString()); + int channelId = paramJson.getIntValue("channelId"); + String startTime = paramJson.getString("startTime"); + String endTime = paramJson.getString("endTime"); + int downloadSpeed = paramJson.getIntValue("downloadSpeed"); + RedisRpcResponse response = request.getResponse(); + + if (channelId <= 0) { + response.setStatusCode(ErrorCode.ERROR400.getCode()); + response.setBody("param error"); + return response; + } + // 获取对应的设备和通道信息 + CommonGBChannel channel = channelService.getOne(channelId); + if (channel == null) { + response.setStatusCode(ErrorCode.ERROR400.getCode()); + response.setBody("param error"); + return response; + } + + InviteMessageInfo inviteInfo = new InviteMessageInfo(); + inviteInfo.setSessionName("Download"); + inviteInfo.setStartTime(DateUtil.yyyy_MM_dd_HH_mm_ssToTimestamp(startTime)); + inviteInfo.setStopTime(DateUtil.yyyy_MM_dd_HH_mm_ssToTimestamp(endTime)); + inviteInfo.setDownloadSpeed(downloadSpeed + ""); + channelPlayService.startInvite(channel, inviteInfo, null, (code, msg, data) ->{ + if (code == InviteErrorCode.SUCCESS.getCode()) { + response.setStatusCode(ErrorCode.SUCCESS.getCode()); + response.setBody(data); + }else { + response.setStatusCode(code); + } + // 手动发送结果 + sendResponse(response); + }); + return null; + } + + /** + * 云台控制 + */ + @RedisRpcMapping("ptz/frontEndCommand") + public RedisRpcResponse frontEndCommand(RedisRpcRequest request) { + JSONObject paramJson = JSONObject.parseObject(request.getParam().toString()); + int channelId = paramJson.getIntValue("channelId"); + int cmdCode = paramJson.getIntValue("cmdCode"); + int parameter1 = paramJson.getIntValue("parameter1"); + int parameter2 = paramJson.getIntValue("parameter2"); + int combindCode2 = paramJson.getIntValue("combindCode2"); + + RedisRpcResponse response = request.getResponse(); + + if (channelId <= 0 || cmdCode < 0 || parameter1 < 0 || parameter2 < 0 || combindCode2 < 0) { + response.setStatusCode(ErrorCode.ERROR400.getCode()); + response.setBody("param error"); + return response; + } + // 获取对应的设备和通道信息 + CommonGBChannel channel = channelService.getOne(channelId); + if (channel == null) { + response.setStatusCode(ErrorCode.ERROR400.getCode()); + response.setBody("param error"); + return response; + } + try { + iptzService.frontEndCommand(channel, cmdCode, parameter1, parameter2, combindCode2); + }catch (ControllerException e) { + response.setStatusCode(ErrorCode.ERROR100.getCode()); + response.setBody(e.getMessage()); + return response; + } + response.setStatusCode(ErrorCode.SUCCESS.getCode()); + return response; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/service/redisMsg/control/RedisRpcCloudRecordController.java b/src/main/java/com/genersoft/iot/vmp/service/redisMsg/control/RedisRpcCloudRecordController.java new file mode 100644 index 0000000..ae6c5a1 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/service/redisMsg/control/RedisRpcCloudRecordController.java @@ -0,0 +1,66 @@ +package com.genersoft.iot.vmp.service.redisMsg.control; + +import com.alibaba.fastjson2.JSONObject; +import com.genersoft.iot.vmp.conf.UserSetting; +import com.genersoft.iot.vmp.conf.redis.RedisRpcConfig; +import com.genersoft.iot.vmp.conf.redis.bean.RedisRpcMessage; +import com.genersoft.iot.vmp.conf.redis.bean.RedisRpcRequest; +import com.genersoft.iot.vmp.conf.redis.bean.RedisRpcResponse; +import com.genersoft.iot.vmp.service.ICloudRecordService; +import com.genersoft.iot.vmp.service.bean.DownloadFileInfo; +import com.genersoft.iot.vmp.service.redisMsg.dto.RedisRpcController; +import com.genersoft.iot.vmp.service.redisMsg.dto.RedisRpcMapping; +import com.genersoft.iot.vmp.service.redisMsg.dto.RpcController; +import com.genersoft.iot.vmp.vmanager.bean.ErrorCode; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Component; + +@Component +@Slf4j +@RedisRpcController("cloudRecord") +public class RedisRpcCloudRecordController extends RpcController { + + @Autowired + private UserSetting userSetting; + + @Autowired + private RedisTemplate redisTemplate; + + @Autowired + private ICloudRecordService cloudRecordService; + + + private void sendResponse(RedisRpcResponse response){ + log.info("[redis-rpc] >> {}", response); + response.setToId(userSetting.getServerId()); + RedisRpcMessage message = new RedisRpcMessage(); + message.setResponse(response); + redisTemplate.convertAndSend(RedisRpcConfig.REDIS_REQUEST_CHANNEL_KEY, message); + } + + /** + * 播放 + */ + @RedisRpcMapping("play") + public RedisRpcResponse play(RedisRpcRequest request) { + int id = Integer.parseInt(request.getParam().toString()); + RedisRpcResponse response = request.getResponse(); + if (id <= 0) { + response.setStatusCode(ErrorCode.ERROR400.getCode()); + response.setBody("param error"); + return response; + } + DownloadFileInfo downloadFileInfo = cloudRecordService.getPlayUrlPath(id); + if (downloadFileInfo == null) { + response.setStatusCode(ErrorCode.ERROR100.getCode()); + response.setBody("get play url error"); + return response; + } + response.setStatusCode(ErrorCode.SUCCESS.getCode()); + response.setBody(JSONObject.toJSONString(downloadFileInfo)); + return response; + } + +} diff --git a/src/main/java/com/genersoft/iot/vmp/service/redisMsg/control/RedisRpcDeviceController.java b/src/main/java/com/genersoft/iot/vmp/service/redisMsg/control/RedisRpcDeviceController.java new file mode 100644 index 0000000..ee6eec4 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/service/redisMsg/control/RedisRpcDeviceController.java @@ -0,0 +1,503 @@ +package com.genersoft.iot.vmp.service.redisMsg.control; + +import com.alibaba.fastjson2.JSONObject; +import com.genersoft.iot.vmp.conf.UserSetting; +import com.genersoft.iot.vmp.conf.exception.ControllerException; +import com.genersoft.iot.vmp.conf.redis.RedisRpcConfig; +import com.genersoft.iot.vmp.conf.redis.bean.RedisRpcMessage; +import com.genersoft.iot.vmp.conf.redis.bean.RedisRpcRequest; +import com.genersoft.iot.vmp.conf.redis.bean.RedisRpcResponse; +import com.genersoft.iot.vmp.gb28181.bean.BasicParam; +import com.genersoft.iot.vmp.gb28181.bean.Device; +import com.genersoft.iot.vmp.gb28181.bean.SyncStatus; +import com.genersoft.iot.vmp.gb28181.service.IDeviceService; +import com.genersoft.iot.vmp.service.redisMsg.dto.RedisRpcController; +import com.genersoft.iot.vmp.service.redisMsg.dto.RedisRpcMapping; +import com.genersoft.iot.vmp.service.redisMsg.dto.RpcController; +import com.genersoft.iot.vmp.streamProxy.service.IStreamProxyService; +import com.genersoft.iot.vmp.vmanager.bean.ErrorCode; +import com.genersoft.iot.vmp.vmanager.bean.WVPResult; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Component; + +@Component +@Slf4j +@RedisRpcController("device") +public class RedisRpcDeviceController extends RpcController { + + @Autowired + private UserSetting userSetting; + + @Autowired + private RedisTemplate redisTemplate; + + @Autowired + private IDeviceService deviceService; + + @Autowired + private IStreamProxyService streamProxyService; + + + private void sendResponse(RedisRpcResponse response){ + log.info("[redis-rpc] >> {}", response); + response.setToId(userSetting.getServerId()); + RedisRpcMessage message = new RedisRpcMessage(); + message.setResponse(response); + redisTemplate.convertAndSend(RedisRpcConfig.REDIS_REQUEST_CHANNEL_KEY, message); + } + + /** + * 通道同步 + */ + @RedisRpcMapping("devicesSync") + public RedisRpcResponse devicesSync(RedisRpcRequest request) { + String deviceId = request.getParam().toString(); + Device device = deviceService.getDeviceByDeviceId(deviceId); + + RedisRpcResponse response = request.getResponse(); + if (device == null || !userSetting.getServerId().equals(device.getServerId())) { + response.setStatusCode(ErrorCode.ERROR400.getCode()); + response.setBody("param error"); + return response; + } + if (device.getRegisterTime() == null) { + response.setStatusCode(ErrorCode.ERROR400.getCode()); + response.setBody("设备尚未注册过"); + return response; + } + WVPResult result = deviceService.devicesSync(device); + response.setStatusCode(ErrorCode.SUCCESS.getCode()); + response.setBody(JSONObject.toJSONString(result)); + return response; + } + + /** + * 获取通道同步状态 + */ + @RedisRpcMapping("getChannelSyncStatus") + public RedisRpcResponse getChannelSyncStatus(RedisRpcRequest request) { + String deviceId = request.getParam().toString(); + + Device device = deviceService.getDeviceByDeviceId(deviceId); + + RedisRpcResponse response = request.getResponse(); + if (device == null || !userSetting.getServerId().equals(device.getServerId())) { + response.setStatusCode(ErrorCode.ERROR400.getCode()); + response.setBody("param error"); + return response; + } + SyncStatus channelSyncStatus = deviceService.getChannelSyncStatus(deviceId); + response.setStatusCode(ErrorCode.SUCCESS.getCode()); + response.setBody(JSONObject.toJSONString(channelSyncStatus)); + return response; + } + + @RedisRpcMapping("deviceBasicConfig") + public RedisRpcResponse deviceBasicConfig(RedisRpcRequest request) { + BasicParam basicParam = JSONObject.parseObject(request.getParam().toString(), BasicParam.class); + + Device device = deviceService.getDeviceByDeviceId(basicParam.getDeviceId()); + + RedisRpcResponse response = request.getResponse(); + if (device == null || !userSetting.getServerId().equals(device.getServerId())) { + response.setStatusCode(ErrorCode.ERROR400.getCode()); + response.setBody("param error"); + return response; + } + deviceService.deviceBasicConfig(device, basicParam, (code, msg, data) -> { + response.setStatusCode(code); + response.setBody(new WVPResult<>(code, msg, data)); + // 手动发送结果 + sendResponse(response); + }); + return null; + } + + @RedisRpcMapping("deviceConfigQuery") + public RedisRpcResponse deviceConfigQuery(RedisRpcRequest request) { + + JSONObject paramJson = JSONObject.parseObject(request.getParam().toString()); + String deviceId = paramJson.getString("deviceId"); + String channelId = paramJson.getString("channelId"); + String configType = paramJson.getString("configType"); + + Device device = deviceService.getDeviceByDeviceId(deviceId); + + RedisRpcResponse response = request.getResponse(); + if (device == null || !userSetting.getServerId().equals(device.getServerId())) { + response.setStatusCode(ErrorCode.ERROR400.getCode()); + response.setBody("param error"); + return response; + } + deviceService.deviceConfigQuery(device, channelId, configType, (code, msg, data) -> { + response.setStatusCode(code); + response.setBody(new WVPResult<>(code, msg, data)); + // 手动发送结果 + sendResponse(response); + }); + return null; + } + + @RedisRpcMapping("teleboot") + public RedisRpcResponse teleboot(RedisRpcRequest request) { + String deviceId = request.getParam().toString(); + + Device device = deviceService.getDeviceByDeviceId(deviceId); + + RedisRpcResponse response = request.getResponse(); + if (device == null || !userSetting.getServerId().equals(device.getServerId())) { + response.setStatusCode(ErrorCode.ERROR400.getCode()); + response.setBody("param error"); + return response; + } + try { + deviceService.teleboot(device); + }catch (ControllerException e) { + response.setStatusCode(e.getCode()); + response.setBody(WVPResult.fail(ErrorCode.ERROR100.getCode(), e.getMsg())); + return response; + } + response.setStatusCode(ErrorCode.SUCCESS.getCode()); + response.setBody(WVPResult.success()); + return response; + } + + @RedisRpcMapping("record") + public RedisRpcResponse record(RedisRpcRequest request) { + JSONObject paramJson = JSONObject.parseObject(request.getParam().toString()); + String deviceId = paramJson.getString("deviceId"); + String channelId = paramJson.getString("channelId"); + String recordCmdStr = paramJson.getString("recordCmdStr"); + + Device device = deviceService.getDeviceByDeviceId(deviceId); + + RedisRpcResponse response = request.getResponse(); + if (device == null || !userSetting.getServerId().equals(device.getServerId())) { + response.setStatusCode(ErrorCode.ERROR400.getCode()); + response.setBody("param error"); + return response; + } + try { + deviceService.record(device, channelId, recordCmdStr, (code, msg, data) -> { + response.setStatusCode(ErrorCode.SUCCESS.getCode()); + response.setBody(new WVPResult<>(code, msg, data)); + // 手动发送结果 + sendResponse(response); + }); + }catch (ControllerException e) { + response.setStatusCode(e.getCode()); + response.setBody(WVPResult.fail(ErrorCode.ERROR100.getCode(), e.getMsg())); + sendResponse(response); + } + return null; + } + + @RedisRpcMapping("guard") + public RedisRpcResponse guard(RedisRpcRequest request) { + JSONObject paramJson = JSONObject.parseObject(request.getParam().toString()); + String deviceId = paramJson.getString("deviceId"); + String guardCmdStr = paramJson.getString("guardCmdStr"); + + Device device = deviceService.getDeviceByDeviceId(deviceId); + + RedisRpcResponse response = request.getResponse(); + if (device == null || !userSetting.getServerId().equals(device.getServerId())) { + response.setStatusCode(ErrorCode.ERROR400.getCode()); + response.setBody("param error"); + return response; + } + try { + deviceService.guard(device, guardCmdStr, (code, msg, data) -> { + response.setStatusCode(ErrorCode.SUCCESS.getCode()); + response.setBody(new WVPResult<>(code, msg, data)); + // 手动发送结果 + sendResponse(response); + }); + }catch (ControllerException e) { + response.setStatusCode(e.getCode()); + response.setBody(WVPResult.fail(ErrorCode.ERROR100.getCode(), e.getMsg())); + sendResponse(response); + } + return null; + } + + @RedisRpcMapping("resetAlarm") + public RedisRpcResponse resetAlarm(RedisRpcRequest request) { + JSONObject paramJson = JSONObject.parseObject(request.getParam().toString()); + String deviceId = paramJson.getString("deviceId"); + String channelId = paramJson.getString("channelId"); + String alarmMethod = paramJson.getString("alarmMethod"); + String alarmType = paramJson.getString("alarmType"); + + Device device = deviceService.getDeviceByDeviceId(deviceId); + + RedisRpcResponse response = request.getResponse(); + if (device == null || !userSetting.getServerId().equals(device.getServerId())) { + response.setStatusCode(ErrorCode.ERROR400.getCode()); + response.setBody("param error"); + return response; + } + try { + deviceService.resetAlarm(device, channelId, alarmMethod, alarmType, (code, msg, data) -> { + response.setStatusCode(ErrorCode.SUCCESS.getCode()); + response.setBody(new WVPResult<>(code, msg, data)); + // 手动发送结果 + sendResponse(response); + }); + }catch (ControllerException e) { + response.setStatusCode(e.getCode()); + response.setBody(WVPResult.fail(ErrorCode.ERROR100.getCode(), e.getMsg())); + sendResponse(response); + } + return null; + } + + @RedisRpcMapping("iFrame") + public RedisRpcResponse iFrame(RedisRpcRequest request) { + JSONObject paramJson = JSONObject.parseObject(request.getParam().toString()); + String deviceId = paramJson.getString("deviceId"); + String channelId = paramJson.getString("channelId"); + + Device device = deviceService.getDeviceByDeviceId(deviceId); + + RedisRpcResponse response = request.getResponse(); + if (device == null || !userSetting.getServerId().equals(device.getServerId())) { + response.setStatusCode(ErrorCode.ERROR400.getCode()); + response.setBody("param error"); + return response; + } + try { + deviceService.iFrame(device, channelId); + }catch (ControllerException e) { + response.setStatusCode(e.getCode()); + response.setBody(WVPResult.fail(ErrorCode.ERROR100.getCode(), e.getMsg())); + sendResponse(response); + } + return null; + } + + @RedisRpcMapping("homePosition") + public RedisRpcResponse homePosition(RedisRpcRequest request) { + JSONObject paramJson = JSONObject.parseObject(request.getParam().toString()); + String deviceId = paramJson.getString("deviceId"); + String channelId = paramJson.getString("channelId"); + + Boolean enabled = paramJson.getBoolean("enabled"); + Integer resetTime = paramJson.getInteger("resetTime"); + Integer presetIndex = paramJson.getInteger("presetIndex"); + + Device device = deviceService.getDeviceByDeviceId(deviceId); + + RedisRpcResponse response = request.getResponse(); + if (device == null || !userSetting.getServerId().equals(device.getServerId())) { + response.setStatusCode(ErrorCode.ERROR400.getCode()); + response.setBody("param error"); + return response; + } + try { + deviceService.homePosition(device, channelId, enabled, resetTime, presetIndex, (code, msg, data) -> { + response.setStatusCode(ErrorCode.SUCCESS.getCode()); + response.setBody(new WVPResult<>(code, msg, data)); + // 手动发送结果 + sendResponse(response); + }); + }catch (ControllerException e) { + response.setStatusCode(e.getCode()); + response.setBody(WVPResult.fail(ErrorCode.ERROR100.getCode(), e.getMsg())); + sendResponse(response); + } + return null; + } + + @RedisRpcMapping("dragZoomIn") + public RedisRpcResponse dragZoomIn(RedisRpcRequest request) { + JSONObject paramJson = JSONObject.parseObject(request.getParam().toString()); + String deviceId = paramJson.getString("deviceId"); + String channelId = paramJson.getString("channelId"); + Integer length = paramJson.getInteger("length"); + Integer width = paramJson.getInteger("width"); + Integer midpointx = paramJson.getInteger("midpointx"); + Integer midpointy = paramJson.getInteger("midpointy"); + Integer lengthx = paramJson.getInteger("lengthx"); + Integer lengthy = paramJson.getInteger("lengthy"); + + Device device = deviceService.getDeviceByDeviceId(deviceId); + + RedisRpcResponse response = request.getResponse(); + if (device == null || !userSetting.getServerId().equals(device.getServerId())) { + response.setStatusCode(ErrorCode.ERROR400.getCode()); + response.setBody("param error"); + return response; + } + try { + deviceService.dragZoomIn(device, channelId, length, width, midpointx, midpointy, lengthx, lengthy, (code, msg, data) -> { + response.setStatusCode(ErrorCode.SUCCESS.getCode()); + response.setBody(new WVPResult<>(code, msg, data)); + // 手动发送结果 + sendResponse(response); + }); + }catch (ControllerException e) { + response.setStatusCode(e.getCode()); + response.setBody(WVPResult.fail(ErrorCode.ERROR100.getCode(), e.getMsg())); + sendResponse(response); + } + return null; + } + + @RedisRpcMapping("dragZoomOut") + public RedisRpcResponse dragZoomOut(RedisRpcRequest request) { + JSONObject paramJson = JSONObject.parseObject(request.getParam().toString()); + String deviceId = paramJson.getString("deviceId"); + String channelId = paramJson.getString("channelId"); + Integer length = paramJson.getInteger("length"); + Integer width = paramJson.getInteger("width"); + Integer midpointx = paramJson.getInteger("midpointx"); + Integer midpointy = paramJson.getInteger("midpointy"); + Integer lengthx = paramJson.getInteger("lengthx"); + Integer lengthy = paramJson.getInteger("lengthy"); + + Device device = deviceService.getDeviceByDeviceId(deviceId); + + RedisRpcResponse response = request.getResponse(); + if (device == null || !userSetting.getServerId().equals(device.getServerId())) { + response.setStatusCode(ErrorCode.ERROR400.getCode()); + response.setBody("param error"); + return response; + } + try { + deviceService.dragZoomOut(device, channelId, length, width, midpointx, midpointy, lengthx, lengthy, (code, msg, data) -> { + response.setStatusCode(ErrorCode.SUCCESS.getCode()); + response.setBody(new WVPResult<>(code, msg, data)); + // 手动发送结果 + sendResponse(response); + }); + }catch (ControllerException e) { + response.setStatusCode(e.getCode()); + response.setBody(WVPResult.fail(ErrorCode.ERROR100.getCode(), e.getMsg())); + sendResponse(response); + } + return null; + } + + @RedisRpcMapping("alarm") + public RedisRpcResponse alarm(RedisRpcRequest request) { + + JSONObject paramJson = JSONObject.parseObject(request.getParam().toString()); + String deviceId = paramJson.getString("deviceId"); + String startPriority = paramJson.getString("startPriority"); + String endPriority = paramJson.getString("endPriority"); + String alarmMethod = paramJson.getString("alarmMethod"); + String alarmType = paramJson.getString("alarmType"); + String startTime = paramJson.getString("startTime"); + String endTime = paramJson.getString("endTime"); + + Device device = deviceService.getDeviceByDeviceId(deviceId); + + RedisRpcResponse response = request.getResponse(); + if (device == null || !userSetting.getServerId().equals(device.getServerId())) { + response.setStatusCode(ErrorCode.ERROR400.getCode()); + response.setBody("param error"); + return response; + } + try { + deviceService.alarm(device, startPriority, endPriority, alarmMethod, alarmType, startTime, endTime, (code, msg, data) -> { + response.setStatusCode(ErrorCode.SUCCESS.getCode()); + response.setBody(new WVPResult<>(code, msg, data)); + // 手动发送结果 + sendResponse(response); + }); + }catch (ControllerException e) { + response.setStatusCode(e.getCode()); + response.setBody(WVPResult.fail(ErrorCode.ERROR100.getCode(), e.getMsg())); + sendResponse(response); + } + return null; + } + + @RedisRpcMapping("deviceStatus") + public RedisRpcResponse deviceStatus(RedisRpcRequest request) { + String deviceId = request.getParam().toString(); + + Device device = deviceService.getDeviceByDeviceId(deviceId); + + RedisRpcResponse response = request.getResponse(); + if (device == null || !userSetting.getServerId().equals(device.getServerId())) { + response.setStatusCode(ErrorCode.ERROR400.getCode()); + response.setBody("param error"); + return response; + } + try { + deviceService.deviceStatus(device, (code, msg, data) -> { + response.setStatusCode(ErrorCode.SUCCESS.getCode()); + response.setBody(new WVPResult<>(code, msg, data)); + // 手动发送结果 + sendResponse(response); + }); + }catch (ControllerException e) { + response.setStatusCode(e.getCode()); + response.setBody(WVPResult.fail(ErrorCode.ERROR100.getCode(), e.getMsg())); + sendResponse(response); + } + return null; + } + + @RedisRpcMapping("info") + public RedisRpcResponse info(RedisRpcRequest request) { + String deviceId = request.getParam().toString(); + + Device device = deviceService.getDeviceByDeviceId(deviceId); + + RedisRpcResponse response = request.getResponse(); + if (device == null || !userSetting.getServerId().equals(device.getServerId())) { + response.setStatusCode(ErrorCode.ERROR400.getCode()); + response.setBody("param error"); + return response; + } + try { + deviceService.deviceInfo(device, (code, msg, data) -> { + response.setStatusCode(ErrorCode.SUCCESS.getCode()); + response.setBody(new WVPResult<>(code, msg, data)); + // 手动发送结果 + sendResponse(response); + }); + }catch (ControllerException e) { + response.setStatusCode(e.getCode()); + response.setBody(WVPResult.fail(ErrorCode.ERROR100.getCode(), e.getMsg())); + sendResponse(response); + } + return null; + } + + @RedisRpcMapping("info") + public RedisRpcResponse queryPreset(RedisRpcRequest request) { + JSONObject paramJson = JSONObject.parseObject(request.getParam().toString()); + String deviceId = paramJson.getString("deviceId"); + String channelId = paramJson.getString("channelId"); + + Device device = deviceService.getDeviceByDeviceId(deviceId); + + RedisRpcResponse response = request.getResponse(); + if (device == null || !userSetting.getServerId().equals(device.getServerId())) { + response.setStatusCode(ErrorCode.ERROR400.getCode()); + response.setBody("param error"); + return response; + } + try { + deviceService.queryPreset(device, channelId, (code, msg, data) -> { + response.setStatusCode(ErrorCode.SUCCESS.getCode()); + response.setBody(new WVPResult<>(code, msg, data)); + // 手动发送结果 + sendResponse(response); + }); + }catch (ControllerException e) { + response.setStatusCode(e.getCode()); + response.setBody(WVPResult.fail(ErrorCode.ERROR100.getCode(), e.getMsg())); + sendResponse(response); + } + return null; + } + + +} diff --git a/src/main/java/com/genersoft/iot/vmp/service/redisMsg/control/RedisRpcDevicePlayController.java b/src/main/java/com/genersoft/iot/vmp/service/redisMsg/control/RedisRpcDevicePlayController.java new file mode 100644 index 0000000..2e83cc7 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/service/redisMsg/control/RedisRpcDevicePlayController.java @@ -0,0 +1,74 @@ +package com.genersoft.iot.vmp.service.redisMsg.control; + +import com.alibaba.fastjson2.JSON; +import com.alibaba.fastjson2.JSONObject; +import com.genersoft.iot.vmp.conf.UserSetting; +import com.genersoft.iot.vmp.conf.redis.RedisRpcConfig; +import com.genersoft.iot.vmp.conf.redis.bean.RedisRpcMessage; +import com.genersoft.iot.vmp.conf.redis.bean.RedisRpcRequest; +import com.genersoft.iot.vmp.conf.redis.bean.RedisRpcResponse; +import com.genersoft.iot.vmp.gb28181.bean.Device; +import com.genersoft.iot.vmp.gb28181.service.IDeviceService; +import com.genersoft.iot.vmp.gb28181.service.IPlayService; +import com.genersoft.iot.vmp.service.redisMsg.dto.RedisRpcController; +import com.genersoft.iot.vmp.service.redisMsg.dto.RedisRpcMapping; +import com.genersoft.iot.vmp.service.redisMsg.dto.RpcController; +import com.genersoft.iot.vmp.vmanager.bean.AudioBroadcastResult; +import com.genersoft.iot.vmp.vmanager.bean.ErrorCode; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Component; + +@Component +@Slf4j +@RedisRpcController("devicePlay") +public class RedisRpcDevicePlayController extends RpcController { + + @Autowired + private UserSetting userSetting; + + @Autowired + private RedisTemplate redisTemplate; + + @Autowired + private IDeviceService deviceService; + + @Autowired + private IPlayService playService; + + + + private void sendResponse(RedisRpcResponse response){ + log.info("[redis-rpc] >> {}", response); + response.setToId(userSetting.getServerId()); + RedisRpcMessage message = new RedisRpcMessage(); + message.setResponse(response); + redisTemplate.convertAndSend(RedisRpcConfig.REDIS_REQUEST_CHANNEL_KEY, message); + } + + /** + * 获取通道同步状态 + */ + @RedisRpcMapping("audioBroadcast") + public RedisRpcResponse audioBroadcast(RedisRpcRequest request) { + JSONObject paramJson = JSON.parseObject(request.getParam().toString()); + String deviceId = paramJson.getString("deviceId"); + String channelDeviceId = paramJson.getString("channelDeviceId"); + Boolean broadcastMode = paramJson.getBoolean("broadcastMode"); + + Device device = deviceService.getDeviceByDeviceId(deviceId); + + RedisRpcResponse response = request.getResponse(); + if (device == null || !userSetting.getServerId().equals(device.getServerId())) { + response.setStatusCode(ErrorCode.ERROR400.getCode()); + response.setBody("param error"); + return response; + } + AudioBroadcastResult audioBroadcastResult = playService.audioBroadcast(deviceId, channelDeviceId, broadcastMode); + response.setStatusCode(ErrorCode.SUCCESS.getCode()); + response.setBody(JSONObject.toJSONString(audioBroadcastResult)); + return response; + } + +} diff --git a/src/main/java/com/genersoft/iot/vmp/service/redisMsg/control/RedisRpcGbDeviceController.java b/src/main/java/com/genersoft/iot/vmp/service/redisMsg/control/RedisRpcGbDeviceController.java new file mode 100644 index 0000000..798c938 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/service/redisMsg/control/RedisRpcGbDeviceController.java @@ -0,0 +1,99 @@ +package com.genersoft.iot.vmp.service.redisMsg.control; + +import com.alibaba.fastjson2.JSONObject; +import com.genersoft.iot.vmp.common.InviteSessionType; +import com.genersoft.iot.vmp.conf.UserSetting; +import com.genersoft.iot.vmp.conf.exception.ControllerException; +import com.genersoft.iot.vmp.conf.redis.RedisRpcConfig; +import com.genersoft.iot.vmp.conf.redis.bean.RedisRpcMessage; +import com.genersoft.iot.vmp.conf.redis.bean.RedisRpcRequest; +import com.genersoft.iot.vmp.conf.redis.bean.RedisRpcResponse; +import com.genersoft.iot.vmp.gb28181.bean.CommonGBChannel; +import com.genersoft.iot.vmp.gb28181.bean.Device; +import com.genersoft.iot.vmp.gb28181.bean.InviteMessageInfo; +import com.genersoft.iot.vmp.gb28181.service.IDeviceService; +import com.genersoft.iot.vmp.gb28181.service.IGbChannelPlayService; +import com.genersoft.iot.vmp.gb28181.service.IGbChannelService; +import com.genersoft.iot.vmp.gb28181.service.IPTZService; +import com.genersoft.iot.vmp.service.bean.InviteErrorCode; +import com.genersoft.iot.vmp.service.redisMsg.dto.RedisRpcController; +import com.genersoft.iot.vmp.service.redisMsg.dto.RedisRpcMapping; +import com.genersoft.iot.vmp.service.redisMsg.dto.RpcController; +import com.genersoft.iot.vmp.utils.DateUtil; +import com.genersoft.iot.vmp.vmanager.bean.ErrorCode; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Component; + +import javax.sip.message.Response; + +@Component +@Slf4j +@RedisRpcController("device") +public class RedisRpcGbDeviceController extends RpcController { + + @Autowired + private UserSetting userSetting; + + @Autowired + private RedisTemplate redisTemplate; + + @Autowired + private IDeviceService deviceService; + + + + private void sendResponse(RedisRpcResponse response){ + log.info("[redis-rpc] >> {}", response); + response.setToId(userSetting.getServerId()); + RedisRpcMessage message = new RedisRpcMessage(); + message.setResponse(response); + redisTemplate.convertAndSend(RedisRpcConfig.REDIS_REQUEST_CHANNEL_KEY, message); + } + + + /** + * 目录订阅 + */ + @RedisRpcMapping("subscribeCatalog") + public RedisRpcResponse subscribeCatalog(RedisRpcRequest request) { + JSONObject paramJson = JSONObject.parseObject(request.getParam().toString()); + int id = paramJson.getIntValue("id"); + int cycle = paramJson.getIntValue("cycle"); + + RedisRpcResponse response = request.getResponse(); + + if (id <= 0) { + response.setStatusCode(ErrorCode.ERROR400.getCode()); + response.setBody("param error"); + return response; + } + deviceService.subscribeCatalog(id, cycle); + response.setStatusCode(ErrorCode.SUCCESS.getCode()); + return response; + } + + /** + * 移动位置订阅 + */ + @RedisRpcMapping("subscribeMobilePosition") + public RedisRpcResponse subscribeMobilePosition(RedisRpcRequest request) { + JSONObject paramJson = JSONObject.parseObject(request.getParam().toString()); + int id = paramJson.getIntValue("id"); + int cycle = paramJson.getIntValue("cycle"); + int interval = paramJson.getIntValue("interval"); + + RedisRpcResponse response = request.getResponse(); + + if (id <= 0) { + response.setStatusCode(ErrorCode.ERROR400.getCode()); + response.setBody("param error"); + return response; + } + deviceService.subscribeMobilePosition(id, cycle, interval); + response.setStatusCode(ErrorCode.SUCCESS.getCode()); + return response; + } + +} diff --git a/src/main/java/com/genersoft/iot/vmp/service/redisMsg/control/RedisRpcPlatformController.java b/src/main/java/com/genersoft/iot/vmp/service/redisMsg/control/RedisRpcPlatformController.java new file mode 100644 index 0000000..a11561e --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/service/redisMsg/control/RedisRpcPlatformController.java @@ -0,0 +1,83 @@ +package com.genersoft.iot.vmp.service.redisMsg.control; + +import com.alibaba.fastjson2.JSONObject; +import com.genersoft.iot.vmp.conf.UserSetting; +import com.genersoft.iot.vmp.conf.redis.RedisRpcConfig; +import com.genersoft.iot.vmp.conf.redis.bean.RedisRpcMessage; +import com.genersoft.iot.vmp.conf.redis.bean.RedisRpcRequest; +import com.genersoft.iot.vmp.conf.redis.bean.RedisRpcResponse; +import com.genersoft.iot.vmp.gb28181.bean.CommonGBChannel; +import com.genersoft.iot.vmp.gb28181.bean.Platform; +import com.genersoft.iot.vmp.gb28181.event.EventPublisher; +import com.genersoft.iot.vmp.gb28181.service.IPlatformChannelService; +import com.genersoft.iot.vmp.gb28181.service.IPlatformService; +import com.genersoft.iot.vmp.service.redisMsg.dto.RedisRpcController; +import com.genersoft.iot.vmp.service.redisMsg.dto.RedisRpcMapping; +import com.genersoft.iot.vmp.service.redisMsg.dto.RpcController; +import com.genersoft.iot.vmp.vmanager.bean.ErrorCode; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Component; + +import java.util.List; + +@Component +@Slf4j +@RedisRpcController("platform") +public class RedisRpcPlatformController extends RpcController { + + @Autowired + private UserSetting userSetting; + + @Autowired + private RedisTemplate redisTemplate; + + @Autowired + private IPlatformService platformService; + + @Autowired + private IPlatformChannelService platformChannelService; + + @Autowired + private EventPublisher eventPublisher; + + + private void sendResponse(RedisRpcResponse response){ + log.info("[redis-rpc] >> {}", response); + response.setToId(userSetting.getServerId()); + RedisRpcMessage message = new RedisRpcMessage(); + message.setResponse(response); + redisTemplate.convertAndSend(RedisRpcConfig.REDIS_REQUEST_CHANNEL_KEY, message); + } + + /** + * 更新 + */ + @RedisRpcMapping("update") + public RedisRpcResponse play(RedisRpcRequest request) { + Platform platform = JSONObject.parseObject(request.getParam().toString(), Platform.class); + RedisRpcResponse response = request.getResponse(); + boolean update = platformService.update(platform); + response.setStatusCode(ErrorCode.SUCCESS.getCode()); + response.setBody(Boolean.toString(update)); + return response; + } + + /** + * 目录更新推送 + */ + @RedisRpcMapping("catalogEventPublish") + public RedisRpcResponse catalogEventPublish(RedisRpcRequest request) { + JSONObject jsonObject = JSONObject.parseObject(request.getParam().toString()); + Platform platform = jsonObject.getObject("platform", Platform.class); + + List channels = jsonObject.getJSONArray("channels").toJavaList(CommonGBChannel.class); + String type = jsonObject.getString("type"); + eventPublisher.catalogEventPublish(platform, channels, type); + RedisRpcResponse response = request.getResponse(); + response.setStatusCode(ErrorCode.SUCCESS.getCode()); + return response; + } + +} diff --git a/src/main/java/com/genersoft/iot/vmp/service/redisMsg/control/RedisRpcSendRtpController.java b/src/main/java/com/genersoft/iot/vmp/service/redisMsg/control/RedisRpcSendRtpController.java new file mode 100644 index 0000000..d809d61 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/service/redisMsg/control/RedisRpcSendRtpController.java @@ -0,0 +1,163 @@ +package com.genersoft.iot.vmp.service.redisMsg.control; + +import com.genersoft.iot.vmp.conf.UserSetting; +import com.genersoft.iot.vmp.conf.exception.ControllerException; +import com.genersoft.iot.vmp.conf.redis.bean.RedisRpcRequest; +import com.genersoft.iot.vmp.conf.redis.bean.RedisRpcResponse; +import com.genersoft.iot.vmp.gb28181.bean.SendRtpInfo; +import com.genersoft.iot.vmp.gb28181.session.SSRCFactory; +import com.genersoft.iot.vmp.media.bean.MediaInfo; +import com.genersoft.iot.vmp.media.bean.MediaServer; +import com.genersoft.iot.vmp.media.service.IMediaServerService; +import com.genersoft.iot.vmp.service.ISendRtpServerService; +import com.genersoft.iot.vmp.service.redisMsg.dto.RedisRpcController; +import com.genersoft.iot.vmp.service.redisMsg.dto.RedisRpcMapping; +import com.genersoft.iot.vmp.service.redisMsg.dto.RpcController; +import com.genersoft.iot.vmp.vmanager.bean.ErrorCode; +import com.genersoft.iot.vmp.vmanager.bean.WVPResult; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +@Component +@Slf4j +@RedisRpcController("sendRtp") +public class RedisRpcSendRtpController extends RpcController { + + @Autowired + private SSRCFactory ssrcFactory; + + @Autowired + private IMediaServerService mediaServerService; + + @Autowired + private ISendRtpServerService sendRtpServerService; + + @Autowired + private UserSetting userSetting; + + + /** + * 获取发流的信息 + */ + @RedisRpcMapping("getSendRtpItem") + public RedisRpcResponse getSendRtpItem(RedisRpcRequest request) { + String callId = request.getParam().toString(); + SendRtpInfo sendRtpItem = sendRtpServerService.queryByCallId(callId); + if (sendRtpItem == null) { + log.info("[redis-rpc] 获取发流的信息, 未找到redis中的发流信息, callId:{}", callId); + RedisRpcResponse response = request.getResponse(); + response.setStatusCode(ErrorCode.SUCCESS.getCode()); + return response; + } + log.info("[redis-rpc] 获取发流的信息: {}/{}, 目标地址: {}:{}", sendRtpItem.getApp(), sendRtpItem.getStream(), sendRtpItem.getIp(), sendRtpItem.getPort()); + // 查询本级是否有这个流 + MediaServer mediaServerItem = mediaServerService.getMediaServerByAppAndStream(sendRtpItem.getApp(), sendRtpItem.getStream()); + if (mediaServerItem == null) { + RedisRpcResponse response = request.getResponse(); + response.setStatusCode(ErrorCode.SUCCESS.getCode()); + } + // 自平台内容 + int localPort = sendRtpServerService.getNextPort(mediaServerItem); + if (localPort <= 0) { + log.info("[redis-rpc] getSendRtpItem->服务器端口资源不足" ); + RedisRpcResponse response = request.getResponse(); + response.setStatusCode(ErrorCode.SUCCESS.getCode()); + } + // 写入redis, 超时时回复 + sendRtpItem.setStatus(1); + sendRtpItem.setServerId(userSetting.getServerId()); + sendRtpItem.setLocalIp(mediaServerItem.getSdpIp()); + if (sendRtpItem.getSsrc() == null) { + // 上级平台点播时不使用上级平台指定的ssrc,使用自定义的ssrc,参考国标文档-点播外域设备媒体流SSRC处理方式 + String ssrc = "Play".equalsIgnoreCase(sendRtpItem.getSessionName()) ? ssrcFactory.getPlaySsrc(mediaServerItem.getId()) : ssrcFactory.getPlayBackSsrc(mediaServerItem.getId()); + sendRtpItem.setSsrc(ssrc); + } + sendRtpServerService.update(sendRtpItem); + RedisRpcResponse response = request.getResponse(); + response.setStatusCode(ErrorCode.SUCCESS.getCode()); + response.setBody(callId); + return response; + } + + /** + * 开始发流 + */ + @RedisRpcMapping("startSendRtp") + public RedisRpcResponse startSendRtp(RedisRpcRequest request) { + String callId = request.getParam().toString(); + SendRtpInfo sendRtpItem = sendRtpServerService.queryByCallId(callId); + RedisRpcResponse response = request.getResponse(); + response.setStatusCode(ErrorCode.SUCCESS.getCode()); + if (sendRtpItem == null) { + log.info("[redis-rpc] 开始发流, 未找到redis中的发流信息, callId:{}", callId); + WVPResult wvpResult = WVPResult.fail(ErrorCode.ERROR100.getCode(), "未找到redis中的发流信息"); + response.setBody(wvpResult); + return response; + } + log.info("[redis-rpc] 开始发流: {}/{}, 目标地址: {}:{}", sendRtpItem.getApp(), sendRtpItem.getStream(), sendRtpItem.getIp(), sendRtpItem.getPort()); + MediaServer mediaServer = mediaServerService.getOne(sendRtpItem.getMediaServerId()); + if (mediaServer == null) { + log.info("[redis-rpc] startSendRtp->未找到MediaServer: {}", sendRtpItem.getMediaServerId() ); + WVPResult wvpResult = WVPResult.fail(ErrorCode.ERROR100.getCode(), "未找到MediaServer"); + response.setBody(wvpResult); + return response; + } + MediaInfo mediaInfo = mediaServerService.getMediaInfo(mediaServer, sendRtpItem.getApp(), sendRtpItem.getStream()); + if (mediaInfo == null) { + log.info("[redis-rpc] startSendRtp->流不在线: {}/{}", sendRtpItem.getApp(), sendRtpItem.getStream() ); + WVPResult wvpResult = WVPResult.fail(ErrorCode.ERROR100.getCode(), "流不在线"); + response.setBody(wvpResult); + return response; + } + try { + mediaServerService.startSendRtp(mediaServer, sendRtpItem); + }catch (ControllerException exception) { + log.info("[redis-rpc] 发流失败: {}/{}, 目标地址: {}:{}, {}", sendRtpItem.getApp(), sendRtpItem.getStream(), sendRtpItem.getIp(), sendRtpItem.getPort(), exception.getMsg()); + WVPResult wvpResult = WVPResult.fail(exception.getCode(), exception.getMsg()); + response.setBody(wvpResult); + return response; + } + log.info("[redis-rpc] 发流成功: {}/{}, 目标地址: {}:{}", sendRtpItem.getApp(), sendRtpItem.getStream(), sendRtpItem.getIp(), sendRtpItem.getPort()); + WVPResult wvpResult = WVPResult.success(); + response.setBody(wvpResult); + return response; + } + + /** + * 停止发流 + */ + @RedisRpcMapping("stopSendRtp") + public RedisRpcResponse stopSendRtp(RedisRpcRequest request) { + String callId = request.getParam().toString(); + SendRtpInfo sendRtpItem = sendRtpServerService.queryByCallId(callId); + RedisRpcResponse response = request.getResponse(); + response.setStatusCode(ErrorCode.SUCCESS.getCode()); + if (sendRtpItem == null) { + log.info("[redis-rpc] 停止推流, 未找到redis中的发流信息, key:{}", callId); + WVPResult wvpResult = WVPResult.fail(ErrorCode.ERROR100.getCode(), "未找到redis中的发流信息"); + response.setBody(wvpResult); + return response; + } + log.info("[redis-rpc] 停止推流: {}/{}, 目标地址: {}:{}", sendRtpItem.getApp(), sendRtpItem.getStream(), sendRtpItem.getIp(), sendRtpItem.getPort() ); + MediaServer mediaServer = mediaServerService.getOne(sendRtpItem.getMediaServerId()); + if (mediaServer == null) { + log.info("[redis-rpc] stopSendRtp->未找到MediaServer: {}", sendRtpItem.getMediaServerId() ); + WVPResult wvpResult = WVPResult.fail(ErrorCode.ERROR100.getCode(), "未找到MediaServer"); + response.setBody(wvpResult); + return response; + } + try { + mediaServerService.stopSendRtp(mediaServer, sendRtpItem.getApp(), sendRtpItem.getStream(), sendRtpItem.getSsrc()); + }catch (ControllerException exception) { + log.info("[redis-rpc] 停止推流失败: {}/{}, 目标地址: {}:{}, code: {}, msg: {}", sendRtpItem.getApp(), + sendRtpItem.getStream(), sendRtpItem.getIp(), sendRtpItem.getPort(), exception.getCode(), exception.getMsg() ); + response.setBody(WVPResult.fail(exception.getCode(), exception.getMsg())); + return response; + } + log.info("[redis-rpc] 停止推流成功: {}/{}, 目标地址: {}:{}", sendRtpItem.getApp(), sendRtpItem.getStream(), sendRtpItem.getIp(), sendRtpItem.getPort() ); + response.setBody(WVPResult.success()); + return response; + } + +} diff --git a/src/main/java/com/genersoft/iot/vmp/service/redisMsg/control/RedisRpcStreamProxyController.java b/src/main/java/com/genersoft/iot/vmp/service/redisMsg/control/RedisRpcStreamProxyController.java new file mode 100644 index 0000000..4a688c3 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/service/redisMsg/control/RedisRpcStreamProxyController.java @@ -0,0 +1,97 @@ +package com.genersoft.iot.vmp.service.redisMsg.control; + +import com.alibaba.fastjson2.JSONObject; +import com.genersoft.iot.vmp.conf.UserSetting; +import com.genersoft.iot.vmp.conf.redis.RedisRpcConfig; +import com.genersoft.iot.vmp.conf.redis.bean.RedisRpcMessage; +import com.genersoft.iot.vmp.conf.redis.bean.RedisRpcRequest; +import com.genersoft.iot.vmp.conf.redis.bean.RedisRpcResponse; +import com.genersoft.iot.vmp.service.redisMsg.dto.RedisRpcController; +import com.genersoft.iot.vmp.service.redisMsg.dto.RedisRpcMapping; +import com.genersoft.iot.vmp.service.redisMsg.dto.RpcController; +import com.genersoft.iot.vmp.streamProxy.bean.StreamProxy; +import com.genersoft.iot.vmp.streamProxy.service.IStreamProxyPlayService; +import com.genersoft.iot.vmp.streamProxy.service.IStreamProxyService; +import com.genersoft.iot.vmp.vmanager.bean.ErrorCode; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Component; + +@Component +@Slf4j +@RedisRpcController("streamProxy") +public class RedisRpcStreamProxyController extends RpcController { + + @Autowired + private UserSetting userSetting; + + @Autowired + private RedisTemplate redisTemplate; + + @Autowired + private IStreamProxyPlayService streamProxyPlayService; + + @Autowired + private IStreamProxyService streamProxyService; + + + private void sendResponse(RedisRpcResponse response){ + log.info("[redis-rpc] >> {}", response); + response.setToId(userSetting.getServerId()); + RedisRpcMessage message = new RedisRpcMessage(); + message.setResponse(response); + redisTemplate.convertAndSend(RedisRpcConfig.REDIS_REQUEST_CHANNEL_KEY, message); + } + + /** + * 播放 + */ + @RedisRpcMapping("play") + public RedisRpcResponse play(RedisRpcRequest request) { + int id = Integer.parseInt(request.getParam().toString()); + RedisRpcResponse response = request.getResponse(); + if (id <= 0) { + response.setStatusCode(ErrorCode.ERROR400.getCode()); + response.setBody("param error"); + return response; + } + StreamProxy streamProxy = streamProxyService.getStreamProxy(id); + if (streamProxy == null) { + response.setStatusCode(ErrorCode.ERROR400.getCode()); + response.setBody("param error"); + return response; + } + streamProxyPlayService.startProxy(streamProxy, (code, msg, streamInfo) -> { + response.setStatusCode(code); + response.setBody(JSONObject.toJSONString(streamInfo)); + sendResponse(response); + }); + + return null; + } + + /** + * 停止 + */ + @RedisRpcMapping("stop") + public RedisRpcResponse stop(RedisRpcRequest request) { + int id = Integer.parseInt(request.getParam().toString()); + RedisRpcResponse response = request.getResponse(); + if (id <= 0) { + response.setStatusCode(ErrorCode.ERROR400.getCode()); + response.setBody("param error"); + return response; + } + StreamProxy streamProxy = streamProxyService.getStreamProxy(id); + if (streamProxy == null) { + response.setStatusCode(ErrorCode.ERROR400.getCode()); + response.setBody("param error"); + return response; + } + streamProxyPlayService.stopProxy(streamProxy); + response.setStatusCode(ErrorCode.SUCCESS.getCode()); + return response; + } + +} diff --git a/src/main/java/com/genersoft/iot/vmp/service/redisMsg/control/RedisRpcStreamPushController.java b/src/main/java/com/genersoft/iot/vmp/service/redisMsg/control/RedisRpcStreamPushController.java new file mode 100644 index 0000000..babaa0f --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/service/redisMsg/control/RedisRpcStreamPushController.java @@ -0,0 +1,208 @@ +package com.genersoft.iot.vmp.service.redisMsg.control; + +import com.alibaba.fastjson2.JSONObject; +import com.genersoft.iot.vmp.common.StreamInfo; +import com.genersoft.iot.vmp.conf.UserSetting; +import com.genersoft.iot.vmp.conf.redis.RedisRpcConfig; +import com.genersoft.iot.vmp.conf.redis.bean.RedisRpcMessage; +import com.genersoft.iot.vmp.conf.redis.bean.RedisRpcRequest; +import com.genersoft.iot.vmp.conf.redis.bean.RedisRpcResponse; +import com.genersoft.iot.vmp.gb28181.bean.SendRtpInfo; +import com.genersoft.iot.vmp.gb28181.session.SSRCFactory; +import com.genersoft.iot.vmp.media.bean.MediaServer; +import com.genersoft.iot.vmp.media.event.hook.Hook; +import com.genersoft.iot.vmp.media.event.hook.HookSubscribe; +import com.genersoft.iot.vmp.media.event.hook.HookType; +import com.genersoft.iot.vmp.media.service.IMediaServerService; +import com.genersoft.iot.vmp.service.ISendRtpServerService; +import com.genersoft.iot.vmp.service.redisMsg.dto.RedisRpcController; +import com.genersoft.iot.vmp.service.redisMsg.dto.RedisRpcMapping; +import com.genersoft.iot.vmp.service.redisMsg.dto.RpcController; +import com.genersoft.iot.vmp.streamPush.service.IStreamPushPlayService; +import com.genersoft.iot.vmp.vmanager.bean.ErrorCode; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Component; + +@Component +@Slf4j +@RedisRpcController("streamPush") +public class RedisRpcStreamPushController extends RpcController { + + @Autowired + private SSRCFactory ssrcFactory; + + @Autowired + private IMediaServerService mediaServerService; + + @Autowired + private ISendRtpServerService sendRtpServerService; + + @Autowired + private UserSetting userSetting; + + @Autowired + private HookSubscribe hookSubscribe; + + @Autowired + private RedisTemplate redisTemplate; + + @Autowired + private IStreamPushPlayService streamPushPlayService; + + + private void sendResponse(RedisRpcResponse response){ + log.info("[redis-rpc] >> {}", response); + response.setToId(userSetting.getServerId()); + RedisRpcMessage message = new RedisRpcMessage(); + message.setResponse(response); + redisTemplate.convertAndSend(RedisRpcConfig.REDIS_REQUEST_CHANNEL_KEY, message); + } + + /** + * 监听流上线 + */ + @RedisRpcMapping("waitePushStreamOnline") + public RedisRpcResponse waitePushStreamOnline(RedisRpcRequest request) { + SendRtpInfo sendRtpItem = JSONObject.parseObject(request.getParam().toString(), SendRtpInfo.class); + log.info("[redis-rpc] 监听流上线: {}/{}, 目标地址: {}:{}", sendRtpItem.getApp(), sendRtpItem.getStream(), sendRtpItem.getIp(), sendRtpItem.getPort()); + // 查询本级是否有这个流 + MediaServer mediaServer = mediaServerService.getMediaServerByAppAndStream(sendRtpItem.getApp(), sendRtpItem.getStream()); + if (mediaServer != null) { + log.info("[redis-rpc] 监听流上线时发现流已存在直接返回: {}/{}, 目标地址: {}:{}", sendRtpItem.getApp(), sendRtpItem.getStream(), sendRtpItem.getIp(), sendRtpItem.getPort() ); + // 读取redis中的上级点播信息,生成sendRtpItm发送出去 + if (sendRtpItem.getSsrc() == null) { + // 上级平台点播时不使用上级平台指定的ssrc,使用自定义的ssrc,参考国标文档-点播外域设备媒体流SSRC处理方式 + String ssrc = "Play".equalsIgnoreCase(sendRtpItem.getSessionName()) ? ssrcFactory.getPlaySsrc(mediaServer.getId()) : ssrcFactory.getPlayBackSsrc(mediaServer.getId()); + sendRtpItem.setSsrc(ssrc); + } + sendRtpItem.setMediaServerId(mediaServer.getId()); + sendRtpItem.setLocalIp(mediaServer.getSdpIp()); + sendRtpItem.setServerId(userSetting.getServerId()); + + sendRtpServerService.update(sendRtpItem); + RedisRpcResponse response = request.getResponse(); + response.setBody(sendRtpItem.getChannelId()); + response.setStatusCode(ErrorCode.SUCCESS.getCode()); + } + // 监听流上线。 流上线直接发送sendRtpItem消息给实际的信令处理者 + Hook hook = Hook.getInstance(HookType.on_media_arrival, sendRtpItem.getApp(), sendRtpItem.getStream(), null); + hookSubscribe.addSubscribe(hook, (hookData) -> { + log.info("[redis-rpc] 监听流上线,流已上线: {}/{}, 目标地址: {}:{}", sendRtpItem.getApp(), sendRtpItem.getStream(), sendRtpItem.getIp(), sendRtpItem.getPort()); + // 读取redis中的上级点播信息,生成sendRtpItm发送出去 + if (sendRtpItem.getSsrc() == null) { + // 上级平台点播时不使用上级平台指定的ssrc,使用自定义的ssrc,参考国标文档-点播外域设备媒体流SSRC处理方式 + String ssrc = "Play".equalsIgnoreCase(sendRtpItem.getSessionName()) ? ssrcFactory.getPlaySsrc(hookData.getMediaServer().getId()) : ssrcFactory.getPlayBackSsrc(hookData.getMediaServer().getId()); + sendRtpItem.setSsrc(ssrc); + } + sendRtpItem.setMediaServerId(hookData.getMediaServer().getId()); + sendRtpItem.setLocalIp(hookData.getMediaServer().getSdpIp()); + sendRtpItem.setServerId(userSetting.getServerId()); + + redisTemplate.opsForValue().set(sendRtpItem.getChannelId(), sendRtpItem); + RedisRpcResponse response = request.getResponse(); + response.setBody(sendRtpItem.getChannelId()); + response.setStatusCode(ErrorCode.SUCCESS.getCode()); + // 手动发送结果 + sendResponse(response); + hookSubscribe.removeSubscribe(hook); + + }); + return null; + } + + /** + * 监听流上线 + */ + @RedisRpcMapping("onStreamOnlineEvent") + public RedisRpcResponse onStreamOnlineEvent(RedisRpcRequest request) { + StreamInfo streamInfo = JSONObject.parseObject(request.getParam().toString(), StreamInfo.class); + log.info("[redis-rpc] 监听流信息,等待流上线: {}/{}", streamInfo.getApp(), streamInfo.getStream()); + // 查询本级是否有这个流 + StreamInfo streamInfoInServer = mediaServerService.getMediaByAppAndStream(streamInfo.getApp(), streamInfo.getStream()); + if (streamInfoInServer != null) { + log.info("[redis-rpc] 监听流上线时发现流已存在直接返回: {}/{}", streamInfo.getApp(), streamInfo.getStream()); + RedisRpcResponse response = request.getResponse(); + response.setBody(JSONObject.toJSONString(streamInfoInServer)); + response.setStatusCode(ErrorCode.SUCCESS.getCode()); + return response; + } + // 监听流上线。 流上线直接发送sendRtpItem消息给实际的信令处理者 + Hook hook = Hook.getInstance(HookType.on_media_arrival, streamInfo.getApp(), streamInfo.getStream()); + hookSubscribe.addSubscribe(hook, (hookData) -> { + log.info("[redis-rpc] 监听流上线,流已上线: {}/{}", streamInfo.getApp(), streamInfo.getStream()); + // 读取redis中的上级点播信息,生成sendRtpItm发送出去 + RedisRpcResponse response = request.getResponse(); + StreamInfo streamInfoByAppAndStream = mediaServerService.getStreamInfoByAppAndStream(hookData.getMediaServer(), + streamInfo.getApp(), streamInfo.getStream(), hookData.getMediaInfo(), + hookData.getMediaInfo() != null ? hookData.getMediaInfo().getCallId() : null); + response.setBody(JSONObject.toJSONString(streamInfoByAppAndStream)); + response.setStatusCode(ErrorCode.SUCCESS.getCode()); + // 手动发送结果 + sendResponse(response); + hookSubscribe.removeSubscribe(hook); + }); + return null; + } + + /** + * 停止监听流上线 + */ + @RedisRpcMapping("stopWaitePushStreamOnline") + public RedisRpcResponse stopWaitePushStreamOnline(RedisRpcRequest request) { + SendRtpInfo sendRtpItem = JSONObject.parseObject(request.getParam().toString(), SendRtpInfo.class); + log.info("[redis-rpc] 停止监听流上线: {}/{}, 目标地址: {}:{}", sendRtpItem.getApp(), sendRtpItem.getStream(), sendRtpItem.getIp(), sendRtpItem.getPort() ); + // 监听流上线。 流上线直接发送sendRtpItem消息给实际的信令处理者 + Hook hook = Hook.getInstance(HookType.on_media_arrival, sendRtpItem.getApp(), sendRtpItem.getStream(), null); + hookSubscribe.removeSubscribe(hook); + RedisRpcResponse response = request.getResponse(); + response.setStatusCode(ErrorCode.SUCCESS.getCode()); + return response; + } + + /** + * 停止监听流上线 + */ + @RedisRpcMapping("unPushStreamOnlineEvent") + public RedisRpcResponse unPushStreamOnlineEvent(RedisRpcRequest request) { + StreamInfo streamInfo = JSONObject.parseObject(request.getParam().toString(), StreamInfo.class); + log.info("[redis-rpc] 停止监听流上线: {}/{}", streamInfo.getApp(), streamInfo.getStream()); + // 监听流上线。 流上线直接发送sendRtpItem消息给实际的信令处理者 + Hook hook = Hook.getInstance(HookType.on_media_arrival, streamInfo.getApp(), streamInfo.getStream(), null); + hookSubscribe.removeSubscribe(hook); + RedisRpcResponse response = request.getResponse(); + response.setStatusCode(ErrorCode.SUCCESS.getCode()); + return response; + } + + /** + * 停止监听流上线 + */ + @RedisRpcMapping("play") + public RedisRpcResponse play(RedisRpcRequest request) { + JSONObject paramJson = JSONObject.parseObject(request.getParam().toString()); + int id = paramJson.getInteger("id"); + RedisRpcResponse response = request.getResponse(); + if (id <= 0) { + response.setStatusCode(ErrorCode.ERROR400.getCode()); + response.setBody("param error"); + return response; + } + try { + streamPushPlayService.start(id, (code, msg, data) -> { + if (code == ErrorCode.SUCCESS.getCode()) { + response.setStatusCode(ErrorCode.SUCCESS.getCode()); + response.setBody(JSONObject.toJSONString(data)); + sendResponse(response); + } + }, null, null); + }catch (IllegalArgumentException e) { + response.setStatusCode(ErrorCode.ERROR100.getCode()); + response.setBody(e.getMessage()); + sendResponse(response); + } + return null; + } + +} diff --git a/src/main/java/com/genersoft/iot/vmp/service/redisMsg/dto/RedisRpcController.java b/src/main/java/com/genersoft/iot/vmp/service/redisMsg/dto/RedisRpcController.java new file mode 100644 index 0000000..f314b0c --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/service/redisMsg/dto/RedisRpcController.java @@ -0,0 +1,13 @@ +package com.genersoft.iot.vmp.service.redisMsg.dto; + +import java.lang.annotation.*; + +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface RedisRpcController { + /** + * 请求路径 + */ + String value() default ""; +} diff --git a/src/main/java/com/genersoft/iot/vmp/service/redisMsg/dto/RedisRpcMapping.java b/src/main/java/com/genersoft/iot/vmp/service/redisMsg/dto/RedisRpcMapping.java new file mode 100644 index 0000000..61f51bb --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/service/redisMsg/dto/RedisRpcMapping.java @@ -0,0 +1,13 @@ +package com.genersoft.iot.vmp.service.redisMsg.dto; + +import java.lang.annotation.*; + +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface RedisRpcMapping { + /** + * 请求路径 + */ + String value() default ""; +} diff --git a/src/main/java/com/genersoft/iot/vmp/service/redisMsg/dto/RpcController.java b/src/main/java/com/genersoft/iot/vmp/service/redisMsg/dto/RpcController.java new file mode 100644 index 0000000..30f8b88 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/service/redisMsg/dto/RpcController.java @@ -0,0 +1,33 @@ +package com.genersoft.iot.vmp.service.redisMsg.dto; + + +import com.genersoft.iot.vmp.conf.redis.RedisRpcConfig; +import com.genersoft.iot.vmp.conf.redis.bean.RedisRpcClassHandler; +import org.springframework.beans.factory.annotation.Autowired; + +import jakarta.annotation.PostConstruct; +import java.lang.reflect.Method; + +public class RpcController { + + @Autowired + private RedisRpcConfig redisRpcConfig; + + + @PostConstruct + public void init() { + String controllerPath = this.getClass().getAnnotation(RedisRpcController.class).value(); + // 扫描其下的方法 + Method[] methods = this.getClass().getDeclaredMethods(); + for (Method method : methods) { + RedisRpcMapping annotation = method.getAnnotation(RedisRpcMapping.class); + if (annotation != null) { + String methodPath = annotation.value(); + if (methodPath != null) { + redisRpcConfig.addHandler(controllerPath + "/" + methodPath, new RedisRpcClassHandler(this, method)); + } + } + + } + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/service/redisMsg/service/RedisRpcPlayServiceImpl.java b/src/main/java/com/genersoft/iot/vmp/service/redisMsg/service/RedisRpcPlayServiceImpl.java new file mode 100644 index 0000000..0e2e2e8 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/service/redisMsg/service/RedisRpcPlayServiceImpl.java @@ -0,0 +1,267 @@ +package com.genersoft.iot.vmp.service.redisMsg.service; + +import com.alibaba.fastjson2.JSON; +import com.alibaba.fastjson2.JSONObject; +import com.genersoft.iot.vmp.common.InviteSessionType; +import com.genersoft.iot.vmp.common.StreamInfo; +import com.genersoft.iot.vmp.conf.UserSetting; +import com.genersoft.iot.vmp.conf.exception.ControllerException; +import com.genersoft.iot.vmp.conf.redis.RedisRpcConfig; +import com.genersoft.iot.vmp.conf.redis.bean.RedisRpcRequest; +import com.genersoft.iot.vmp.conf.redis.bean.RedisRpcResponse; +import com.genersoft.iot.vmp.gb28181.bean.RecordInfo; +import com.genersoft.iot.vmp.service.bean.DownloadFileInfo; +import com.genersoft.iot.vmp.service.bean.ErrorCallback; +import com.genersoft.iot.vmp.service.bean.InviteErrorCode; +import com.genersoft.iot.vmp.service.redisMsg.IRedisRpcPlayService; +import com.genersoft.iot.vmp.vmanager.bean.AudioBroadcastResult; +import com.genersoft.iot.vmp.vmanager.bean.ErrorCode; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.concurrent.TimeUnit; + +@Slf4j +@Service +public class RedisRpcPlayServiceImpl implements IRedisRpcPlayService { + + + @Autowired + private RedisRpcConfig redisRpcConfig; + + @Autowired + private UserSetting userSetting; + + + private RedisRpcRequest buildRequest(String uri, Object param) { + RedisRpcRequest request = new RedisRpcRequest(); + request.setFromId(userSetting.getServerId()); + request.setParam(param); + request.setUri(uri); + return request; + } + + @Override + public void play(String serverId, Integer channelId, ErrorCallback callback) { + RedisRpcRequest request = buildRequest("channel/play", channelId); + request.setToId(serverId); + RedisRpcResponse response = redisRpcConfig.request(request, userSetting.getPlayTimeout(), TimeUnit.MILLISECONDS); + if (response == null) { + callback.run(ErrorCode.ERROR100.getCode(), ErrorCode.ERROR100.getMsg(), null); + }else { + if (response.getStatusCode() == ErrorCode.SUCCESS.getCode()) { + StreamInfo streamInfo = JSON.parseObject(response.getBody().toString(), StreamInfo.class); + callback.run(InviteErrorCode.SUCCESS.getCode(), InviteErrorCode.SUCCESS.getMsg(), streamInfo); + }else { + callback.run(response.getStatusCode(), response.getBody().toString(), null); + } + } + } + + @Override + public void stop(String serverId, InviteSessionType type, int channelId, String stream) { + JSONObject jsonObject = new JSONObject(); + jsonObject.put("channelId", channelId); + jsonObject.put("stream", stream); + jsonObject.put("inviteSessionType", type); + RedisRpcRequest request = buildRequest("channel/stop", jsonObject.toJSONString()); + request.setToId(serverId); + RedisRpcResponse response = redisRpcConfig.request(request, 50, TimeUnit.MICROSECONDS); + if (response == null) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), ErrorCode.ERROR100.getMsg()); + }else { + if (response.getStatusCode() != ErrorCode.SUCCESS.getCode()) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), ErrorCode.ERROR100.getMsg()); + } + } + } + + @Override + public void queryRecordInfo(String serverId, Integer channelId, String startTime, String endTime, ErrorCallback callback) { + JSONObject jsonObject = new JSONObject(); + jsonObject.put("channelId", channelId); + jsonObject.put("startTime", startTime); + jsonObject.put("endTime", endTime); + RedisRpcRequest request = buildRequest("channel/queryRecordInfo", jsonObject); + request.setToId(serverId); + RedisRpcResponse response = redisRpcConfig.request(request, userSetting.getRecordInfoTimeout(), TimeUnit.MILLISECONDS); + if (response == null) { + callback.run(ErrorCode.ERROR100.getCode(), ErrorCode.ERROR100.getMsg(), null); + }else { + if (response.getStatusCode() == ErrorCode.SUCCESS.getCode()) { + RecordInfo recordInfo = JSON.parseObject(response.getBody().toString(), RecordInfo.class); + callback.run(InviteErrorCode.SUCCESS.getCode(), InviteErrorCode.SUCCESS.getMsg(), recordInfo); + }else { + callback.run(response.getStatusCode(), response.getBody().toString(), null); + } + } + } + + @Override + public void playback(String serverId, Integer channelId, String startTime, String endTime, ErrorCallback callback) { + + JSONObject jsonObject = new JSONObject(); + jsonObject.put("channelId", channelId); + jsonObject.put("startTime", startTime); + jsonObject.put("endTime", endTime); + RedisRpcRequest request = buildRequest("channel/playback", jsonObject.toString()); + request.setToId(serverId); + RedisRpcResponse response = redisRpcConfig.request(request, userSetting.getPlayTimeout(), TimeUnit.MILLISECONDS); + if (response == null) { + callback.run(ErrorCode.ERROR100.getCode(), ErrorCode.ERROR100.getMsg(), null); + }else { + if (response.getStatusCode() == ErrorCode.SUCCESS.getCode()) { + StreamInfo streamInfo = JSON.parseObject(response.getBody().toString(), StreamInfo.class); + callback.run(InviteErrorCode.SUCCESS.getCode(), InviteErrorCode.SUCCESS.getMsg(), streamInfo); + }else { + callback.run(response.getStatusCode(), response.getBody().toString(), null); + } + } + } + + @Override + public void playbackPause(String serverId, String streamId) { + RedisRpcRequest request = buildRequest("channel/playbackPause", streamId); + request.setToId(serverId); + RedisRpcResponse response = redisRpcConfig.request(request, 5, TimeUnit.SECONDS); + if (response == null) { + log.info("[RPC 暂停回放] 失败, streamId: {}", streamId); + }else { + if (response.getStatusCode() != ErrorCode.SUCCESS.getCode()) { + log.info("[RPC 暂停回放] 失败, {}, streamId: {}", response.getBody(), streamId); + } + } + } + + @Override + public void playbackResume(String serverId, String streamId) { + RedisRpcRequest request = buildRequest("channel/playbackResume", streamId); + request.setToId(serverId); + RedisRpcResponse response = redisRpcConfig.request(request, 5, TimeUnit.SECONDS); + if (response == null) { + log.info("[RPC 恢复回放] 失败, streamId: {}", streamId); + }else { + if (response.getStatusCode() != ErrorCode.SUCCESS.getCode()) { + log.info("[RPC 恢复回放] 失败, {}, streamId: {}", response.getBody(), streamId); + } + } + } + + @Override + public void download(String serverId, Integer channelId, String startTime, String endTime, int downloadSpeed, ErrorCallback callback) { + + JSONObject jsonObject = new JSONObject(); + jsonObject.put("channelId", channelId); + jsonObject.put("startTime", startTime); + jsonObject.put("endTime", endTime); + jsonObject.put("downloadSpeed", downloadSpeed); + RedisRpcRequest request = buildRequest("channel/download", jsonObject.toString()); + request.setToId(serverId); + RedisRpcResponse response = redisRpcConfig.request(request, userSetting.getPlayTimeout(), TimeUnit.MILLISECONDS); + if (response == null) { + callback.run(ErrorCode.ERROR100.getCode(), ErrorCode.ERROR100.getMsg(), null); + }else { + if (response.getStatusCode() == ErrorCode.SUCCESS.getCode()) { + StreamInfo streamInfo = JSON.parseObject(response.getBody().toString(), StreamInfo.class); + callback.run(InviteErrorCode.SUCCESS.getCode(), InviteErrorCode.SUCCESS.getMsg(), streamInfo); + }else { + callback.run(response.getStatusCode(), response.getBody().toString(), null); + } + } + } + + @Override + public String frontEndCommand(String serverId, Integer channelId, int cmdCode, int parameter1, int parameter2, int combindCode2) { + JSONObject jsonObject = new JSONObject(); + jsonObject.put("channelId", channelId); + jsonObject.put("cmdCode", cmdCode); + jsonObject.put("parameter1", parameter1); + jsonObject.put("parameter2", parameter2); + jsonObject.put("combindCode2", combindCode2); + RedisRpcRequest request = buildRequest("channel/ptz/frontEndCommand", jsonObject.toString()); + request.setToId(serverId); + RedisRpcResponse response = redisRpcConfig.request(request, userSetting.getPlayTimeout(), TimeUnit.MILLISECONDS); + if (response == null) { + return ErrorCode.ERROR100.getMsg(); + }else { + if (response.getStatusCode() != ErrorCode.SUCCESS.getCode()) { + return response.getBody().toString(); + } + } + return null; + } + + @Override + public void playPush(String serverId, Integer id, ErrorCallback callback) { + JSONObject jsonObject = new JSONObject(); + jsonObject.put("id", id); + RedisRpcRequest request = buildRequest("streamPush/play", jsonObject); + request.setToId(serverId); + RedisRpcResponse response = redisRpcConfig.request(request, userSetting.getPlayTimeout(), TimeUnit.SECONDS); + if (response == null) { + callback.run(ErrorCode.ERROR100.getCode(), ErrorCode.ERROR100.getMsg(), null); + }else { + if (response.getStatusCode() == ErrorCode.SUCCESS.getCode()) { + StreamInfo streamInfo = JSON.parseObject(response.getBody().toString(), StreamInfo.class); + callback.run(InviteErrorCode.SUCCESS.getCode(), InviteErrorCode.SUCCESS.getMsg(), streamInfo); + }else { + callback.run(response.getStatusCode(), response.getBody().toString(), null); + } + } + } + + @Override + public void playProxy(String serverId, int id, ErrorCallback callback) { + RedisRpcRequest request = buildRequest("streamProxy/play", id); + request.setToId(serverId); + RedisRpcResponse response = redisRpcConfig.request(request, userSetting.getPlayTimeout(), TimeUnit.SECONDS); + if (response == null) { + callback.run(ErrorCode.ERROR100.getCode(), ErrorCode.ERROR100.getMsg(), null); + }else { + if (response.getStatusCode() == ErrorCode.SUCCESS.getCode()) { + StreamInfo streamInfo = JSON.parseObject(response.getBody().toString(), StreamInfo.class); + callback.run(InviteErrorCode.SUCCESS.getCode(), InviteErrorCode.SUCCESS.getMsg(), streamInfo); + }else { + callback.run(response.getStatusCode(), response.getBody().toString(), null); + } + } + } + + @Override + public void stopProxy(String serverId, int id) { + RedisRpcRequest request = buildRequest("streamProxy/stop", id); + RedisRpcResponse response = redisRpcConfig.request(request, userSetting.getPlayTimeout(), TimeUnit.SECONDS); + if (response != null && response.getStatusCode() == ErrorCode.SUCCESS.getCode()) { + log.info("[rpc 拉流代理] 停止成功: id: {}", id); + }else { + log.info("[rpc 拉流代理] 停止失败 id: {}", id); + } + } + + @Override + public DownloadFileInfo getRecordPlayUrl(String serverId, Integer recordId) { + RedisRpcRequest request = buildRequest("cloudRecord/play", recordId); + RedisRpcResponse response = redisRpcConfig.request(request, userSetting.getPlayTimeout(), TimeUnit.SECONDS); + if (response != null && response.getStatusCode() == ErrorCode.SUCCESS.getCode()) { + return JSON.parseObject(response.getBody().toString(), DownloadFileInfo.class); + } + return null; + } + + @Override + public AudioBroadcastResult audioBroadcast(String serverId, String deviceId, String channelDeviceId, Boolean broadcastMode) { + JSONObject jsonObject = new JSONObject(); + jsonObject.put("deviceId", deviceId); + jsonObject.put("channelDeviceId", channelDeviceId); + jsonObject.put("broadcastMode", broadcastMode); + RedisRpcRequest request = buildRequest("devicePlay/audioBroadcast", jsonObject.toString()); + request.setToId(serverId); + RedisRpcResponse response = redisRpcConfig.request(request, userSetting.getPlayTimeout(), TimeUnit.SECONDS); + if (response != null && response.getStatusCode() == ErrorCode.SUCCESS.getCode()) { + return JSON.parseObject(response.getBody().toString(), AudioBroadcastResult.class); + } + return null; + } +} + diff --git a/src/main/java/com/genersoft/iot/vmp/service/redisMsg/service/RedisRpcServiceImpl.java b/src/main/java/com/genersoft/iot/vmp/service/redisMsg/service/RedisRpcServiceImpl.java new file mode 100644 index 0000000..5c82c27 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/service/redisMsg/service/RedisRpcServiceImpl.java @@ -0,0 +1,437 @@ +package com.genersoft.iot.vmp.service.redisMsg.service; + +import com.alibaba.fastjson2.JSON; +import com.alibaba.fastjson2.JSONObject; +import com.genersoft.iot.vmp.common.CommonCallback; +import com.genersoft.iot.vmp.common.StreamInfo; +import com.genersoft.iot.vmp.conf.UserSetting; +import com.genersoft.iot.vmp.conf.exception.ControllerException; +import com.genersoft.iot.vmp.conf.redis.RedisRpcConfig; +import com.genersoft.iot.vmp.conf.redis.bean.RedisRpcRequest; +import com.genersoft.iot.vmp.conf.redis.bean.RedisRpcResponse; +import com.genersoft.iot.vmp.gb28181.bean.*; +import com.genersoft.iot.vmp.gb28181.event.subscribe.catalog.CatalogEvent; +import com.genersoft.iot.vmp.gb28181.session.SSRCFactory; +import com.genersoft.iot.vmp.media.event.hook.Hook; +import com.genersoft.iot.vmp.media.event.hook.HookSubscribe; +import com.genersoft.iot.vmp.media.event.hook.HookType; +import com.genersoft.iot.vmp.media.service.IMediaServerService; +import com.genersoft.iot.vmp.service.ISendRtpServerService; +import com.genersoft.iot.vmp.service.redisMsg.IRedisRpcService; +import com.genersoft.iot.vmp.vmanager.bean.ErrorCode; +import com.genersoft.iot.vmp.vmanager.bean.WVPResult; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.concurrent.TimeUnit; + +@Slf4j +@Service +public class RedisRpcServiceImpl implements IRedisRpcService { + + + @Autowired + private RedisRpcConfig redisRpcConfig; + + @Autowired + private UserSetting userSetting; + + @Autowired + private HookSubscribe hookSubscribe; + + @Autowired + private SSRCFactory ssrcFactory; + + @Autowired + private RedisTemplate redisTemplate; + + @Autowired + private IMediaServerService mediaServerService; + + @Autowired + private ISendRtpServerService sendRtpServerService; + + private RedisRpcRequest buildRequest(String uri, Object param) { + RedisRpcRequest request = new RedisRpcRequest(); + request.setFromId(userSetting.getServerId()); + request.setParam(param); + request.setUri(uri); + return request; + } + + @Override + public SendRtpInfo getSendRtpItem(String callId) { + RedisRpcRequest request = buildRequest("sendRtp/getSendRtpItem", callId); + RedisRpcResponse response = redisRpcConfig.request(request, 10, TimeUnit.MILLISECONDS); + if (response.getBody() == null) { + return null; + } + return (SendRtpInfo)redisTemplate.opsForValue().get(response.getBody().toString()); + } + + @Override + public WVPResult startSendRtp(String callId, SendRtpInfo sendRtpItem) { + log.info("[请求其他WVP] 开始推流,wvp:{}, {}/{}", sendRtpItem.getServerId(), sendRtpItem.getApp(), sendRtpItem.getStream()); + RedisRpcRequest request = buildRequest("sendRtp/startSendRtp", callId); + request.setToId(sendRtpItem.getServerId()); + RedisRpcResponse response = redisRpcConfig.request(request, 10, TimeUnit.MILLISECONDS); + return JSON.parseObject(response.getBody().toString(), WVPResult.class); + } + + @Override + public WVPResult stopSendRtp(String callId) { + SendRtpInfo sendRtpItem = (SendRtpInfo)redisTemplate.opsForValue().get(callId); + if (sendRtpItem == null) { + log.info("[请求其他WVP] 停止推流, 未找到redis中的发流信息, key:{}", callId); + return WVPResult.fail(ErrorCode.ERROR100.getCode(), "未找到发流信息"); + } + log.info("[请求其他WVP] 停止推流,wvp:{}, {}/{}", sendRtpItem.getServerId(), sendRtpItem.getApp(), sendRtpItem.getStream()); + RedisRpcRequest request = buildRequest("sendRtp/stopSendRtp", callId); + request.setToId(sendRtpItem.getServerId()); + RedisRpcResponse response = redisRpcConfig.request(request, 10, TimeUnit.MILLISECONDS); + return JSON.parseObject(response.getBody().toString(), WVPResult.class); + } + + @Override + public long waitePushStreamOnline(SendRtpInfo sendRtpItem, CommonCallback callback) { + log.info("[请求所有WVP监听流上线] {}/{}", sendRtpItem.getApp(), sendRtpItem.getStream()); + // 监听流上线。 流上线直接发送sendRtpItem消息给实际的信令处理者 + Hook hook = Hook.getInstance(HookType.on_media_arrival, sendRtpItem.getApp(), sendRtpItem.getStream(), null); + RedisRpcRequest request = buildRequest("streamPush/waitePushStreamOnline", sendRtpItem); + request.setToId(sendRtpItem.getServerId()); + hookSubscribe.addSubscribe(hook, (hookData) -> { + + // 读取redis中的上级点播信息,生成sendRtpItm发送出去 + if (sendRtpItem.getSsrc() == null) { + // 上级平台点播时不使用上级平台指定的ssrc,使用自定义的ssrc,参考国标文档-点播外域设备媒体流SSRC处理方式 + String ssrc = "Play".equalsIgnoreCase(sendRtpItem.getSessionName()) ? ssrcFactory.getPlaySsrc(hookData.getMediaServer().getId()) : ssrcFactory.getPlayBackSsrc(hookData.getMediaServer().getId()); + sendRtpItem.setSsrc(ssrc); + } + sendRtpItem.setMediaServerId(hookData.getMediaServer().getId()); + sendRtpItem.setLocalIp(hookData.getMediaServer().getSdpIp()); + sendRtpItem.setServerId(userSetting.getServerId()); + sendRtpServerService.update(sendRtpItem); + if (callback != null) { + callback.run(sendRtpItem.getChannelId()); + } + hookSubscribe.removeSubscribe(hook); + redisRpcConfig.removeCallback(request.getSn()); + }); + + redisRpcConfig.request(request, response -> { + if (response.getBody() == null) { + log.info("[请求所有WVP监听流上线] 流上线,但是未找到发流信息:{}/{}", sendRtpItem.getApp(), sendRtpItem.getStream()); + return; + } + log.info("[请求所有WVP监听流上线] 流上线 {}/{}->{}", sendRtpItem.getApp(), sendRtpItem.getStream(), sendRtpItem.toString()); + + if (callback != null) { + callback.run(Integer.parseInt(response.getBody().toString())); + } + hookSubscribe.removeSubscribe(hook); + }); + return request.getSn(); + } + + @Override + public void stopWaitePushStreamOnline(SendRtpInfo sendRtpItem) { + log.info("[停止WVP监听流上线] {}/{}", sendRtpItem.getApp(), sendRtpItem.getStream()); + Hook hook = Hook.getInstance(HookType.on_media_arrival, sendRtpItem.getApp(), sendRtpItem.getStream(), null); + hookSubscribe.removeSubscribe(hook); + RedisRpcRequest request = buildRequest("streamPush/stopWaitePushStreamOnline", sendRtpItem); + request.setToId(sendRtpItem.getServerId()); + redisRpcConfig.request(request, 10, TimeUnit.MILLISECONDS); + } + + @Override + public void rtpSendStopped(String callId) { + SendRtpInfo sendRtpItem = (SendRtpInfo)redisTemplate.opsForValue().get(callId); + if (sendRtpItem == null) { + log.info("[停止WVP监听流上线] 未找到redis中的发流信息, key:{}", callId); + return; + } + RedisRpcRequest request = buildRequest("streamPush/rtpSendStopped", callId); + request.setToId(sendRtpItem.getServerId()); + redisRpcConfig.request(request, 10, TimeUnit.MILLISECONDS); + } + + @Override + public void removeCallback(long key) { + redisRpcConfig.removeCallback(key); + } + + @Override + public long onStreamOnlineEvent(String app, String stream, CommonCallback callback) { + + log.info("[请求所有WVP监听流上线] {}/{}", app, stream); + // 监听流上线。 流上线直接发送sendRtpItem消息给实际的信令处理者 + Hook hook = Hook.getInstance(HookType.on_media_arrival, app, stream); + StreamInfo streamInfoParam = new StreamInfo(); + streamInfoParam.setApp(app); + streamInfoParam.setStream(stream); + RedisRpcRequest request = buildRequest("streamPush/onStreamOnlineEvent", streamInfoParam); + hookSubscribe.addSubscribe(hook, (hookData) -> { + log.info("[请求所有WVP监听流上线] 监听流上线 {}/{}", app, stream); + if (callback != null) { + callback.run(mediaServerService.getStreamInfoByAppAndStream(hookData.getMediaServer(), + app, stream, hookData.getMediaInfo(), + hookData.getMediaInfo() != null ? hookData.getMediaInfo().getCallId() : null)); + } + hookSubscribe.removeSubscribe(hook); + redisRpcConfig.removeCallback(request.getSn()); + }); + + redisRpcConfig.request(request, response -> { + if (response.getBody() == null) { + log.info("[请求所有WVP监听流上线] 流上线,但是未找到发流信息:{}/{}", app, stream); + return; + } + log.info("[请求所有WVP监听流上线] 流上线 {}/{}", app, stream); + + if (callback != null) { + callback.run(JSON.parseObject(response.getBody().toString(), StreamInfo.class)); + } + hookSubscribe.removeSubscribe(hook); + }); + return request.getSn(); + } + + @Override + public void unPushStreamOnlineEvent(String app, String stream) { + StreamInfo streamInfoParam = new StreamInfo(); + streamInfoParam.setApp(app); + streamInfoParam.setStream(stream); + RedisRpcRequest request = buildRequest("streamPush/unPushStreamOnlineEvent", streamInfoParam); + redisRpcConfig.request(request, 10, TimeUnit.MILLISECONDS); + } + + @Override + public void subscribeCatalog(int id, int cycle) { + JSONObject jsonObject = new JSONObject(); + jsonObject.put("id", id); + jsonObject.put("cycle", cycle); + RedisRpcRequest request = buildRequest("device/subscribeCatalog", jsonObject); + redisRpcConfig.request(request, 10, TimeUnit.MILLISECONDS); + } + + @Override + public void subscribeMobilePosition(int id, int cycle, int interval) { + JSONObject jsonObject = new JSONObject(); + jsonObject.put("id", id); + jsonObject.put("cycle", cycle); + jsonObject.put("interval", cycle); + RedisRpcRequest request = buildRequest("device/subscribeMobilePosition", jsonObject); + redisRpcConfig.request(request, 10, TimeUnit.MILLISECONDS); + } + + @Override + public boolean updatePlatform(String serverId, Platform platform) { + RedisRpcRequest request = buildRequest("platform/update", platform); + request.setToId(serverId); + RedisRpcResponse response = redisRpcConfig.request(request, 40, TimeUnit.MILLISECONDS); + if(response == null) { + return false; + } + return Boolean.parseBoolean(response.getBody().toString()); + } + + @Override + public void catalogEventPublish(String serverId, CatalogEvent event) { + JSONObject jsonObject = new JSONObject(); + jsonObject.put("platform", event.getPlatform()); + jsonObject.put("channels", event.getChannels()); + jsonObject.put("type", event.getType()); + RedisRpcRequest request = buildRequest("platform/catalogEventPublish", jsonObject); + if (serverId != null) { + request.setToId(serverId); + } + redisRpcConfig.request(request, 10, TimeUnit.MILLISECONDS); + } + + @Override + public WVPResult devicesSync(String serverId, String deviceId) { + RedisRpcRequest request = buildRequest("device/devicesSync", deviceId); + request.setToId(serverId); + RedisRpcResponse response = redisRpcConfig.request(request, 100, TimeUnit.MILLISECONDS); + return JSON.parseObject(response.getBody().toString(), WVPResult.class); + } + + @Override + public SyncStatus getChannelSyncStatus(String serverId, String deviceId) { + RedisRpcRequest request = buildRequest("device/getChannelSyncStatus", deviceId); + request.setToId(serverId); + RedisRpcResponse response = redisRpcConfig.request(request, 100, TimeUnit.MILLISECONDS); + return JSON.parseObject(response.getBody().toString(), SyncStatus.class); + } + + @Override + public WVPResult deviceBasicConfig(String serverId, Device device, BasicParam basicParam) { + RedisRpcRequest request = buildRequest("device/deviceBasicConfig", JSONObject.toJSONString(basicParam)); + request.setToId(serverId); + RedisRpcResponse response = redisRpcConfig.request(request, 50, TimeUnit.MILLISECONDS); + return JSON.parseObject(response.getBody().toString(), WVPResult.class); + } + + @Override + public WVPResult deviceConfigQuery(String serverId, Device device, String channelId, String configType) { + JSONObject jsonObject = new JSONObject(); + jsonObject.put("device", device.getDeviceId()); + jsonObject.put("channelId", channelId); + jsonObject.put("configType", configType); + RedisRpcRequest request = buildRequest("device/deviceConfigQuery", jsonObject); + request.setToId(serverId); + RedisRpcResponse response = redisRpcConfig.request(request, 50, TimeUnit.MILLISECONDS); + return JSON.parseObject(response.getBody().toString(), WVPResult.class); + } + + @Override + public void teleboot(String serverId, Device device) { + RedisRpcRequest request = buildRequest("device/teleboot", device.getDeviceId()); + request.setToId(serverId); + RedisRpcResponse response = redisRpcConfig.request(request, 50, TimeUnit.MILLISECONDS); + if (response.getStatusCode() != ErrorCode.SUCCESS.getCode()) { + throw new ControllerException(response.getStatusCode(), response.getBody().toString()); + } + } + + @Override + public WVPResult recordControl(String serverId, Device device, String channelId, String recordCmdStr) { + JSONObject jsonObject = new JSONObject(); + jsonObject.put("device", device.getDeviceId()); + jsonObject.put("channelId", channelId); + jsonObject.put("recordCmdStr", recordCmdStr); + RedisRpcRequest request = buildRequest("device/record", jsonObject); + request.setToId(serverId); + RedisRpcResponse response = redisRpcConfig.request(request, 50, TimeUnit.MILLISECONDS); + return JSON.parseObject(response.getBody().toString(), WVPResult.class); + } + + @Override + public WVPResult guard(String serverId, Device device, String guardCmdStr) { + JSONObject jsonObject = new JSONObject(); + jsonObject.put("device", device.getDeviceId()); + jsonObject.put("guardCmdStr", guardCmdStr); + RedisRpcRequest request = buildRequest("device/guard", jsonObject); + request.setToId(serverId); + RedisRpcResponse response = redisRpcConfig.request(request, 50, TimeUnit.MILLISECONDS); + return JSON.parseObject(response.getBody().toString(), WVPResult.class); + } + + @Override + public WVPResult resetAlarm(String serverId, Device device, String channelId, String alarmMethod, String alarmType) { + JSONObject jsonObject = new JSONObject(); + jsonObject.put("device", device.getDeviceId()); + jsonObject.put("channelId", channelId); + jsonObject.put("alarmMethod", alarmMethod); + jsonObject.put("alarmType", alarmType); + RedisRpcRequest request = buildRequest("device/resetAlarm", jsonObject); + request.setToId(serverId); + RedisRpcResponse response = redisRpcConfig.request(request, 50, TimeUnit.MILLISECONDS); + return JSON.parseObject(response.getBody().toString(), WVPResult.class); + } + + @Override + public void iFrame(String serverId, Device device, String channelId) { + JSONObject jsonObject = new JSONObject(); + jsonObject.put("device", device.getDeviceId()); + jsonObject.put("channelId", channelId); + RedisRpcRequest request = buildRequest("device/iFrame", jsonObject); + request.setToId(serverId); + redisRpcConfig.request(request, 50, TimeUnit.MILLISECONDS); + } + + @Override + public WVPResult homePosition(String serverId, Device device, String channelId, Boolean enabled, Integer resetTime, Integer presetIndex) { + JSONObject jsonObject = new JSONObject(); + jsonObject.put("device", device.getDeviceId()); + jsonObject.put("channelId", channelId); + jsonObject.put("enabled", enabled); + jsonObject.put("resetTime", resetTime); + jsonObject.put("presetIndex", presetIndex); + RedisRpcRequest request = buildRequest("device/homePosition", jsonObject); + request.setToId(serverId); + RedisRpcResponse response = redisRpcConfig.request(request, 50, TimeUnit.MILLISECONDS); + return JSON.parseObject(response.getBody().toString(), WVPResult.class); + } + + @Override + public void dragZoomIn(String serverId, Device device, String channelId, int length, int width, int midpointx, + int midpointy, int lengthx, int lengthy) { + JSONObject jsonObject = new JSONObject(); + jsonObject.put("device", device.getDeviceId()); + jsonObject.put("channelId", channelId); + jsonObject.put("length", length); + jsonObject.put("width", width); + jsonObject.put("midpointx", midpointx); + jsonObject.put("midpointy", midpointy); + jsonObject.put("lengthx", lengthx); + jsonObject.put("lengthy", lengthy); + RedisRpcRequest request = buildRequest("device/dragZoomIn", jsonObject); + request.setToId(serverId); + redisRpcConfig.request(request, 50, TimeUnit.MILLISECONDS); + } + + @Override + public void dragZoomOut(String serverId, Device device, String channelId, int length, int width, int midpointx, int midpointy, int lengthx, int lengthy) { + JSONObject jsonObject = new JSONObject(); + jsonObject.put("device", device.getDeviceId()); + jsonObject.put("channelId", channelId); + jsonObject.put("length", length); + jsonObject.put("width", width); + jsonObject.put("midpointx", midpointx); + jsonObject.put("midpointy", midpointy); + jsonObject.put("lengthx", lengthx); + jsonObject.put("lengthy", lengthy); + RedisRpcRequest request = buildRequest("device/dragZoomOut", jsonObject); + request.setToId(serverId); + redisRpcConfig.request(request, 50, TimeUnit.MILLISECONDS); + } + + @Override + public WVPResult deviceStatus(String serverId, Device device) { + RedisRpcRequest request = buildRequest("device/deviceStatus", device.getDeviceId()); + request.setToId(serverId); + RedisRpcResponse response = redisRpcConfig.request(request, 50, TimeUnit.MILLISECONDS); + return JSON.parseObject(response.getBody().toString(), WVPResult.class); + } + + @Override + public WVPResult deviceInfo(String serverId, Device device) { + RedisRpcRequest request = buildRequest("device/info", device.getDeviceId()); + request.setToId(serverId); + RedisRpcResponse response = redisRpcConfig.request(request, 50, TimeUnit.MILLISECONDS); + return JSON.parseObject(response.getBody().toString(), WVPResult.class); + } + + @Override + public WVPResult> queryPreset(String serverId, Device device, String channelId) { + JSONObject jsonObject = new JSONObject(); + jsonObject.put("device", device.getDeviceId()); + jsonObject.put("channelId", channelId); + RedisRpcRequest request = buildRequest("device/queryPreset", jsonObject); + request.setToId(serverId); + RedisRpcResponse response = redisRpcConfig.request(request, 60000, TimeUnit.MILLISECONDS); + return JSON.parseObject(response.getBody().toString(), WVPResult.class); + } + + @Override + public WVPResult alarm(String serverId, Device device, String startPriority, String endPriority, + String alarmMethod, String alarmType, String startTime, String endTime) { + JSONObject jsonObject = new JSONObject(); + jsonObject.put("device", device.getDeviceId()); +// jsonObject.put("channelId", channelId); + jsonObject.put("startPriority", startPriority); + jsonObject.put("endPriority", endPriority); + jsonObject.put("alarmMethod", alarmMethod); + jsonObject.put("alarmType", alarmType); + jsonObject.put("startTime", startTime); + jsonObject.put("endTime", endTime); + RedisRpcRequest request = buildRequest("device/alarm", jsonObject); + request.setToId(serverId); + RedisRpcResponse response = redisRpcConfig.request(request, 50, TimeUnit.MILLISECONDS); + return JSON.parseObject(response.getBody().toString(), WVPResult.class); + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/storager/IRedisCatchStorage.java b/src/main/java/com/genersoft/iot/vmp/storager/IRedisCatchStorage.java new file mode 100755 index 0000000..e7bdffa --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/storager/IRedisCatchStorage.java @@ -0,0 +1,182 @@ +package com.genersoft.iot.vmp.storager; + +import com.alibaba.fastjson2.JSONObject; +import com.genersoft.iot.vmp.common.ServerInfo; +import com.genersoft.iot.vmp.common.SystemAllInfo; +import com.genersoft.iot.vmp.gb28181.bean.*; +import com.genersoft.iot.vmp.media.bean.MediaInfo; +import com.genersoft.iot.vmp.media.bean.MediaServer; +import com.genersoft.iot.vmp.media.zlm.dto.StreamAuthorityInfo; +import com.genersoft.iot.vmp.service.bean.GPSMsgInfo; +import com.genersoft.iot.vmp.service.bean.MessageForPushChannel; + +import java.util.List; +import java.util.Map; + +public interface IRedisCatchStorage { + + /** + * 计数器。为cseq进行计数 + * + * @return + */ + Long getCSEQ(); + + /** + * 在redis添加wvp的信息 + */ + void updateWVPInfo(ServerInfo serverInfo, int time); + + void removeOfflineWVPInfo(String serverId); + + /** + * 发送推流生成与推流消失消息 + * @param jsonObject 消息内容 + */ + void sendStreamChangeMsg(String type, JSONObject jsonObject); + + /** + * 发送报警消息 + * @param msg 消息内容 + */ + void sendAlarmMsg(AlarmChannelMessage msg); + + /** + * 添加流信息到redis + * @param mediaServerItem + * @param app + * @param streamId + */ + void addStream(MediaServer mediaServerItem, String type, String app, String streamId, MediaInfo item); + + /** + * 移除流信息从redis + * @param mediaServerId + * @param app + * @param streamId + */ + void removeStream(String mediaServerId, String type, String app, String streamId); + + + /** + * 移除流信息从redis + * @param mediaServerId + */ + void removeStream(String mediaServerId, String type); + + List getStreams(String mediaServerId, String pull); + + /** + * 将device信息写入redis + * @param device + */ + void updateDevice(Device device); + + void removeDevice(String deviceId); + + /** + * 获取Device + */ + Device getDevice(String deviceId); + + void resetAllCSEQ(); + + void updateGpsMsgInfo(GPSMsgInfo gpsMsgInfo); + + GPSMsgInfo getGpsMsgInfo(String gbId); + + List getAllGpsMsgInfo(); + + MediaInfo getStreamInfo(String app, String streamId, String mediaServerId); + + MediaInfo getProxyStream(String app, String streamId); + + void addCpuInfo(double cpuInfo); + + void addMemInfo(double memInfo); + + void addNetInfo(Map networkInterfaces); + + void sendStreamPushRequestedMsg(MessageForPushChannel messageForPushChannel); + + /** + * 判断设备状态 + */ + boolean deviceIsOnline(String deviceId); + + /** + * 存储推流的鉴权信息 + * @param app 应用名 + * @param stream 流 + * @param streamAuthorityInfo 鉴权信息 + */ + void updateStreamAuthorityInfo(String app, String stream, StreamAuthorityInfo streamAuthorityInfo); + + /** + * 移除推流的鉴权信息 + * @param app 应用名 + * @param streamId 流 + */ + void removeStreamAuthorityInfo(String app, String streamId); + + /** + * 获取推流的鉴权信息 + * @param app 应用名 + * @param stream 流 + * @return + */ + StreamAuthorityInfo getStreamAuthorityInfo(String app, String stream); + + List getAllStreamAuthorityInfo(); + + /** + * 发送redis消息 查询所有推流设备的状态 + */ + void sendStreamPushRequestedMsgForStatus(); + + SystemAllInfo getSystemInfo(); + + int getPushStreamCount(String id); + + int getProxyStreamCount(String id); + + int getGbSendCount(String id); + + void addDiskInfo(List> diskInfo); + + List queryAllSendRTPServer(); + + List getAllDevices(); + + void removeAllDevice(); + + void sendDeviceOrChannelStatus(String deviceId, String channelId, boolean online); + + void sendChannelAddOrDelete(String deviceId, String channelId, boolean add); + + void sendPlatformStartPlayMsg(SendRtpInfo sendRtpItem, DeviceChannel channel, Platform platform); + + void sendPlatformStopPlayMsg(SendRtpInfo sendRtpItem, Platform platform, CommonGBChannel channel); + + void addPushListItem(String app, String stream, MediaInfo param); + + MediaInfo getPushListItem(String app, String stream); + + void removePushListItem(String app, String stream, String mediaServerId); + + void sendPushStreamClose(MessageForPushChannel messageForPushChannel); + + void addWaiteSendRtpItem(SendRtpInfo sendRtpItem, int platformPlayTimeout); + + SendRtpInfo getWaiteSendRtpItem(String app, String stream); + + void sendStartSendRtp(SendRtpInfo sendRtpItem); + + void sendPushStreamOnline(SendRtpInfo sendRtpItem); + + ServerInfo queryServerInfo(String serverId); + + String chooseOneServer(String serverId); + + +} diff --git a/src/main/java/com/genersoft/iot/vmp/storager/dao/CloudRecordServiceMapper.java b/src/main/java/com/genersoft/iot/vmp/storager/dao/CloudRecordServiceMapper.java new file mode 100644 index 0000000..7789200 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/storager/dao/CloudRecordServiceMapper.java @@ -0,0 +1,172 @@ +package com.genersoft.iot.vmp.storager.dao; + +import com.genersoft.iot.vmp.media.bean.MediaServer; +import com.genersoft.iot.vmp.service.bean.CloudRecordItem; +import org.apache.ibatis.annotations.*; + +import java.util.Collection; +import java.util.List; + +@Mapper +public interface CloudRecordServiceMapper { + + @Insert(" ") + int add(CloudRecordItem cloudRecordItem); + + @Select(" ") + List getList(@Param("query") String query, @Param("app") String app, @Param("stream") String stream, + @Param("startTimeStamp")Long startTimeStamp, @Param("endTimeStamp")Long endTimeStamp, + @Param("callId")String callId, List mediaServerItemList, + List ids, @Param("ascOrder") Boolean ascOrder); + + + @Select(" ") + List queryRecordFilePathList(@Param("app") String app, @Param("stream") String stream, + @Param("startTimeStamp")Long startTimeStamp, @Param("endTimeStamp")Long endTimeStamp, + @Param("callId")String callId, List mediaServerItemList); + + @Update(" ") + int updateCollectList(@Param("collect") boolean collect, List cloudRecordItemList); + + @Delete(" ") + void deleteByFileList(List filePathList, @Param("mediaServerId") String mediaServerId); + + + @Select(" ") + List queryRecordListForDelete(@Param("endTimeStamp")Long endTimeStamp, String mediaServerId); + + @Update(" ") + int changeCollectById(@Param("collect") boolean collect, @Param("recordId") Integer recordId); + + @Delete(" ") + int deleteList(List cloudRecordItemIdList); + + @Select(" ") + List getListByCallId(@Param("callId") String callId); + + @Select(" ") + CloudRecordItem queryOne(@Param("id") Integer id); + + @Select(" ") + CloudRecordItem getListByFileName(@Param("app") String app, @Param("stream") String stream, @Param("fileName") String fileName); + + @Update(" ") + void updateTimeLen(@Param("id") int id, @Param("time") Long time, @Param("endTime") long endTime); + + @Select(" ") + List queryMediaServerId(@Param("app") String app, + @Param("stream") String stream, + @Param("startTimeStamp")Long startTimeStamp, + @Param("endTimeStamp")Long endTimeStamp); + + @Select(" ") + List queryRecordByIds(Collection ids); + + @Select(" ") + List queryRecordByAppStreamAndCallId(String app, String stream, String callId); +} diff --git a/src/main/java/com/genersoft/iot/vmp/storager/dao/MediaServerMapper.java b/src/main/java/com/genersoft/iot/vmp/storager/dao/MediaServerMapper.java new file mode 100755 index 0000000..f892be2 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/storager/dao/MediaServerMapper.java @@ -0,0 +1,174 @@ +package com.genersoft.iot.vmp.storager.dao; + +import com.genersoft.iot.vmp.media.bean.MediaServer; +import org.apache.ibatis.annotations.*; +import org.apache.ibatis.annotations.Param; +import org.springframework.stereotype.Repository; + +import java.util.List; + + +@Mapper +@Repository +public interface MediaServerMapper { + + @Insert("INSERT INTO wvp_media_server (" + + "id,"+ + "ip,"+ + "hook_ip,"+ + "sdp_ip,"+ + "stream_ip,"+ + "http_port,"+ + "http_ssl_port,"+ + "rtmp_port,"+ + "rtmp_ssl_port,"+ + "rtp_proxy_port,"+ + "jtt_proxy_port,"+ + "rtsp_port,"+ + "flv_port," + + "mp4_port," + + "flv_ssl_port," + + "ws_flv_port," + + "ws_flv_ssl_port," + + "rtsp_ssl_port,"+ + "auto_config,"+ + "secret,"+ + "rtp_enable,"+ + "rtp_port_range,"+ + "send_rtp_port_range,"+ + "record_assist_port,"+ + "record_day,"+ + "record_path,"+ + "default_server,"+ + "type,"+ + "create_time,"+ + "update_time,"+ + "transcode_suffix,"+ + "server_id,"+ + "hook_alive_interval"+ + ") VALUES " + + "(" + + "#{id}, " + + "#{ip}, " + + "#{hookIp}, " + + "#{sdpIp}, " + + "#{streamIp}, " + + "#{httpPort}, " + + "#{httpSSlPort}, " + + "#{rtmpPort}, " + + "#{rtmpSSlPort}, " + + "#{rtpProxyPort}, " + + "#{jttProxyPort}, " + + "#{rtspPort}, " + + "#{flvPort}, " + + "#{mp4Port}, " + + "#{flvSSLPort}, " + + "#{wsFlvPort}, " + + "#{wsFlvSSLPort}, " + + "#{rtspSSLPort}, " + + "#{autoConfig}, " + + "#{secret}, " + + "#{rtpEnable}, " + + "#{rtpPortRange}, " + + "#{sendRtpPortRange}, " + + "#{recordAssistPort}, " + + "#{recordDay}, " + + "#{recordPath}, " + + "#{defaultServer}, " + + "#{type}, " + + "#{createTime}, " + + "#{updateTime}, " + + "#{transcodeSuffix}, " + + "#{serverId}, " + + "#{hookAliveInterval})") + int add(MediaServer mediaServerItem); + + @Update(value = {" "}) + int update(MediaServer mediaServerItem); + + @Update(value = {" "}) + int updateByHostAndPort(MediaServer mediaServerItem); + + @Select("SELECT * FROM wvp_media_server WHERE id=#{id} and server_id = #{serverId}") + MediaServer queryOne(@Param("id") String id, @Param("serverId") String serverId); + + @Select("SELECT * FROM wvp_media_server where server_id = #{serverId}") + List queryAll(@Param("serverId") String serverId); + + @Delete("DELETE FROM wvp_media_server WHERE id=#{id} and server_id = #{serverId}") + void delOne(String id, @Param("serverId") String serverId); + + @Select("SELECT * FROM wvp_media_server WHERE ip=#{host} and http_port=#{port} and server_id = #{serverId}") + MediaServer queryOneByHostAndPort(@Param("host") String host, @Param("port") int port, @Param("serverId") String serverId); + + @Select("SELECT * FROM wvp_media_server WHERE default_server=true and server_id = #{serverId}") + MediaServer queryDefault(@Param("serverId") String serverId); + + @Select("SELECT * FROM wvp_media_server WHERE record_assist_port > 0 and server_id = #{serverId}") + List queryAllWithAssistPort(@Param("serverId") String serverId); + +} diff --git a/src/main/java/com/genersoft/iot/vmp/storager/dao/RecordPlanMapper.java b/src/main/java/com/genersoft/iot/vmp/storager/dao/RecordPlanMapper.java new file mode 100644 index 0000000..ae0649a --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/storager/dao/RecordPlanMapper.java @@ -0,0 +1,67 @@ +package com.genersoft.iot.vmp.storager.dao; + +import com.genersoft.iot.vmp.service.bean.RecordPlan; +import com.genersoft.iot.vmp.service.bean.RecordPlanItem; +import org.apache.ibatis.annotations.*; + +import java.util.List; + +@Mapper +public interface RecordPlanMapper { + + @Insert(" ") + @Options(useGeneratedKeys = true, keyProperty = "id", keyColumn = "id") + void add(RecordPlan plan); + + @Insert(" ") + void batchAddItem(@Param("planId") int planId, List planItemList); + + @Select("select * from wvp_record_plan where id = #{planId}") + RecordPlan get(@Param("planId") Integer planId); + + @Select(" ") + List query(@Param("query") String query); + + @Update("UPDATE wvp_record_plan SET update_time=#{updateTime}, name=#{name}, snap=#{snap} WHERE id=#{id}") + void update(RecordPlan plan); + + @Delete("DELETE FROM wvp_record_plan WHERE id=#{planId}") + void delete(@Param("planId") Integer planId); + + @Select("select * from wvp_record_plan_item where plan_id = #{planId}") + List getItemList(@Param("planId") Integer planId); + + @Delete("DELETE FROM wvp_record_plan_item WHERE plan_id = #{planId}") + void cleanItems(@Param("planId") Integer planId); + + @Select(" ") + List queryRecordIng(@Param("week") int week, @Param("index") int index); +} diff --git a/src/main/java/com/genersoft/iot/vmp/storager/dao/RoleMapper.java b/src/main/java/com/genersoft/iot/vmp/storager/dao/RoleMapper.java new file mode 100755 index 0000000..3df7469 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/storager/dao/RoleMapper.java @@ -0,0 +1,34 @@ +package com.genersoft.iot.vmp.storager.dao; + +import com.genersoft.iot.vmp.storager.dao.dto.Role; +import org.apache.ibatis.annotations.*; +import org.springframework.stereotype.Repository; + +import java.util.List; + +@Mapper +@Repository +public interface RoleMapper { + + @Insert("INSERT INTO wvp_user_role (name, authority, create_time, update_time) VALUES" + + "(#{name}, #{authority}, #{createTime}, #{updateTime})") + int add(Role role); + + @Update(value = {" "}) + int update(Role role); + + @Delete("DELETE from wvp_user_role WHERE id != 1 and id=#{id}") + int delete(int id); + + @Select("select * from wvp_user_role WHERE id=#{id}") + Role selectById(int id); + + @Select("select * from wvp_user_role") + List selectAll(); +} diff --git a/src/main/java/com/genersoft/iot/vmp/storager/dao/UserApiKeyMapper.java b/src/main/java/com/genersoft/iot/vmp/storager/dao/UserApiKeyMapper.java new file mode 100644 index 0000000..18539f1 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/storager/dao/UserApiKeyMapper.java @@ -0,0 +1,61 @@ +package com.genersoft.iot.vmp.storager.dao; + +import com.genersoft.iot.vmp.storager.dao.dto.UserApiKey; +import org.apache.ibatis.annotations.*; +import org.springframework.stereotype.Repository; + +import java.util.List; + +@Mapper +@Repository +public interface UserApiKeyMapper { + + @SelectKey(databaseId = "postgresql", statement = "SELECT currval('wvp_user_api_key_id_seq'::regclass) AS id", keyProperty = "id", before = false, resultType = Integer.class) + @SelectKey(databaseId = "mysql", statement = "SELECT LAST_INSERT_ID() AS id", keyProperty = "id", before = false, resultType = Integer.class) + @Insert("INSERT INTO wvp_user_api_key (user_id, app, api_key, expired_at, remark, enable, create_time, update_time) VALUES" + + "(#{userId}, #{app}, #{apiKey}, #{expiredAt}, #{remark}, #{enable}, #{createTime}, #{updateTime})") + int add(UserApiKey userApiKey); + + @Update(value = {""}) + int update(UserApiKey userApiKey); + + @Update("UPDATE wvp_user_api_key SET enable = true WHERE id = #{id}") + int enable(@Param("id") int id); + + @Update("UPDATE wvp_user_api_key SET enable = false WHERE id = #{id}") + int disable(@Param("id") int id); + + @Update("UPDATE wvp_user_api_key SET api_key = #{apiKey} WHERE id = #{id}") + int apiKey(@Param("id") int id, @Param("apiKey") String apiKey); + + @Update("UPDATE wvp_user_api_key SET remark = #{remark} WHERE id = #{id}") + int remark(@Param("id") int id, @Param("remark") String remark); + + @Delete("DELETE FROM wvp_user_api_key WHERE id = #{id}") + int delete(@Param("id") int id); + + @Select("SELECT uak.id, uak.user_id, uak.app, uak.api_key, uak.expired_at, uak.remark, uak.enable, uak.create_time, uak.update_time, u.username AS username FROM wvp_user_api_key uak LEFT JOIN wvp_user u on u.id = uak.user_id WHERE uak.id = #{id}") + UserApiKey selectById(@Param("id") int id); + + @Select("SELECT uak.id, uak.user_id, uak.app, uak.api_key, uak.expired_at, uak.remark, uak.enable, uak.create_time, uak.update_time, u.username AS username FROM wvp_user_api_key uak LEFT JOIN wvp_user u on u.id = uak.user_id WHERE uak.api_key = #{apiKey}") + UserApiKey selectByApiKey(@Param("apiKey") String apiKey); + + @Select("SELECT uak.id, uak.user_id, uak.app, uak.api_key, uak.expired_at, uak.remark, uak.enable, uak.create_time, uak.update_time, u.username AS username FROM wvp_user_api_key uak LEFT JOIN wvp_user u on u.id = uak.user_id") + List selectAll(); + + @Select("SELECT uak.id, uak.user_id, uak.app, uak.api_key, uak.expired_at, uak.remark, uak.enable, uak.create_time, uak.update_time, u.username AS username FROM wvp_user_api_key uak LEFT JOIN wvp_user u on u.id = uak.user_id") + List getUserApiKeys(); + + @Select("SELECT COUNT(0) FROM wvp_user_api_key WHERE api_key = #{apiKey}") + boolean isApiKeyExists(@Param("apiKey") String apiKey); + +} diff --git a/src/main/java/com/genersoft/iot/vmp/storager/dao/UserMapper.java b/src/main/java/com/genersoft/iot/vmp/storager/dao/UserMapper.java new file mode 100755 index 0000000..8bfeab7 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/storager/dao/UserMapper.java @@ -0,0 +1,60 @@ +package com.genersoft.iot.vmp.storager.dao; + +import com.genersoft.iot.vmp.storager.dao.dto.User; +import org.apache.ibatis.annotations.*; +import org.apache.ibatis.annotations.Param; +import org.springframework.stereotype.Repository; + +import java.util.List; + +@Mapper +@Repository +public interface UserMapper { + + @Insert("INSERT INTO wvp_user (username, password, role_id, push_key, create_time, update_time) VALUES" + + "(#{username}, #{password}, #{role.id}, #{pushKey}, #{createTime}, #{updateTime})") + int add(User user); + + @Update(value = {" "}) + int update(User user); + + @Delete("DELETE from wvp_user WHERE id != 1 and id=#{id}") + int delete(int id); + + @Select("select u.*, r.name as roleName, r.authority as roleAuthority , r.create_time as role_create_time , r.update_time as role_update_time from wvp_user u, wvp_user_role r WHERE u.role_id=r.id and u.username=#{username} AND u.password=#{password}") + @Results(id = "roleMap", value = { + @Result(column = "role_id", property = "role.id"), + @Result(column = "role_name", property = "role.name"), + @Result(column = "role_authority", property = "role.authority"), + @Result(column = "role_create_time", property = "role.createTime"), + @Result(column = "role_update_time", property = "role.updateTime") + }) + User select(@Param("username") String username, @Param("password") String password); + + @Select("select u.*, r.name as role_name, r.authority as role_authority , r.create_time as role_create_time , r.update_time as role_update_time from wvp_user u, wvp_user_role r WHERE u.role_id=r.id and u.id=#{id}") + @ResultMap(value="roleMap") + User selectById(int id); + + @Select("select u.*, r.name as role_name, r.authority as role_authority , r.create_time as role_create_time , r.update_time as role_update_time from wvp_user u, wvp_user_role r WHERE u.role_id=r.id and u.username=#{username}") + @ResultMap(value="roleMap") + User getUserByUsername(String username); + + @Select("select u.*, r.name as role_name, r.authority as role_authority , r.create_time as role_create_time , r.update_time as role_update_time from wvp_user u, wvp_user_role r WHERE u.role_id=r.id") + @ResultMap(value="roleMap") + List selectAll(); + + @Select("select u.id, u.username,u.push_key,u.role_id, r.name as role_name, r.authority as role_authority , r.create_time as role_create_time , r.update_time as role_update_time from wvp_user u join wvp_user_role r on u.role_id=r.id") + @ResultMap(value="roleMap") + List getUsers(); + + @Update("UPDATE wvp_user set push_key=#{pushKey} where id=#{id}") + int changePushKey(@Param("id") int id, @Param("pushKey") String pushKey); +} diff --git a/src/main/java/com/genersoft/iot/vmp/storager/dao/dto/ChannelSourceInfo.java b/src/main/java/com/genersoft/iot/vmp/storager/dao/dto/ChannelSourceInfo.java new file mode 100755 index 0000000..e8b91e7 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/storager/dao/dto/ChannelSourceInfo.java @@ -0,0 +1,22 @@ +package com.genersoft.iot.vmp.storager.dao.dto; + +public class ChannelSourceInfo { + private String name; + private int count; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public int getCount() { + return count; + } + + public void setCount(int count) { + this.count = count; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/storager/dao/dto/LogDto.java b/src/main/java/com/genersoft/iot/vmp/storager/dao/dto/LogDto.java new file mode 100755 index 0000000..cfe29f5 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/storager/dao/dto/LogDto.java @@ -0,0 +1,86 @@ +package com.genersoft.iot.vmp.storager.dao.dto; + +public class LogDto { + + private int id; + private String name; + private String type; + private String uri; + private String address; + private String result; + private long timing; + private String username; + private String createTime; + + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + public String getUri() { + return uri; + } + + public void setUri(String uri) { + this.uri = uri; + } + + public String getAddress() { + return address; + } + + public void setAddress(String address) { + this.address = address; + } + + public String getResult() { + return result; + } + + public void setResult(String result) { + this.result = result; + } + + public long getTiming() { + return timing; + } + + public void setTiming(long timing) { + this.timing = timing; + } + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getCreateTime() { + return createTime; + } + + public void setCreateTime(String createTime) { + this.createTime = createTime; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/storager/dao/dto/PlatformRegisterInfo.java b/src/main/java/com/genersoft/iot/vmp/storager/dao/dto/PlatformRegisterInfo.java new file mode 100755 index 0000000..16f6636 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/storager/dao/dto/PlatformRegisterInfo.java @@ -0,0 +1,41 @@ +package com.genersoft.iot.vmp.storager.dao.dto; + +/** + * 平台发送注册/注销消息时缓存此消息 + * @author lin + */ +public class PlatformRegisterInfo { + + /** + * 平台Id + */ + private String platformId; + + /** + * 是否时注册,false为注销 + */ + private boolean register; + + public static PlatformRegisterInfo getInstance(String platformId, boolean register) { + PlatformRegisterInfo platformRegisterInfo = new PlatformRegisterInfo(); + platformRegisterInfo.setPlatformId(platformId); + platformRegisterInfo.setRegister(register); + return platformRegisterInfo; + } + + public String getPlatformId() { + return platformId; + } + + public void setPlatformId(String platformId) { + this.platformId = platformId; + } + + public boolean isRegister() { + return register; + } + + public void setRegister(boolean register) { + this.register = register; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/storager/dao/dto/RecordInfo.java b/src/main/java/com/genersoft/iot/vmp/storager/dao/dto/RecordInfo.java new file mode 100755 index 0000000..278e3e4 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/storager/dao/dto/RecordInfo.java @@ -0,0 +1,133 @@ +package com.genersoft.iot.vmp.storager.dao.dto; + +/** + * 录像记录 + */ +public class RecordInfo { + + /** + * ID + */ + private int id; + + /** + * 应用名 + */ + private String app; + + /** + * 流ID + */ + private String stream; + + /** + * 对应的zlm流媒体的ID + */ + private String mediaServerId; + + /** + * 创建时间 + */ + private String createTime; + + /** + * 类型 对应zlm的 originType + * unknown = 0, + * rtmp_push=1, + * rtsp_push=2, + * rtp_push=3, + * pull=4, + * ffmpeg_pull=5, + * mp4_vod=6, + * device_chn=7, + * rtc_push=8 + */ + private int type; + + /** + * 国标录像时的设备ID + */ + private String deviceId; + + /** + * 国标录像时的通道ID + */ + private String channelId; + + /** + * 拉流代理录像时的名称 + */ + private String name; + + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + + public String getApp() { + return app; + } + + public void setApp(String app) { + this.app = app; + } + + public String getStream() { + return stream; + } + + public void setStream(String stream) { + this.stream = stream; + } + + public String getMediaServerId() { + return mediaServerId; + } + + public void setMediaServerId(String mediaServerId) { + this.mediaServerId = mediaServerId; + } + + public String getCreateTime() { + return createTime; + } + + public void setCreateTime(String createTime) { + this.createTime = createTime; + } + + public int getType() { + return type; + } + + public void setType(int type) { + this.type = type; + } + + public String getDeviceId() { + return deviceId; + } + + public void setDeviceId(String deviceId) { + this.deviceId = deviceId; + } + + public String getChannelId() { + return channelId; + } + + public void setChannelId(String channelId) { + this.channelId = channelId; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/storager/dao/dto/Role.java b/src/main/java/com/genersoft/iot/vmp/storager/dao/dto/Role.java new file mode 100755 index 0000000..44631f8 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/storager/dao/dto/Role.java @@ -0,0 +1,50 @@ +package com.genersoft.iot.vmp.storager.dao.dto; + +public class Role { + + private int id; + private String name; + private String authority; + private String createTime; + private String updateTime; + + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getAuthority() { + return authority; + } + + public void setAuthority(String authority) { + this.authority = authority; + } + + public String getCreateTime() { + return createTime; + } + + public void setCreateTime(String createTime) { + this.createTime = createTime; + } + + public String getUpdateTime() { + return updateTime; + } + + public void setUpdateTime(String updateTime) { + this.updateTime = updateTime; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/storager/dao/dto/User.java b/src/main/java/com/genersoft/iot/vmp/storager/dao/dto/User.java new file mode 100755 index 0000000..c9b4002 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/storager/dao/dto/User.java @@ -0,0 +1,68 @@ +package com.genersoft.iot.vmp.storager.dao.dto; + +public class User { + + private int id; + private String username; + private String password; + private String createTime; + private String updateTime; + private String pushKey; + private Role role; + + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } + + public String getCreateTime() { + return createTime; + } + + public void setCreateTime(String createTime) { + this.createTime = createTime; + } + + public String getUpdateTime() { + return updateTime; + } + + public void setUpdateTime(String updateTime) { + this.updateTime = updateTime; + } + + public Role getRole() { + return role; + } + + public void setRole(Role role) { + this.role = role; + } + + public String getPushKey() { + return pushKey; + } + + public void setPushKey(String pushKey) { + this.pushKey = pushKey; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/storager/dao/dto/UserApiKey.java b/src/main/java/com/genersoft/iot/vmp/storager/dao/dto/UserApiKey.java new file mode 100644 index 0000000..b631295 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/storager/dao/dto/UserApiKey.java @@ -0,0 +1,151 @@ +package com.genersoft.iot.vmp.storager.dao.dto; + +import io.swagger.v3.oas.annotations.media.Schema; + +import java.io.Serializable; + +/** + * 用户信息 + */ +@Schema(description = "用户ApiKey信息") +public class UserApiKey implements Serializable { + + /** + * Id + */ + @Schema(description = "Id") + private int id; + + /** + * 用户Id + */ + @Schema(description = "用户Id") + private int userId; + + /** + * 应用名 + */ + @Schema(description = "应用名") + private String app; + + /** + * ApiKey + */ + @Schema(description = "ApiKey") + private String apiKey; + + /** + * 过期时间(null=永不过期) + */ + @Schema(description = "过期时间(null=永不过期)") + private long expiredAt; + + /** + * 备注信息 + */ + @Schema(description = "备注信息") + private String remark; + + /** + * 是否启用 + */ + @Schema(description = "是否启用") + private boolean enable; + + /** + * 创建时间 + */ + @Schema(description = "创建时间") + private String createTime; + + /** + * 更新时间 + */ + @Schema(description = "更新时间") + private String updateTime; + + /** + * 用户名 + */ + private String username; + + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + + public int getUserId() { + return userId; + } + + public void setUserId(int userId) { + this.userId = userId; + } + + public String getApp() { + return app; + } + + public void setApp(String app) { + this.app = app; + } + + public String getApiKey() { + return apiKey; + } + + public void setApiKey(String apiKey) { + this.apiKey = apiKey; + } + + public long getExpiredAt() { + return expiredAt; + } + + public void setExpiredAt(long expiredAt) { + this.expiredAt = expiredAt; + } + + public String getRemark() { + return remark; + } + + public void setRemark(String remark) { + this.remark = remark; + } + + public boolean isEnable() { + return enable; + } + + public void setEnable(boolean enable) { + this.enable = enable; + } + + public String getCreateTime() { + return createTime; + } + + public void setCreateTime(String createTime) { + this.createTime = createTime; + } + + public String getUpdateTime() { + return updateTime; + } + + public void setUpdateTime(String updateTime) { + this.updateTime = updateTime; + } + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/storager/impl/RedisCatchStorageImpl.java b/src/main/java/com/genersoft/iot/vmp/storager/impl/RedisCatchStorageImpl.java new file mode 100755 index 0000000..7e45bc5 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/storager/impl/RedisCatchStorageImpl.java @@ -0,0 +1,531 @@ +package com.genersoft.iot.vmp.storager.impl; + +import com.alibaba.fastjson2.JSON; +import com.alibaba.fastjson2.JSONObject; +import com.genersoft.iot.vmp.common.ServerInfo; +import com.genersoft.iot.vmp.common.SystemAllInfo; +import com.genersoft.iot.vmp.common.VideoManagerConstants; +import com.genersoft.iot.vmp.conf.UserSetting; +import com.genersoft.iot.vmp.gb28181.bean.*; +import com.genersoft.iot.vmp.gb28181.dao.DeviceChannelMapper; +import com.genersoft.iot.vmp.gb28181.dao.DeviceMapper; +import com.genersoft.iot.vmp.media.bean.MediaInfo; +import com.genersoft.iot.vmp.media.bean.MediaServer; +import com.genersoft.iot.vmp.media.zlm.dto.StreamAuthorityInfo; +import com.genersoft.iot.vmp.service.bean.GPSMsgInfo; +import com.genersoft.iot.vmp.service.bean.MessageForPushChannel; +import com.genersoft.iot.vmp.storager.IRedisCatchStorage; +import com.genersoft.iot.vmp.utils.DateUtil; +import com.genersoft.iot.vmp.utils.JsonUtil; +import com.genersoft.iot.vmp.utils.SystemInfoUtils; +import com.genersoft.iot.vmp.utils.redis.RedisUtil; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.stereotype.Component; + +import java.time.Duration; +import java.util.*; + +@SuppressWarnings("rawtypes") +@Slf4j +@Component +public class RedisCatchStorageImpl implements IRedisCatchStorage { + + + @Autowired + private DeviceChannelMapper deviceChannelMapper; + + @Autowired + private DeviceMapper deviceMapper; + + @Autowired + private UserSetting userSetting; + + @Autowired + private RedisTemplate redisTemplate; + + @Autowired + private StringRedisTemplate stringRedisTemplate; + + @Override + public List queryAllSendRTPServer() { + return Collections.emptyList(); + } + + @Override + public Long getCSEQ() { + String key = VideoManagerConstants.SIP_CSEQ_PREFIX + userSetting.getServerId(); + + Long result = redisTemplate.opsForValue().increment(key, 1L); + if (result != null && result > Integer.MAX_VALUE) { + redisTemplate.opsForValue().set(key, 1); + result = 1L; + } + return result; + } + + @Override + public void resetAllCSEQ() { + String key = VideoManagerConstants.SIP_CSEQ_PREFIX + userSetting.getServerId(); + redisTemplate.opsForValue().set(key, 1); + } + + + @Override + public void updateWVPInfo(ServerInfo serverInfo, int time) { + String key = VideoManagerConstants.WVP_SERVER_PREFIX + userSetting.getServerId(); + Duration duration = Duration.ofSeconds(time); + redisTemplate.opsForValue().set(key, serverInfo, duration); + // 设置平台的分数值 + String setKey = VideoManagerConstants.WVP_SERVER_LIST; + // 首次设置就设置为0, 后续值越小说明越是最近启动的 + redisTemplate.opsForZSet().add(setKey, userSetting.getServerId(), System.currentTimeMillis()); + } + + @Override + public void removeOfflineWVPInfo(String serverId) { + String setKey = VideoManagerConstants.WVP_SERVER_LIST; + // 首次设置就设置为0, 后续值越小说明越是最近启动的 + redisTemplate.opsForZSet().remove(setKey, serverId); + } + + @Override + public void sendStreamChangeMsg(String type, JSONObject jsonObject) { + String key = VideoManagerConstants.WVP_MSG_STREAM_CHANGE_PREFIX + type; + log.info("[redis 流变化事件] 发送 {}: {}", key, jsonObject.toString()); + redisTemplate.convertAndSend(key, jsonObject); + } + + @Override + public void addStream(MediaServer mediaServerItem, String type, String app, String streamId, MediaInfo mediaInfo) { + // 查找是否使用了callID + StreamAuthorityInfo streamAuthorityInfo = getStreamAuthorityInfo(app, streamId); + String key = VideoManagerConstants.WVP_SERVER_STREAM_PREFIX + userSetting.getServerId() + "_" + type.toUpperCase() + "_" + app + "_" + streamId + "_" + mediaServerItem.getId(); + if (streamAuthorityInfo != null) { + mediaInfo.setCallId(streamAuthorityInfo.getCallId()); + } + redisTemplate.opsForValue().set(key, JSON.toJSONString(mediaInfo)); + } + + @Override + public void removeStream(String mediaServerId, String type, String app, String streamId) { + String key = VideoManagerConstants.WVP_SERVER_STREAM_PREFIX + userSetting.getServerId() + "_" + type.toUpperCase() + "_" + app + "_" + streamId + "_" + mediaServerId; + redisTemplate.delete(key); + } + + @Override + public void removeStream(String mediaServerId, String type) { + String key = VideoManagerConstants.WVP_SERVER_STREAM_PREFIX + userSetting.getServerId() + "_" + type.toUpperCase() + "_*_*_" + mediaServerId; + List streams = RedisUtil.scan(redisTemplate, key); + for (Object stream : streams) { + redisTemplate.delete(stream); + } + } + + @Override + public List getStreams(String mediaServerId, String type) { + List result = new ArrayList<>(); + String key = VideoManagerConstants.WVP_SERVER_STREAM_PREFIX + userSetting.getServerId() + "_" + type.toUpperCase() + "_*_*_" + mediaServerId; + List streams = RedisUtil.scan(redisTemplate, key); + for (Object stream : streams) { + String mediaInfoJson = (String)redisTemplate.opsForValue().get(stream); + MediaInfo mediaInfo = JSON.parseObject(mediaInfoJson, MediaInfo.class); + result.add(mediaInfo); + } + return result; + } + + @Override + public void updateDevice(Device device) { + String key = VideoManagerConstants.DEVICE_PREFIX; + redisTemplate.opsForHash().put(key, device.getDeviceId(), device); + } + + @Override + public void removeDevice(String deviceId) { + String key = VideoManagerConstants.DEVICE_PREFIX; + redisTemplate.opsForHash().delete(key, deviceId); + } + + @Override + public void removeAllDevice() { + String key = VideoManagerConstants.DEVICE_PREFIX; + redisTemplate.delete(key); + } + + @Override + public List getAllDevices() { + String key = VideoManagerConstants.DEVICE_PREFIX; + List result = new ArrayList<>(); + List values = redisTemplate.opsForHash().values(key); + for (Object value : values) { + if (Objects.nonNull(value)) { + result.add((Device)value); + } + } + return result; + } + + @Override + public Device getDevice(String deviceId) { + String key = VideoManagerConstants.DEVICE_PREFIX; + Device device; + Object object = redisTemplate.opsForHash().get(key, deviceId); + if (object == null){ + device = deviceMapper.getDeviceByDeviceId(deviceId); + if (device != null) { + updateDevice(device); + } + }else { + device = (Device)object; + } + return device; + } + + @Override + public void updateGpsMsgInfo(GPSMsgInfo gpsMsgInfo) { + String key = VideoManagerConstants.WVP_STREAM_GPS_MSG_PREFIX + userSetting.getServerId(); + Duration duration = Duration.ofSeconds(60L); + gpsMsgInfo.setStored(false); + redisTemplate.opsForHash().put(key, gpsMsgInfo.getId(),gpsMsgInfo); + redisTemplate.expire(key, duration); + // 默认GPS消息保存1分钟 + } + + @Override + public GPSMsgInfo getGpsMsgInfo(String channelId) { + String key = VideoManagerConstants.WVP_STREAM_GPS_MSG_PREFIX + userSetting.getServerId(); + return (GPSMsgInfo) redisTemplate.opsForHash().get(key, channelId); + } + + @Override + public List getAllGpsMsgInfo() { + String key = VideoManagerConstants.WVP_STREAM_GPS_MSG_PREFIX + userSetting.getServerId(); + List result = new ArrayList<>(); + List values = redisTemplate.opsForHash().values(key); + for (Object value : values) { + result.add((GPSMsgInfo)value); + } + return result; + } + + @Override + public void updateStreamAuthorityInfo(String app, String stream, StreamAuthorityInfo streamAuthorityInfo) { + String key = VideoManagerConstants.MEDIA_STREAM_AUTHORITY; + String objectKey = app+ "_" + stream; + redisTemplate.opsForHash().put(key, objectKey, streamAuthorityInfo); + } + + @Override + public void removeStreamAuthorityInfo(String app, String stream) { + String key = VideoManagerConstants.MEDIA_STREAM_AUTHORITY; + String objectKey = app+ "_" + stream; + redisTemplate.opsForHash().delete(key, objectKey); + } + + @Override + public StreamAuthorityInfo getStreamAuthorityInfo(String app, String stream) { + String key = VideoManagerConstants.MEDIA_STREAM_AUTHORITY; + String objectKey = app+ "_" + stream; + return (StreamAuthorityInfo)redisTemplate.opsForHash().get(key, objectKey); + + } + + @Override + public List getAllStreamAuthorityInfo() { + String key = VideoManagerConstants.MEDIA_STREAM_AUTHORITY; + List result = new ArrayList<>(); + List values = redisTemplate.opsForHash().values(key); + for (Object value : values) { + result.add((StreamAuthorityInfo)value); + } + return result; + } + + + @Override + public MediaInfo getStreamInfo(String app, String streamId, String mediaServerId) { + String scanKey = VideoManagerConstants.WVP_SERVER_STREAM_PREFIX + userSetting.getServerId() + "_*_" + app + "_" + streamId + "_" + mediaServerId; + + MediaInfo result = null; + List keys = RedisUtil.scan(redisTemplate, scanKey); + if (keys.size() > 0) { + String key = (String) keys.get(0); + String mediaInfoJson = (String)redisTemplate.opsForValue().get(key); + result = JSON.parseObject(mediaInfoJson, MediaInfo.class); + } + + return result; + } + + @Override + public MediaInfo getProxyStream(String app, String streamId) { + String scanKey = VideoManagerConstants.WVP_SERVER_STREAM_PREFIX + userSetting.getServerId() + "_PULL_" + app + "_" + streamId + "_*"; + + MediaInfo result = null; + List keys = RedisUtil.scan(redisTemplate, scanKey); + if (keys.size() > 0) { + String key = (String) keys.get(0); + String mediaInfoJson = (String)redisTemplate.opsForValue().get(key); + result = JSON.parseObject(mediaInfoJson, MediaInfo.class); + } + + return result; + } + + @Override + public void addCpuInfo(double cpuInfo) { + String key = VideoManagerConstants.SYSTEM_INFO_CPU_PREFIX + userSetting.getServerId(); + Map infoMap = new HashMap<>(); + infoMap.put("time", DateUtil.getNow()); + infoMap.put("data", String.valueOf(cpuInfo)); + redisTemplate.opsForList().rightPush(key, infoMap); + // 每秒一个,最多只存30个 + Long size = redisTemplate.opsForList().size(key); + if (size != null && size >= 30) { + for (int i = 0; i < size - 30; i++) { + redisTemplate.opsForList().leftPop(key); + } + } + } + + @Override + public void addMemInfo(double memInfo) { + String key = VideoManagerConstants.SYSTEM_INFO_MEM_PREFIX + userSetting.getServerId(); + Map infoMap = new HashMap<>(); + infoMap.put("time", DateUtil.getNow()); + infoMap.put("data", String.valueOf(memInfo)); + redisTemplate.opsForList().rightPush(key, infoMap); + // 每秒一个,最多只存30个 + Long size = redisTemplate.opsForList().size(key); + if (size != null && size >= 30) { + for (int i = 0; i < size - 30; i++) { + redisTemplate.opsForList().leftPop(key); + } + } + } + + @Override + public void addNetInfo(Map networkInterfaces) { + String key = VideoManagerConstants.SYSTEM_INFO_NET_PREFIX + userSetting.getServerId(); + Map infoMap = new HashMap<>(); + infoMap.put("time", DateUtil.getNow()); + for (String netKey : networkInterfaces.keySet()) { + infoMap.put(netKey, networkInterfaces.get(netKey)); + } + redisTemplate.opsForList().rightPush(key, infoMap); + // 每秒一个,最多只存30个 + Long size = redisTemplate.opsForList().size(key); + if (size != null && size >= 30) { + for (int i = 0; i < size - 30; i++) { + redisTemplate.opsForList().leftPop(key); + } + } + } + + @Override + public void addDiskInfo(List> diskInfo) { + + String key = VideoManagerConstants.SYSTEM_INFO_DISK_PREFIX + userSetting.getServerId(); + redisTemplate.opsForValue().set(key, diskInfo); + } + + @Override + public SystemAllInfo getSystemInfo() { + String cpuKey = VideoManagerConstants.SYSTEM_INFO_CPU_PREFIX + userSetting.getServerId(); + String memKey = VideoManagerConstants.SYSTEM_INFO_MEM_PREFIX + userSetting.getServerId(); + String netKey = VideoManagerConstants.SYSTEM_INFO_NET_PREFIX + userSetting.getServerId(); + String diskKey = VideoManagerConstants.SYSTEM_INFO_DISK_PREFIX + userSetting.getServerId(); + SystemAllInfo systemAllInfo = new SystemAllInfo(); + systemAllInfo.setCpu(redisTemplate.opsForList().range(cpuKey, 0, -1)); + systemAllInfo.setMem(redisTemplate.opsForList().range(memKey, 0, -1)); + systemAllInfo.setNet(redisTemplate.opsForList().range(netKey, 0, -1)); + + systemAllInfo.setDisk(redisTemplate.opsForValue().get(diskKey)); + systemAllInfo.setNetTotal(SystemInfoUtils.getNetworkTotal()); + return systemAllInfo; + } + + @Override + public void sendStreamPushRequestedMsg(MessageForPushChannel msg) { + String key = VideoManagerConstants.VM_MSG_STREAM_PUSH_REQUESTED; + log.info("[redis发送通知] 发送 推流被请求 {}: {}/{}", key, msg.getApp(), msg.getStream()); + redisTemplate.convertAndSend(key, JSON.toJSON(msg)); + } + + @Override + public void sendAlarmMsg(AlarmChannelMessage msg) { + // 此消息用于对接第三方服务下级来的消息内容 + String key = VideoManagerConstants.VM_MSG_SUBSCRIBE_ALARM; + log.info("[redis发送通知] 发送 报警{}: {}", key, JSON.toJSON(msg)); + redisTemplate.convertAndSend(key, JSON.toJSON(msg)); + } + + @Override + public boolean deviceIsOnline(String deviceId) { + return getDevice(deviceId).isOnLine(); + } + + + @Override + public void sendStreamPushRequestedMsgForStatus() { + String key = VideoManagerConstants.VM_MSG_GET_ALL_ONLINE_REQUESTED; + log.info("[redis通知] 发送 获取所有推流设备的状态"); + JSONObject jsonObject = new JSONObject(); + jsonObject.put(key, key); + redisTemplate.convertAndSend(key, jsonObject); + } + + @Override + public int getPushStreamCount(String id) { + String key = VideoManagerConstants.WVP_SERVER_STREAM_PREFIX + userSetting.getServerId() + "_PUSH_*_*_" + id; + return RedisUtil.scan(redisTemplate, key).size(); + } + + @Override + public int getProxyStreamCount(String id) { + String key = VideoManagerConstants.WVP_SERVER_STREAM_PREFIX + userSetting.getServerId() + "_PULL_*_*_" + id; + return RedisUtil.scan(redisTemplate, key).size(); + } + + @Override + public int getGbSendCount(String id) { + String key = VideoManagerConstants.SEND_RTP_INFO_CALLID; + return redisTemplate.opsForHash().size(key).intValue(); + } + + @Override + public void sendDeviceOrChannelStatus(String deviceId, String channelId, boolean online) { + String key = VideoManagerConstants.VM_MSG_SUBSCRIBE_DEVICE_STATUS; + StringBuilder msg = new StringBuilder(); + msg.append(deviceId); + if (channelId != null) { + msg.append(":").append(channelId); + } + msg.append(" ").append(online? "ON":"OFF"); + log.info("[redis通知] 推送设备/通道状态-> {} ", msg); + // 使用 RedisTemplate 发送字符串消息会导致发送的消息多带了双引号 + stringRedisTemplate.convertAndSend(key, msg.toString()); + } + + @Override + public void sendChannelAddOrDelete(String deviceId, String channelId, boolean add) { + String key = VideoManagerConstants.VM_MSG_SUBSCRIBE_DEVICE_STATUS; + + + StringBuilder msg = new StringBuilder(); + msg.append(deviceId); + if (channelId != null) { + msg.append(":").append(channelId); + } + msg.append(" ").append(add? "ADD":"DELETE"); + log.info("[redis通知] 推送通道-> {}", msg); + // 使用 RedisTemplate 发送字符串消息会导致发送的消息多带了双引号 + stringRedisTemplate.convertAndSend(key, msg.toString()); + } + + @Override + public void sendPlatformStartPlayMsg(SendRtpInfo sendRtpItem, DeviceChannel channel, Platform platform) { + if (platform == null) { + log.info("[redis发送通知] 失败, 平台信息为NULL"); + return; + } + if (sendRtpItem.getPlayType() != InviteStreamType.PUSH) { + log.info("[redis发送通知] 取消, 流来源通道不是推流设备"); + return; + } + MessageForPushChannel messageForPushChannel = MessageForPushChannel.getInstance(0, sendRtpItem.getApp(), sendRtpItem.getStream(), + channel.getDeviceId(), platform.getServerGBId(), platform.getName(), userSetting.getServerId(), + sendRtpItem.getMediaServerId()); + messageForPushChannel.setPlatFormIndex(platform.getId()); + String key = VideoManagerConstants.VM_MSG_STREAM_START_PLAY_NOTIFY; + log.info("[redis发送通知] 发送 推流被上级平台观看 {}: {}/{}->{}", key, sendRtpItem.getApp(), sendRtpItem.getStream(), platform.getServerGBId()); + redisTemplate.convertAndSend(key, JSON.toJSON(messageForPushChannel)); + } + + @Override + public void sendPlatformStopPlayMsg(SendRtpInfo sendRtpItem, Platform platform, CommonGBChannel channel) { + + MessageForPushChannel msg = MessageForPushChannel.getInstance(0, + sendRtpItem.getApp(), sendRtpItem.getStream(), channel.getGbDeviceId(), + sendRtpItem.getTargetId(), platform.getName(), userSetting.getServerId(), sendRtpItem.getMediaServerId()); + msg.setPlatFormIndex(platform.getId()); + + String key = VideoManagerConstants.VM_MSG_STREAM_STOP_PLAY_NOTIFY; + log.info("[redis发送通知] 发送 上级平台停止观看 {}: {}/{}->{}", key, sendRtpItem.getApp(), sendRtpItem.getStream(), platform.getServerGBId()); + redisTemplate.convertAndSend(key, JSON.toJSON(msg)); + } + + @Override + public void addPushListItem(String app, String stream, MediaInfo mediaInfo) { + String key = VideoManagerConstants.PUSH_STREAM_LIST + app + "_" + stream; + redisTemplate.opsForValue().set(key, mediaInfo); + } + + @Override + public MediaInfo getPushListItem(String app, String stream) { + String key = VideoManagerConstants.PUSH_STREAM_LIST + app + "_" + stream; + return (MediaInfo)redisTemplate.opsForValue().get(key); + } + + @Override + public void removePushListItem(String app, String stream, String mediaServerId) { + String key = VideoManagerConstants.PUSH_STREAM_LIST + app + "_" + stream; + MediaInfo param = (MediaInfo)redisTemplate.opsForValue().get(key); + if (param != null) { + redisTemplate.delete(key); + } + } + + @Override + public void sendPushStreamClose(MessageForPushChannel msg) { + String key = VideoManagerConstants.VM_MSG_STREAM_PUSH_CLOSE_REQUESTED; + log.info("[redis发送通知] 发送 停止向上级推流 {}: {}/{}->{}", key, msg.getApp(), msg.getStream(), msg.getPlatFormId()); + redisTemplate.convertAndSend(key, JSON.toJSON(msg)); + } + + @Override + public void addWaiteSendRtpItem(SendRtpInfo sendRtpItem, int platformPlayTimeout) { + String key = VideoManagerConstants.WAITE_SEND_PUSH_STREAM + sendRtpItem.getApp() + "_" + sendRtpItem.getStream(); + redisTemplate.opsForValue().set(key, sendRtpItem); + } + + @Override + public SendRtpInfo getWaiteSendRtpItem(String app, String stream) { + String key = VideoManagerConstants.WAITE_SEND_PUSH_STREAM + app + "_" + stream; + return JsonUtil.redisJsonToObject(redisTemplate, key, SendRtpInfo.class); + } + + @Override + public void sendStartSendRtp(SendRtpInfo sendRtpItem) { + String key = VideoManagerConstants.START_SEND_PUSH_STREAM + sendRtpItem.getApp() + "_" + sendRtpItem.getStream(); + log.info("[redis发送通知] 通知其他WVP推流 {}: {}/{}->{}", key, sendRtpItem.getApp(), sendRtpItem.getStream(), sendRtpItem.getTargetId()); + redisTemplate.convertAndSend(key, JSON.toJSON(sendRtpItem)); + } + + @Override + public void sendPushStreamOnline(SendRtpInfo sendRtpItem) { + String key = VideoManagerConstants.VM_MSG_STREAM_PUSH_CLOSE_REQUESTED; + log.info("[redis发送通知] 流上线 {}: {}/{}->{}", key, sendRtpItem.getApp(), sendRtpItem.getStream(), sendRtpItem.getTargetId()); + redisTemplate.convertAndSend(key, JSON.toJSON(sendRtpItem)); + } + + @Override + public ServerInfo queryServerInfo(String serverId) { + String key = VideoManagerConstants.WVP_SERVER_PREFIX + serverId; + return (ServerInfo)redisTemplate.opsForValue().get(key); + } + + @Override + public String chooseOneServer(String serverId) { + String key = VideoManagerConstants.WVP_SERVER_LIST; + redisTemplate.opsForZSet().remove(key, serverId); + Set range = redisTemplate.opsForZSet().range(key, 0, 0); + if (range == null || range.isEmpty()) { + return null; + } + return (String) range.iterator().next(); + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/streamProxy/bean/StreamProxy.java b/src/main/java/com/genersoft/iot/vmp/streamProxy/bean/StreamProxy.java new file mode 100755 index 0000000..14f2a6d --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/streamProxy/bean/StreamProxy.java @@ -0,0 +1,83 @@ +package com.genersoft.iot.vmp.streamProxy.bean; + +import com.genersoft.iot.vmp.common.enums.ChannelDataType; +import com.genersoft.iot.vmp.gb28181.bean.CommonGBChannel; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.springframework.util.ObjectUtils; + +/** + * @author lin + */ +@Data +@Schema(description = "拉流代理的信息") +@EqualsAndHashCode(callSuper = true) +public class StreamProxy extends CommonGBChannel { + + /** + * 数据库自增ID + */ + @Schema(description = "数据库自增ID") + private int id; + + @Schema(description = "类型,取值,default: 流媒体直接拉流(默认),ffmpeg: ffmpeg实现拉流") + private String type; + + @Schema(description = "应用名") + private String app; + + @Schema(description = "流ID") + private String stream; + + @Schema(description = "当前拉流使用的流媒体服务ID") + private String mediaServerId; + + @Schema(description = "固定选择的流媒体服务ID") + private String relatesMediaServerId; + + @Schema(description = "服务ID") + private String serverId; + + @Schema(description = "拉流地址") + private String srcUrl; + + @Schema(description = "超时时间:秒") + private int timeout; + + @Schema(description = "ffmpeg模板KEY") + private String ffmpegCmdKey; + + @Schema(description = "rtsp拉流时,拉流方式,0:tcp,1:udp,2:组播") + private String rtspType; + + @Schema(description = "是否启用") + private boolean enable; + + @Schema(description = "是否启用音频") + private boolean enableAudio; + + @Schema(description = "是否启用MP4") + private boolean enableMp4; + + @Schema(description = "是否 无人观看时自动停用") + private boolean enableDisableNoneReader; + + @Schema(description = "拉流代理时zlm返回的key,用于停止拉流代理") + private String streamKey; + + @Schema(description = "拉流状态") + private Boolean pulling; + + public CommonGBChannel buildCommonGBChannel() { + if (ObjectUtils.isEmpty(this.getGbDeviceId())) { + return null; + } + if (ObjectUtils.isEmpty(this.getGbName())) { + this.setGbName( app+ "-" +stream); + } + this.setDataType(ChannelDataType.STREAM_PROXY); + this.setDataDeviceId(this.getId()); + return this; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/streamProxy/bean/StreamProxyParam.java b/src/main/java/com/genersoft/iot/vmp/streamProxy/bean/StreamProxyParam.java new file mode 100755 index 0000000..3954fdf --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/streamProxy/bean/StreamProxyParam.java @@ -0,0 +1,71 @@ +package com.genersoft.iot.vmp.streamProxy.bean; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +/** + * @author lin + */ +@Data +@Schema(description = "拉流代理的信息") +public class StreamProxyParam { + + @Schema(description = "类型,取值,default: 流媒体直接拉流(默认),ffmpeg: ffmpeg实现拉流") + private String type; + + @Schema(description = "应用名") + private String app; + + @Schema(description = "名称") + private String name; + + @Schema(description = "流ID") + private String stream; + + @Schema(description = "流媒体服务ID") + private String mediaServerId; + + @Schema(description = "拉流地址") + private String url; + + @Schema(description = "超时时间:秒") + private int timeoutMs; + + @Schema(description = "ffmpeg模板KEY") + private String ffmpegCmdKey; + + @Schema(description = "rtsp拉流时,拉流方式,0:tcp,1:udp,2:组播") + private String rtpType; + + @Schema(description = "是否启用") + private boolean enable; + + @Schema(description = "是否启用音频") + private boolean enableAudio; + + @Schema(description = "是否启用MP4") + private boolean enableMp4; + + @Schema(description = "是否 无人观看时自动停用") + private boolean enableDisableNoneReader; + + + public StreamProxy buildStreamProxy(String serverId) { + StreamProxy streamProxy = new StreamProxy(); + streamProxy.setApp(app); + streamProxy.setStream(stream); + streamProxy.setRelatesMediaServerId(mediaServerId); + streamProxy.setServerId(serverId); + streamProxy.setSrcUrl(url); + streamProxy.setTimeout(timeoutMs/1000); + streamProxy.setRtspType(rtpType); + streamProxy.setEnable(enable); + streamProxy.setEnableAudio(enableAudio); + streamProxy.setEnableMp4(enableMp4); + streamProxy.setEnableDisableNoneReader(enableDisableNoneReader); + streamProxy.setFfmpegCmdKey(ffmpegCmdKey); + streamProxy.setGbName(name); + return streamProxy; + + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/streamProxy/controller/StreamProxyController.java b/src/main/java/com/genersoft/iot/vmp/streamProxy/controller/StreamProxyController.java new file mode 100755 index 0000000..a2cce93 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/streamProxy/controller/StreamProxyController.java @@ -0,0 +1,223 @@ +package com.genersoft.iot.vmp.streamProxy.controller; + +import com.alibaba.fastjson2.JSONObject; +import com.genersoft.iot.vmp.common.StreamInfo; +import com.genersoft.iot.vmp.conf.UserSetting; +import com.genersoft.iot.vmp.conf.exception.ControllerException; +import com.genersoft.iot.vmp.conf.security.JwtUtils; +import com.genersoft.iot.vmp.media.bean.MediaServer; +import com.genersoft.iot.vmp.media.service.IMediaServerService; +import com.genersoft.iot.vmp.service.bean.ErrorCallback; +import com.genersoft.iot.vmp.service.bean.InviteErrorCode; +import com.genersoft.iot.vmp.streamProxy.bean.StreamProxy; +import com.genersoft.iot.vmp.streamProxy.service.IStreamProxyPlayService; +import com.genersoft.iot.vmp.streamProxy.service.IStreamProxyService; +import com.genersoft.iot.vmp.vmanager.bean.ErrorCode; +import com.genersoft.iot.vmp.vmanager.bean.StreamContent; +import com.genersoft.iot.vmp.vmanager.bean.WVPResult; +import com.github.pagehelper.PageInfo; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.servlet.http.HttpServletRequest; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.context.request.async.DeferredResult; + +import java.net.MalformedURLException; +import java.net.URL; +import java.util.Map; + +@SuppressWarnings("rawtypes") +/** + * 拉流代理接口 + */ +@Tag(name = "拉流代理", description = "") +@RestController +@Slf4j +@RequestMapping(value = "/api/proxy") +public class StreamProxyController { + + @Autowired + private IMediaServerService mediaServerService; + + @Autowired + private IStreamProxyService streamProxyService; + + @Autowired + private IStreamProxyPlayService streamProxyPlayService; + + @Autowired + private UserSetting userSetting; + + + @Operation(summary = "分页查询流代理", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "page", description = "当前页") + @Parameter(name = "count", description = "每页查询数量") + @Parameter(name = "query", description = "查询内容") + @Parameter(name = "pulling", description = "是否正在拉流") + @Parameter(name = "mediaServerId", description = "流媒体ID") + @GetMapping(value = "/list") + @ResponseBody + public PageInfo list(@RequestParam(required = false)Integer page, + @RequestParam(required = false)Integer count, + @RequestParam(required = false)String query, + @RequestParam(required = false)Boolean pulling, + @RequestParam(required = false)String mediaServerId){ + + if (ObjectUtils.isEmpty(mediaServerId)) { + mediaServerId = null; + } + if (ObjectUtils.isEmpty(query)) { + query = null; + } + return streamProxyService.getAll(page, count, query, pulling, mediaServerId); + } + + @Operation(summary = "查询流代理", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "app", description = "应用名") + @Parameter(name = "stream", description = "流Id") + @GetMapping(value = "/one") + @ResponseBody + public StreamProxy one(String app, String stream){ + + return streamProxyService.getStreamProxyByAppAndStream(app, stream); + } + + @Operation(summary = "新增代理", security = @SecurityRequirement(name = JwtUtils.HEADER), parameters = { + @Parameter(name = "param", description = "代理参数", required = true), + }) + @PostMapping(value = "/add") + @ResponseBody + public StreamProxy add(@RequestBody StreamProxy param){ + log.info("添加代理: " + JSONObject.toJSONString(param)); + if (ObjectUtils.isEmpty(param.getRelatesMediaServerId())) { + param.setRelatesMediaServerId(null); + } + if (ObjectUtils.isEmpty(param.getType())) { + param.setType("default"); + } + if (ObjectUtils.isEmpty(param.getGbId())) { + param.setGbDeviceId(null); + } + param.setServerId(userSetting.getServerId()); + streamProxyService.add(param); + return param; + } + + @Operation(summary = "更新代理", security = @SecurityRequirement(name = JwtUtils.HEADER), parameters = { + @Parameter(name = "param", description = "代理参数", required = true), + }) + @PostMapping(value = "/update") + @ResponseBody + public StreamProxy update(@RequestBody StreamProxy param){ + log.info("更新代理: " + JSONObject.toJSONString(param)); + if (param.getId() == 0) { + throw new ControllerException(ErrorCode.ERROR400.getCode(), "缺少代理信息的ID"); + } + if (ObjectUtils.isEmpty(param.getRelatesMediaServerId())) { + param.setRelatesMediaServerId(null); + } + if (ObjectUtils.isEmpty(param.getGbId())) { + param.setGbDeviceId(null); + } + streamProxyService.update(param); + return param; + } + + @GetMapping(value = "/ffmpeg_cmd/list") + @ResponseBody + @Operation(summary = "获取ffmpeg.cmd模板", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "mediaServerId", description = "流媒体ID", required = true) + public Map getFFmpegCMDs(@RequestParam String mediaServerId){ + log.debug("获取节点[ {} ]ffmpeg.cmd模板", mediaServerId ); + + MediaServer mediaServerItem = mediaServerService.getOne(mediaServerId); + if (mediaServerItem == null) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "流媒体: " + mediaServerId + "未找到"); + } + return streamProxyService.getFFmpegCMDs(mediaServerItem); + } + + @DeleteMapping(value = "/del") + @ResponseBody + @Operation(summary = "移除代理", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "app", description = "应用名", required = true) + @Parameter(name = "stream", description = "流id", required = true) + public void del(@RequestParam String app, @RequestParam String stream){ + log.info("移除代理: " + app + "/" + stream); + if (app == null || stream == null) { + throw new ControllerException(ErrorCode.ERROR400.getCode(), app == null ?"app不能为null":"stream不能为null"); + }else { + streamProxyService.delteByAppAndStream(app, stream); + } + } + + @DeleteMapping(value = "/delete") + @ResponseBody + @Operation(summary = "移除代理", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "id", description = "代理ID", required = true) + public void delte(int id){ + log.info("移除代理: {}", id); + streamProxyService.delete(id); + } + + @GetMapping(value = "/start") + @ResponseBody + @Operation(summary = "播放代理", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "id", description = "代理Id", required = true) + public DeferredResult> start(HttpServletRequest request, int id){ + log.info("播放代理: {}", id); + StreamProxy streamProxy = streamProxyService.getStreamProxy(id); + Assert.notNull(streamProxy, "代理信息不存在"); + + DeferredResult> result = new DeferredResult<>(userSetting.getPlayTimeout().longValue()); + + ErrorCallback callback = (code, msg, streamInfo) -> { + if (code == InviteErrorCode.SUCCESS.getCode()) { + WVPResult wvpResult = WVPResult.success(); + if (streamInfo != null) { + if (userSetting.getUseSourceIpAsStreamIp()) { + streamInfo=streamInfo.clone();//深拷贝 + String host; + try { + URL url=new URL(request.getRequestURL().toString()); + host=url.getHost(); + } catch (MalformedURLException e) { + host=request.getLocalAddr(); + } + streamInfo.changeStreamIp(host); + } + if (!ObjectUtils.isEmpty(streamInfo.getMediaServer().getTranscodeSuffix()) + && !"null".equalsIgnoreCase(streamInfo.getMediaServer().getTranscodeSuffix())) { + streamInfo.setStream(streamInfo.getStream() + "_" + streamInfo.getMediaServer().getTranscodeSuffix()); + } + wvpResult.setData(new StreamContent(streamInfo)); + }else { + wvpResult.setCode(code); + wvpResult.setMsg(msg); + } + + result.setResult(wvpResult); + }else { + result.setResult(WVPResult.fail(code, msg)); + } + }; + + streamProxyPlayService.start(id, null, callback); + return result; + } + + @GetMapping(value = "/stop") + @ResponseBody + @Operation(summary = "停止播放", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "id", description = "代理Id", required = true) + public void stop(int id){ + log.info("停止播放: {}", id); + streamProxyPlayService.stop(id); + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/streamProxy/dao/StreamProxyMapper.java b/src/main/java/com/genersoft/iot/vmp/streamProxy/dao/StreamProxyMapper.java new file mode 100755 index 0000000..e9970b6 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/streamProxy/dao/StreamProxyMapper.java @@ -0,0 +1,95 @@ +package com.genersoft.iot.vmp.streamProxy.dao; + +import com.genersoft.iot.vmp.streamProxy.bean.StreamProxy; +import com.genersoft.iot.vmp.streamProxy.dao.provider.StreamProxyProvider; +import org.apache.ibatis.annotations.*; +import org.springframework.stereotype.Repository; + +import java.util.List; + +@Mapper +@Repository +public interface StreamProxyMapper { + + @Insert("INSERT INTO wvp_stream_proxy (type, app, stream,relates_media_server_id, src_url, " + + "timeout, ffmpeg_cmd_key, rtsp_type, enable_audio, enable_mp4, enable, pulling, " + + "enable_disable_none_reader, server_id, create_time) VALUES" + + "(#{type}, #{app}, #{stream}, #{relatesMediaServerId}, #{srcUrl}, " + + "#{timeout}, #{ffmpegCmdKey}, #{rtspType}, #{enableAudio}, #{enableMp4}, #{enable}, #{pulling}, " + + "#{enableDisableNoneReader}, #{serverId}, #{createTime} )") + @Options(useGeneratedKeys = true, keyProperty = "id", keyColumn = "id") + int add(StreamProxy streamProxyDto); + + @Update("UPDATE wvp_stream_proxy " + + "SET type=#{type}, " + + "app=#{app}," + + "stream=#{stream}," + + "relates_media_server_id=#{relatesMediaServerId}, " + + "src_url=#{srcUrl}," + + "timeout=#{timeout}, " + + "ffmpeg_cmd_key=#{ffmpegCmdKey}, " + + "rtsp_type=#{rtspType}, " + + "enable_audio=#{enableAudio}, " + + "enable=#{enable}, " + + "pulling=#{pulling}, " + + "enable_disable_none_reader=#{enableDisableNoneReader}, " + + "enable_mp4=#{enableMp4} " + + "WHERE id=#{id}") + int update(StreamProxy streamProxyDto); + + @Delete("DELETE FROM wvp_stream_proxy WHERE app=#{app} AND stream=#{stream}") + int delByAppAndStream(String app, String stream); + + @SelectProvider(type = StreamProxyProvider.class, method = "selectAll") + List selectAll(@Param("query") String query, @Param("pulling") Boolean pulling, @Param("mediaServerId") String mediaServerId); + + @SelectProvider(type = StreamProxyProvider.class, method = "selectOneByAppAndStream") + StreamProxy selectOneByAppAndStream(@Param("app") String app, @Param("stream") String stream); + + @SelectProvider(type = StreamProxyProvider.class, method = "selectForPushingInMediaServer") + List selectForPushingInMediaServer(@Param("mediaServerId") String mediaServerId, @Param("enable") boolean enable); + + + @Select("select count(1) from wvp_stream_proxy") + int getAllCount(); + + @Select("select count(1) from wvp_stream_proxy where pulling = true") + int getOnline(); + + @Delete("DELETE FROM wvp_stream_proxy WHERE id=#{id}") + int delete(@Param("id") int id); + + @Delete(value = "") + void deleteByList(List streamProxiesForRemove); + + @Update("UPDATE wvp_stream_proxy " + + "SET pulling=true " + + "WHERE id=#{id}") + int online(@Param("id") int id); + + @Update("UPDATE wvp_stream_proxy " + + "SET pulling=false " + + "WHERE id=#{id}") + int offline(@Param("id") int id); + + @SelectProvider(type = StreamProxyProvider.class, method = "select") + StreamProxy select(@Param("id") int id); + + @Update("UPDATE wvp_stream_proxy " + + " SET pulling=false, media_server_id = null," + + " stream_key = null " + + " WHERE id=#{id}") + void removeStream(@Param("id")int id); + + @Update("UPDATE wvp_stream_proxy " + + " SET pulling=#{pulling}, media_server_id = #{mediaServerId}, " + + " stream_key = #{streamKey} " + + " WHERE id=#{id}") + void updateStream(StreamProxy streamProxy); +} diff --git a/src/main/java/com/genersoft/iot/vmp/streamProxy/dao/provider/StreamProxyProvider.java b/src/main/java/com/genersoft/iot/vmp/streamProxy/dao/provider/StreamProxyProvider.java new file mode 100644 index 0000000..e61c710 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/streamProxy/dao/provider/StreamProxyProvider.java @@ -0,0 +1,65 @@ +package com.genersoft.iot.vmp.streamProxy.dao.provider; + +import com.genersoft.iot.vmp.common.enums.ChannelDataType; + +import java.util.Map; + +public class StreamProxyProvider { + + public String getBaseSelectSql(){ + return "SELECT " + + " st.*, " + + ChannelDataType.STREAM_PROXY + " as data_type, " + + " st.id as data_device_id, " + + " wdc.*, " + + " wdc.id as gb_id" + + " FROM wvp_stream_proxy st " + + " LEFT join wvp_device_channel wdc " + + " on wdc.data_type = 3 and st.id = wdc.data_device_id "; + } + + public String select(Map params ){ + return getBaseSelectSql() + " WHERE st.id = " + params.get("id"); + } + + public String selectForPushingInMediaServer(Map params ){ + return getBaseSelectSql() + " WHERE st.pulling=true and st.media_server_id=#{mediaServerId} order by st.create_time desc"; + } + + public String selectOneByAppAndStream(Map params ){ + return getBaseSelectSql() + String.format(" WHERE st.app='%s' AND st.stream='%s' order by st.create_time desc", + params.get("app"), params.get("stream")); + } + + public String selectAll(Map params ){ + StringBuilder sqlBuild = new StringBuilder(); + sqlBuild.append(getBaseSelectSql()); + sqlBuild.append(" WHERE 1=1 "); + if (params.get("query") != null) { + sqlBuild.append(" AND ") + .append(" (") + .append(" st.app LIKE ").append("'%").append(params.get("query")).append("%' escape '/'") + .append(" OR") + .append(" st.stream LIKE ").append("'%").append(params.get("query")).append("%' escape '/'") + .append(" OR") + .append(" wdc.gb_device_id LIKE ").append("'%").append(params.get("query")).append("%' escape '/'") + .append(" OR") + .append(" wdc.gb_name LIKE ").append("'%").append(params.get("query")).append("%' escape '/'") + .append(" )") + ; + } + Object pulling = params.get("pulling"); + if (pulling != null) { + if ((Boolean) pulling) { + sqlBuild.append(" AND st.pulling=1 "); + }else { + sqlBuild.append(" AND st.pulling=0 "); + } + } + if (params.get("mediaServerId") != null) { + sqlBuild.append(" AND st.media_server_id='").append(params.get("mediaServerId")).append("'"); + } + sqlBuild.append(" order by st.create_time desc"); + return sqlBuild.toString(); + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/streamProxy/service/IStreamProxyPlayService.java b/src/main/java/com/genersoft/iot/vmp/streamProxy/service/IStreamProxyPlayService.java new file mode 100755 index 0000000..a96532d --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/streamProxy/service/IStreamProxyPlayService.java @@ -0,0 +1,18 @@ +package com.genersoft.iot.vmp.streamProxy.service; + +import com.genersoft.iot.vmp.common.StreamInfo; +import com.genersoft.iot.vmp.service.bean.ErrorCallback; +import com.genersoft.iot.vmp.streamProxy.bean.StreamProxy; +import jakarta.validation.constraints.NotNull; + + +public interface IStreamProxyPlayService { + + void start(int id, Boolean record, ErrorCallback callback); + + void startProxy(@NotNull StreamProxy streamProxy, ErrorCallback callback); + + void stop(int id); + + void stopProxy(StreamProxy streamProxy); +} diff --git a/src/main/java/com/genersoft/iot/vmp/streamProxy/service/IStreamProxyService.java b/src/main/java/com/genersoft/iot/vmp/streamProxy/service/IStreamProxyService.java new file mode 100755 index 0000000..ce3fef1 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/streamProxy/service/IStreamProxyService.java @@ -0,0 +1,90 @@ +package com.genersoft.iot.vmp.streamProxy.service; + +import com.genersoft.iot.vmp.common.StreamInfo; +import com.genersoft.iot.vmp.media.bean.MediaServer; +import com.genersoft.iot.vmp.service.bean.ErrorCallback; +import com.genersoft.iot.vmp.streamProxy.bean.StreamProxy; +import com.genersoft.iot.vmp.vmanager.bean.ResourceBaseInfo; +import com.github.pagehelper.PageInfo; + +import java.util.Map; + +public interface IStreamProxyService { + + /** + * 分页查询 + * @param page + * @param count + * @return + */ + PageInfo getAll(Integer page, Integer count, String query, Boolean pulling,String mediaServerId); + + /** + * 删除视频代理 + * @param app + * @param stream + */ + void delteByAppAndStream(String app, String stream); + + /** + * 启用视频代理 + * @param app + * @param stream + * @return + */ + void startByAppAndStream(String app, String stream, ErrorCallback callback); + + /** + * 停用用视频代理 + * @param app + * @param stream + * @return + */ + void stopByAppAndStream(String app, String stream); + + /** + * 获取ffmpeg.cmd模板 + * + * @return + */ + Map getFFmpegCMDs(MediaServer mediaServerItem); + + /** + * 根据app与stream获取streamProxy + * @return + */ + StreamProxy getStreamProxyByAppAndStream(String app, String streamId); + + + /** + * 新的节点加入 + * @param mediaServer + * @return + */ + void zlmServerOnline(MediaServer mediaServer); + + /** + * 节点离线 + * @param mediaServer + * @return + */ + void zlmServerOffline(MediaServer mediaServer); + + /** + * 更新代理流 + */ + boolean update(StreamProxy streamProxyItem); + + /** + * 获取统计信息 + * @return + */ + ResourceBaseInfo getOverview(); + + void add(StreamProxy streamProxy); + + StreamProxy getStreamProxy(int id); + + void delete(int id); + +} diff --git a/src/main/java/com/genersoft/iot/vmp/streamProxy/service/impl/SourcePlayServiceForStreamProxyImpl.java b/src/main/java/com/genersoft/iot/vmp/streamProxy/service/impl/SourcePlayServiceForStreamProxyImpl.java new file mode 100644 index 0000000..984d565 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/streamProxy/service/impl/SourcePlayServiceForStreamProxyImpl.java @@ -0,0 +1,42 @@ +package com.genersoft.iot.vmp.streamProxy.service.impl; + +import com.genersoft.iot.vmp.common.StreamInfo; +import com.genersoft.iot.vmp.common.enums.ChannelDataType; +import com.genersoft.iot.vmp.gb28181.bean.CommonGBChannel; +import com.genersoft.iot.vmp.gb28181.bean.Platform; +import com.genersoft.iot.vmp.gb28181.service.ISourcePlayService; +import com.genersoft.iot.vmp.service.bean.ErrorCallback; +import com.genersoft.iot.vmp.streamProxy.service.IStreamProxyPlayService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import javax.sip.message.Response; + +@Slf4j +@Service(ChannelDataType.PLAY_SERVICE + ChannelDataType.STREAM_PROXY) +public class SourcePlayServiceForStreamProxyImpl implements ISourcePlayService { + + @Autowired + private IStreamProxyPlayService playService; + + @Override + public void play(CommonGBChannel channel, Platform platform, Boolean record, ErrorCallback callback) { + // 拉流代理通道 + try { + playService.start(channel.getDataDeviceId(), record, callback); + }catch (Exception e) { + callback.run(Response.BUSY_HERE, "busy here", null); + } + } + + @Override + public void stopPlay(CommonGBChannel channel) { + // 拉流代理通道 + try { + playService.stop(channel.getDataDeviceId()); + }catch (Exception e) { + log.error("[停止点播失败] {}({})", channel.getGbName(), channel.getGbDeviceId(), e); + } + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/streamProxy/service/impl/StreamProxyPlayServiceImpl.java b/src/main/java/com/genersoft/iot/vmp/streamProxy/service/impl/StreamProxyPlayServiceImpl.java new file mode 100755 index 0000000..9e0d444 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/streamProxy/service/impl/StreamProxyPlayServiceImpl.java @@ -0,0 +1,161 @@ +package com.genersoft.iot.vmp.streamProxy.service.impl; + +import com.genersoft.iot.vmp.common.StreamInfo; +import com.genersoft.iot.vmp.conf.DynamicTask; +import com.genersoft.iot.vmp.conf.UserSetting; +import com.genersoft.iot.vmp.conf.exception.ControllerException; +import com.genersoft.iot.vmp.media.bean.MediaServer; +import com.genersoft.iot.vmp.media.event.hook.Hook; +import com.genersoft.iot.vmp.media.event.hook.HookSubscribe; +import com.genersoft.iot.vmp.media.event.hook.HookType; +import com.genersoft.iot.vmp.media.service.IMediaServerService; +import com.genersoft.iot.vmp.service.bean.ErrorCallback; +import com.genersoft.iot.vmp.service.bean.InviteErrorCode; +import com.genersoft.iot.vmp.service.redisMsg.IRedisRpcPlayService; +import com.genersoft.iot.vmp.streamProxy.bean.StreamProxy; +import com.genersoft.iot.vmp.streamProxy.dao.StreamProxyMapper; +import com.genersoft.iot.vmp.streamProxy.service.IStreamProxyPlayService; +import com.genersoft.iot.vmp.vmanager.bean.ErrorCode; +import jakarta.validation.constraints.NotNull; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; + +import java.util.UUID; + +/** + * 视频代理业务 + */ +@Slf4j +@Service +public class StreamProxyPlayServiceImpl implements IStreamProxyPlayService { + + @Autowired + private StreamProxyMapper streamProxyMapper; + + @Autowired + private IMediaServerService mediaServerService; + + @Autowired + private HookSubscribe subscribe; + + @Autowired + private DynamicTask dynamicTask; + + @Autowired + private UserSetting userSetting; + + @Autowired + private IRedisRpcPlayService redisRpcPlayService; + + @Override + public void start(int id, Boolean record, ErrorCallback callback) { + log.info("[拉流代理], 开始拉流,ID:{}", id); + StreamProxy streamProxy = streamProxyMapper.select(id); + if (streamProxy == null) { + throw new ControllerException(ErrorCode.ERROR404.getCode(), "代理信息未找到"); + } + log.info("[拉流代理] 类型: {}, app:{}, stream: {}, 流地址: {}", streamProxy.getType(), streamProxy.getApp(), streamProxy.getStream(), streamProxy.getSrcUrl()); + if (record != null) { + streamProxy.setEnableMp4(record); + } + + startProxy(streamProxy, callback); + } + + @Override + public void startProxy(@NotNull StreamProxy streamProxy, ErrorCallback callback){ + if (!streamProxy.isEnable()) { + callback.run(ErrorCode.ERROR100.getCode(), "代理未启用", null); + return; + } + if (streamProxy.getServerId() == null) { + streamProxy.setServerId(userSetting.getServerId()); + } + if (!userSetting.getServerId().equals(streamProxy.getServerId())) { + log.info("[拉流代理] 由其他服务{}管理", streamProxy.getServerId()); + redisRpcPlayService.playProxy(streamProxy.getServerId(), streamProxy.getId(), callback); + return; + } + + if (streamProxy.getMediaServerId() != null) { + StreamInfo streamInfo = mediaServerService.getStreamInfoByAppAndStreamWithCheck(streamProxy.getApp(), streamProxy.getStream(), streamProxy.getMediaServerId(), null, false); + if (streamInfo != null) { + callback.run(ErrorCode.SUCCESS.getCode(), ErrorCode.SUCCESS.getMsg(), streamInfo); + return; + } + } + + MediaServer mediaServer; + String mediaServerId = streamProxy.getRelatesMediaServerId(); + if (mediaServerId == null) { + mediaServer = mediaServerService.getMediaServerForMinimumLoad(null); + }else { + mediaServer = mediaServerService.getOne(mediaServerId); + } + if (mediaServer == null) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), mediaServerId == null?"未找到可用的媒体节点":"未找到节点" + mediaServerId); + } + + // 设置流超时的定时任务 + String timeOutTaskKey = UUID.randomUUID().toString(); + Hook rtpHook = Hook.getInstance(HookType.on_media_arrival, streamProxy.getApp(), streamProxy.getStream(), mediaServer.getId()); + dynamicTask.startDelay(timeOutTaskKey, () -> { + log.info("[拉流代理] 收流超时,app:{},stream: {}", streamProxy.getApp(), streamProxy.getStream()); + // 收流超时 + subscribe.removeSubscribe(rtpHook); + callback.run(InviteErrorCode.ERROR_FOR_STREAM_TIMEOUT.getCode(), InviteErrorCode.ERROR_FOR_STREAM_TIMEOUT.getMsg(), null); + }, userSetting.getPlayTimeout()); + + // 开启流到来的监听 + subscribe.addSubscribe(rtpHook, (hookData) -> { + log.info("[拉流代理] 收流成功,app:{},stream: {}", hookData.getApp(), hookData.getStream()); + dynamicTask.stop(timeOutTaskKey); + StreamInfo streamInfo = mediaServerService.getStreamInfoByAppAndStream(mediaServer, hookData.getApp(), hookData.getStream(), hookData.getMediaInfo(), null); + // hook响应 + callback.run(InviteErrorCode.SUCCESS.getCode(), InviteErrorCode.SUCCESS.getMsg(), streamInfo); + subscribe.removeSubscribe(rtpHook); + streamProxy.setPulling(true); + streamProxyMapper.updateStream(streamProxy); + }); + + String key = mediaServerService.startProxy(mediaServer, streamProxy); + streamProxy.setStreamKey(key); + streamProxy.setMediaServerId(mediaServer.getId()); + streamProxyMapper.updateStream(streamProxy); + } + + @Override + public void stop(int id) { + StreamProxy streamProxy = streamProxyMapper.select(id); + if (streamProxy == null) { + throw new ControllerException(ErrorCode.ERROR404.getCode(), "代理信息未找到"); + } + if (!userSetting.getServerId().equals(streamProxy.getServerId())) { + redisRpcPlayService.stopProxy(streamProxy.getServerId(), streamProxy.getId()); + return; + } + stopProxy(streamProxy); + } + + @Override + public void stopProxy(StreamProxy streamProxy){ + + String mediaServerId = streamProxy.getMediaServerId(); + Assert.notNull(mediaServerId, "代理节点不存在"); + + MediaServer mediaServer = mediaServerService.getOne(mediaServerId); + if (mediaServer == null) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "媒体节点不存在"); + } + if (ObjectUtils.isEmpty(streamProxy.getStreamKey())) { + mediaServerService.closeStreams(mediaServer, streamProxy.getApp(), streamProxy.getStream()); + }else { + mediaServerService.stopProxy(mediaServer, streamProxy.getStreamKey(), streamProxy.getType()); + } + streamProxyMapper.removeStream(streamProxy.getId()); + } + +} diff --git a/src/main/java/com/genersoft/iot/vmp/streamProxy/service/impl/StreamProxyServiceImpl.java b/src/main/java/com/genersoft/iot/vmp/streamProxy/service/impl/StreamProxyServiceImpl.java new file mode 100755 index 0000000..ca51412 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/streamProxy/service/impl/StreamProxyServiceImpl.java @@ -0,0 +1,383 @@ +package com.genersoft.iot.vmp.streamProxy.service.impl; + +import com.alibaba.fastjson2.JSONObject; +import com.genersoft.iot.vmp.common.StreamInfo; +import com.genersoft.iot.vmp.common.enums.ChannelDataType; +import com.genersoft.iot.vmp.conf.UserSetting; +import com.genersoft.iot.vmp.conf.exception.ControllerException; +import com.genersoft.iot.vmp.gb28181.bean.CommonGBChannel; +import com.genersoft.iot.vmp.gb28181.service.IGbChannelService; +import com.genersoft.iot.vmp.media.bean.MediaServer; +import com.genersoft.iot.vmp.media.event.media.MediaArrivalEvent; +import com.genersoft.iot.vmp.media.event.media.MediaDepartureEvent; +import com.genersoft.iot.vmp.media.event.media.MediaNotFoundEvent; +import com.genersoft.iot.vmp.media.event.mediaServer.MediaServerOfflineEvent; +import com.genersoft.iot.vmp.media.event.mediaServer.MediaServerOnlineEvent; +import com.genersoft.iot.vmp.media.service.IMediaServerService; +import com.genersoft.iot.vmp.media.zlm.dto.hook.OriginType; +import com.genersoft.iot.vmp.service.bean.ErrorCallback; +import com.genersoft.iot.vmp.storager.IRedisCatchStorage; +import com.genersoft.iot.vmp.streamProxy.bean.StreamProxy; +import com.genersoft.iot.vmp.streamProxy.dao.StreamProxyMapper; +import com.genersoft.iot.vmp.streamProxy.service.IStreamProxyPlayService; +import com.genersoft.iot.vmp.streamProxy.service.IStreamProxyService; +import com.genersoft.iot.vmp.utils.DateUtil; +import com.genersoft.iot.vmp.vmanager.bean.ErrorCode; +import com.genersoft.iot.vmp.vmanager.bean.ResourceBaseInfo; +import com.github.pagehelper.PageHelper; +import com.github.pagehelper.PageInfo; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.event.EventListener; +import org.springframework.jdbc.datasource.DataSourceTransactionManager; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Service; +import org.springframework.transaction.TransactionDefinition; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * 视频代理业务 + */ +@Slf4j +@Service +public class StreamProxyServiceImpl implements IStreamProxyService { + + @Autowired + private StreamProxyMapper streamProxyMapper; + + @Autowired + private IRedisCatchStorage redisCatchStorage; + + @Autowired + private UserSetting userSetting; + + @Autowired + private IStreamProxyPlayService playService; + + @Autowired + private IMediaServerService mediaServerService; + + @Autowired + private IGbChannelService gbChannelService; + + @Autowired + DataSourceTransactionManager dataSourceTransactionManager; + + @Autowired + TransactionDefinition transactionDefinition; + + /** + * 流到来的处理 + */ + @Async("taskExecutor") + @Transactional + @org.springframework.context.event.EventListener + public void onApplicationEvent(MediaArrivalEvent event) { + if ("rtsp".equals(event.getSchema())) { + streamChangeHandler(event.getApp(), event.getStream(), event.getMediaServer().getId(), true); + } + } + + /** + * 流离开的处理 + */ + @Async("taskExecutor") + @EventListener + @Transactional + public void onApplicationEvent(MediaDepartureEvent event) { + if ("rtsp".equals(event.getSchema())) { + streamChangeHandler(event.getApp(), event.getStream(), event.getMediaServer().getId(), false); + } + } + + /** + * 流未找到的处理 + */ + @Async("taskExecutor") + @EventListener + public void onApplicationEvent(MediaNotFoundEvent event) { + if ("rtp".equals(event.getApp())) { + return; + } + // 拉流代理 + StreamProxy streamProxyByAppAndStream = getStreamProxyByAppAndStream(event.getApp(), event.getStream()); + if (streamProxyByAppAndStream != null && streamProxyByAppAndStream.isEnableDisableNoneReader()) { + startByAppAndStream(event.getApp(), event.getStream(), ((code, msg, data) -> { + log.info("[拉流代理] 自动点播成功, app: {}, stream: {}", event.getApp(), event.getStream()); + })); + } + } + + /** + * 流媒体节点上线 + */ + @Async("taskExecutor") + @EventListener + @Transactional + public void onApplicationEvent(MediaServerOnlineEvent event) { + zlmServerOnline(event.getMediaServer()); + } + + /** + * 流媒体节点离线 + */ + @Async("taskExecutor") + @EventListener + @Transactional + public void onApplicationEvent(MediaServerOfflineEvent event) { + zlmServerOffline(event.getMediaServer()); + } + + + @Override + @Transactional + public void add(StreamProxy streamProxy) { + StreamProxy streamProxyInDb = streamProxyMapper.selectOneByAppAndStream(streamProxy.getApp(), streamProxy.getStream()); + if (streamProxyInDb != null) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "APP+STREAM已经存在"); + } + if (streamProxy.getGbDeviceId() != null) { + gbChannelService.add(streamProxy.buildCommonGBChannel()); + } + streamProxy.setCreateTime(DateUtil.getNow()); + streamProxy.setUpdateTime(DateUtil.getNow()); + streamProxyMapper.add(streamProxy); + streamProxy.setDataType(ChannelDataType.STREAM_PROXY); + streamProxy.setDataDeviceId(streamProxy.getId()); + } + + @Override + public void delete(int id) { + StreamProxy streamProxy = getStreamProxy(id); + if (streamProxy == null) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "代理不存在"); + } + delete(streamProxy); + } + + private void delete(StreamProxy streamProxy) { + Assert.notNull(streamProxy, "代理不可为NULL"); + if (streamProxy.getPulling() != null && streamProxy.getPulling()) { + playService.stopProxy(streamProxy); + } + if (streamProxy.getGbId() > 0) { + gbChannelService.delete(streamProxy.getGbId()); + } + streamProxyMapper.delete(streamProxy.getId()); + } + + @Override + @Transactional + public void delteByAppAndStream(String app, String stream) { + StreamProxy streamProxy = streamProxyMapper.selectOneByAppAndStream(app, stream); + if (streamProxy == null) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "代理不存在"); + } + delete(streamProxy); + } + + /** + * 更新代理流 + */ + @Override + public boolean update(StreamProxy streamProxy) { + streamProxy.setUpdateTime(DateUtil.getNow()); + StreamProxy streamProxyInDb = streamProxyMapper.select(streamProxy.getId()); + if (streamProxyInDb == null) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "代理不存在"); + } + int updateResult = streamProxyMapper.update(streamProxy); + if (updateResult > 0 && !ObjectUtils.isEmpty(streamProxy.getGbDeviceId())) { + if (streamProxy.getGbId() > 0) { + gbChannelService.update(streamProxy.buildCommonGBChannel()); + } else { + gbChannelService.add(streamProxy.buildCommonGBChannel()); + } + } + return true; + } + + @Override + public PageInfo getAll(Integer page, Integer count, String query, Boolean pulling, String mediaServerId) { + PageHelper.startPage(page, count); + if (query != null) { + query = query.replaceAll("/", "//") + .replaceAll("%", "/%") + .replaceAll("_", "/_"); + } + List all = streamProxyMapper.selectAll(query, pulling, mediaServerId); + return new PageInfo<>(all); + } + + + @Override + public void startByAppAndStream(String app, String stream, ErrorCallback callback) { + StreamProxy streamProxy = streamProxyMapper.selectOneByAppAndStream(app, stream); + if (streamProxy == null) { + throw new ControllerException(ErrorCode.ERROR404.getCode(), "代理信息未找到"); + } + playService.startProxy(streamProxy, callback); + } + + @Override + public void stopByAppAndStream(String app, String stream) { + StreamProxy streamProxy = streamProxyMapper.selectOneByAppAndStream(app, stream); + if (streamProxy == null) { + throw new ControllerException(ErrorCode.ERROR404.getCode(), "代理信息未找到"); + } + playService.stopProxy(streamProxy); + } + + + @Override + public Map getFFmpegCMDs(MediaServer mediaServer) { + return mediaServerService.getFFmpegCMDs(mediaServer); + } + + + @Override + public StreamProxy getStreamProxyByAppAndStream(String app, String stream) { + return streamProxyMapper.selectOneByAppAndStream(app, stream); + } + + @Override + @Transactional + public void zlmServerOnline(MediaServer mediaServer) { + if (mediaServer == null) { + return; + } + // 这里主要是控制数据库/redis缓存/以及zlm中存在的代理流 三者状态一致。以数据库中数据为根本 + redisCatchStorage.removeStream(mediaServer.getId(), "PULL"); + + List streamProxies = streamProxyMapper.selectForPushingInMediaServer(mediaServer.getId(), true); + if (streamProxies.isEmpty()) { + return; + } + Map streamProxyMapForDb = new HashMap<>(); + for (StreamProxy streamProxy : streamProxies) { + streamProxyMapForDb.put(streamProxy.getApp() + "_" + streamProxy.getStream(), streamProxy); + } + + List streamInfoList = mediaServerService.getMediaList(mediaServer, null, null, null); + + List channelListForOnline = new ArrayList<>(); + for (StreamInfo streamInfo : streamInfoList) { + String key = streamInfo.getApp() + streamInfo.getStream(); + StreamProxy streamProxy = streamProxyMapForDb.get(key); + if (streamProxy == null) { + // 流媒体存在,数据库中不存在 + continue; + } + if (streamInfo.getOriginType() == OriginType.PULL.ordinal() + || streamInfo.getOriginType() == OriginType.FFMPEG_PULL.ordinal()) { + if (streamProxyMapForDb.get(key) != null) { + redisCatchStorage.addStream(mediaServer, "pull", streamInfo.getApp(), streamInfo.getStream(), streamInfo.getMediaInfo()); + if ("OFF".equalsIgnoreCase(streamProxy.getGbStatus()) && streamProxy.getGbId() > 0) { + streamProxy.setGbStatus("ON"); + channelListForOnline.add(streamProxy.buildCommonGBChannel()); + } + streamProxyMapForDb.remove(key); + } + } + } + + if (!channelListForOnline.isEmpty()) { + gbChannelService.online(channelListForOnline); + } + List channelListForOffline = new ArrayList<>(); + List streamProxiesForRemove = new ArrayList<>(); + if (!streamProxyMapForDb.isEmpty()) { + for (StreamProxy streamProxy : streamProxyMapForDb.values()) { + if ("ON".equalsIgnoreCase(streamProxy.getGbStatus()) && streamProxy.getGbId() > 0) { + streamProxy.setGbStatus("OFF"); + channelListForOffline.add(streamProxy.buildCommonGBChannel()); + } + } + } + if (!channelListForOffline.isEmpty()) { + gbChannelService.offline(channelListForOffline); + } + if (!streamProxiesForRemove.isEmpty()) { + streamProxyMapper.deleteByList(streamProxiesForRemove); + } + + if (!streamProxyMapForDb.isEmpty()) { + for (StreamProxy streamProxy : streamProxyMapForDb.values()) { + streamProxyMapper.offline(streamProxy.getId()); + } + } + } + + @Override + public void zlmServerOffline(MediaServer mediaServer) { + List streamProxies = streamProxyMapper.selectForPushingInMediaServer(mediaServer.getId(), true); + + // 清理redis相关的缓存 + redisCatchStorage.removeStream(mediaServer.getId(), "PULL"); + + if (streamProxies.isEmpty()) { + return; + } + List streamProxiesForSendMessage = new ArrayList<>(); + List channelListForOffline = new ArrayList<>(); + + for (StreamProxy streamProxy : streamProxies) { + if (streamProxy.getGbId() > 0 && "ON".equalsIgnoreCase(streamProxy.getGbStatus())) { + channelListForOffline.add(streamProxy.buildCommonGBChannel()); + } + if ("ON".equalsIgnoreCase(streamProxy.getGbStatus())) { + streamProxiesForSendMessage.add(streamProxy); + } + } + if (!channelListForOffline.isEmpty()) { + // 修改国标关联的国标通道的状态 + gbChannelService.offline(channelListForOffline); + } + if (!streamProxiesForSendMessage.isEmpty()) { + for (StreamProxy streamProxy : streamProxiesForSendMessage) { + JSONObject jsonObject = new JSONObject(); + jsonObject.put("serverId", userSetting.getServerId()); + jsonObject.put("app", streamProxy.getApp()); + jsonObject.put("stream", streamProxy.getStream()); + jsonObject.put("register", false); + jsonObject.put("mediaServerId", mediaServer); + redisCatchStorage.sendStreamChangeMsg("pull", jsonObject); + } + } + } + + @Transactional + public void streamChangeHandler(String app, String stream, String mediaServerId, boolean status) { + // 状态变化时推送到国标上级 + StreamProxy streamProxy = streamProxyMapper.selectOneByAppAndStream(app, stream); + if (streamProxy == null) { + return; + } + streamProxy.setPulling(status); + streamProxy.setMediaServerId(mediaServerId); + streamProxy.setUpdateTime(DateUtil.getNow()); + streamProxyMapper.updateStream(streamProxy); + } + + @Override + public ResourceBaseInfo getOverview() { + + int total = streamProxyMapper.getAllCount(); + int online = streamProxyMapper.getOnline(); + + return new ResourceBaseInfo(total, online); + } + + @Override + public StreamProxy getStreamProxy(int id) { + return streamProxyMapper.select(id); + } + +} diff --git a/src/main/java/com/genersoft/iot/vmp/streamPush/bean/BatchRemoveParam.java b/src/main/java/com/genersoft/iot/vmp/streamPush/bean/BatchRemoveParam.java new file mode 100644 index 0000000..307ef54 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/streamPush/bean/BatchRemoveParam.java @@ -0,0 +1,10 @@ +package com.genersoft.iot.vmp.streamPush.bean; + +import lombok.Data; + +import java.util.Set; + +@Data +public class BatchRemoveParam { + private Set ids; +} diff --git a/src/main/java/com/genersoft/iot/vmp/streamPush/bean/RedisPushStreamMessage.java b/src/main/java/com/genersoft/iot/vmp/streamPush/bean/RedisPushStreamMessage.java new file mode 100644 index 0000000..a459b97 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/streamPush/bean/RedisPushStreamMessage.java @@ -0,0 +1,40 @@ +package com.genersoft.iot.vmp.streamPush.bean; + +import lombok.Data; + +@Data +public class RedisPushStreamMessage { + + private String gbId; + private String app; + private String stream; + private String name; + private Boolean status; + // 终端所属的虚拟组织 + private String groupGbId; + // 终端所属的虚拟组织别名 可选,可作为地方同步组织结构到wvp时的关联关系 + private String groupAlias; + // 生产商 + private String manufacturer; + // 设备型号 + private String model; + // 摄像机类型 + private Integer ptzType; + + public StreamPush buildstreamPush() { + StreamPush push = new StreamPush(); + push.setApp(app); + push.setStream(stream); + push.setGbName(name); + push.setGbDeviceId(gbId); + push.setStartOfflinePush(true); + push.setGbManufacturer(manufacturer); + push.setGbModel(model); + push.setGbPtzType(ptzType); + if (status != null) { + push.setGbStatus(status?"ON":"OFF"); + } + push.setEnableBroadcast(0); + return push; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/streamPush/bean/StreamPush.java b/src/main/java/com/genersoft/iot/vmp/streamPush/bean/StreamPush.java new file mode 100755 index 0000000..6e460d8 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/streamPush/bean/StreamPush.java @@ -0,0 +1,155 @@ +package com.genersoft.iot.vmp.streamPush.bean; + +import com.genersoft.iot.vmp.common.StreamInfo; +import com.genersoft.iot.vmp.common.enums.ChannelDataType; +import com.genersoft.iot.vmp.gb28181.bean.CommonGBChannel; +import com.genersoft.iot.vmp.media.event.media.MediaArrivalEvent; +import com.genersoft.iot.vmp.utils.DateUtil; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import org.jetbrains.annotations.NotNull; +import org.springframework.util.ObjectUtils; + + +@Data +@Schema(description = "推流信息") +@EqualsAndHashCode(callSuper = true) +@NoArgsConstructor +public class StreamPush extends CommonGBChannel implements Comparable{ + + /** + * id + */ + @Schema(description = "id") + private Integer id; + + /** + * 应用名 + */ + @Schema(description = "应用名") + private String app; + + /** + * 流id + */ + @Schema(description = "流id") + private String stream; + + /** + * 使用的流媒体ID + */ + @Schema(description = "使用的流媒体ID") + private String mediaServerId; + + /** + * 使用的服务ID + */ + @Schema(description = "使用的服务ID") + private String serverId; + + /** + * 推流时间 + */ + @Schema(description = "推流时间") + private String pushTime; + + /** + * 更新时间 + */ + @Schema(description = "更新时间") + private String updateTime; + + /** + * 创建时间 + */ + @Schema(description = "创建时间") + private String createTime; + + /** + * 是否正在推流 + */ + @Schema(description = "是否正在推流") + private boolean pushing; + + /** + * 拉起离线推流 + */ + @Schema(description = "拉起离线推流") + private boolean startOfflinePush; + + /** + * 速度,单位:km/h (可选) + */ + @Schema(description = "GPS的速度") + private Double gpsSpeed; + + /** + * 方向,取值为当前摄像头方向与正北方的顺时针夹角,取值范围0°~360°,单位:(°)(可选) + */ + @Schema(description = "GPS的方向") + private Double gpsDirection; + + /** + * 海拔高度,单位:m(可选) + */ + @Schema(description = "GPS的海拔高度") + private Double gpsAltitude; + + /** + * GPS的更新时间 + */ + @Schema(description = "GPS的更新时间") + private String gpsTime; + + private String uniqueKey; + + private Integer dataType = ChannelDataType.STREAM_PUSH; + + + @Override + public int compareTo(@NotNull StreamPush streamPushItem) { + return Long.valueOf(DateUtil.yyyy_MM_dd_HH_mm_ssToTimestamp(this.createTime) + - DateUtil.yyyy_MM_dd_HH_mm_ssToTimestamp(streamPushItem.getCreateTime())).intValue(); + } + + public static StreamPush getInstance(StreamInfo streamInfo) { + StreamPush streamPush = new StreamPush(); + streamPush.setApp(streamInfo.getApp()); + if (streamInfo.getMediaServer() != null) { + streamPush.setMediaServerId(streamInfo.getMediaServer().getId()); + } + + streamPush.setStream(streamInfo.getStream()); + streamPush.setCreateTime(DateUtil.getNow()); + streamPush.setServerId(streamInfo.getServerId()); + return streamPush; + + } + + public static StreamPush getInstance(MediaArrivalEvent event, String serverId){ + StreamPush streamPushItem = new StreamPush(); + streamPushItem.setApp(event.getApp()); + streamPushItem.setMediaServerId(event.getMediaServer().getId()); + streamPushItem.setStream(event.getStream()); + streamPushItem.setCreateTime(DateUtil.getNow()); + streamPushItem.setServerId(serverId); + return streamPushItem; + } + + public CommonGBChannel buildCommonGBChannel() { + if (ObjectUtils.isEmpty(this.getGbDeviceId())) { + return null; + } + if (ObjectUtils.isEmpty(this.getGbName())) { + this.setGbName( app+ "-" +stream); + } + this.setDataType(ChannelDataType.STREAM_PUSH); + this.setDataDeviceId(this.getId()); + return this; + } + + +} + diff --git a/src/main/java/com/genersoft/iot/vmp/streamPush/bean/StreamPushExcelDto.java b/src/main/java/com/genersoft/iot/vmp/streamPush/bean/StreamPushExcelDto.java new file mode 100755 index 0000000..4ccc751 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/streamPush/bean/StreamPushExcelDto.java @@ -0,0 +1,30 @@ +package com.genersoft.iot.vmp.streamPush.bean; + +import com.alibaba.excel.annotation.ExcelProperty; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Data +public class StreamPushExcelDto { + + @ExcelProperty("名称") + private String name; + + @ExcelProperty("应用名") + private String app; + + @ExcelProperty("流ID") + private String stream; + + @ExcelProperty("国标ID") + private String gbDeviceId; + + @ExcelProperty("在线状态") + private boolean status; + + @Schema(description = "经度 WGS-84坐标系") + private Double longitude; + + @Schema(description = "纬度 WGS-84坐标系") + private Double latitude; +} diff --git a/src/main/java/com/genersoft/iot/vmp/streamPush/controller/StreamPushController.java b/src/main/java/com/genersoft/iot/vmp/streamPush/controller/StreamPushController.java new file mode 100755 index 0000000..136dd67 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/streamPush/controller/StreamPushController.java @@ -0,0 +1,295 @@ +package com.genersoft.iot.vmp.streamPush.controller; + +import com.alibaba.excel.EasyExcel; +import com.alibaba.excel.ExcelReader; +import com.alibaba.excel.exception.ExcelDataConvertException; +import com.alibaba.excel.read.metadata.ReadSheet; +import com.genersoft.iot.vmp.common.enums.ChannelDataType; +import com.genersoft.iot.vmp.conf.UserSetting; +import com.genersoft.iot.vmp.conf.exception.ControllerException; +import com.genersoft.iot.vmp.conf.security.JwtUtils; +import com.genersoft.iot.vmp.gb28181.transmit.callback.DeferredResultHolder; +import com.genersoft.iot.vmp.gb28181.transmit.callback.RequestMessage; +import com.genersoft.iot.vmp.media.service.IMediaServerService; +import com.genersoft.iot.vmp.service.IMediaService; +import com.genersoft.iot.vmp.streamPush.bean.BatchRemoveParam; +import com.genersoft.iot.vmp.streamPush.bean.StreamPush; +import com.genersoft.iot.vmp.streamPush.bean.StreamPushExcelDto; +import com.genersoft.iot.vmp.streamPush.enent.StreamPushUploadFileHandler; +import com.genersoft.iot.vmp.streamPush.service.IStreamPushPlayService; +import com.genersoft.iot.vmp.streamPush.service.IStreamPushService; +import com.genersoft.iot.vmp.vmanager.bean.ErrorCode; +import com.genersoft.iot.vmp.vmanager.bean.StreamContent; +import com.genersoft.iot.vmp.vmanager.bean.WVPResult; +import com.github.pagehelper.PageInfo; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.context.request.async.DeferredResult; +import org.springframework.web.multipart.MultipartFile; + +import jakarta.servlet.http.HttpServletRequest; +import java.io.IOException; +import java.io.InputStream; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +@Tag(name = "推流信息管理") +@RestController +@Slf4j +@RequestMapping(value = "/api/push") +public class StreamPushController { + + @Autowired + private IStreamPushService streamPushService; + + @Autowired + private IStreamPushPlayService streamPushPlayService; + + @Autowired + private IMediaServerService mediaServerService; + + @Autowired + private DeferredResultHolder resultHolder; + + @Autowired + private IMediaService mediaService; + + @Autowired + private UserSetting userSetting; + + @GetMapping(value = "/list") + @ResponseBody + @Operation(summary = "推流列表查询", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "page", description = "当前页") + @Parameter(name = "count", description = "每页查询数量") + @Parameter(name = "query", description = "查询内容") + @Parameter(name = "pushing", description = "是否正在推流") + @Parameter(name = "mediaServerId", description = "流媒体ID") + public PageInfo list(@RequestParam(required = false)Integer page, + @RequestParam(required = false)Integer count, + @RequestParam(required = false)String query, + @RequestParam(required = false)Boolean pushing, + @RequestParam(required = false)String mediaServerId ){ + + if (ObjectUtils.isEmpty(query)) { + query = null; + } + if (ObjectUtils.isEmpty(mediaServerId)) { + mediaServerId = null; + } + PageInfo pushList = streamPushService.getPushList(page, count, query, pushing, mediaServerId); + return pushList; + } + + + @PostMapping(value = "/remove") + @ResponseBody + @Operation(summary = "删除", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "id", description = "应用名", required = true) + public void delete(int id){ + if (streamPushService.delete(id) <= 0){ + throw new ControllerException(ErrorCode.ERROR100); + } + } + + @PostMapping(value = "upload") + @ResponseBody + public DeferredResult>> uploadChannelFile(@RequestParam(value = "file") MultipartFile file){ + + // 最多处理文件一个小时 + DeferredResult>> result = new DeferredResult<>(60*60*1000L); + // 录像查询以channelId作为deviceId查询 + String key = DeferredResultHolder.UPLOAD_FILE_CHANNEL; + String uuid = UUID.randomUUID().toString(); + log.info("通道导入文件类型: {}",file.getContentType() ); + if (file.isEmpty()) { + log.warn("通道导入文件为空"); + WVPResult wvpResult = new WVPResult<>(); + wvpResult.setCode(-1); + wvpResult.setMsg("文件为空"); + result.setResult(ResponseEntity.status(HttpStatus.BAD_REQUEST).body(wvpResult)); + return result; + } + if (file.getContentType() == null) { + WVPResult wvpResult = new WVPResult<>(); + wvpResult.setCode(-1); + wvpResult.setMsg("无法识别文件类型"); + result.setResult(ResponseEntity.status(HttpStatus.BAD_REQUEST).body(wvpResult)); + return result; + } + // 同时只处理一个文件 + if (resultHolder.exist(key, null)) { + log.warn("已有导入任务正在执行"); + WVPResult wvpResult = new WVPResult<>(); + wvpResult.setCode(-1); + wvpResult.setMsg("已有导入任务正在执行"); + result.setResult(ResponseEntity.status(HttpStatus.TOO_MANY_REQUESTS).body(wvpResult)); + return result; + } + + resultHolder.put(key, uuid, result); + result.onTimeout(()->{ + log.warn("通道导入超时,可能文件过大"); + RequestMessage msg = new RequestMessage(); + msg.setKey(key); + WVPResult wvpResult = new WVPResult<>(); + wvpResult.setCode(-1); + wvpResult.setMsg("导入超时,可能文件过大"); + msg.setData(wvpResult); + resultHolder.invokeAllResult(msg); + }); + //获取文件流 + InputStream inputStream = null; + try { + String name = file.getName(); + inputStream = file.getInputStream(); + } catch (IOException e) { + log.error("未处理的异常 ", e); + } + try { + //传入参数 + ExcelReader excelReader = EasyExcel.read(inputStream, StreamPushExcelDto.class, + new StreamPushUploadFileHandler(streamPushService, mediaServerService.getDefaultMediaServer().getId(), (errorStreams, errorGBs)->{ + log.info("通道导入成功,存在重复App+Stream为{}个,存在国标ID为{}个", errorStreams.size(), errorGBs.size()); + RequestMessage msg = new RequestMessage(); + msg.setKey(key); + WVPResult>> wvpResult = new WVPResult<>(); + if (errorStreams.isEmpty() && errorGBs.isEmpty()) { + wvpResult.setCode(0); + wvpResult.setMsg("成功"); + }else { + wvpResult.setCode(1); + wvpResult.setMsg("导入成功。但是存在重复数据"); + Map> errorData = new HashMap<>(); + errorData.put("gbId", errorGBs); + errorData.put("stream", errorStreams); + wvpResult.setData(errorData); + } + msg.setData(wvpResult); + resultHolder.invokeAllResult(msg); + })).build(); + ReadSheet readSheet = EasyExcel.readSheet(0).build(); + excelReader.read(readSheet); + excelReader.finish(); + }catch (ExcelDataConvertException e) { + log.error("通道导入失败:行: {}, 列: {}, 内容: {}", e.getRowIndex(), e.getColumnIndex(), e.getCellData().getStringValue()); + RequestMessage msg = new RequestMessage(); + msg.setKey(key); + WVPResult wvpResult = new WVPResult<>(); + wvpResult.setCode(ErrorCode.ERROR100.getCode()); + wvpResult.setMsg("数据异常: " + e.getRowIndex() +"行" + e.getColumnIndex() + "列, 内容:" + e.getCellData().getStringValue() ); + msg.setData(wvpResult); + resultHolder.invokeAllResult(msg); + }catch (Exception e) { + log.warn("通道导入失败:", e); + RequestMessage msg = new RequestMessage(); + msg.setKey(key); + WVPResult wvpResult = new WVPResult<>(); + wvpResult.setCode(ErrorCode.ERROR100.getCode()); + wvpResult.setMsg("通道导入失败: " + e.getMessage() ); + msg.setData(wvpResult); + resultHolder.invokeAllResult(msg); + } + + + return result; + } + + /** + * 添加推流信息 + * @param stream 推流信息 + * @return + */ + @PostMapping(value = "/add") + @ResponseBody + @Operation(summary = "添加推流信息", security = @SecurityRequirement(name = JwtUtils.HEADER)) + public StreamPush add(@RequestBody StreamPush stream){ + if (ObjectUtils.isEmpty(stream.getGbId())) { + throw new ControllerException(ErrorCode.ERROR400.getCode(), "国标ID不可为空"); + } + if (ObjectUtils.isEmpty(stream.getApp()) && ObjectUtils.isEmpty(stream.getStream())) { + throw new ControllerException(ErrorCode.ERROR400.getCode(), "app或stream不可为空"); + } + stream.setGbStatus("OFF"); + stream.setPushing(false); + if (!streamPushService.add(stream)) { + throw new ControllerException(ErrorCode.ERROR100); + } + stream.setDataType(ChannelDataType.STREAM_PUSH); + stream.setDataDeviceId(stream.getId()); + return stream; + } + + @PostMapping(value = "/update") + @ResponseBody + @Operation(summary = "更新推流信息", security = @SecurityRequirement(name = JwtUtils.HEADER)) + public void update(@RequestBody StreamPush stream){ + if (ObjectUtils.isEmpty(stream.getId())) { + throw new ControllerException(ErrorCode.ERROR400.getCode(), "ID不可为空"); + } + if (!streamPushService.update(stream)) { + throw new ControllerException(ErrorCode.ERROR100); + } + } + + @DeleteMapping(value = "/batchRemove") + @ResponseBody + @Operation(summary = "删除多个推流", security = @SecurityRequirement(name = JwtUtils.HEADER)) + public void batchStop(@RequestBody BatchRemoveParam ids){ + if(ids.getIds().isEmpty()) { + return; + } + streamPushService.batchRemove(ids.getIds()); + } + + @GetMapping(value = "/start") + @ResponseBody + @Operation(summary = "开始播放", security = @SecurityRequirement(name = JwtUtils.HEADER)) + public DeferredResult> start(HttpServletRequest request, Integer id){ + Assert.notNull(id, "推流ID不可为NULL"); + DeferredResult> result = new DeferredResult<>(userSetting.getPlayTimeout().longValue()); + result.onTimeout(()->{ + WVPResult fail = WVPResult.fail(ErrorCode.ERROR100.getCode(), "等待推流超时"); + result.setResult(fail); + }); + streamPushPlayService.start(id, (code, msg, streamInfo) -> { + if (code == 0 && streamInfo != null) { + if (userSetting.getUseSourceIpAsStreamIp()) { + streamInfo=streamInfo.clone();//深拷贝 + String host; + try { + URL url=new URL(request.getRequestURL().toString()); + host=url.getHost(); + } catch (MalformedURLException e) { + host=request.getLocalAddr(); + } + streamInfo.changeStreamIp(host); + } + WVPResult success = WVPResult.success(new StreamContent(streamInfo)); + result.setResult(success); + } + }, null, null); + return result; + } + + @GetMapping(value = "/forceClose") + @ResponseBody + @Operation(summary = "强制停止推流", security = @SecurityRequirement(name = JwtUtils.HEADER)) + public void stop(String app, String stream){ + + streamPushPlayService.stop(app, stream); + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/streamPush/dao/StreamPushMapper.java b/src/main/java/com/genersoft/iot/vmp/streamPush/dao/StreamPushMapper.java new file mode 100755 index 0000000..39b7d13 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/streamPush/dao/StreamPushMapper.java @@ -0,0 +1,160 @@ +package com.genersoft.iot.vmp.streamPush.dao; + +import com.genersoft.iot.vmp.common.enums.ChannelDataType; +import com.genersoft.iot.vmp.streamPush.bean.StreamPush; +import com.genersoft.iot.vmp.service.bean.StreamPushItemFromRedis; +import org.apache.ibatis.annotations.*; +import org.springframework.stereotype.Repository; + +import java.util.List; +import java.util.Map; +import java.util.Set; + +@Mapper +@Repository +public interface StreamPushMapper { + + Integer dataType = ChannelDataType.GB28181; + + @Insert("INSERT INTO wvp_stream_push (app, stream, media_server_id, server_id, push_time, update_time, create_time, pushing, start_offline_push) VALUES" + + "(#{app}, #{stream}, #{mediaServerId} , #{serverId} , #{pushTime} ,#{updateTime}, #{createTime}, #{pushing}, #{startOfflinePush})") + @Options(useGeneratedKeys = true, keyProperty = "id", keyColumn = "id") + int add(StreamPush streamPushItem); + + + @Update(value = {" "}) + int update(StreamPush streamPushItem); + + @Delete("DELETE FROM wvp_stream_push WHERE id=#{id}") + int del(@Param("id") int id); + + @Select(value = {" "}) + List selectAll(@Param("query") String query, @Param("pushing") Boolean pushing, @Param("mediaServerId") String mediaServerId); + + @Select("SELECT st.*, st.id as data_device_id, wdc.*, wdc.id as gb_id FROM wvp_stream_push st LEFT join wvp_device_channel wdc on wdc.data_type = 2 and st.id = wdc.data_device_id WHERE st.app=#{app} AND st.stream=#{stream}") + StreamPush selectByAppAndStream(@Param("app") String app, @Param("stream") String stream); + + @Insert("") + @Options(useGeneratedKeys = true, keyProperty = "id", keyColumn = "id") + int addAll(List streamPushItems); + + @Select("SELECT st.*, st.id as data_device_id, wdc.*, wdc.id as gb_id FROM wvp_stream_push st LEFT join wvp_device_channel wdc on wdc.data_type = 2 and st.id = wdc.data_device_id WHERE st.media_server_id=#{mediaServerId}") + List selectAllByMediaServerId(String mediaServerId); + + @Select("SELECT st.*, st.id as data_device_id, wdc.*, wdc.id as gb_id FROM wvp_stream_push st LEFT join wvp_device_channel wdc on wdc.data_type = 2 and st.id = wdc.data_device_id WHERE st.media_server_id=#{mediaServerId} and wdc.gb_device_id is null") + List selectAllByMediaServerIdWithOutGbID(String mediaServerId); + + @Update("UPDATE wvp_stream_push " + + "SET pushing=#{pushing}, server_id=#{serverId}, media_server_id=#{mediaServerId} " + + "WHERE id=#{id}") + int updatePushStatus(StreamPush streamPush); + + @Select("") + List getListInList(List offlineStreams); + + + @Select("SELECT CONCAT(app,stream) from wvp_stream_push") + List getAllAppAndStream(); + + @Select("select count(1) from wvp_stream_push ") + int getAllCount(); + + @Select(value = {" "}) + int getAllPushing(Boolean usePushingAsStatus); + + @MapKey("uniqueKey") + @Select("SELECT CONCAT(wsp.app, wsp.stream) as unique_key, wsp.*, wdc.* , " + + " wdc.id as gb_id " + + " from wvp_stream_push wsp " + + " LEFT join wvp_device_channel wdc on wdc.data_type = 2 and wsp.id = wdc.data_device_id") + Map getAllAppAndStreamMap(); + + + @MapKey("gbDeviceId") + @Select("SELECT wdc.gb_device_id, wsp.id as data_device_id, wsp.*, wsp.* , wdc.id as gb_id " + + " from wvp_stream_push wsp " + + " LEFT join wvp_device_channel wdc on wdc.data_type = 2 and wsp.id = wdc.data_device_id") + Map getAllGBId(); + + @Select("SELECT st.*, st.id as data_device_id, wdc.*, wdc.id as gb_id FROM wvp_stream_push st LEFT join wvp_device_channel wdc on wdc.data_type = 2 and st.id = wdc.data_device_id WHERE st.id=#{id}") + StreamPush queryOne(@Param("id") int id); + + @Select("") + List selectInSet(Set ids); + + @Delete("") + void batchDel(List streamPushList); + + + @Update({""}) + int batchUpdate(List streamPushItemForUpdate); +} diff --git a/src/main/java/com/genersoft/iot/vmp/streamPush/enent/StreamPushUploadFileHandler.java b/src/main/java/com/genersoft/iot/vmp/streamPush/enent/StreamPushUploadFileHandler.java new file mode 100755 index 0000000..9ccc8c8 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/streamPush/enent/StreamPushUploadFileHandler.java @@ -0,0 +1,140 @@ +package com.genersoft.iot.vmp.streamPush.enent; + +import com.alibaba.excel.context.AnalysisContext; +import com.alibaba.excel.event.AnalysisEventListener; +import com.genersoft.iot.vmp.streamPush.bean.StreamPush; +import com.genersoft.iot.vmp.streamPush.bean.StreamPushExcelDto; +import com.genersoft.iot.vmp.streamPush.service.IStreamPushService; +import com.genersoft.iot.vmp.utils.DateUtil; +import com.google.common.collect.BiMap; +import com.google.common.collect.HashBiMap; +import org.springframework.util.ObjectUtils; + +import java.util.*; + +public class StreamPushUploadFileHandler extends AnalysisEventListener { + + /** + * 错误数据的回调,用于将错误数据发送给页面 + */ + private final ErrorDataHandler errorDataHandler; + + /** + * 推流的业务类用于存储数据 + */ + private final IStreamPushService pushService; + + /** + * 默认流媒体节点ID + */ + private final String defaultMediaServerId; + + /** + * 用于存储更具APP+Stream过滤后的数据,可以直接存入stream_push表与gb_stream表 + */ + private final Map streamPushItemForSave = new HashMap<>(); + + /** + * 用于存储APP+Stream->国标ID 的数据结构, 数据一一对应,全局判断APP+Stream->国标ID是否存在不对应 + */ + private final BiMap gBMap = HashBiMap.create(); + + /** + * 用于存储APP+Stream-> 在数据库中的数据 + */ + private final BiMap pushMapInDb = HashBiMap.create(); + + /** + * 记录错误的APP+Stream + */ + private final List errorStreamList = new ArrayList<>(); + + + /** + * 记录错误的国标ID + */ + private final List errorInfoList = new ArrayList<>(); + + /** + * 读取数量计数器 + */ + private int loadedSize = 0; + + public StreamPushUploadFileHandler(IStreamPushService pushService, String defaultMediaServerId, ErrorDataHandler errorDataHandler) { + this.pushService = pushService; + this.defaultMediaServerId = defaultMediaServerId; + this.errorDataHandler = errorDataHandler; + // 获取数据库已有的数据,已经存在的则忽略 + List allAppAndStreams = pushService.getAllAppAndStream(); + if (!allAppAndStreams.isEmpty()) { + for (String allAppAndStream : allAppAndStreams) { + pushMapInDb.put(allAppAndStream, allAppAndStream); + } + } + } + + public interface ErrorDataHandler{ + void handle(List streams, List gbId); + } + + @Override + public void invoke(StreamPushExcelDto streamPushExcelDto, AnalysisContext analysisContext) { + if (ObjectUtils.isEmpty(streamPushExcelDto.getApp()) + || ObjectUtils.isEmpty(streamPushExcelDto.getStream()) + || ObjectUtils.isEmpty(streamPushExcelDto.getGbDeviceId())) { + return; + } + Integer rowIndex = analysisContext.readRowHolder().getRowIndex(); + + if (gBMap.get(streamPushExcelDto.getApp() + streamPushExcelDto.getStream()) == null) { + try { + gBMap.put(streamPushExcelDto.getApp() + streamPushExcelDto.getStream(), streamPushExcelDto.getGbDeviceId()); + }catch (IllegalArgumentException e) { + errorInfoList.add("行:" + rowIndex + ", " + streamPushExcelDto.getGbDeviceId() + " 国标ID重复使用"); + return; + } + }else { + if (!gBMap.get(streamPushExcelDto.getApp() + streamPushExcelDto.getStream()).equals(streamPushExcelDto.getGbDeviceId())) { + errorInfoList.add("行:" + rowIndex + ", " + streamPushExcelDto.getGbDeviceId() + " 同样的应用名和流ID使用了不同的国标ID"); + return; + } + } + + StreamPush streamPush = new StreamPush(); + streamPush.setApp(streamPushExcelDto.getApp()); + streamPush.setStream(streamPushExcelDto.getStream()); + streamPush.setGbDeviceId(streamPushExcelDto.getGbDeviceId()); + streamPush.setGbStatus(streamPushExcelDto.isStatus()?"ON":"OFF"); + streamPush.setCreateTime(DateUtil.getNow()); + streamPush.setMediaServerId(defaultMediaServerId); + streamPush.setGbName(streamPushExcelDto.getName()); + streamPush.setGbLongitude(streamPushExcelDto.getLongitude()); + streamPush.setGbLatitude(streamPushExcelDto.getLatitude()); + streamPush.setUpdateTime(DateUtil.getNow()); + streamPushItemForSave.put(streamPush.getApp() + streamPush.getStream(), streamPush); + + loadedSize ++; + if (loadedSize > 1000) { + saveData(); + streamPushItemForSave.clear(); + loadedSize = 0; + } + + } + + @Override + public void doAfterAllAnalysed(AnalysisContext analysisContext) { + // 这里也要保存数据,确保最后遗留的数据也存储到数据库 + saveData(); + streamPushItemForSave.clear(); + gBMap.clear(); + errorDataHandler.handle(errorStreamList, errorInfoList); + } + + private void saveData(){ + if (!streamPushItemForSave.isEmpty()) { + // 向数据库查询是否存在重复的app + pushService.batchAdd(new ArrayList<>(streamPushItemForSave.values())); + } + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/streamPush/service/IStreamPushPlayService.java b/src/main/java/com/genersoft/iot/vmp/streamPush/service/IStreamPushPlayService.java new file mode 100644 index 0000000..657e5cb --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/streamPush/service/IStreamPushPlayService.java @@ -0,0 +1,12 @@ +package com.genersoft.iot.vmp.streamPush.service; + +import com.genersoft.iot.vmp.common.StreamInfo; +import com.genersoft.iot.vmp.service.bean.ErrorCallback; + +public interface IStreamPushPlayService { + void start(Integer id, ErrorCallback callback, String platformDeviceId, String platformName ); + + void stop(String app, String stream); + + void stop(Integer id); +} diff --git a/src/main/java/com/genersoft/iot/vmp/streamPush/service/IStreamPushService.java b/src/main/java/com/genersoft/iot/vmp/streamPush/service/IStreamPushService.java new file mode 100755 index 0000000..9621d8c --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/streamPush/service/IStreamPushService.java @@ -0,0 +1,100 @@ +package com.genersoft.iot.vmp.streamPush.service; + +import com.genersoft.iot.vmp.media.bean.MediaServer; +import com.genersoft.iot.vmp.service.bean.StreamPushItemFromRedis; +import com.genersoft.iot.vmp.streamPush.bean.StreamPush; +import com.genersoft.iot.vmp.vmanager.bean.ResourceBaseInfo; +import com.github.pagehelper.PageInfo; + +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * @author lin + */ +public interface IStreamPushService { + + /** + * 获取 + */ + PageInfo getPushList(Integer page, Integer count, String query, Boolean pushing, String mediaServerId); + + List getPushList(String mediaSererId); + + StreamPush getPush(String app, String streamId); + + boolean stop(StreamPush streamPush); + + /** + * 停止一路推流 + * @param app 应用名 + * @param stream 流ID + */ + boolean stopByAppAndStream(String app, String stream); + + /** + * 新的节点加入 + */ + void zlmServerOnline(MediaServer mediaServer); + + /** + * 节点离线 + */ + void zlmServerOffline(MediaServer mediaServer); + + /** + * 批量添加 + */ + void batchAdd(List streamPushExcelDtoList); + + + /** + * 全部离线 + */ + void allOffline(); + + /** + * 推流离线 + */ + void offline(List offlineStreams); + + /** + * 推流上线 + */ + void online(List onlineStreams); + + /** + * 增加推流 + */ + boolean add(StreamPush stream); + + boolean update(StreamPush stream); + + /** + * 获取全部的app+Streanm 用于判断推流列表是新增还是修改 + * @return + */ + List getAllAppAndStream(); + + /** + * 获取统计信息 + * @return + */ + ResourceBaseInfo getOverview(); + + Map getAllAppAndStreamMap(); + + Map getAllGBId(); + + void deleteByAppAndStream(String app, String stream); + + void updatePushStatus(StreamPush streamPush); + + void batchUpdate(List streamPushItemForUpdate); + + int delete(int id); + + void batchRemove(Set ids); + +} diff --git a/src/main/java/com/genersoft/iot/vmp/streamPush/service/impl/SourcePlayServiceForStreamPushImpl.java b/src/main/java/com/genersoft/iot/vmp/streamPush/service/impl/SourcePlayServiceForStreamPushImpl.java new file mode 100644 index 0000000..5197687 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/streamPush/service/impl/SourcePlayServiceForStreamPushImpl.java @@ -0,0 +1,53 @@ +package com.genersoft.iot.vmp.streamPush.service.impl; + +import com.genersoft.iot.vmp.common.StreamInfo; +import com.genersoft.iot.vmp.common.enums.ChannelDataType; +import com.genersoft.iot.vmp.gb28181.bean.CommonGBChannel; +import com.genersoft.iot.vmp.gb28181.bean.Platform; +import com.genersoft.iot.vmp.gb28181.bean.PlayException; +import com.genersoft.iot.vmp.gb28181.service.ISourcePlayService; +import com.genersoft.iot.vmp.service.bean.ErrorCallback; +import com.genersoft.iot.vmp.streamPush.service.IStreamPushPlayService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import javax.sip.message.Response; + +@Slf4j +@Service(ChannelDataType.PLAY_SERVICE + ChannelDataType.STREAM_PUSH) +public class SourcePlayServiceForStreamPushImpl implements ISourcePlayService { + + @Autowired + private IStreamPushPlayService playService; + + @Override + public void play(CommonGBChannel channel, Platform platform, Boolean record, ErrorCallback callback) { + String serverGBId = null; + String platformName = null; + if (platform != null) { + // 推流 + serverGBId = platform.getServerGBId(); + platformName = platform.getName(); + } + // 推流 + try { + playService.start(channel.getDataDeviceId(), callback, serverGBId, platformName); + }catch (PlayException e) { + callback.run(e.getCode(), e.getMsg(), null); + }catch (Exception e) { + log.error("[点播推流通道失败] 通道: {}({})", channel.getGbName(), channel.getGbDeviceId(), e); + callback.run(Response.BUSY_HERE, "busy here", null); + } + } + + @Override + public void stopPlay(CommonGBChannel channel) { + // 推流 + try { + playService.stop(channel.getDataDeviceId()); + }catch (Exception e) { + log.error("[停止点播失败] {}({})", channel.getGbName(), channel.getGbDeviceId(), e); + } + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/streamPush/service/impl/StreamPushPlayServiceImpl.java b/src/main/java/com/genersoft/iot/vmp/streamPush/service/impl/StreamPushPlayServiceImpl.java new file mode 100644 index 0000000..d8adfa3 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/streamPush/service/impl/StreamPushPlayServiceImpl.java @@ -0,0 +1,143 @@ +package com.genersoft.iot.vmp.streamPush.service.impl; + +import com.genersoft.iot.vmp.common.StreamInfo; +import com.genersoft.iot.vmp.conf.DynamicTask; +import com.genersoft.iot.vmp.conf.UserSetting; +import com.genersoft.iot.vmp.media.bean.MediaInfo; +import com.genersoft.iot.vmp.media.bean.MediaServer; +import com.genersoft.iot.vmp.media.service.IMediaServerService; +import com.genersoft.iot.vmp.media.zlm.dto.StreamAuthorityInfo; +import com.genersoft.iot.vmp.service.bean.ErrorCallback; +import com.genersoft.iot.vmp.service.bean.MessageForPushChannel; +import com.genersoft.iot.vmp.service.redisMsg.IRedisRpcPlayService; +import com.genersoft.iot.vmp.service.redisMsg.IRedisRpcService; +import com.genersoft.iot.vmp.service.redisMsg.RedisPushStreamResponseListener; +import com.genersoft.iot.vmp.storager.IRedisCatchStorage; +import com.genersoft.iot.vmp.streamPush.bean.StreamPush; +import com.genersoft.iot.vmp.streamPush.dao.StreamPushMapper; +import com.genersoft.iot.vmp.streamPush.service.IStreamPushPlayService; +import com.genersoft.iot.vmp.vmanager.bean.ErrorCode; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.util.Assert; + +import java.util.UUID; + +@Service +@Slf4j +public class StreamPushPlayServiceImpl implements IStreamPushPlayService { + + @Autowired + private StreamPushMapper streamPushMapper; + + @Autowired + private IMediaServerService mediaServerService; + + @Autowired + private IRedisCatchStorage redisCatchStorage; + + @Autowired + private UserSetting userSetting; + + @Autowired + private DynamicTask dynamicTask; + + @Autowired + private IRedisRpcService redisRpcService; + + @Autowired + private IRedisRpcPlayService redisRpcPlayService; + + @Autowired + private RedisPushStreamResponseListener redisPushStreamResponseListener; + + @Override + public void start(Integer id, ErrorCallback callback, String platformDeviceId, String platformName ) { + StreamPush streamPush = streamPushMapper.queryOne(id); + Assert.notNull(streamPush, "推流信息未找到"); + + if (streamPush.isPushing() && !userSetting.getServerId().equals(streamPush.getServerId())) { + redisRpcPlayService.playPush(streamPush.getServerId(), id, callback); + return; + } + + MediaServer mediaServer = mediaServerService.getOne(streamPush.getMediaServerId()); + if (mediaServer != null) { + MediaInfo mediaInfo = mediaServerService.getMediaInfo(mediaServer, streamPush.getApp(), streamPush.getStream()); + if (mediaInfo != null) { + String callId = null; + StreamAuthorityInfo streamAuthorityInfo = redisCatchStorage.getStreamAuthorityInfo(streamPush.getApp(), streamPush.getStream()); + if (streamAuthorityInfo != null) { + callId = streamAuthorityInfo.getCallId(); + } + callback.run(ErrorCode.SUCCESS.getCode(), ErrorCode.SUCCESS.getMsg(), mediaServerService.getStreamInfoByAppAndStream(mediaServer, + streamPush.getApp(), streamPush.getStream(), mediaInfo, callId)); + if (!streamPush.isPushing()) { + streamPush.setPushing(true); + streamPushMapper.update(streamPush); + } + return; + } + } + Assert.isTrue(streamPush.isStartOfflinePush(), "通道未推流"); + // 发送redis消息以使设备上线,流上线后被 + log.info("[ app={}, stream={} ]通道未推流,发送redis信息控制设备开始推流", streamPush.getApp(), streamPush.getStream()); + MessageForPushChannel messageForPushChannel = MessageForPushChannel.getInstance(1, + streamPush.getApp(), streamPush.getStream(), streamPush.getGbDeviceId(), platformDeviceId, + platformName, userSetting.getServerId(), null); + redisCatchStorage.sendStreamPushRequestedMsg(messageForPushChannel); + // 设置超时 + String timeOutTaskKey = UUID.randomUUID().toString(); + dynamicTask.startDelay(timeOutTaskKey, () -> { + redisRpcService.unPushStreamOnlineEvent(streamPush.getApp(), streamPush.getStream()); + log.info("[ app={}, stream={} ] 等待设备开始推流超时", streamPush.getApp(), streamPush.getStream()); + callback.run(ErrorCode.ERROR100.getCode(), "timeout", null); + + }, userSetting.getPlatformPlayTimeout()); + // + long key = redisRpcService.onStreamOnlineEvent(streamPush.getApp(), streamPush.getStream(), (streamInfo) -> { + dynamicTask.stop(timeOutTaskKey); + if (streamInfo == null) { + log.warn("等待推流得到结果未空: {}/{}", streamPush.getApp(), streamPush.getStream()); + callback.run(ErrorCode.ERROR100.getCode(), "fail", null); + }else { + callback.run(ErrorCode.SUCCESS.getCode(), ErrorCode.SUCCESS.getMsg(), streamInfo); + } + }); + // 添加回复的拒绝或者错误的通知 + // redis消息例如: PUBLISH VM_MSG_STREAM_PUSH_RESPONSE '{"code":1,"msg":"失败","app":"1","stream":"2"}' + redisPushStreamResponseListener.addEvent(streamPush.getApp(), streamPush.getStream(), response -> { + if (response.getCode() != 0) { + dynamicTask.stop(timeOutTaskKey); + redisRpcService.unPushStreamOnlineEvent(streamPush.getApp(), streamPush.getStream()); + redisRpcService.removeCallback(key); + callback.run(response.getCode(), response.getMsg(), null); + } + }); + } + + @Override + public void stop(String app, String stream) { + StreamPush streamPush = streamPushMapper.selectByAppAndStream(app, stream); + if (streamPush == null || !streamPush.isPushing()) { + return; + } + String mediaServerId = streamPush.getMediaServerId(); + MediaServer mediaServer = mediaServerService.getOne(mediaServerId); + Assert.notNull(mediaServer, "未找到使用的节点"); + mediaServerService.closeStreams(mediaServer, app, stream); + } + + @Override + public void stop(Integer id) { + StreamPush streamPush = streamPushMapper.queryOne(id); + if (streamPush == null || !streamPush.isPushing()) { + return; + } + String mediaServerId = streamPush.getMediaServerId(); + MediaServer mediaServer = mediaServerService.getOne(mediaServerId); + Assert.notNull(mediaServer, "未找到使用的节点"); + mediaServerService.closeStreams(mediaServer, streamPush.getApp(), streamPush.getStream()); + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/streamPush/service/impl/StreamPushServiceImpl.java b/src/main/java/com/genersoft/iot/vmp/streamPush/service/impl/StreamPushServiceImpl.java new file mode 100755 index 0000000..a33d7ca --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/streamPush/service/impl/StreamPushServiceImpl.java @@ -0,0 +1,600 @@ +package com.genersoft.iot.vmp.streamPush.service.impl; + +import com.alibaba.fastjson2.JSONObject; +import com.genersoft.iot.vmp.common.StreamInfo; +import com.genersoft.iot.vmp.conf.UserSetting; +import com.genersoft.iot.vmp.conf.exception.ControllerException; +import com.genersoft.iot.vmp.gb28181.bean.CommonGBChannel; +import com.genersoft.iot.vmp.gb28181.service.IGbChannelService; +import com.genersoft.iot.vmp.media.bean.MediaInfo; +import com.genersoft.iot.vmp.media.bean.MediaServer; +import com.genersoft.iot.vmp.media.event.media.MediaArrivalEvent; +import com.genersoft.iot.vmp.media.event.media.MediaDepartureEvent; +import com.genersoft.iot.vmp.media.event.mediaServer.MediaServerOfflineEvent; +import com.genersoft.iot.vmp.media.event.mediaServer.MediaServerOnlineEvent; +import com.genersoft.iot.vmp.media.service.IMediaServerService; +import com.genersoft.iot.vmp.media.zlm.dto.StreamAuthorityInfo; +import com.genersoft.iot.vmp.media.zlm.dto.hook.OriginType; +import com.genersoft.iot.vmp.service.ISendRtpServerService; +import com.genersoft.iot.vmp.service.bean.StreamPushItemFromRedis; +import com.genersoft.iot.vmp.storager.IRedisCatchStorage; +import com.genersoft.iot.vmp.streamPush.bean.StreamPush; +import com.genersoft.iot.vmp.streamPush.dao.StreamPushMapper; +import com.genersoft.iot.vmp.streamPush.service.IStreamPushService; +import com.genersoft.iot.vmp.utils.DateUtil; +import com.genersoft.iot.vmp.vmanager.bean.ErrorCode; +import com.genersoft.iot.vmp.vmanager.bean.ResourceBaseInfo; +import com.github.pagehelper.PageHelper; +import com.github.pagehelper.PageInfo; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.event.EventListener; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; + +import java.util.*; + +@Service +@Slf4j +public class StreamPushServiceImpl implements IStreamPushService { + + @Autowired + private StreamPushMapper streamPushMapper; + + @Autowired + private IRedisCatchStorage redisCatchStorage; + + @Autowired + private UserSetting userSetting; + + @Autowired + + private IMediaServerService mediaServerService; + + @Autowired + private ISendRtpServerService sendRtpServerService; + + @Autowired + private IGbChannelService gbChannelService; + + /** + * 流到来的处理 + */ + @Async("taskExecutor") + @EventListener + @Transactional + public void onApplicationEvent(MediaArrivalEvent event) { + MediaInfo mediaInfo = event.getMediaInfo(); + if (mediaInfo == null) { + return; + } + if (mediaInfo.getOriginType() != OriginType.RTMP_PUSH.ordinal() + && mediaInfo.getOriginType() != OriginType.RTSP_PUSH.ordinal() + && mediaInfo.getOriginType() != OriginType.RTC_PUSH.ordinal()) { + return; + } + + StreamAuthorityInfo streamAuthorityInfo = redisCatchStorage.getStreamAuthorityInfo(event.getApp(), event.getStream()); + if (streamAuthorityInfo == null) { + streamAuthorityInfo = StreamAuthorityInfo.getInstanceByHook(event); + } else { + streamAuthorityInfo.setOriginType(mediaInfo.getOriginType()); + } + redisCatchStorage.updateStreamAuthorityInfo(event.getApp(), event.getStream(), streamAuthorityInfo); + + StreamPush streamPushInDb = getPush(event.getApp(), event.getStream()); + if (streamPushInDb == null) { + StreamPush streamPush = StreamPush.getInstance(event, userSetting.getServerId()); + streamPush.setPushing(true); + streamPush.setServerId(userSetting.getServerId()); + streamPush.setUpdateTime(DateUtil.getNow()); + streamPush.setPushTime(DateUtil.getNow()); + add(streamPush); + }else { + streamPushInDb.setPushTime(DateUtil.getNow()); + streamPushInDb.setPushing(true); + streamPushInDb.setServerId(userSetting.getServerId()); + streamPushInDb.setMediaServerId(mediaInfo.getMediaServer().getId()); + updatePushStatus(streamPushInDb); + } + // 冗余数据,自己系统中自用 + if (!"broadcast".equals(event.getApp()) && !"talk".equals(event.getApp())) { + redisCatchStorage.addPushListItem(event.getApp(), event.getStream(), event.getMediaInfo()); + } + + // 发送流变化redis消息 + JSONObject jsonObject = new JSONObject(); + jsonObject.put("serverId", userSetting.getServerId()); + jsonObject.put("app", event.getApp()); + jsonObject.put("stream", event.getStream()); + jsonObject.put("register", true); + jsonObject.put("mediaServerId", event.getMediaServer().getId()); + redisCatchStorage.sendStreamChangeMsg(OriginType.values()[event.getMediaInfo().getOriginType()].getType(), jsonObject); + } + + /** + * 流离开的处理 + */ + @Async("taskExecutor") + @EventListener + @Transactional + public void onApplicationEvent(MediaDepartureEvent event) { + + // 兼容流注销时类型从redis记录获取 + MediaInfo mediaInfo = redisCatchStorage.getPushListItem(event.getApp(), event.getStream()); + + if (mediaInfo != null) { + log.info("[推流信息] 查询到redis存在推流缓存, 开始清理,{}/{}", event.getApp(), event.getStream()); + String type = OriginType.values()[mediaInfo.getOriginType()].getType(); + // 冗余数据,自己系统中自用 + redisCatchStorage.removePushListItem(event.getApp(), event.getStream(), event.getMediaServer().getId()); + // 发送流变化redis消息 + JSONObject jsonObject = new JSONObject(); + jsonObject.put("serverId", userSetting.getServerId()); + jsonObject.put("app", event.getApp()); + jsonObject.put("stream", event.getStream()); + jsonObject.put("register", false); + jsonObject.put("mediaServerId", event.getMediaServer().getId()); + redisCatchStorage.sendStreamChangeMsg(type, jsonObject); + } + StreamPush streamPush = getPush(event.getApp(), event.getStream()); + if (streamPush == null) { + return; + } + if (streamPush.getGbDeviceId() != null) { + streamPush.setPushing(false); + updatePushStatus(streamPush); + }else { + deleteByAppAndStream(event.getApp(), event.getStream()); + } + } + + /** + * 流媒体节点上线 + */ + @Async("taskExecutor") + @EventListener + @Transactional + public void onApplicationEvent(MediaServerOnlineEvent event) { + zlmServerOnline(event.getMediaServer()); + } + + /** + * 流媒体节点离线 + */ + @Async("taskExecutor") + @EventListener + @Transactional + public void onApplicationEvent(MediaServerOfflineEvent event) { + zlmServerOffline(event.getMediaServer()); + } + + @Override + public PageInfo getPushList(Integer page, Integer count, String query, Boolean pushing, String mediaServerId) { + PageHelper.startPage(page, count); + if (query != null) { + query = query.replaceAll("/", "//") + .replaceAll("%", "/%") + .replaceAll("_", "/_"); + } + List all = streamPushMapper.selectAll(query, pushing, mediaServerId); + return new PageInfo<>(all); + } + + @Override + public List getPushList(String mediaServerId) { + return streamPushMapper.selectAllByMediaServerIdWithOutGbID(mediaServerId); + } + + + @Override + public StreamPush getPush(String app, String stream) { + return streamPushMapper.selectByAppAndStream(app, stream); + } + + @Override + @Transactional + public boolean add(StreamPush stream) { + log.info("[添加推流] app: {}, stream: {}, 国标编号: {}", stream.getApp(), stream.getStream(), stream.getGbDeviceId()); + StreamPush streamPushInDb = streamPushMapper.selectByAppAndStream(stream.getApp(), stream.getStream()); + if (streamPushInDb != null) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "应用名+流ID已存在"); + } + stream.setUpdateTime(DateUtil.getNow()); + stream.setCreateTime(DateUtil.getNow()); + int addResult = streamPushMapper.add(stream); + if (addResult <= 0) { + return false; + } + if (ObjectUtils.isEmpty(stream.getGbDeviceId())) { + return true; + } + CommonGBChannel channel = gbChannelService.queryByDeviceId(stream.getGbDeviceId()); + if (channel != null) { + log.info("[添加推流]失败,国标编号已存在: {} app: {}, stream: {}, ", stream.getGbDeviceId(), stream.getApp(), stream.getStream()); + } + int addChannelResult = gbChannelService.add(stream.buildCommonGBChannel()); + return addChannelResult > 0; + } + + @Override + @Transactional + public void deleteByAppAndStream(String app, String stream) { + log.info("[删除推流] app: {}, stream: {}, ", app, stream); + StreamPush streamPush = streamPushMapper.selectByAppAndStream(app, stream); + if (streamPush == null) { + log.info("[删除推流]失败, 不存在 app: {}, stream: {}, ", app, stream); + return; + } + if (streamPush.isPushing()) { + stop(streamPush); + } + if (streamPush.getGbId() > 0) { + gbChannelService.delete(streamPush.getGbId()); + } + streamPushMapper.del(streamPush.getId()); + } + @Override + @Transactional + public boolean update(StreamPush streamPush) { + Assert.notNull(streamPush, "推流信息不可为NULL"); + Assert.isTrue(streamPush.getId() > 0, "推流信息ID必须存在"); + log.info("[更新推流]:id: {}, app: {}, stream: {}, ", streamPush.getId(), streamPush.getApp(), streamPush.getStream()); + StreamPush streamPushInDb = streamPushMapper.queryOne(streamPush.getId()); + if (!streamPushInDb.getApp().equals(streamPush.getApp()) || !streamPushInDb.getStream().equals(streamPush.getStream())) { + // app或者stream变化 + StreamPush streamPushInDbForAppAndStream = streamPushMapper.selectByAppAndStream(streamPush.getApp(), streamPush.getStream()); + if (streamPushInDbForAppAndStream != null && !streamPushInDbForAppAndStream.getId().equals(streamPush.getId())) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "应用名+流ID已存在"); + } + } + streamPush.setUpdateTime(DateUtil.getNow()); + streamPushMapper.update(streamPush); + if (streamPush.getGbId() > 0) { + gbChannelService.update(streamPush.buildCommonGBChannel()); + } + return true; + } + + + @Override + @Transactional + public boolean stop(StreamPush streamPush) { + log.info("[主动停止推流] id: {}, app: {}, stream: {}, ", streamPush.getId(), streamPush.getApp(), streamPush.getStream()); + MediaServer mediaServer = null; + if (streamPush.getMediaServerId() == null) { + log.info("[主动停止推流]未找到使用MediaServer,开始自动检索 id: {}, app: {}, stream: {}, ", streamPush.getId(), streamPush.getApp(), streamPush.getStream()); + mediaServer = mediaServerService.getMediaServerByAppAndStream(streamPush.getApp(), streamPush.getStream()); + if (mediaServer != null) { + log.info("[主动停止推流] 检索到MediaServer为{}, id: {}, app: {}, stream: {}, ", mediaServer.getId(), streamPush.getId(), streamPush.getApp(), streamPush.getStream()); + }else { + log.info("[主动停止推流]未找到使用MediaServer id: {}, app: {}, stream: {}, ", streamPush.getId(), streamPush.getApp(), streamPush.getStream()); + } + }else { + mediaServer = mediaServerService.getOne(streamPush.getMediaServerId()); + if (mediaServer == null) { + log.info("[主动停止推流]未找到使用的MediaServer: {},开始自动检索 id: {}, app: {}, stream: {}, ",streamPush.getMediaServerId(), streamPush.getId(), streamPush.getApp(), streamPush.getStream()); + mediaServer = mediaServerService.getMediaServerByAppAndStream(streamPush.getApp(), streamPush.getStream()); + if (mediaServer != null) { + log.info("[主动停止推流] 检索到MediaServer为{}, id: {}, app: {}, stream: {}, ", mediaServer.getId(), streamPush.getId(), streamPush.getApp(), streamPush.getStream()); + }else { + log.info("[主动停止推流]未找到使用MediaServer id: {}, app: {}, stream: {}, ", streamPush.getId(), streamPush.getApp(), streamPush.getStream()); + } + } + } + if (mediaServer != null) { + mediaServerService.closeStreams(mediaServer, streamPush.getApp(), streamPush.getStream()); + } + streamPush.setPushing(false); + if (userSetting.getUsePushingAsStatus()) { + CommonGBChannel commonGBChannel = streamPush.buildCommonGBChannel(); + if (commonGBChannel != null) { + gbChannelService.offline(commonGBChannel); + } + } + sendRtpServerService.deleteByStream(streamPush.getStream()); + mediaServerService.stopSendRtp(mediaServer, streamPush.getApp(), streamPush.getStream(), null); + streamPush.setUpdateTime(DateUtil.getNow()); + streamPushMapper.update(streamPush); + return true; + } + + @Override + @Transactional + public boolean stopByAppAndStream(String app, String stream) { + log.info("[主动停止推流] : app: {}, stream: {}, ", app, stream); + StreamPush streamPushItem = streamPushMapper.selectByAppAndStream(app, stream); + if (streamPushItem != null) { + stop(streamPushItem); + } + return true; + } + + @Override + @Transactional + public void zlmServerOnline(MediaServer mediaServer) { + // 同步zlm推流信息 + if (mediaServer == null) { + return; + } + // 数据库记录 + List pushList = getPushList(mediaServer.getId()); + Map pushItemMap = new HashMap<>(); + // redis记录 + List mediaInfoList = redisCatchStorage.getStreams(mediaServer.getId(), "PUSH"); + Map streamInfoPushItemMap = new HashMap<>(); + if (!pushList.isEmpty()) { + for (StreamPush streamPushItem : pushList) { + if (ObjectUtils.isEmpty(streamPushItem.getGbId())) { + pushItemMap.put(streamPushItem.getApp() + streamPushItem.getStream(), streamPushItem); + } + } + } + if (!mediaInfoList.isEmpty()) { + for (MediaInfo mediaInfo : mediaInfoList) { + if (mediaInfo == null) { + continue; + } + streamInfoPushItemMap.put(mediaInfo.getApp() + mediaInfo.getStream(), mediaInfo); + } + } + // 获取所有推流鉴权信息,清理过期的 + List allStreamAuthorityInfo = redisCatchStorage.getAllStreamAuthorityInfo(); + Map streamAuthorityInfoInfoMap = new HashMap<>(); + for (StreamAuthorityInfo streamAuthorityInfo : allStreamAuthorityInfo) { + streamAuthorityInfoInfoMap.put(streamAuthorityInfo.getApp() + streamAuthorityInfo.getStream(), streamAuthorityInfo); + } + List mediaList = mediaServerService.getMediaList(mediaServer, null, null, null); + if (mediaList == null) { + return; + } + List streamPushItems = handleJSON(mediaList); + if (streamPushItems != null) { + for (StreamPush streamPushItem : streamPushItems) { + pushItemMap.remove(streamPushItem.getApp() + streamPushItem.getStream()); + streamInfoPushItemMap.remove(streamPushItem.getApp() + streamPushItem.getStream()); + streamAuthorityInfoInfoMap.remove(streamPushItem.getApp() + streamPushItem.getStream()); + } + } + List changedStreamPushList = new ArrayList<>(pushItemMap.values()); + if (!changedStreamPushList.isEmpty()) { + for (StreamPush streamPush : changedStreamPushList) { + stop(streamPush); + } + } + + Collection mediaInfos = streamInfoPushItemMap.values(); + if (!mediaInfos.isEmpty()) { + String type = "PUSH"; + for (MediaInfo mediaInfo : mediaInfos) { + JSONObject jsonObject = new JSONObject(); + jsonObject.put("serverId", userSetting.getServerId()); + jsonObject.put("app", mediaInfo.getApp()); + jsonObject.put("stream", mediaInfo.getStream()); + jsonObject.put("register", false); + jsonObject.put("mediaServerId", mediaServer.getId()); + redisCatchStorage.sendStreamChangeMsg(type, jsonObject); + // 移除redis内流的信息 + redisCatchStorage.removeStream(mediaServer.getId(), "PUSH", mediaInfo.getApp(), mediaInfo.getStream()); + // 冗余数据,自己系统中自用 + redisCatchStorage.removePushListItem(mediaInfo.getApp(), mediaInfo.getStream(), mediaServer.getId()); + } + } + + Collection streamAuthorityInfos = streamAuthorityInfoInfoMap.values(); + if (!streamAuthorityInfos.isEmpty()) { + for (StreamAuthorityInfo streamAuthorityInfo : streamAuthorityInfos) { + // 移除redis内流的信息 + redisCatchStorage.removeStreamAuthorityInfo(streamAuthorityInfo.getApp(), streamAuthorityInfo.getStream()); + } + } + } + + @Override + @Transactional + public void zlmServerOffline(MediaServer mediaServer) { + List streamPushItems = streamPushMapper.selectAllByMediaServerId(mediaServer.getId()); + if (!streamPushItems.isEmpty()) { + for (StreamPush streamPushItem : streamPushItems) { + stop(streamPushItem); + } + } +// // 移除没有GBId的推流 +// streamPushMapper.deleteWithoutGBId(mediaServerId); +// // 其他的流设置未启用 +// streamPushMapper.updateStatusByMediaServerId(mediaServerId, false); +// streamProxyMapper.updateStatusByMediaServerId(mediaServerId, false); + // 发送流停止消息 + String type = "PUSH"; + // 发送redis消息 + List mediaInfoList = redisCatchStorage.getStreams(mediaServer.getId(), type); + if (!mediaInfoList.isEmpty()) { + for (MediaInfo mediaInfo : mediaInfoList) { + // 移除redis内流的信息 + redisCatchStorage.removeStream(mediaServer.getId(), type, mediaInfo.getApp(), mediaInfo.getStream()); + JSONObject jsonObject = new JSONObject(); + jsonObject.put("serverId", userSetting.getServerId()); + jsonObject.put("app", mediaInfo.getApp()); + jsonObject.put("stream", mediaInfo.getStream()); + jsonObject.put("register", false); + jsonObject.put("mediaServerId", mediaServer.getId()); + redisCatchStorage.sendStreamChangeMsg(type, jsonObject); + + // 冗余数据,自己系统中自用 + redisCatchStorage.removePushListItem(mediaInfo.getApp(), mediaInfo.getStream(), mediaServer.getId()); + } + } + } + + @Override + @Transactional + public void batchAdd(List streamPushItems) { + streamPushMapper.addAll(streamPushItems); + List commonGBChannels = new ArrayList<>(); + for (StreamPush streamPush : streamPushItems) { + if (!ObjectUtils.isEmpty(streamPush.getGbDeviceId())) { + commonGBChannels.add(streamPush.buildCommonGBChannel()); + } + } + gbChannelService.batchAdd(commonGBChannels); + } + + @Override + public void allOffline() { + List streamPushList = streamPushMapper.selectAll(null, null, null); + if (streamPushList.isEmpty()) { + return; + } + List commonGBChannelList = new ArrayList<>(); + for (StreamPush streamPush : streamPushList) { + CommonGBChannel commonGBChannel = streamPush.buildCommonGBChannel(); + if (commonGBChannel != null) { + commonGBChannelList.add(streamPush.buildCommonGBChannel()); + } + } + gbChannelService.offline(commonGBChannelList); + } + + @Override + public void offline(List offlineStreams) { + // 更新部分设备离线 + List streamPushList = streamPushMapper.getListInList(offlineStreams); + if (streamPushList.isEmpty()) { + log.info("[推流设备] 设备离线操作未发现可操作数据。"); + return; + } + List commonGBChannelList = gbChannelService.queryListByStreamPushList(streamPushList); + gbChannelService.offline(commonGBChannelList); + } + + @Override + public void online(List onlineStreams) { + if (onlineStreams.isEmpty()) { + log.info("[设备上线] 推流设备列表为空"); + return; + } + // 更新部分设备上线streamPushService + List streamPushList = streamPushMapper.getListInList(onlineStreams); + if (streamPushList.isEmpty()) { + for (StreamPushItemFromRedis onlineStream : onlineStreams) { + log.info("[设备上线] 未查询到这些通道: {}/{}", onlineStream.getApp(), onlineStream.getStream()); + } + return; + } + List commonGBChannelList = gbChannelService.queryListByStreamPushList(streamPushList); + gbChannelService.online(commonGBChannelList); + } + + @Override + public List getAllAppAndStream() { + return streamPushMapper.getAllAppAndStream(); + } + + @Override + public ResourceBaseInfo getOverview() { + int total = streamPushMapper.getAllCount(); + int online = streamPushMapper.getAllPushing(userSetting.getUsePushingAsStatus()); + + return new ResourceBaseInfo(total, online); + } + + @Override + public Map getAllAppAndStreamMap() { + return streamPushMapper.getAllAppAndStreamMap(); + } + + @Override + public Map getAllGBId() { + return streamPushMapper.getAllGBId(); + } + + @Override + @Transactional + public void updatePushStatus(StreamPush streamPush) { + if (userSetting.getUsePushingAsStatus()) { + streamPush.setGbStatus(streamPush.isPushing()?"ON":"OFF"); + } + streamPushMapper.updatePushStatus(streamPush); + if (ObjectUtils.isEmpty(streamPush.getGbDeviceId())) { + return; + } + if (userSetting.getUsePushingAsStatus()) { + if ("ON".equalsIgnoreCase(streamPush.getGbStatus()) ) { + gbChannelService.online(streamPush.buildCommonGBChannel()); + }else { + gbChannelService.offline(streamPush.buildCommonGBChannel()); + } + } + } + + private List handleJSON(List streamInfoList) { + if (streamInfoList == null || streamInfoList.isEmpty()) { + return null; + } + Map result = new HashMap<>(); + for (StreamInfo streamInfo : streamInfoList) { + // 不保存国标推理以及拉流代理的流 + if (streamInfo.getOriginType() == OriginType.RTSP_PUSH.ordinal() + || streamInfo.getOriginType() == OriginType.RTMP_PUSH.ordinal() + || streamInfo.getOriginType() == OriginType.RTC_PUSH.ordinal() ) { + String key = streamInfo.getApp() + "_" + streamInfo.getStream(); + StreamPush streamPushItem = result.get(key); + if (streamPushItem == null) { + streamPushItem = StreamPush.getInstance(streamInfo); + result.put(key, streamPushItem); + } + } + } + return new ArrayList<>(result.values()); + } + + @Override + @Transactional + public void batchUpdate(List streamPushItemForUpdate) { + streamPushMapper.batchUpdate(streamPushItemForUpdate); + List commonGBChannels = new ArrayList<>(); + for (StreamPush streamPush : streamPushItemForUpdate) { + if (!ObjectUtils.isEmpty(streamPush.getGbDeviceId())) { + commonGBChannels.add(streamPush.buildCommonGBChannel()); + } + } + gbChannelService.batchUpdate(commonGBChannels); + } + + @Override + @Transactional + public int delete(int id) { + StreamPush streamPush = streamPushMapper.queryOne(id); + if (streamPush == null) { + return 0; + } + if(streamPush.isPushing()) { + MediaServer mediaServer = mediaServerService.getOne(streamPush.getMediaServerId()); + mediaServerService.closeStreams(mediaServer, streamPush.getApp(), streamPush.getStream()); + } + if (streamPush.getGbDeviceId() != null) { + gbChannelService.delete(streamPush.getGbId()); + } + return streamPushMapper.del(id); + } + + @Override + @Transactional + public void batchRemove(Set ids) { + List streamPushList = streamPushMapper.selectInSet(ids); + if (streamPushList.isEmpty()) { + return; + } + Set channelIds = new HashSet<>(); + streamPushList.stream().forEach(streamPush -> { + if (streamPush.getGbDeviceId() != null) { + channelIds.add(streamPush.getGbId()); + } + }); + streamPushMapper.batchDel(streamPushList); + gbChannelService.delete(channelIds); + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/utils/CivilCodeUtil.java b/src/main/java/com/genersoft/iot/vmp/utils/CivilCodeUtil.java new file mode 100644 index 0000000..33c3e47 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/utils/CivilCodeUtil.java @@ -0,0 +1,103 @@ +package com.genersoft.iot.vmp.utils; + +import com.genersoft.iot.vmp.common.CivilCodePo; +import com.genersoft.iot.vmp.gb28181.bean.Region; +import lombok.extern.slf4j.Slf4j; +import org.springframework.util.ObjectUtils; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +@Slf4j +public enum CivilCodeUtil { + + INSTANCE; + // 用与消息的缓存 + private final Map civilCodeMap = new ConcurrentHashMap<>(); + + CivilCodeUtil() { + } + + public void add(List civilCodePoList) { + if (!civilCodePoList.isEmpty()) { + for (CivilCodePo civilCodePo : civilCodePoList) { + civilCodeMap.put(civilCodePo.getCode(), civilCodePo); + } + } + } + + public void add(CivilCodePo civilCodePo) { + civilCodeMap.put(civilCodePo.getCode(), civilCodePo); + } + + public CivilCodePo get(String code) { + return civilCodeMap.get(code); + } + + public CivilCodePo getParentCode(String code) { + if (code.length() > 8) { + return null; + } + if (code.length() == 8) { + String parentCode = code.substring(0, 6); + return civilCodeMap.get(parentCode); + }else { + CivilCodePo civilCodePo = civilCodeMap.get(code); + if (civilCodePo == null){ + return null; + } + String parentCode = civilCodePo.getParentCode(); + if (parentCode == null) { + return null; + } + return civilCodeMap.get(parentCode); + } + } + + public CivilCodePo getCivilCodePo(String code) { + if (code.length() > 8) { + return null; + }else { + return civilCodeMap.get(code); + } + } + + public List getAllParentCode(String civilCode) { + List civilCodePoList = new ArrayList<>(); + CivilCodePo parentCode = getParentCode(civilCode); + if (parentCode != null) { + civilCodePoList.add(parentCode); + List allParentCode = getAllParentCode(parentCode.getCode()); + if (!allParentCode.isEmpty()) { + civilCodePoList.addAll(allParentCode); + }else { + return civilCodePoList; + } + } + return civilCodePoList; + } + + public boolean isEmpty() { + return civilCodeMap.isEmpty(); + } + + public int size() { + return civilCodeMap.size(); + } + + public List getAllChild(String parent) { + List result = new ArrayList<>(); + for (String key : civilCodeMap.keySet()) { + if (parent == null) { + if (ObjectUtils.isEmpty(civilCodeMap.get(key).getParentCode().trim())) { + result.add(Region.getInstance(key, civilCodeMap.get(key).getName(), civilCodeMap.get(key).getParentCode())); + } + }else if (civilCodeMap.get(key).getParentCode().equals(parent)) { + result.add(Region.getInstance(key, civilCodeMap.get(key).getName(), civilCodeMap.get(key).getParentCode())); + } + } + return result; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/utils/Coordtransform.java b/src/main/java/com/genersoft/iot/vmp/utils/Coordtransform.java new file mode 100755 index 0000000..c10357c --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/utils/Coordtransform.java @@ -0,0 +1,126 @@ +package com.genersoft.iot.vmp.utils; + +/** + * 坐标转换 + * 一个提供了百度坐标(BD09)、国测局坐标(火星坐标,GCJ02)、和WGS84坐标系之间的转换的工具类 + * 参考https://github.com/wandergis/coordtransform 写的Java版本 + * @author Xinconan + * @date 2016-03-18 + * @url https://github.com/xinconan/coordtransform + */ +public class Coordtransform { + + private static double x_PI = 3.14159265358979324 * 3000.0 / 180.0; + private static double PI = 3.1415926535897932384626; + private static double a = 6378245.0; + private static double ee = 0.00669342162296594323; + + /** + * 百度坐标系 (BD-09) 与 火星坐标系 (GCJ-02)的转换 + * 即 百度 转 谷歌、高德 + * @param bd_lon + * @param bd_lat + * @return Double[lon,lat] + */ + public static Double[] BD09ToGCJ02(Double bd_lon,Double bd_lat){ + double x = bd_lon - 0.0065; + double y = bd_lat - 0.006; + double z = Math.sqrt(x * x + y * y) - 0.00002 * Math.sin(y * x_PI); + double theta = Math.atan2(y, x) - 0.000003 * Math.cos(x * x_PI); + Double[] arr = new Double[2]; + arr[0] = z * Math.cos(theta); + arr[1] = z * Math.sin(theta); + return arr; + } + + /** + * 火星坐标系 (GCJ-02) 与百度坐标系 (BD-09) 的转换 + * 即谷歌、高德 转 百度 + * @param gcj_lon + * @param gcj_lat + * @return Double[lon,lat] + */ + public static Double[] GCJ02ToBD09(Double gcj_lon,Double gcj_lat){ + double z = Math.sqrt(gcj_lon * gcj_lon + gcj_lat * gcj_lat) + 0.00002 * Math.sin(gcj_lat * x_PI); + double theta = Math.atan2(gcj_lat, gcj_lon) + 0.000003 * Math.cos(gcj_lon * x_PI); + Double[] arr = new Double[2]; + arr[0] = z * Math.cos(theta) + 0.0065; + arr[1] = z * Math.sin(theta) + 0.006; + return arr; + } + + /** + * WGS84转GCJ02 + * @param wgs_lon + * @param wgs_lat + * @return Double[lon,lat] + */ + public static Double[] WGS84ToGCJ02(Double wgs_lon,Double wgs_lat){ + if(outOfChina(wgs_lon, wgs_lat)){ + return new Double[]{wgs_lon,wgs_lat}; + } + double dlat = transformlat(wgs_lon - 105.0, wgs_lat - 35.0); + double dlng = transformlng(wgs_lon - 105.0, wgs_lat - 35.0); + double radlat = wgs_lat / 180.0 * PI; + double magic = Math.sin(radlat); + magic = 1 - ee * magic * magic; + double sqrtmagic = Math.sqrt(magic); + dlat = (dlat * 180.0) / ((a * (1 - ee)) / (magic * sqrtmagic) * PI); + dlng = (dlng * 180.0) / (a / sqrtmagic * Math.cos(radlat) * PI); + Double[] arr = new Double[2]; + arr[0] = wgs_lon + dlng; + arr[1] = wgs_lat + dlat; + return arr; + } + + /** + * GCJ02转WGS84 + * @param gcj_lon + * @param gcj_lat + * @return Double[lon,lat] + */ + public static Double[] GCJ02ToWGS84(Double gcj_lon,Double gcj_lat){ + if(outOfChina(gcj_lon, gcj_lat)){ + return new Double[]{gcj_lon,gcj_lat}; + } + double dlat = transformlat(gcj_lon - 105.0, gcj_lat - 35.0); + double dlng = transformlng(gcj_lon - 105.0, gcj_lat - 35.0); + double radlat = gcj_lat / 180.0 * PI; + double magic = Math.sin(radlat); + magic = 1 - ee * magic * magic; + double sqrtmagic = Math.sqrt(magic); + dlat = (dlat * 180.0) / ((a * (1 - ee)) / (magic * sqrtmagic) * PI); + dlng = (dlng * 180.0) / (a / sqrtmagic * Math.cos(radlat) * PI); + double mglat = gcj_lat + dlat; + double mglng = gcj_lon + dlng; + return new Double[]{gcj_lon * 2 - mglng, gcj_lat * 2 - mglat}; + } + + private static Double transformlat(double lng, double lat) { + double ret = -100.0 + 2.0 * lng + 3.0 * lat + 0.2 * lat * lat + 0.1 * lng * lat + 0.2 * Math.sqrt(Math.abs(lng)); + ret += (20.0 * Math.sin(6.0 * lng * PI) + 20.0 * Math.sin(2.0 * lng * PI)) * 2.0 / 3.0; + ret += (20.0 * Math.sin(lat * PI) + 40.0 * Math.sin(lat / 3.0 * PI)) * 2.0 / 3.0; + ret += (160.0 * Math.sin(lat / 12.0 * PI) + 320 * Math.sin(lat * PI / 30.0)) * 2.0 / 3.0; + return ret; + } + + private static Double transformlng(double lng,double lat) { + double ret = 300.0 + lng + 2.0 * lat + 0.1 * lng * lng + 0.1 * lng * lat + 0.1 * Math.sqrt(Math.abs(lng)); + ret += (20.0 * Math.sin(6.0 * lng * PI) + 20.0 * Math.sin(2.0 * lng * PI)) * 2.0 / 3.0; + ret += (20.0 * Math.sin(lng * PI) + 40.0 * Math.sin(lng / 3.0 * PI)) * 2.0 / 3.0; + ret += (150.0 * Math.sin(lng / 12.0 * PI) + 300.0 * Math.sin(lng / 30.0 * PI)) * 2.0 / 3.0; + return ret; + } + + /** + * outOfChina + * @描述: 判断是否在国内,不在国内则不做偏移 + * @param lng + * @param lat + * @return {boolean} + */ + private static boolean outOfChina(Double lng,Double lat) { + return (lng < 72.004 || lng > 137.8347) || ((lat < 0.8293 || lat > 55.8271) || false); + }; + +} diff --git a/src/main/java/com/genersoft/iot/vmp/utils/DateUtil.java b/src/main/java/com/genersoft/iot/vmp/utils/DateUtil.java new file mode 100755 index 0000000..41a0310 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/utils/DateUtil.java @@ -0,0 +1,229 @@ +package com.genersoft.iot.vmp.utils; + + +import jakarta.validation.constraints.NotNull; +import org.apache.commons.lang3.ObjectUtils; + +import java.time.Instant; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; +import java.time.temporal.ChronoUnit; +import java.time.temporal.TemporalAccessor; +import java.util.Locale; + +/** + * 全局时间工具类 + * @author lin + */ +public class DateUtil { + + /** + * 兼容不规范的iso8601时间格式 + */ + private static final String ISO8601_COMPATIBLE_PATTERN = "yyyy-M-d'T'H:m:s"; + + /** + * 用以输出标准的iso8601时间格式 + */ + private static final String ISO8601_PATTERN = "yyyy-MM-dd'T'HH:mm:ss"; + + /** + * iso8601时间格式带时区,例如:2024-02-21T11:10:36+08:00 + */ + private static final String ISO8601_ZONE_PATTERN = "yyyy-MM-dd'T'HH:mm:ssXXX"; + + /** + * 兼容的时间格式 iso8601时间格式带毫秒 + */ + private static final String ISO8601_MILLISECOND_PATTERN = "yyyy-MM-dd'T'HH:mm:ss.SSS"; + + /** + * wvp内部统一时间格式 + */ + public static final String PATTERN = "yyyy-MM-dd HH:mm:ss"; + + /** + * wvp内部统一时间格式 + */ + public static final String URL_PATTERN = "yyyyMMddHHmmss"; + public static final String PATTERN1078 = "yyMMddHHmmss"; + public static final String PATTERN1078Date = "yyyyMMdd"; + + /** + * 日期格式 + */ + public static final String date_PATTERN = "yyyy-MM-dd"; + + public static final String zoneStr = "Asia/Shanghai"; + + public static final DateTimeFormatter formatterCompatibleISO8601 = DateTimeFormatter.ofPattern(ISO8601_COMPATIBLE_PATTERN, Locale.getDefault()).withZone(ZoneId.of(zoneStr)); + public static final DateTimeFormatter formatterISO8601 = DateTimeFormatter.ofPattern(ISO8601_PATTERN, Locale.getDefault()).withZone(ZoneId.of(zoneStr)); + public static final DateTimeFormatter formatterZoneISO8601 = DateTimeFormatter.ofPattern(ISO8601_ZONE_PATTERN, Locale.getDefault()).withZone(ZoneId.of(zoneStr)); + public static final DateTimeFormatter formatterMillisecondISO8601 = DateTimeFormatter.ofPattern(ISO8601_MILLISECOND_PATTERN, Locale.getDefault()).withZone(ZoneId.of(zoneStr)); + + public static final DateTimeFormatter formatter = DateTimeFormatter.ofPattern(PATTERN, Locale.getDefault()).withZone(ZoneId.of(zoneStr)); + public static final DateTimeFormatter DateFormatter = DateTimeFormatter.ofPattern(date_PATTERN, Locale.getDefault()).withZone(ZoneId.of(zoneStr)); + public static final DateTimeFormatter urlFormatter = DateTimeFormatter.ofPattern(URL_PATTERN, Locale.getDefault()).withZone(ZoneId.of(zoneStr)); + public static final DateTimeFormatter formatter1078 = DateTimeFormatter.ofPattern(PATTERN1078, Locale.getDefault()).withZone(ZoneId.of(zoneStr)); + public static final DateTimeFormatter formatter1078date = DateTimeFormatter.ofPattern(PATTERN1078Date, Locale.getDefault()).withZone(ZoneId.of(zoneStr)); + + public static String yyyy_MM_dd_HH_mm_ssToISO8601(@NotNull String formatTime) { + return formatterISO8601.format(formatter.parse(formatTime)); + } + + public static String yyyy_MM_dd_HH_mm_ssToUrl(@NotNull String formatTime) { + return urlFormatter.format(formatter.parse(formatTime)); + } + + public static String ISO8601Toyyyy_MM_dd_HH_mm_ss(String formatTime) { + // 三种日期格式都尝试,为了兼容不同厂家的日期格式 + if (verification(formatTime, formatterCompatibleISO8601)) { + return formatter.format(formatterCompatibleISO8601.parse(formatTime)); + } else if (verification(formatTime, formatterZoneISO8601)) { + return formatter.format(formatterZoneISO8601.parse(formatTime)); + } else if (verification(formatTime, formatterMillisecondISO8601)) { + return formatter.format(formatterMillisecondISO8601.parse(formatTime)); + } + return formatter.format(formatterISO8601.parse(formatTime)); + } + + public static String urlToyyyy_MM_dd_HH_mm_ss(String formatTime) { + return formatter.format(urlFormatter.parse(formatTime)); + } + public static String yyyy_MM_dd_HH_mm_ssTo1078(String formatTime) { + return formatter1078.format(formatter.parse(formatTime)); + } + public static String jt1078Toyyyy_MM_dd_HH_mm_ss(String formatTime) { + return formatter.format(formatter1078.parse(formatTime)); + } + public static String jt1078dateToyyyy_MM_dd(String formatTime) { + return DateFormatter.format(formatter1078date.parse(formatTime)); + } + + /** + * yyyy_MM_dd_HH_mm_ss 转时间戳 + * @param formatTime + * @return + */ + public static long yyyy_MM_dd_HH_mm_ssToTimestamp(String formatTime) { + TemporalAccessor temporalAccessor = formatter.parse(formatTime); + Instant instant = Instant.from(temporalAccessor); + return instant.getEpochSecond(); + } + + /** + * 时间戳 转 yyyy_MM_dd_HH_mm_ss + */ + public static String timestampTo_yyyy_MM_dd_HH_mm_ss(long timestamp) { + Instant instant = Instant.ofEpochSecond(timestamp); + return formatter.format(LocalDateTime.ofInstant(instant, ZoneId.of(zoneStr))); + } + + /** + * 时间戳 转 yyyy_MM_dd_HH_mm_ss + */ + public static String timestampMsToUrlToyyyy_MM_dd_HH_mm_ss(long timestamp) { + Instant instant = Instant.ofEpochMilli(timestamp); + return urlFormatter.format(LocalDateTime.ofInstant(instant, ZoneId.of(zoneStr))); + } + + /** + * yyyy_MM_dd_HH_mm_ss 转时间戳(毫秒) + * + * @param formatTime + * @return + */ + public static long yyyy_MM_dd_HH_mm_ssToTimestampMs(String formatTime) { + TemporalAccessor temporalAccessor = formatter.parse(formatTime); + Instant instant = Instant.from(temporalAccessor); + return instant.toEpochMilli(); + } + + /** + * 时间戳(毫秒) 转 yyyy_MM_dd_HH_mm_ss + */ + public static String timestampMsTo_yyyy_MM_dd_HH_mm_ss(long timestamp) { + Instant instant = Instant.ofEpochMilli(timestamp); + return formatter.format(LocalDateTime.ofInstant(instant, ZoneId.of(zoneStr))); + } + + /** + * yyyy_MM_dd_HH_mm_ss 转时间戳(毫秒) + * + * @param formatTime + * @return + */ + public static long urlToTimestampMs(String formatTime) { + TemporalAccessor temporalAccessor = urlFormatter.parse(formatTime); + Instant instant = Instant.from(temporalAccessor); + return instant.toEpochMilli(); + } + + /** + * 时间戳 转 yyyy_MM_dd + */ + public static String timestampTo_yyyy_MM_dd(long timestamp) { + Instant instant = Instant.ofEpochMilli(timestamp); + return DateFormatter.format(LocalDateTime.ofInstant(instant, ZoneId.of(zoneStr))); + } + + /** + * 获取当前时间 + * @return + */ + public static String getNow() { + LocalDateTime nowDateTime = LocalDateTime.now(); + return formatter.format(nowDateTime); + } + + /** + * 获取当前时间 + * @return + */ + public static String getNowForUrl() { + LocalDateTime nowDateTime = LocalDateTime.now(); + return urlFormatter.format(nowDateTime); + } + + + /** + * 格式校验 + * @param timeStr 时间字符串 + * @param dateTimeFormatter 待校验的格式 + * @return + */ + public static boolean verification(String timeStr, DateTimeFormatter dateTimeFormatter) { + try { + LocalDate.parse(timeStr, dateTimeFormatter); + return true; + }catch (DateTimeParseException exception) { + return false; + } + } + + public static String getNowForISO8601() { + LocalDateTime nowDateTime = LocalDateTime.now(); + return formatterISO8601.format(nowDateTime); + } + + public static long getDifferenceForNow(String keepaliveTime) { + if (ObjectUtils.isEmpty(keepaliveTime)) { + return 0; + } + Instant beforeInstant = Instant.from(formatter.parse(keepaliveTime)); + return ChronoUnit.MILLIS.between(beforeInstant, Instant.now()); + } + + public static long getDifference(String startTime, String endTime) { + if (ObjectUtils.isEmpty(startTime) || ObjectUtils.isEmpty(endTime)) { + return 0; + } + Instant startInstant = Instant.from(formatter.parse(startTime)); + Instant endInstant = Instant.from(formatter.parse(endTime)); + return ChronoUnit.MILLIS.between(startInstant, endInstant); + } + +} diff --git a/src/main/java/com/genersoft/iot/vmp/utils/GitUtil.java b/src/main/java/com/genersoft/iot/vmp/utils/GitUtil.java new file mode 100755 index 0000000..ca637dd --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/utils/GitUtil.java @@ -0,0 +1,59 @@ +package com.genersoft.iot.vmp.utils; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.PropertySource; +import org.springframework.stereotype.Component; + +/** + * 一个优秀的颓废程序猿(CSDN) + */ +@Component +@PropertySource(value = {"classpath:git.properties" }, ignoreResourceNotFound = true) +public class GitUtil { + + @Value("${git.branch:}") + private String branch; + @Value("${git.commit.id:}") + private String gitCommitId; + @Value("${git.remote.origin.url:}") + private String gitUrl; + @Value("${git.build.time:}") + private String buildDate; + + @Value("${git.build.version:}") + private String buildVersion; + + @Value("${git.commit.id.abbrev:}") + private String commitIdShort; + + @Value("${git.commit.time:}") + private String commitTime; + + public String getGitCommitId() { + return gitCommitId; + } + + public String getBranch() { + return branch; + } + + public String getGitUrl() { + return gitUrl; + } + + public String getBuildDate() { + return buildDate; + } + + public String getCommitIdShort() { + return commitIdShort; + } + + public String getBuildVersion() { + return buildVersion; + } + + public String getCommitTime() { + return commitTime; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/utils/GpsUtil.java b/src/main/java/com/genersoft/iot/vmp/utils/GpsUtil.java new file mode 100755 index 0000000..1af61c7 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/utils/GpsUtil.java @@ -0,0 +1,34 @@ +package com.genersoft.iot.vmp.utils; + +import com.genersoft.iot.vmp.gb28181.bean.BaiduPoint; +import lombok.extern.slf4j.Slf4j; + +import java.util.Base64; + +@Slf4j +public class GpsUtil { + + + public static BaiduPoint Wgs84ToBd09(String xx, String yy) { + double lng = Double.parseDouble(xx); + double lat = Double.parseDouble(yy); + Double[] gcj02 = Coordtransform.WGS84ToGCJ02(lng, lat); + Double[] doubles = Coordtransform.GCJ02ToBD09(gcj02[0], gcj02[1]); + BaiduPoint bdPoint= new BaiduPoint(); + bdPoint.setBdLng(doubles[0] + ""); + bdPoint.setBdLat(doubles[1] + ""); + return bdPoint; + } + + /** + * BASE64解码 + * @param str + * @return string + */ + public static byte[] decode(String str) { + byte[] bt = null; + final Base64.Decoder decoder = Base64.getDecoder(); + bt = decoder.decode(str); // .decodeBuffer(str); + return bt; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/utils/HttpUtils.java b/src/main/java/com/genersoft/iot/vmp/utils/HttpUtils.java new file mode 100644 index 0000000..407a813 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/utils/HttpUtils.java @@ -0,0 +1,51 @@ +package com.genersoft.iot.vmp.utils; + +import lombok.extern.slf4j.Slf4j; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.Response; + +import java.io.IOException; +import java.io.InputStream; +import java.util.zip.ZipOutputStream; + +@Slf4j +public class HttpUtils { + + public static boolean downLoadFile(String url, ZipOutputStream zos) { + OkHttpClient client = new OkHttpClient(); + Request request = new Request.Builder() + .url(url) + .build(); + + try (Response response = client.newCall(request).execute()) { + if (!response.isSuccessful()) { + log.error("下载失败,HTTP 状态码: {}, URL: {}", response.code(), url); + return false; + } + + // 获取响应体的输入流 + InputStream inputStream = null; + if (response.body() != null) { + inputStream = response.body().byteStream(); + } + if (inputStream == null) { + log.error("响应体为空,无法下载文件: {}", url); + return false; + } + + // 将输入流写入zip文件 + byte[] buffer = new byte[8192]; // 8KB 缓冲区,提高性能 + int bytesRead; + while ((bytesRead = inputStream.read(buffer)) != -1) { + zos.write(buffer, 0, bytesRead); + } + + log.debug("成功下载文件: {}, 大小: {} bytes", url, response.body().contentLength()); + return true; + } catch (IOException e) { + log.error("下载过程中出错: {}, URL: {}", e.getMessage(), url); + return false; + } + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/utils/IpPortUtil.java b/src/main/java/com/genersoft/iot/vmp/utils/IpPortUtil.java new file mode 100644 index 0000000..2ce0a55 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/utils/IpPortUtil.java @@ -0,0 +1,90 @@ +package com.genersoft.iot.vmp.utils; + +import java.net.Inet6Address; +import java.net.InetAddress; +import java.net.UnknownHostException; + +public class IpPortUtil { + + /** + * 拼接IP和端口 + * @param ip IP地址字符串 + * @param port 端口号字符串 + * @return 拼接后的字符串 + * @throws IllegalArgumentException 如果IP地址无效或端口无效 + */ + public static String concatenateIpAndPort(String ip, String port) { + if (port == null || port.isEmpty()) { + throw new IllegalArgumentException("端口号不能为空"); + } + + // 验证端口是否为有效数字 + try { + int portNum = Integer.parseInt(port); + if (portNum < 0 || portNum > 65535) { + throw new IllegalArgumentException("端口号必须在0-65535范围内"); + } + } catch (NumberFormatException e) { + throw new IllegalArgumentException("端口号必须是有效数字", e); + } + + try { + InetAddress inetAddress = InetAddress.getByName(ip); + + if (inetAddress instanceof Inet6Address) { + // IPv6地址需要加上方括号 + return "[" + ip + "]:" + port; + } else { + // IPv4地址直接拼接 + return ip + ":" + port; + } + } catch (UnknownHostException e) { + throw new IllegalArgumentException("无效的IP地址: " + ip, e); + } + } + + // 测试用例 + public static void main(String[] args) { + // IPv4测试 + String ipv4 = "192.168.1.1"; + String port1 = "8080"; + System.out.println(concatenateIpAndPort(ipv4, port1)); // 输出: 192.168.1.1:8080 + + // IPv6测试 + String ipv6 = "2001:0db8:85a3:0000:0000:8a2e:0370:7334"; + String port2 = "80"; + System.out.println(concatenateIpAndPort(ipv6, port2)); // 输出: [2001:0db8:85a3:0000:0000:8a2e:0370:7334]:80 + + // 压缩格式IPv6测试 + String ipv6Compressed = "2001:db8::1"; + System.out.println(concatenateIpAndPort(ipv6Compressed, port2)); // 输出: [2001:db8::1]:80 + + // 无效IP测试 + try { + System.out.println(concatenateIpAndPort("invalid.ip", "1234")); + } catch (IllegalArgumentException e) { + System.out.println("捕获到预期异常: " + e.getMessage()); + } + + // 无效端口测试 - 非数字 + try { + System.out.println(concatenateIpAndPort(ipv4, "abc")); + } catch (IllegalArgumentException e) { + System.out.println("捕获到预期异常: " + e.getMessage()); + } + + // 无效端口测试 - 超出范围 + try { + System.out.println(concatenateIpAndPort(ipv4, "70000")); + } catch (IllegalArgumentException e) { + System.out.println("捕获到预期异常: " + e.getMessage()); + } + + // 空端口测试 + try { + System.out.println(concatenateIpAndPort(ipv4, "")); + } catch (IllegalArgumentException e) { + System.out.println("捕获到预期异常: " + e.getMessage()); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/genersoft/iot/vmp/utils/JsonUtil.java b/src/main/java/com/genersoft/iot/vmp/utils/JsonUtil.java new file mode 100755 index 0000000..b44af10 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/utils/JsonUtil.java @@ -0,0 +1,45 @@ +package com.genersoft.iot.vmp.utils; + +import org.springframework.data.redis.core.RedisTemplate; + +import java.util.Objects; + +/** + * JsonUtil + * + * @author KunLong-Luo + * @version 1.0.0 + * @since 2023/2/2 15:24 + */ +public final class JsonUtil { + + private JsonUtil() { + } + + /** + * safe json type conversion + * + * @param key redis key + * @param clazz cast type + * @param + * @return result type + */ + public static T redisJsonToObject(RedisTemplate redisTemplate, String key, Class clazz) { + Object jsonObject = redisTemplate.opsForValue().get(key); + if (Objects.isNull(jsonObject)) { + return null; + } + return clazz.cast(jsonObject); + } + + public static T redisHashJsonToObject(RedisTemplate redisTemplate, String key, String objKey, Class clazz) { +// if (key == null || objKey == null) { +// return null; +// } + Object jsonObject = redisTemplate.opsForHash().get(key, objKey); + if (Objects.isNull(jsonObject)) { + return null; + } + return clazz.cast(jsonObject); + } +} \ No newline at end of file diff --git a/src/main/java/com/genersoft/iot/vmp/utils/MediaServerUtils.java b/src/main/java/com/genersoft/iot/vmp/utils/MediaServerUtils.java new file mode 100644 index 0000000..bb57547 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/utils/MediaServerUtils.java @@ -0,0 +1,26 @@ +package com.genersoft.iot.vmp.utils; + +import org.springframework.util.ObjectUtils; + +import java.util.HashMap; +import java.util.Map; + +public class MediaServerUtils { + public static Map urlParamToMap(String params) { + HashMap map = new HashMap<>(); + if (ObjectUtils.isEmpty(params)) { + return map; + } + String[] paramsArray = params.split("&"); + if (paramsArray.length == 0) { + return map; + } + for (String param : paramsArray) { + String[] paramArray = param.split("="); + if (paramArray.length == 2) { + map.put(paramArray[0], paramArray[1]); + } + } + return map; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/utils/SSLSocketClientUtil.java b/src/main/java/com/genersoft/iot/vmp/utils/SSLSocketClientUtil.java new file mode 100644 index 0000000..c85b163 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/utils/SSLSocketClientUtil.java @@ -0,0 +1,53 @@ +package com.genersoft.iot.vmp.utils; + +import javax.net.ssl.*; +import java.security.KeyManagementException; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; + +public class SSLSocketClientUtil { + public static SSLSocketFactory getSocketFactory(TrustManager manager) { + SSLSocketFactory socketFactory = null; + try { + SSLContext sslContext = SSLContext.getInstance("SSL"); + sslContext.init(null, new TrustManager[]{manager}, new SecureRandom()); + socketFactory = sslContext.getSocketFactory(); + } catch (NoSuchAlgorithmException e) { + e.printStackTrace(); + } catch (KeyManagementException e) { + e.printStackTrace(); + } + return socketFactory; + } + + public static X509TrustManager getX509TrustManager() { + return new X509TrustManager() { + @Override + public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException { + + } + + @Override + public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException { + + } + + @Override + public X509Certificate[] getAcceptedIssuers() { + return new X509Certificate[0]; + } + }; + } + + public static HostnameVerifier getHostnameVerifier() { + HostnameVerifier hostnameVerifier = new HostnameVerifier() { + @Override + public boolean verify(String s, SSLSession sslSession) { + return true; + } + }; + return hostnameVerifier; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/utils/SpringBeanFactory.java b/src/main/java/com/genersoft/iot/vmp/utils/SpringBeanFactory.java new file mode 100755 index 0000000..1806524 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/utils/SpringBeanFactory.java @@ -0,0 +1,50 @@ +package com.genersoft.iot.vmp.utils; + +import org.springframework.beans.BeansException; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.stereotype.Component; + +/** + * @description:spring bean获取工厂,获取spring中的已初始化的bean + * @author: swwheihei + * @date: 2019年6月25日 下午4:51:52 + * + */ +@Component +public class SpringBeanFactory implements ApplicationContextAware { + + // Spring应用上下文环境 + private static ApplicationContext applicationContext; + + /** + * 实现ApplicationContextAware接口的回调方法,设置上下文环境 + */ + @Override + public void setApplicationContext(ApplicationContext applicationContext) + throws BeansException { + SpringBeanFactory.applicationContext = applicationContext; + } + + public static ApplicationContext getApplicationContext() { + return applicationContext; + } + + /** + * 获取对象 这里重写了bean方法,起主要作用 + */ + public static T getBean(String beanId) throws BeansException { + if (applicationContext == null) { + return null; + } + return (T) applicationContext.getBean(beanId); + } + + /** + * 获取当前环境 + */ + public static String getActiveProfile() { + return applicationContext.getEnvironment().getActiveProfiles()[0]; + } + +} diff --git a/src/main/java/com/genersoft/iot/vmp/utils/SystemInfoUtils.java b/src/main/java/com/genersoft/iot/vmp/utils/SystemInfoUtils.java new file mode 100755 index 0000000..b389d69 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/utils/SystemInfoUtils.java @@ -0,0 +1,162 @@ +package com.genersoft.iot.vmp.utils; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.util.DigestUtils; +import oshi.SystemInfo; +import oshi.hardware.CentralProcessor; +import oshi.hardware.GlobalMemory; +import oshi.hardware.HardwareAbstractionLayer; +import oshi.hardware.NetworkIF; +import oshi.software.os.OperatingSystem; + +import java.io.File; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +/** + * 实现参考自xiaozhangnomoney原创文章, + * 版权声明:本文为xiaozhangnomoney原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明 + * 原文出处链接:https://blog.csdn.net/xiaozhangnomoney/article/details/107769147 + */ +@Slf4j +public class SystemInfoUtils { + + /** + * 获取cpu信息 + * @return + * @throws InterruptedException + */ + public static double getCpuInfo() throws InterruptedException { + SystemInfo systemInfo = new SystemInfo(); + CentralProcessor processor = systemInfo.getHardware().getProcessor(); + long[] prevTicks = processor.getSystemCpuLoadTicks(); + // 睡眠1s + TimeUnit.SECONDS.sleep(1); + long[] ticks = processor.getSystemCpuLoadTicks(); + long nice = ticks[CentralProcessor.TickType.NICE.getIndex()] - prevTicks[CentralProcessor.TickType.NICE.getIndex()]; + long irq = ticks[CentralProcessor.TickType.IRQ.getIndex()] - prevTicks[CentralProcessor.TickType.IRQ.getIndex()]; + long softirq = ticks[CentralProcessor.TickType.SOFTIRQ.getIndex()] - prevTicks[CentralProcessor.TickType.SOFTIRQ.getIndex()]; + long steal = ticks[CentralProcessor.TickType.STEAL.getIndex()] - prevTicks[CentralProcessor.TickType.STEAL.getIndex()]; + long cSys = ticks[CentralProcessor.TickType.SYSTEM.getIndex()] - prevTicks[CentralProcessor.TickType.SYSTEM.getIndex()]; + long user = ticks[CentralProcessor.TickType.USER.getIndex()] - prevTicks[CentralProcessor.TickType.USER.getIndex()]; + long iowait = ticks[CentralProcessor.TickType.IOWAIT.getIndex()] - prevTicks[CentralProcessor.TickType.IOWAIT.getIndex()]; + long idle = ticks[CentralProcessor.TickType.IDLE.getIndex()] - prevTicks[CentralProcessor.TickType.IDLE.getIndex()]; + long totalCpu = user + nice + cSys + idle + iowait + irq + softirq + steal; + return 1.0-(idle * 1.0 / totalCpu); + } + + /** + * 获取内存使用率 + * @return + */ + public static double getMemInfo(){ + SystemInfo systemInfo = new SystemInfo(); + GlobalMemory memory = systemInfo.getHardware().getMemory(); + //总内存 + long totalByte = memory.getTotal(); + //剩余 + long acaliableByte = memory.getAvailable(); + return (totalByte-acaliableByte)*1.0/totalByte; + } + + /** + * 获取网络上传和下载 + * @return + */ + public static Map getNetworkInterfaces() { + SystemInfo si = new SystemInfo(); + HardwareAbstractionLayer hal = si.getHardware(); + List beforeRecvNetworkIFs = hal.getNetworkIFs(); + NetworkIF beforeBet= beforeRecvNetworkIFs.get(beforeRecvNetworkIFs.size() - 1); + long beforeRecv = beforeBet.getBytesRecv(); + long beforeSend = beforeBet.getBytesSent(); + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + log.error("[线程休眠失败] : {}", e.getMessage()); + } + List afterNetworkIFs = hal.getNetworkIFs(); + NetworkIF afterNet = afterNetworkIFs.get(afterNetworkIFs.size() - 1); + + HashMap map = new HashMap<>(); + // 速度单位: Mbps + map.put("in",formatUnits(afterNet.getBytesRecv()-beforeRecv, 1048576L)); + map.put("out",formatUnits(afterNet.getBytesSent()-beforeSend, 1048576L)); + return map; + } + + /** + * 获取带宽总值 + * @return + */ + public static long getNetworkTotal() { + SystemInfo si = new SystemInfo(); + HardwareAbstractionLayer hal = si.getHardware(); + List recvNetworkIFs = hal.getNetworkIFs(); + NetworkIF networkIF= recvNetworkIFs.get(recvNetworkIFs.size() - 1); + + return networkIF.getSpeed()/1048576L/8L; + } + + public static double formatUnits(long value, long prefix) { + return (double)value / (double)prefix; + } + + /** + * 获取进程数 + * @return + */ + public static int getProcessesCount(){ + SystemInfo si = new SystemInfo(); + OperatingSystem os = si.getOperatingSystem(); + + int processCount = os.getProcessCount(); + return processCount; + } + + public static List> getDiskInfo() { + List> result = new ArrayList<>(); + + String osName = System.getProperty("os.name"); + List pathArray = new ArrayList<>(); + if (osName.startsWith("Mac OS")) { + // 苹果 + pathArray.add("/"); + } else if (osName.startsWith("Windows")) { + // windows + pathArray.add("C:"); + } else { + pathArray.add("/"); + pathArray.add("/home"); + } + for (String path : pathArray) { + Map infoMap = new HashMap<>(); + infoMap.put("path", path); + File partitionFile = new File(path); + // 单位: GB + infoMap.put("use", (partitionFile.getTotalSpace() - partitionFile.getFreeSpace())/1024/1024/1024D); + infoMap.put("free", partitionFile.getFreeSpace()/1024/1024/1024D); + result.add(infoMap); + } + return result; + } + + public static String getHardwareId(){ + SystemInfo systemInfo = new SystemInfo(); + HardwareAbstractionLayer hardware = systemInfo.getHardware(); + // CPU ID + String cpuId = hardware.getProcessor().getProcessorIdentifier().getProcessorID(); + // 主板序号 + String serialNumber = hardware.getComputerSystem().getSerialNumber(); + + return DigestUtils.md5DigestAsHex( + ( + DigestUtils.md5DigestAsHex(cpuId.getBytes(StandardCharsets.UTF_8)) + + DigestUtils.md5DigestAsHex(serialNumber.getBytes(StandardCharsets.UTF_8)) + ).getBytes(StandardCharsets.UTF_8)); + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/utils/TileUtils.java b/src/main/java/com/genersoft/iot/vmp/utils/TileUtils.java new file mode 100644 index 0000000..a989ab5 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/utils/TileUtils.java @@ -0,0 +1,160 @@ +package com.genersoft.iot.vmp.utils; + +import com.genersoft.iot.vmp.gb28181.controller.bean.Extent; + +import java.util.ArrayList; +import java.util.List; + +public class TileUtils { + private static final double MAX_LATITUDE = 85.05112878; + + /** + * 根据坐标获取指定层级的x y 值 + * + * @param lon 经度 + * @param lat 纬度 + * @param z 层级 + * @return double[2] = {xTileFloat, yTileFloat} + */ + public static double[] lonLatToTileXY(double lon, double lat, int z) { + double n = Math.pow(2.0, z); + double x = (lon + 180.0) / 360.0 * n; + + // clamp latitude to WebMercator bounds + double latClamped = Math.max(Math.min(lat, MAX_LATITUDE), -MAX_LATITUDE); + double latRad = Math.toRadians(latClamped); + double y = (1.0 - (Math.log(Math.tan(latRad) + 1.0 / Math.cos(latRad)) / Math.PI)) / 2.0 * n; + + return new double[]{x, y}; + } + + /** + * 根据坐标范围获取指定层级的x y 范围 + * + * @param bbox array length 4: {minLon, minLat, maxLon, maxLat} + * @param z zoom level + * @return TileRange object with xMin,xMax,yMin,yMax,z + */ + public static TileRange bboxToTileRange(double[] bbox, int z) { + double minLon = bbox[0], minLat = bbox[1], maxLon = bbox[2], maxLat = bbox[3]; + + // If bbox crosses antimeridian (minLon > maxLon), caller should split into two bboxes. + if (minLon > maxLon) { + throw new IllegalArgumentException("bbox crosses antimeridian; split it before calling bboxToTileRange."); + } + + double[] tMin = lonLatToTileXY(minLon, maxLat, z); // top-left (use maxLat) + double[] tMax = lonLatToTileXY(maxLon, minLat, z); // bottom-right (use minLat) + + int xMin = (int) Math.floor(Math.min(tMin[0], tMax[0])); + int xMax = (int) Math.floor(Math.max(tMin[0], tMax[0])); + int yMin = (int) Math.floor(Math.min(tMin[1], tMax[1])); + int yMax = (int) Math.floor(Math.max(tMin[1], tMax[1])); + + int maxIndex = ((int) Math.pow(2, z)) - 1; + xMin = clamp(xMin, 0, maxIndex); + xMax = clamp(xMax, 0, maxIndex); + yMin = clamp(yMin, 0, maxIndex); + yMax = clamp(yMax, 0, maxIndex); + + return new TileRange(xMin, xMax, yMin, yMax, z); + } + + /** + * If bbox crosses antimeridian (minLon > maxLon), split into two bboxes: + * [minLon, minLat, 180, maxLat] and [-180, minLat, maxLon, maxLat] + * + * @param bbox input bbox array length 4 + * @return list of 1 or 2 bboxes (each double[4]) + */ + public static List splitAntimeridian(double[] bbox) { + double minLon = bbox[0], minLat = bbox[1], maxLon = bbox[2], maxLat = bbox[3]; + List parts = new ArrayList<>(); + if (minLon <= maxLon) { + parts.add(new double[]{minLon, minLat, maxLon, maxLat}); + } else { + parts.add(new double[]{minLon, minLat, 180.0, maxLat}); + parts.add(new double[]{-180.0, minLat, maxLon, maxLat}); + } + return parts; + } + + private static int clamp(int v, int a, int b) { + return Math.max(a, Math.min(b, v)); + } + + /** + * Return list of tile coordinates (x,y,z) covering bbox at zoom z. + * Be careful: the number of tiles can be large for high zooms & big bbox. + * + */ + public static List tilesForBoxAtZoom(Extent extent, int z) { + List tiles = new ArrayList<>(); + List parts = splitAntimeridian(new double[]{extent.getMinLng(), extent.getMinLat(), + extent.getMaxLng(), extent.getMaxLat()}); + for (double[] part : parts) { + TileRange range = bboxToTileRange(part, z); + for (int x = range.xMin; x <= range.xMax; x++) { + for (int y = range.yMin; y <= range.yMax; y++) { + tiles.add(new TileCoord(x, y, z)); + } + } + } + return tiles; + } + + // Simple helper classes + public static class TileRange { + public final int xMin, xMax, yMin, yMax, z; + public TileRange(int xMin, int xMax, int yMin, int yMax, int z) { + this.xMin = xMin; this.xMax = xMax; this.yMin = yMin; this.yMax = yMax; this.z = z; + } + } + + public static class TileCoord { + public final int x, y, z; + public TileCoord(int x, int y, int z) { + this.x = x; + this.y = y; + this.z = z; + + } + @Override + public String toString() { + return "{" + "z=" + z + ", x=" + x + ", y=" + y + '}'; + } + } + + /** + * tile X/Z -> longitude (deg) + */ + public static double tile2lon(int x, int z) { + double n = Math.pow(2.0, z); + return x / n * 360.0 - 180.0; + } + + /** + * tile Y/Z -> latitude (deg) + */ + public static double tile2lat(int y, int z) { + double n = Math.pow(2.0, z); + double latRad = Math.atan(Math.sinh(Math.PI * (1 - 2.0 * y / n))); + return Math.toDegrees(latRad); + } + + /** + * lon/lat -> pixel in tile (0..256) + */ + public static double[] lonLatToTilePixel(double lon, double lat, int z, int tileX, int tileY) { + double n = Math.pow(2.0, z); + double xtile = (lon + 180.0) / 360.0 * n; + + double latRad = Math.toRadians(lat); + double ytile = (1.0 - Math.log(Math.tan(latRad) + 1.0 / Math.cos(latRad)) / Math.PI) / 2.0 * n; + + double pixelX = (xtile - tileX) * 256.0; + double pixelY = (ytile - tileY) * 256.0; + + return new double[] { pixelX, pixelY }; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/utils/UJson.java b/src/main/java/com/genersoft/iot/vmp/utils/UJson.java new file mode 100755 index 0000000..e39147c --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/utils/UJson.java @@ -0,0 +1,149 @@ +package com.genersoft.iot.vmp.utils; + +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; + +import java.util.Iterator; +import java.util.Map; +import java.util.Objects; + +/** + * @author gaofuwang + * @version 1.0 + * @date 2022/3/11 10:17 + */ +@Slf4j +public class UJson { + + public static final ObjectMapper JSON_MAPPER = new ObjectMapper(); + + static { + JSON_MAPPER.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES,false); + } + + private ObjectNode node; + + public UJson(){ + this.node = JSON_MAPPER.createObjectNode(); + } + + public UJson(String json){ + if(StringUtils.isBlank(json)){ + this.node = JSON_MAPPER.createObjectNode(); + }else{ + try { + this.node = JSON_MAPPER.readValue(json, ObjectNode.class); + }catch (Exception e){ + log.error(e.getMessage(), e); + this.node = JSON_MAPPER.createObjectNode(); + } + } + } + + public UJson(ObjectNode node){ + this.node = node; + } + + public String asText(String key){ + JsonNode jsonNode = node.get(key); + if(Objects.isNull(jsonNode)){ + return ""; + } + return jsonNode.asText(); + } + + public String asText(String key, String defaultVal){ + JsonNode jsonNode = node.get(key); + if(Objects.isNull(jsonNode)){ + return ""; + } + return jsonNode.asText(defaultVal); + } + + public UJson put(String key, String value){ + this.node.put(key, value); + return this; + } + + public UJson put(String key, Integer value){ + this.node.put(key, value); + return this; + } + + public static UJson json(){ + return new UJson(); + } + + public static UJson json(String json){ + return new UJson(json); + } + + public static T readJson(String json, Class clazz){ + if(StringUtils.isBlank(json)){ + return null; + } + try { + return JSON_MAPPER.readValue(json, clazz); + }catch (Exception e){ + log.error(e.getMessage(), e); + return null; + } + } + + public static String writeJson(Object object) { + try{ + return JSON_MAPPER.writeValueAsString(object); + }catch (Exception e){ + log.error(e.getMessage(), e); + return ""; + } + } + + @Override + public String toString() { + return node.toString(); + } + + public int asInt(String key, int defValue) { + JsonNode jsonNode = this.node.get(key); + if(Objects.isNull(jsonNode)){ + return defValue; + } + return jsonNode.asInt(defValue); + } + + public UJson getSon(String key) { + JsonNode sonNode = this.node.get(key); + if(Objects.isNull(sonNode)){ + return new UJson(); + } + return new UJson((ObjectNode) sonNode); + } + + public UJson set(String key, ObjectNode sonNode) { + this.node.set(key, sonNode); + return this; + } + + public UJson set(String key, UJson sonNode) { + this.node.set(key, sonNode.node); + return this; + } + + public Iterator> fields() { + return this.node.fields(); + } + + public ObjectNode getNode() { + return this.node; + } + + public UJson setAll(UJson json) { + this.node.setAll(json.node); + return this; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/utils/redis/FastJsonRedisSerializer.java b/src/main/java/com/genersoft/iot/vmp/utils/redis/FastJsonRedisSerializer.java new file mode 100755 index 0000000..86b7dce --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/utils/redis/FastJsonRedisSerializer.java @@ -0,0 +1,45 @@ +package com.genersoft.iot.vmp.utils.redis; + +import com.alibaba.fastjson2.JSON; +import com.alibaba.fastjson2.JSONReader; +import com.alibaba.fastjson2.JSONWriter; +import org.springframework.data.redis.serializer.RedisSerializer; +import org.springframework.data.redis.serializer.SerializationException; + +import java.nio.charset.Charset; + +/** + * @description:使用fastjson实现redis的序列化 + * @author: swwheihei + * @date: 2020年5月6日 下午8:40:11 + */ +public class FastJsonRedisSerializer implements RedisSerializer { + + public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8"); + + private Class clazz; + + public FastJsonRedisSerializer(Class clazz) { + super(); + this.clazz = clazz; + } + + @Override + public byte[] serialize(T t) throws SerializationException { + if (t == null) { + return new byte[0]; + } + return JSON.toJSONString(t, JSONWriter.Feature.WriteClassName, JSONWriter.Feature.WritePairAsJavaBean).getBytes(DEFAULT_CHARSET); + } + + @Override + public T deserialize(byte[] bytes) throws SerializationException { + if (bytes == null || bytes.length <= 0) { + return null; + } + String str = new String(bytes, DEFAULT_CHARSET); + return JSON.parseObject(str, clazz, JSONReader.Feature.SupportAutoType); + } + + +} diff --git a/src/main/java/com/genersoft/iot/vmp/utils/redis/RedisUtil.java b/src/main/java/com/genersoft/iot/vmp/utils/redis/RedisUtil.java new file mode 100755 index 0000000..101a3b3 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/utils/redis/RedisUtil.java @@ -0,0 +1,47 @@ +package com.genersoft.iot.vmp.utils.redis; + +import com.google.common.collect.Lists; +import org.springframework.data.redis.core.Cursor; +import org.springframework.data.redis.core.RedisCallback; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.core.ScanOptions; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** + * Redis工具类 + * + * @author swwheihei + * @date 2020年5月6日 下午8:27:29 + */ +@SuppressWarnings(value = {"rawtypes", "unchecked"}) +public class RedisUtil { + + /** + * 模糊查询 + * + * @param query 查询参数 + * @return + */ + public static List scan(RedisTemplate redisTemplate, String query) { + + Set resultKeys = (Set) redisTemplate.execute((RedisCallback>) connection -> { + ScanOptions scanOptions = ScanOptions.scanOptions().match("*" + query + "*").count(1000).build(); + Cursor scan = connection.scan(scanOptions); + Set keys = new HashSet<>(); + while (scan.hasNext()) { + byte[] next = scan.next(); + keys.add(new String(next)); + } + return keys; + }); + + return Lists.newArrayList(resultKeys); + } +} + + + diff --git a/src/main/java/com/genersoft/iot/vmp/utils/redis/RedisUtil2.java b/src/main/java/com/genersoft/iot/vmp/utils/redis/RedisUtil2.java new file mode 100755 index 0000000..18c1fd5 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/utils/redis/RedisUtil2.java @@ -0,0 +1,899 @@ +//package com.genersoft.iot.vmp.utils.redis; +// +//import com.alibaba.fastjson2.JSONObject; +//import com.genersoft.iot.vmp.utils.SpringBeanFactory; +//import org.springframework.data.redis.core.*; +//import org.springframework.util.CollectionUtils; +// +//import java.util.*; +//import java.util.concurrent.TimeUnit; +// +///** +// * Redis工具类 +// * @author swwheihei +// * @date 2020年5月6日 下午8:27:29 +// */ +//@SuppressWarnings(value = {"rawtypes", "unchecked"}) +//public class RedisUtil2 { +// +// private static RedisTemplate redisTemplate; +// +// static { +// redisTemplate = SpringBeanFactory.getBean("redisTemplate"); +// } +// +// /** +// * 指定缓存失效时间 +// * @param key 键 +// * @param time 时间(秒) +// * @return true / false +// */ +// public static boolean expire(String key, long time) { +// if (redisTemplate == null) { +// redisTemplate = SpringBeanFactory.getBean("redisTemplate"); +// } +// try { +// if (time > 0) { +// redisTemplate.expire(key, time, TimeUnit.SECONDS); +// } +// return true; +// } catch (Exception e) { +// e.printStackTrace(); +// return false; +// } +// } +// +// /** +// * 根据 key 获取过期时间 +// * @param key 键 +// */ +// public static long getExpire(String key) { +// if (redisTemplate == null) { +// redisTemplate = SpringBeanFactory.getBean("redisTemplate"); +// } +// return redisTemplate.getExpire(key, TimeUnit.SECONDS); +// } +// +// /** +// * 判断 key 是否存在 +// * @param key 键 +// * @return true / false +// */ +// public static boolean hasKey(String key) { +// if (redisTemplate == null) { +// redisTemplate = SpringBeanFactory.getBean("redisTemplate"); +// } +// try { +// return redisTemplate.hasKey(key); +// } catch (Exception e) { +// e.printStackTrace(); +// return false; +// } +// } +// +// /** +// * 删除缓存 +// * @SuppressWarnings("unchecked") 忽略类型转换警告 +// * @param key 键(一个或者多个) +// */ +// public static boolean del(String... key) { +// if (redisTemplate == null) { +// redisTemplate = SpringBeanFactory.getBean("redisTemplate"); +// } +// try { +// if (key != null && key.length > 0) { +// if (key.length == 1) { +// redisTemplate.delete(key[0]); +// } else { +//// 传入一个 Collection 集合 +// redisTemplate.delete(CollectionUtils.arrayToList(key)); +// } +// } +// return true; +// } catch (Exception e) { +// e.printStackTrace(); +// return false; +// } +// } +// +//// ============================== String ============================== +// +// /** +// * 普通缓存获取 +// * @param key 键 +// * @return 值 +// */ +// public static Object get(String key) { +// if (redisTemplate == null) { +// redisTemplate = SpringBeanFactory.getBean("redisTemplate"); +// } +// return key == null ? null : redisTemplate.opsForValue().get(key); +// } +// +// /** +// * 普通缓存放入 +// * @param key 键 +// * @param value 值 +// * @return true / false +// */ +// public static boolean set(String key, Object value) { +// if (redisTemplate == null) { +// redisTemplate = SpringBeanFactory.getBean("redisTemplate"); +// } +// try { +// redisTemplate.opsForValue().set(key, value); +// return true; +// } catch (Exception e) { +// e.printStackTrace(); +// return false; +// } +// } +// +// /** +// * 普通缓存放入并设置时间 +// * @param key 键 +// * @param value 值 +// * @param time 时间(秒),如果 time < 0 则设置无限时间 +// * @return true / false +// */ +// public static boolean set(String key, Object value, long time) { +// if (redisTemplate == null) { +// redisTemplate = SpringBeanFactory.getBean("redisTemplate"); +// } +// try { +// if (time > 0) { +// redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS); +// } else { +// set(key, value); +// } +// return true; +// } catch (Exception e) { +// e.printStackTrace(); +// return false; +// } +// } +// +// /** +// * 递增 +// * @param key 键 +// * @param delta 递增大小 +// * @return +// */ +// public static long incr(String key, long delta) { +// if (redisTemplate == null) { +// redisTemplate = SpringBeanFactory.getBean("redisTemplate"); +// } +// if (delta < 0) { +// throw new RuntimeException("递增因子必须大于 0"); +// } +// return redisTemplate.opsForValue().increment(key, delta); +// } +// +// /** +// * 递减 +// * @param key 键 +// * @param delta 递减大小 +// * @return +// */ +// public static long decr(String key, long delta) { +// if (redisTemplate == null) { +// redisTemplate = SpringBeanFactory.getBean("redisTemplate"); +// } +// if (delta < 0) { +// throw new RuntimeException("递减因子必须大于 0"); +// } +// return redisTemplate.opsForValue().increment(key, delta); +// } +// +//// ============================== Map ============================== +// +// /** +// * HashGet +// * @param key 键(no null) +// * @param item 项(no null) +// * @return 值 +// */ +// public static Object hget(String key, String item) { +// if (redisTemplate == null) { +// redisTemplate = SpringBeanFactory.getBean("redisTemplate"); +// } +// return redisTemplate.opsForHash().get(key, item); +// } +// +// /** +// * 获取 key 对应的 map +// * @param key 键(no null) +// * @return 对应的多个键值 +// */ +// public static Map hmget(String key) { +// if (redisTemplate == null) { +// redisTemplate = SpringBeanFactory.getBean("redisTemplate"); +// } +// return redisTemplate.opsForHash().entries(key); +// } +// +// /** +// * HashSet +// * @param key 键 +// * @param map 值 +// * @return true / false +// */ +// public static boolean hmset(String key, Map map) { +// if (redisTemplate == null) { +// redisTemplate = SpringBeanFactory.getBean("redisTemplate"); +// } +// try { +// redisTemplate.opsForHash().putAll(key, map); +// return true; +// } catch (Exception e) { +// e.printStackTrace(); +// return false; +// } +// } +// +// /** +// * HashSet 并设置时间 +// * @param key 键 +// * @param map 值 +// * @param time 时间 +// * @return true / false +// */ +// public static boolean hmset(String key, Map map, long time) { +// if (redisTemplate == null) { +// redisTemplate = SpringBeanFactory.getBean("redisTemplate"); +// } +// try { +// redisTemplate.opsForHash().putAll(key, map); +// if (time > 0) { +// expire(key, time); +// } +// return true; +// } catch (Exception e) { +// e.printStackTrace(); +// return false; +// } +// } +// +// /** +// * 向一张 Hash表 中放入数据,如不存在则创建 +// * @param key 键 +// * @param item 项 +// * @param value 值 +// * @return true / false +// */ +// public static boolean hset(String key, String item, Object value) { +// if (redisTemplate == null) { +// redisTemplate = SpringBeanFactory.getBean("redisTemplate"); +// } +// try { +// redisTemplate.opsForHash().put(key, item, value); +// return true; +// } catch (Exception e) { +// e.printStackTrace(); +// return false; +// } +// } +// +// /** +// * 向一张 Hash表 中放入数据,并设置时间,如不存在则创建 +// * @param key 键 +// * @param item 项 +// * @param value 值 +// * @param time 时间(如果原来的 Hash表 设置了时间,这里会覆盖) +// * @return true / false +// */ +// public static boolean hset(String key, String item, Object value, long time) { +// if (redisTemplate == null) { +// redisTemplate = SpringBeanFactory.getBean("redisTemplate"); +// } +// try { +// redisTemplate.opsForHash().put(key, item, value); +// if (time > 0) { +// expire(key, time); +// } +// return true; +// } catch (Exception e) { +// e.printStackTrace(); +// return false; +// } +// } +// +// /** +// * 删除 Hash表 中的值 +// * @param key 键 +// * @param item 项(可以多个,no null) +// */ +// public static void hdel(String key, Object... item) { +// if (redisTemplate == null) { +// redisTemplate = SpringBeanFactory.getBean("redisTemplate"); +// } +// redisTemplate.opsForHash().delete(key, item); +// } +// +// /** +// * 判断 Hash表 中是否有该键的值 +// * @param key 键(no null) +// * @param item 值(no null) +// * @return true / false +// */ +// public static boolean hHasKey(String key, String item) { +// if (redisTemplate == null) { +// redisTemplate = SpringBeanFactory.getBean("redisTemplate"); +// } +// return redisTemplate.opsForHash().hasKey(key, item); +// } +// +// /** +// * Hash递增,如果不存在则创建一个,并把新增的值返回 +// * @param key 键 +// * @param item 项 +// * @param by 递增大小 > 0 +// * @return +// */ +// public static Double hincr(String key, String item, Double by) { +// if (redisTemplate == null) { +// redisTemplate = SpringBeanFactory.getBean("redisTemplate"); +// } +// return redisTemplate.opsForHash().increment(key, item, by); +// } +// +// /** +// * Hash递减 +// * @param key 键 +// * @param item 项 +// * @param by 递减大小 +// * @return +// */ +// public static Double hdecr(String key, String item, Double by) { +// if (redisTemplate == null) { +// redisTemplate = SpringBeanFactory.getBean("redisTemplate"); +// } +// return redisTemplate.opsForHash().increment(key, item, -by); +// } +// +//// ============================== Set ============================== +// +// /** +// * 根据 key 获取 set 中的所有值 +// * @param key 键 +// * @return 值 +// */ +// public static Set sGet(String key) { +// if (redisTemplate == null) { +// redisTemplate = SpringBeanFactory.getBean("redisTemplate"); +// } +// try { +// return redisTemplate.opsForSet().members(key); +// } catch (Exception e) { +// e.printStackTrace(); +// return null; +// } +// } +// +// /** +// * 从键为 key 的 set 中,根据 value 查询是否存在 +// * @param key 键 +// * @param value 值 +// * @return true / false +// */ +// public static boolean sHasKey(String key, Object value) { +// if (redisTemplate == null) { +// redisTemplate = SpringBeanFactory.getBean("redisTemplate"); +// } +// try { +// return redisTemplate.opsForSet().isMember(key, value); +// } catch (Exception e) { +// e.printStackTrace(); +// return false; +// } +// } +// +// /** +// * 将数据放入 set缓存 +// * @param key 键值 +// * @param values 值(可以多个) +// * @return 成功个数 +// */ +// public static long sSet(String key, Object... values) { +// if (redisTemplate == null) { +// redisTemplate = SpringBeanFactory.getBean("redisTemplate"); +// } +// try { +// return redisTemplate.opsForSet().add(key, values); +// } catch (Exception e) { +// e.printStackTrace(); +// return 0; +// } +// } +// +// /** +// * 将数据放入 set缓存,并设置时间 +// * @param key 键 +// * @param time 时间 +// * @param values 值(可以多个) +// * @return 成功放入个数 +// */ +// public static long sSet(String key, long time, Object... values) { +// if (redisTemplate == null) { +// redisTemplate = SpringBeanFactory.getBean("redisTemplate"); +// } +// try { +// long count = redisTemplate.opsForSet().add(key, values); +// if (time > 0) { +// expire(key, time); +// } +// return count; +// } catch (Exception e) { +// e.printStackTrace(); +// return 0; +// } +// } +// +// /** +// * 获取 set缓存的长度 +// * @param key 键 +// * @return 长度 +// */ +// public static long sGetSetSize(String key) { +// if (redisTemplate == null) { +// redisTemplate = SpringBeanFactory.getBean("redisTemplate"); +// } +// try { +// return redisTemplate.opsForSet().size(key); +// } catch (Exception e) { +// e.printStackTrace(); +// return 0; +// } +// } +// +// /** +// * 移除 set缓存中,值为 value 的 +// * @param key 键 +// * @param values 值 +// * @return 成功移除个数 +// */ +// public static long setRemove(String key, Object... values) { +// if (redisTemplate == null) { +// redisTemplate = SpringBeanFactory.getBean("redisTemplate"); +// } +// try { +// return redisTemplate.opsForSet().remove(key, values); +// } catch (Exception e) { +// e.printStackTrace(); +// return 0; +// } +// } +//// ============================== ZSet ============================== +// +// /** +// * 添加一个元素, zset与set最大的区别就是每个元素都有一个score,因此有个排序的辅助功能; zadd +// * +// * @param key +// * @param value +// * @param score +// */ +// public static void zAdd(Object key, Object value, double score) { +// if (redisTemplate == null) { +// redisTemplate = SpringBeanFactory.getBean("redisTemplate"); +// } +// redisTemplate.opsForZSet().add(key, value, score); +// } +// +// /** +// * 删除元素 zrem +// * +// * @param key +// * @param value +// */ +// public static void zRemove(Object key, Object value) { +// if (redisTemplate == null) { +// redisTemplate = SpringBeanFactory.getBean("redisTemplate"); +// } +// redisTemplate.opsForZSet().remove(key, value); +// } +// +// /** +// * score的增加or减少 zincrby +// * +// * @param key +// * @param value +// * @param delta -1 表示减 1 表示加1 +// */ +// public static Double zIncrScore(Object key, Object value, double delta) { +// if (redisTemplate == null) { +// redisTemplate = SpringBeanFactory.getBean("redisTemplate"); +// } +// return redisTemplate.opsForZSet().incrementScore(key, value, delta); +// } +// +// /** +// * 查询value对应的score zscore +// * +// * @param key +// * @param value +// * @return +// */ +// public static Double zScore(Object key, Object value) { +// if (redisTemplate == null) { +// redisTemplate = SpringBeanFactory.getBean("redisTemplate"); +// } +// return redisTemplate.opsForZSet().score(key, value); +// } +// +// /** +// * 判断value在zset中的排名 zrank +// * +// * @param key +// * @param value +// * @return +// */ +// public static Long zRank(Object key, Object value) { +// if (redisTemplate == null) { +// redisTemplate = SpringBeanFactory.getBean("redisTemplate"); +// } +// return redisTemplate.opsForZSet().rank(key, value); +// } +// +// /** +// * 返回集合的长度 +// * +// * @param key +// * @return +// */ +// public static Long zSize(Object key) { +// if (redisTemplate == null) { +// redisTemplate = SpringBeanFactory.getBean("redisTemplate"); +// } +// return redisTemplate.opsForZSet().zCard(key); +// } +// +// /** +// * 查询集合中指定顺序的值, 0 -1 表示获取全部的集合内容 zrange +// * +// * 返回有序的集合,score小的在前面 +// * +// * @param key +// * @param start +// * @param end +// * @return +// */ +// public static Set zRange(Object key, int start, int end) { +// if (redisTemplate == null) { +// redisTemplate = SpringBeanFactory.getBean("redisTemplate"); +// } +// return redisTemplate.opsForZSet().range(key, start, end); +// } +// /** +// * 查询集合中指定顺序的值和score,0, -1 表示获取全部的集合内容 +// * +// * @param key +// * @param start +// * @param end +// * @return +// */ +// public static Set> zRangeWithScore(Object key, int start, int end) { +// if (redisTemplate == null) { +// redisTemplate = SpringBeanFactory.getBean("redisTemplate"); +// } +// return redisTemplate.opsForZSet().rangeWithScores(key, start, end); +// } +// /** +// * 查询集合中指定顺序的值 zrevrange +// * +// * 返回有序的集合中,score大的在前面 +// * +// * @param key +// * @param start +// * @param end +// * @return +// */ +// public static Set zRevRange(Object key, int start, int end) { +// if (redisTemplate == null) { +// redisTemplate = SpringBeanFactory.getBean("redisTemplate"); +// } +// return redisTemplate.opsForZSet().reverseRange(key, start, end); +// } +// /** +// * 根据score的值,来获取满足条件的集合 zrangebyscore +// * +// * @param key +// * @param min +// * @param max +// * @return +// */ +// public static Set zSortRange(Object key, int min, int max) { +// if (redisTemplate == null) { +// redisTemplate = SpringBeanFactory.getBean("redisTemplate"); +// } +// return redisTemplate.opsForZSet().rangeByScore(key, min, max); +// } +// +// +//// ============================== List ============================== +// +// /** +// * 获取 list缓存的内容 +// * @param key 键 +// * @param start 开始 +// * @param end 结束(0 到 -1 代表所有值) +// * @return +// */ +// public static List lGet(String key, long start, long end) { +// if (redisTemplate == null) { +// redisTemplate = SpringBeanFactory.getBean("redisTemplate"); +// } +// try { +// return redisTemplate.opsForList().range(key, start, end); +// } catch (Exception e) { +// e.printStackTrace(); +// return null; +// } +// } +// +// /** +// * 获取 list缓存的长度 +// * @param key 键 +// * @return 长度 +// */ +// public static long lGetListSize(String key) { +// if (redisTemplate == null) { +// redisTemplate = SpringBeanFactory.getBean("redisTemplate"); +// } +// try { +// return redisTemplate.opsForList().size(key); +// } catch (Exception e) { +// e.printStackTrace(); +// return 0; +// } +// } +// +// /** +// * 根据索引 index 获取键为 key 的 list 中的元素 +// * @param key 键 +// * @param index 索引 +// * 当 index >= 0 时 {0:表头, 1:第二个元素} +// * 当 index < 0 时 {-1:表尾, -2:倒数第二个元素} +// * @return 值 +// */ +// public static Object lGetIndex(String key, long index) { +// if (redisTemplate == null) { +// redisTemplate = SpringBeanFactory.getBean("redisTemplate"); +// } +// try { +// return redisTemplate.opsForList().index(key, index); +// } catch (Exception e) { +// e.printStackTrace(); +// return null; +// } +// } +// +// /** +// * 将值 value 插入键为 key 的 list 中,如果 list 不存在则创建空 list +// * @param key 键 +// * @param value 值 +// * @return true / false +// */ +// public static boolean lSet(String key, Object value) { +// if (redisTemplate == null) { +// redisTemplate = SpringBeanFactory.getBean("redisTemplate"); +// } +// try { +// redisTemplate.opsForList().rightPush(key, value); +// return true; +// } catch (Exception e) { +// e.printStackTrace(); +// return false; +// } +// } +// +// /** +// * 将值 value 插入键为 key 的 list 中,并设置时间 +// * @param key 键 +// * @param value 值 +// * @param time 时间 +// * @return true / false +// */ +// public static boolean lSet(String key, Object value, long time) { +// if (redisTemplate == null) { +// redisTemplate = SpringBeanFactory.getBean("redisTemplate"); +// } +// try { +// redisTemplate.opsForList().rightPush(key, value); +// if (time > 0) { +// expire(key, time); +// } +// return true; +// } catch (Exception e) { +// e.printStackTrace(); +// return false; +// } +// } +// +// /** +// * 将 values 插入键为 key 的 list 中 +// * @param key 键 +// * @param values 值 +// * @return true / false +// */ +// public static boolean lSetList(String key, List values) { +// if (redisTemplate == null) { +// redisTemplate = SpringBeanFactory.getBean("redisTemplate"); +// } +// try { +// redisTemplate.opsForList().rightPushAll(key, values); +// return true; +// } catch (Exception e) { +// e.printStackTrace(); +// return false; +// } +// } +// +// /** +// * 将 values 插入键为 key 的 list 中,并设置时间 +// * @param key 键 +// * @param values 值 +// * @param time 时间 +// * @return true / false +// */ +// public static boolean lSetList(String key, List values, long time) { +// if (redisTemplate == null) { +// redisTemplate = SpringBeanFactory.getBean("redisTemplate"); +// } +// try { +// redisTemplate.opsForList().rightPushAll(key, values); +// if (time > 0) { +// expire(key, time); +// } +// return true; +// } catch (Exception e) { +// e.printStackTrace(); +// return false; +// } +// } +// +// /** +// * 根据索引 index 修改键为 key 的值 +// * @param key 键 +// * @param index 索引 +// * @param value 值 +// * @return true / false +// */ +// public static boolean lUpdateIndex(String key, long index, Object value) { +// if (redisTemplate == null) { +// redisTemplate = SpringBeanFactory.getBean("redisTemplate"); +// } +// try { +// redisTemplate.opsForList().set(key, index, value); +// return true; +// } catch (Exception e) { +// e.printStackTrace(); +// return false; +// } +// } +// +// /** +// * 在键为 key 的 list 中删除值为 value 的元素 +// * @param key 键 +// * @param count 如果 count == 0 则删除 list 中所有值为 value 的元素 +// * 如果 count > 0 则删除 list 中最左边那个值为 value 的元素 +// * 如果 count < 0 则删除 list 中最右边那个值为 value 的元素 +// * @param value +// * @return +// */ +// public static long lRemove(String key, long count, Object value) { +// if (redisTemplate == null) { +// redisTemplate = SpringBeanFactory.getBean("redisTemplate"); +// } +// try { +// return redisTemplate.opsForList().remove(key, count, value); +// } catch (Exception e) { +// e.printStackTrace(); +// return 0; +// } +// } +// +// /** +// * 在键为 key 的 list中移除第一个元素 +// * @param key 键 +// * @return +// */ +// public static Object lLeftPop(String key) { +// if (redisTemplate == null) { +// redisTemplate = SpringBeanFactory.getBean("redisTemplate"); +// } +// return redisTemplate.opsForList().leftPop(key); +// } +// +// /** +// * 在键为 key 的 list中移除、最后一个元素 +// * @param key 键 +// * @return +// */ +// public static Object lrightPop(String key) { +// if (redisTemplate == null) { +// redisTemplate = SpringBeanFactory.getBean("redisTemplate"); +// } +// return redisTemplate.opsForList().rightPop(key); +// } +// +// /** +// * 模糊查询 +// * @param key 键 +// * @return true / false +// */ +// public static List keys(String key) { +// if (redisTemplate == null) { +// redisTemplate = SpringBeanFactory.getBean("redisTemplate"); +// } +// try { +// Set set = redisTemplate.keys(key); +// return new ArrayList<>(set); +// } catch (Exception e) { +// e.printStackTrace(); +// return null; +// } +// } +// +// +// /** +// * 模糊查询 +// * @param query 查询参数 +// * @return +// */ +//// public static List scan(String query) { +//// List result = new ArrayList<>(); +//// try { +//// Cursor> cursor = redisTemplate.opsForHash().scan("field", +//// ScanOptions.scanOptions().match(query).count(1000).build()); +//// while (cursor.hasNext()) { +//// Map.Entry entry = cursor.next(); +//// result.add(entry.getKey()); +//// Object key = entry.getKey(); +//// Object valueSet = entry.getValue(); +//// } +//// //关闭cursor +//// cursor.close(); +//// } catch (Exception e) { +//// e.printStackTrace(); +//// } +//// return result; +//// } +// +// /** +// * 模糊查询 +// * @param query 查询参数 +// * @return +// */ +// public static List scan(String query) { +// if (redisTemplate == null) { +// redisTemplate = SpringBeanFactory.getBean("redisTemplate"); +// } +// Set resultKeys = (Set) redisTemplate.execute((RedisCallback>) connection -> { +// ScanOptions scanOptions = ScanOptions.scanOptions().match("*" + query + "*").count(1000).build(); +// Cursor scan = connection.scan(scanOptions); +// Set keys = new HashSet<>(); +// while (scan.hasNext()) { +// byte[] next = scan.next(); +// keys.add(new String(next)); +// } +// return keys; +// }); +// +// return new ArrayList<>(resultKeys); +// } +// public static List scan2(String query) { +// if (redisTemplate == null) { +// redisTemplate = SpringBeanFactory.getBean("redisTemplate"); +// } +// Set keys = redisTemplate.keys(query); +// return new ArrayList<>(keys); +// } +// // ============================== 消息发送与订阅 ============================== +// public static void convertAndSend(String channel, JSONObject msg) { +// if (redisTemplate == null) { +// redisTemplate = SpringBeanFactory.getBean("redisTemplate"); +// } +// redisTemplate.convertAndSend(channel, msg); +// } +// +//} diff --git a/src/main/java/com/genersoft/iot/vmp/vmanager/TestController.java b/src/main/java/com/genersoft/iot/vmp/vmanager/TestController.java new file mode 100644 index 0000000..6df13ba --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/vmanager/TestController.java @@ -0,0 +1,74 @@ +package com.genersoft.iot.vmp.vmanager; + +import com.genersoft.iot.vmp.common.InviteInfo; +import com.genersoft.iot.vmp.common.InviteSessionType; +import com.genersoft.iot.vmp.common.VideoManagerConstants; +import com.genersoft.iot.vmp.media.event.hook.Hook; +import com.genersoft.iot.vmp.media.event.hook.HookSubscribe; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.Cursor; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.core.ScanOptions; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +@RestController +@RequestMapping("/api/test") +public class TestController { + + @Autowired + private HookSubscribe subscribe; + + @Autowired + private RedisTemplate redisTemplate; + + @GetMapping("/hook/list") + public List all(){ + return subscribe.getAll(); + } + + + @GetMapping("/redis") + public List redis(){ + InviteSessionType type = InviteSessionType.PLAY; + String channelId = null; + String stream = null; + + String key = VideoManagerConstants.INVITE_PREFIX; + String keyPattern = (type != null ? type : "*") + + ":" + (channelId != null ? channelId : "*") + + ":" + (stream != null ? stream : "*") + + ":*"; + ScanOptions options = ScanOptions.scanOptions().match(keyPattern).count(20).build(); + Cursor> cursor = redisTemplate.opsForHash().scan(key, options); + List result = new ArrayList<>(); + try { + while (cursor.hasNext()) { + System.out.println(cursor.next().getKey()); + result.add((InviteInfo) cursor.next().getValue()); + } + }catch (Exception e) { + + }finally { + cursor.close(); + } + return result; + } + +// @Bean +// public ServletRegistrationBean druidStatViewServlet() { +// ServletRegistrationBean registrationBean = new ServletRegistrationBean<>(new StatViewServlet(), "/druid/*"); +// registrationBean.addInitParameter("allow", "127.0.0.1");// IP白名单 (没有配置或者为空,则允许所有访问) +// registrationBean.addInitParameter("deny", "");// IP黑名单 (存在共同时,deny优先于allow) +// registrationBean.addInitParameter("loginUsername", "admin"); +// registrationBean.addInitParameter("loginPassword", "admin"); +// registrationBean.addInitParameter("resetEnable", "false"); +// return registrationBean; +// } + +} diff --git a/src/main/java/com/genersoft/iot/vmp/vmanager/bean/AudioBroadcastResult.java b/src/main/java/com/genersoft/iot/vmp/vmanager/bean/AudioBroadcastResult.java new file mode 100644 index 0000000..1126081 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/vmanager/bean/AudioBroadcastResult.java @@ -0,0 +1,59 @@ +package com.genersoft.iot.vmp.vmanager.bean; + +/** + * @author lin + */ +public class AudioBroadcastResult { + /** + * 推流的各个方式流地址 + */ + private StreamContent streamInfo; + + /** + * 编码格式 + */ + private String codec; + + /** + * 向zlm推流的应用名 + */ + private String app; + + /** + * 向zlm推流的流ID + */ + private String stream; + + + public StreamContent getStreamInfo() { + return streamInfo; + } + + public void setStreamInfo(StreamContent streamInfo) { + this.streamInfo = streamInfo; + } + + public String getCodec() { + return codec; + } + + public void setCodec(String codec) { + this.codec = codec; + } + + public String getApp() { + return app; + } + + public void setApp(String app) { + this.app = app; + } + + public String getStream() { + return stream; + } + + public void setStream(String stream) { + this.stream = stream; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/vmanager/bean/BatchGBStreamParam.java b/src/main/java/com/genersoft/iot/vmp/vmanager/bean/BatchGBStreamParam.java new file mode 100755 index 0000000..0090488 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/vmanager/bean/BatchGBStreamParam.java @@ -0,0 +1,23 @@ +package com.genersoft.iot.vmp.vmanager.bean; + +import com.genersoft.iot.vmp.gb28181.bean.GbStream; +import io.swagger.v3.oas.annotations.media.Schema; + +import java.util.List; + +/** + * @author lin + */ +@Schema(description = "多个推流信息") +public class BatchGBStreamParam { + @Schema(description = "推流信息列表") + private List gbStreams; + + public List getGbStreams() { + return gbStreams; + } + + public void setGbStreams(List gbStreams) { + this.gbStreams = gbStreams; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/vmanager/bean/DeferredResultEx.java b/src/main/java/com/genersoft/iot/vmp/vmanager/bean/DeferredResultEx.java new file mode 100755 index 0000000..0b9d3d9 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/vmanager/bean/DeferredResultEx.java @@ -0,0 +1,31 @@ +package com.genersoft.iot.vmp.vmanager.bean; + +import org.springframework.web.context.request.async.DeferredResult; + +public class DeferredResultEx { + + private DeferredResult deferredResult; + + private DeferredResultFilter filter; + + public DeferredResultEx(DeferredResult result) { + this.deferredResult = result; + } + + + public DeferredResult getDeferredResult() { + return deferredResult; + } + + public void setDeferredResult(DeferredResult deferredResult) { + this.deferredResult = deferredResult; + } + + public DeferredResultFilter getFilter() { + return filter; + } + + public void setFilter(DeferredResultFilter filter) { + this.filter = filter; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/vmanager/bean/DeferredResultFilter.java b/src/main/java/com/genersoft/iot/vmp/vmanager/bean/DeferredResultFilter.java new file mode 100755 index 0000000..18c2240 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/vmanager/bean/DeferredResultFilter.java @@ -0,0 +1,6 @@ +package com.genersoft.iot.vmp.vmanager.bean; + +public interface DeferredResultFilter { + + Object handler(Object o); +} diff --git a/src/main/java/com/genersoft/iot/vmp/vmanager/bean/ErrorCode.java b/src/main/java/com/genersoft/iot/vmp/vmanager/bean/ErrorCode.java new file mode 100755 index 0000000..c0a0dc3 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/vmanager/bean/ErrorCode.java @@ -0,0 +1,32 @@ +package com.genersoft.iot.vmp.vmanager.bean; + +/** + * 全局错误码 + */ +public enum ErrorCode { + SUCCESS(0, "成功"), + ERROR100(100, "失败"), + ERROR400(400, "参数或方法错误"), + ERROR404(404, "资源未找到"), + ERROR403(403, "无权限操作"), + ERROR486(486, "超时或无响应"), + ERROR401(401, "请登录后重新请求"), + ERROR408(408, "请求超时"), + ERROR500(500, "系统异常"); + + private final int code; + private final String msg; + + ErrorCode(int code, String msg) { + this.code = code; + this.msg = msg; + } + + public int getCode() { + return code; + } + + public String getMsg() { + return msg; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/vmanager/bean/MapConfig.java b/src/main/java/com/genersoft/iot/vmp/vmanager/bean/MapConfig.java new file mode 100644 index 0000000..a9d9068 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/vmanager/bean/MapConfig.java @@ -0,0 +1,16 @@ +package com.genersoft.iot.vmp.vmanager.bean; + +import lombok.Data; + +@Data +public class MapConfig { + + private String name; + private String coordinateSystem; + private String tilesUrl; + private Integer tileSize; + private Integer zoom; + private Double[] center; + private Integer maxZoom; + private Integer minZoom; +} diff --git a/src/main/java/com/genersoft/iot/vmp/vmanager/bean/MapModelIcon.java b/src/main/java/com/genersoft/iot/vmp/vmanager/bean/MapModelIcon.java new file mode 100644 index 0000000..20838f5 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/vmanager/bean/MapModelIcon.java @@ -0,0 +1,26 @@ +package com.genersoft.iot.vmp.vmanager.bean; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Data +public class MapModelIcon { + + @Schema(description = "名称") + private String name; + + @Schema(description = "别名") + private String alias; + + @Schema(description = "路径") + private String path; + + + public static MapModelIcon getInstance(String name, String alias, String path) { + MapModelIcon mapModelIcon = new MapModelIcon(); + mapModelIcon.setAlias(alias); + mapModelIcon.setName(name); + mapModelIcon.setPath(path); + return mapModelIcon; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/vmanager/bean/OtherPsSendInfo.java b/src/main/java/com/genersoft/iot/vmp/vmanager/bean/OtherPsSendInfo.java new file mode 100755 index 0000000..ac98409 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/vmanager/bean/OtherPsSendInfo.java @@ -0,0 +1,137 @@ +package com.genersoft.iot.vmp.vmanager.bean; + +public class OtherPsSendInfo { + + /** + * 发流IP + */ + private String sendLocalIp; + + /** + * 发流端口 + */ + private int sendLocalPort; + + /** + * 收流IP + */ + private String receiveIp; + + /** + * 收流端口 + */ + private int receivePort; + + + /** + * 会话ID + */ + private String callId; + + /** + * 流ID + */ + private String stream; + + /** + * 推流应用名 + */ + private String pushApp; + + /** + * 推流流ID + */ + private String pushStream; + + /** + * 推流SSRC + */ + private String pushSSRC; + + public String getSendLocalIp() { + return sendLocalIp; + } + + public void setSendLocalIp(String sendLocalIp) { + this.sendLocalIp = sendLocalIp; + } + + public int getSendLocalPort() { + return sendLocalPort; + } + + public void setSendLocalPort(int sendLocalPort) { + this.sendLocalPort = sendLocalPort; + } + + public String getReceiveIp() { + return receiveIp; + } + + public void setReceiveIp(String receiveIp) { + this.receiveIp = receiveIp; + } + + public int getReceivePort() { + return receivePort; + } + + public void setReceivePort(int receivePort) { + this.receivePort = receivePort; + } + + public String getCallId() { + return callId; + } + + public void setCallId(String callId) { + this.callId = callId; + } + + public String getStream() { + return stream; + } + + public void setStream(String stream) { + this.stream = stream; + } + + public String getPushApp() { + return pushApp; + } + + public void setPushApp(String pushApp) { + this.pushApp = pushApp; + } + + public String getPushStream() { + return pushStream; + } + + public void setPushStream(String pushStream) { + this.pushStream = pushStream; + } + + public String getPushSSRC() { + return pushSSRC; + } + + public void setPushSSRC(String pushSSRC) { + this.pushSSRC = pushSSRC; + } + + @Override + public String toString() { + return "OtherPsSendInfo{" + + "sendLocalIp='" + sendLocalIp + '\'' + + ", sendLocalPort=" + sendLocalPort + + ", receiveIp='" + receiveIp + '\'' + + ", receivePort=" + receivePort + + ", callId='" + callId + '\'' + + ", stream='" + stream + '\'' + + ", pushApp='" + pushApp + '\'' + + ", pushStream='" + pushStream + '\'' + + ", pushSSRC='" + pushSSRC + '\'' + + '}'; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/vmanager/bean/OtherRtpSendInfo.java b/src/main/java/com/genersoft/iot/vmp/vmanager/bean/OtherRtpSendInfo.java new file mode 100755 index 0000000..75c05d3 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/vmanager/bean/OtherRtpSendInfo.java @@ -0,0 +1,166 @@ +package com.genersoft.iot.vmp.vmanager.bean; + +public class OtherRtpSendInfo { + + /** + * 发流IP + */ + private String sendLocalIp; + + /** + * 音频发流端口 + */ + private int sendLocalPortForAudio; + + /** + * 视频发流端口 + */ + private int sendLocalPortForVideo; + + /** + * 收流IP + */ + private String receiveIp; + + /** + * 音频收流端口 + */ + private int receivePortForAudio; + + /** + * 视频收流端口 + */ + private int receivePortForVideo; + + /** + * 会话ID + */ + private String callId; + + /** + * 流ID + */ + private String stream; + + /** + * 推流应用名 + */ + private String pushApp; + + /** + * 推流流ID + */ + private String pushStream; + + /** + * 推流SSRC + */ + private String pushSSRC; + + + public String getReceiveIp() { + return receiveIp; + } + + public void setReceiveIp(String receiveIp) { + this.receiveIp = receiveIp; + } + + public int getReceivePortForAudio() { + return receivePortForAudio; + } + + public void setReceivePortForAudio(int receivePortForAudio) { + this.receivePortForAudio = receivePortForAudio; + } + + public int getReceivePortForVideo() { + return receivePortForVideo; + } + + public void setReceivePortForVideo(int receivePortForVideo) { + this.receivePortForVideo = receivePortForVideo; + } + + public String getCallId() { + return callId; + } + + public void setCallId(String callId) { + this.callId = callId; + } + + public String getStream() { + return stream; + } + + public void setStream(String stream) { + this.stream = stream; + } + + public String getPushApp() { + return pushApp; + } + + public void setPushApp(String pushApp) { + this.pushApp = pushApp; + } + + public String getPushStream() { + return pushStream; + } + + public void setPushStream(String pushStream) { + this.pushStream = pushStream; + } + + public String getPushSSRC() { + return pushSSRC; + } + + public void setPushSSRC(String pushSSRC) { + this.pushSSRC = pushSSRC; + } + + + public String getSendLocalIp() { + return sendLocalIp; + } + + public void setSendLocalIp(String sendLocalIp) { + this.sendLocalIp = sendLocalIp; + } + + public int getSendLocalPortForAudio() { + return sendLocalPortForAudio; + } + + public void setSendLocalPortForAudio(int sendLocalPortForAudio) { + this.sendLocalPortForAudio = sendLocalPortForAudio; + } + + public int getSendLocalPortForVideo() { + return sendLocalPortForVideo; + } + + public void setSendLocalPortForVideo(int sendLocalPortForVideo) { + this.sendLocalPortForVideo = sendLocalPortForVideo; + } + + @Override + public String toString() { + return "OtherRtpSendInfo{" + + "sendLocalIp='" + sendLocalIp + '\'' + + ", sendLocalPortForAudio=" + sendLocalPortForAudio + + ", sendLocalPortForVideo=" + sendLocalPortForVideo + + ", receiveIp='" + receiveIp + '\'' + + ", receivePortForAudio=" + receivePortForAudio + + ", receivePortForVideo=" + receivePortForVideo + + ", callId='" + callId + '\'' + + ", stream='" + stream + '\'' + + ", pushApp='" + pushApp + '\'' + + ", pushStream='" + pushStream + '\'' + + ", pushSSRC='" + pushSSRC + '\'' + + '}'; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/vmanager/bean/PlayTypeEnum.java b/src/main/java/com/genersoft/iot/vmp/vmanager/bean/PlayTypeEnum.java new file mode 100755 index 0000000..d2ee56b --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/vmanager/bean/PlayTypeEnum.java @@ -0,0 +1,23 @@ +package com.genersoft.iot.vmp.vmanager.bean; + +public enum PlayTypeEnum { + + PLAY("0", "直播"), + PLAY_BACK("1", "回放"); + + private String value; + private String name; + + PlayTypeEnum(String value, String name) { + this.value = value; + this.name = name; + } + + public String getValue() { + return value; + } + + public String getName() { + return name; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/vmanager/bean/RecordFile.java b/src/main/java/com/genersoft/iot/vmp/vmanager/bean/RecordFile.java new file mode 100755 index 0000000..72d6561 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/vmanager/bean/RecordFile.java @@ -0,0 +1,53 @@ +package com.genersoft.iot.vmp.vmanager.bean; + +public class RecordFile { + private String app; + private String stream; + + private String fileName; + + private String mediaServerId; + + private String date; + + + public String getApp() { + return app; + } + + public void setApp(String app) { + this.app = app; + } + + public String getStream() { + return stream; + } + + public void setStream(String stream) { + this.stream = stream; + } + + public String getFileName() { + return fileName; + } + + public void setFileName(String fileName) { + this.fileName = fileName; + } + + public String getMediaServerId() { + return mediaServerId; + } + + public void setMediaServerId(String mediaServerId) { + this.mediaServerId = mediaServerId; + } + + public String getDate() { + return date; + } + + public void setDate(String date) { + this.date = date; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/vmanager/bean/ResourceBaseInfo.java b/src/main/java/com/genersoft/iot/vmp/vmanager/bean/ResourceBaseInfo.java new file mode 100755 index 0000000..dab9b0a --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/vmanager/bean/ResourceBaseInfo.java @@ -0,0 +1,30 @@ +package com.genersoft.iot.vmp.vmanager.bean; + +public class ResourceBaseInfo { + private int total; + private int online; + + public ResourceBaseInfo() { + } + + public ResourceBaseInfo(int total, int online) { + this.total = total; + this.online = online; + } + + public int getTotal() { + return total; + } + + public void setTotal(int total) { + this.total = total; + } + + public int getOnline() { + return online; + } + + public void setOnline(int online) { + this.online = online; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/vmanager/bean/ResourceInfo.java b/src/main/java/com/genersoft/iot/vmp/vmanager/bean/ResourceInfo.java new file mode 100755 index 0000000..3b0ee0d --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/vmanager/bean/ResourceInfo.java @@ -0,0 +1,41 @@ +package com.genersoft.iot.vmp.vmanager.bean; + +public class ResourceInfo { + + private ResourceBaseInfo device; + private ResourceBaseInfo channel; + private ResourceBaseInfo push; + private ResourceBaseInfo proxy; + + public ResourceBaseInfo getDevice() { + return device; + } + + public void setDevice(ResourceBaseInfo device) { + this.device = device; + } + + public ResourceBaseInfo getChannel() { + return channel; + } + + public void setChannel(ResourceBaseInfo channel) { + this.channel = channel; + } + + public ResourceBaseInfo getPush() { + return push; + } + + public void setPush(ResourceBaseInfo push) { + this.push = push; + } + + public ResourceBaseInfo getProxy() { + return proxy; + } + + public void setProxy(ResourceBaseInfo proxy) { + this.proxy = proxy; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/vmanager/bean/SnapPath.java b/src/main/java/com/genersoft/iot/vmp/vmanager/bean/SnapPath.java new file mode 100755 index 0000000..ce34d72 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/vmanager/bean/SnapPath.java @@ -0,0 +1,50 @@ +package com.genersoft.iot.vmp.vmanager.bean; + +import io.swagger.v3.oas.annotations.media.Schema; + +@Schema(description = "截图地址信息") +public class SnapPath { + + @Schema(description = "相对地址") + private String path; + + @Schema(description = "绝对地址") + private String absoluteFilePath; + + @Schema(description = "请求地址") + private String url; + + + public static SnapPath getInstance(String path, String absoluteFilePath, String url) { + SnapPath snapPath = new SnapPath(); + snapPath.setPath(path); + snapPath.setAbsoluteFilePath(absoluteFilePath); + snapPath.setUrl(url); + return snapPath; + } + + + public String getPath() { + return path; + } + + public void setPath(String path) { + this.path = path; + } + + public String getAbsoluteFilePath() { + return absoluteFilePath; + } + + public void setAbsoluteFilePath(String absoluteFilePath) { + this.absoluteFilePath = absoluteFilePath; + } + + public String getUrl() { + return url; + } + + public void setUrl(String url) { + this.url = url; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/vmanager/bean/StreamContent.java b/src/main/java/com/genersoft/iot/vmp/vmanager/bean/StreamContent.java new file mode 100755 index 0000000..2d3861b --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/vmanager/bean/StreamContent.java @@ -0,0 +1,206 @@ +package com.genersoft.iot.vmp.vmanager.bean; + +import com.genersoft.iot.vmp.common.StreamInfo; +import com.genersoft.iot.vmp.media.bean.MediaInfo; +import com.genersoft.iot.vmp.service.bean.DownloadFileInfo; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Data +@Schema(description = "流信息") +public class StreamContent { + + @Schema(description = "应用名") + private String app; + + @Schema(description = "流ID") + private String stream; + + @Schema(description = "IP") + private String ip; + + @Schema(description = "HTTP-FLV流地址") + private String flv; + + @Schema(description = "HTTPS-FLV流地址") + private String https_flv; + + @Schema(description = "Websocket-FLV流地址") + private String ws_flv; + + @Schema(description = "Websockets-FLV流地址") + private String wss_flv; + + @Schema(description = "HTTP-FMP4流地址") + private String fmp4; + + @Schema(description = "HTTPS-FMP4流地址") + private String https_fmp4; + + @Schema(description = "Websocket-FMP4流地址") + private String ws_fmp4; + + @Schema(description = "Websockets-FMP4流地址") + private String wss_fmp4; + + @Schema(description = "HLS流地址") + private String hls; + + @Schema(description = "HTTPS-HLS流地址") + private String https_hls; + + @Schema(description = "Websocket-HLS流地址") + private String ws_hls; + + @Schema(description = "Websockets-HLS流地址") + private String wss_hls; + + @Schema(description = "HTTP-TS流地址") + private String ts; + + @Schema(description = "HTTPS-TS流地址") + private String https_ts; + + @Schema(description = "Websocket-TS流地址") + private String ws_ts; + + @Schema(description = "Websockets-TS流地址") + private String wss_ts; + + @Schema(description = "RTMP流地址") + private String rtmp; + + @Schema(description = "RTMPS流地址") + private String rtmps; + + @Schema(description = "RTSP流地址") + private String rtsp; + + @Schema(description = "RTSPS流地址") + private String rtsps; + + @Schema(description = "RTC流地址") + private String rtc; + + @Schema(description = "RTCS流地址") + private String rtcs; + + @Schema(description = "流媒体ID") + private String mediaServerId; + + @Schema(description = "流编码信息") + private MediaInfo mediaInfo; + + @Schema(description = "开始时间") + private String startTime; + + @Schema(description = "结束时间") + private String endTime; + + @Schema(description = "时长(回放时使用)") + private Double duration; + + @Schema(description = "文件下载地址(录像下载使用)") + private DownloadFileInfo downLoadFilePath; + + @Schema(description = "转码后的视频流") + private StreamContent transcodeStream; + + private double progress; + + @Schema(description = "拉流代理返回的KEY") + private String key; + + @Schema(description = "使用的WVP ID") + private String serverId; + + public StreamContent(StreamInfo streamInfo) { + if (streamInfo == null) { + return; + } + this.app = streamInfo.getApp(); + this.stream = streamInfo.getStream(); + if (streamInfo.getFlv() != null) { + this.flv = streamInfo.getFlv().getUrl(); + } + if (streamInfo.getHttps_flv() != null) { + this.https_flv = streamInfo.getHttps_flv().getUrl(); + } + if (streamInfo.getWs_flv() != null) { + this.ws_flv = streamInfo.getWs_flv().getUrl(); + } + if (streamInfo.getWss_flv() != null) { + this.wss_flv = streamInfo.getWss_flv().getUrl(); + } + if (streamInfo.getFmp4() != null) { + this.fmp4 = streamInfo.getFmp4().getUrl(); + } + if (streamInfo.getHttps_fmp4() != null) { + this.https_fmp4 = streamInfo.getHttps_fmp4().getUrl(); + } + if (streamInfo.getWs_fmp4() != null) { + this.ws_fmp4 = streamInfo.getWs_fmp4().getUrl(); + } + if (streamInfo.getWss_fmp4() != null) { + this.wss_fmp4 = streamInfo.getWss_fmp4().getUrl(); + } + if (streamInfo.getHls() != null) { + this.hls = streamInfo.getHls().getUrl(); + } + if (streamInfo.getHttps_hls() != null) { + this.https_hls = streamInfo.getHttps_hls().getUrl(); + } + if (streamInfo.getWs_hls() != null) { + this.ws_hls = streamInfo.getWs_hls().getUrl(); + } + if (streamInfo.getWss_hls() != null) { + this.wss_hls = streamInfo.getWss_hls().getUrl(); + } + if (streamInfo.getTs() != null) { + this.ts = streamInfo.getTs().getUrl(); + } + if (streamInfo.getHttps_ts() != null) { + this.https_ts = streamInfo.getHttps_ts().getUrl(); + } + if (streamInfo.getWs_ts() != null) { + this.ws_ts = streamInfo.getWs_ts().getUrl(); + } + if (streamInfo.getRtmp() != null) { + this.rtmp = streamInfo.getRtmp().getUrl(); + } + if (streamInfo.getRtmps() != null) { + this.rtmps = streamInfo.getRtmps().getUrl(); + } + if (streamInfo.getRtsp() != null) { + this.rtsp = streamInfo.getRtsp().getUrl(); + } + if (streamInfo.getRtsps() != null) { + this.rtsps = streamInfo.getRtsps().getUrl(); + } + if (streamInfo.getRtc() != null) { + this.rtc = streamInfo.getRtc().getUrl(); + } + if (streamInfo.getRtcs() != null) { + this.rtcs = streamInfo.getRtcs().getUrl(); + } + if (streamInfo.getMediaServer() != null) { + this.mediaServerId = streamInfo.getMediaServer().getId(); + } + + this.mediaInfo = streamInfo.getMediaInfo(); + this.startTime = streamInfo.getStartTime(); + this.endTime = streamInfo.getEndTime(); + this.progress = streamInfo.getProgress(); + this.duration = streamInfo.getDuration(); + this.key = streamInfo.getKey(); + this.serverId = streamInfo.getServerId(); + + if (streamInfo.getDownLoadFilePath() != null) { + this.downLoadFilePath = streamInfo.getDownLoadFilePath(); + } + if (streamInfo.getTranscodeStream() != null) { + this.transcodeStream = new StreamContent(streamInfo.getTranscodeStream()); + } + } + +} diff --git a/src/main/java/com/genersoft/iot/vmp/vmanager/bean/SystemConfigInfo.java b/src/main/java/com/genersoft/iot/vmp/vmanager/bean/SystemConfigInfo.java new file mode 100755 index 0000000..838e706 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/vmanager/bean/SystemConfigInfo.java @@ -0,0 +1,19 @@ +package com.genersoft.iot.vmp.vmanager.bean; + +import com.genersoft.iot.vmp.common.VersionPo; +import com.genersoft.iot.vmp.conf.SipConfig; +import com.genersoft.iot.vmp.conf.UserSetting; +import com.genersoft.iot.vmp.jt1078.config.JT1078Config; +import lombok.Data; + +@Data +public class SystemConfigInfo { + + private int serverPort; + private SipConfig sip; + private UserSetting addOn; + private VersionPo version; + private JT1078Config jt1078Config; + +} + diff --git a/src/main/java/com/genersoft/iot/vmp/vmanager/bean/TablePageInfo.java b/src/main/java/com/genersoft/iot/vmp/vmanager/bean/TablePageInfo.java new file mode 100755 index 0000000..73d4223 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/vmanager/bean/TablePageInfo.java @@ -0,0 +1,99 @@ +package com.genersoft.iot.vmp.vmanager.bean; + +import java.util.ArrayList; +import java.util.List; + +public class TablePageInfo { + //当前页 + private int pageNum; + //每页的数量 + private int pageSize; + //当前页的数量 + private int size; + //总页数 + private int pages; + //总数 + private int total; + + private List resultData; + + private List list; + + public TablePageInfo(List resultData) { + this.resultData = resultData; + } + + public TablePageInfo() { + } + + public void startPage(int page, int count) { + if (count <= 0) count = 10; + if (page <= 0) page = 1; + this.pageNum = page; + this.pageSize = count; + this.total = resultData.size(); + + this.pages = total % count == 0 ? total / count : total / count + 1; + int fromIndx = (page - 1) * count; + if (fromIndx > this.total - 1) { + this.list = new ArrayList<>(); + this.size = 0; + return; + } + + int toIndx = page * count; + if (toIndx > this.total) { + toIndx = this.total; + } + this.list = this.resultData.subList(fromIndx, toIndx); + this.size = this.list.size(); + } + + public int getPageNum() { + return pageNum; + } + + public void setPageNum(int pageNum) { + this.pageNum = pageNum; + } + + public int getPageSize() { + return pageSize; + } + + public void setPageSize(int pageSize) { + this.pageSize = pageSize; + } + + public int getSize() { + return size; + } + + public void setSize(int size) { + this.size = size; + } + + public int getPages() { + return pages; + } + + public void setPages(int pages) { + this.pages = pages; + } + + public int getTotal() { + return total; + } + + public void setTotal(int total) { + this.total = total; + } + + public List getList() { + return list; + } + + public void setList(List list) { + this.list = list; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/vmanager/bean/WVPResult.java b/src/main/java/com/genersoft/iot/vmp/vmanager/bean/WVPResult.java new file mode 100755 index 0000000..3699d08 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/vmanager/bean/WVPResult.java @@ -0,0 +1,55 @@ +package com.genersoft.iot.vmp.vmanager.bean; + + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; +import lombok.Setter; + +@Setter +@Getter +@Schema(description = "统一返回结果") +public class WVPResult implements Cloneable{ + + public WVPResult() { + } + + public WVPResult(int code, String msg, T data) { + this.code = code; + this.msg = msg; + this.data = data; + } + + + @Schema(description = "错误码,0为成功") + private int code; + @Schema(description = "描述,错误时描述错误原因") + private String msg; + @Schema(description = "数据") + private T data; + + + public static WVPResult success(T t, String msg) { + return new WVPResult<>(ErrorCode.SUCCESS.getCode(), msg, t); + } + + public static WVPResult success() { + return new WVPResult<>(ErrorCode.SUCCESS.getCode(), ErrorCode.SUCCESS.getMsg(), null); + } + + public static WVPResult success(T t) { + return success(t, ErrorCode.SUCCESS.getMsg()); + } + + public static WVPResult fail(int code, String msg) { + return new WVPResult<>(code, msg, null); + } + + public static WVPResult fail(ErrorCode errorCode) { + return fail(errorCode.getCode(), errorCode.getMsg()); + } + + @Override + public Object clone() throws CloneNotSupportedException { + return super.clone(); + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/vmanager/cloudRecord/CloudRecordController.java b/src/main/java/com/genersoft/iot/vmp/vmanager/cloudRecord/CloudRecordController.java new file mode 100755 index 0000000..addd943 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/vmanager/cloudRecord/CloudRecordController.java @@ -0,0 +1,604 @@ +package com.genersoft.iot.vmp.vmanager.cloudRecord; + +import com.alibaba.fastjson2.JSONArray; +import com.genersoft.iot.vmp.common.StreamInfo; +import com.genersoft.iot.vmp.conf.UserSetting; +import com.genersoft.iot.vmp.conf.exception.ControllerException; +import com.genersoft.iot.vmp.conf.security.JwtUtils; +import com.genersoft.iot.vmp.media.bean.MediaServer; +import com.genersoft.iot.vmp.media.service.IMediaServerService; +import com.genersoft.iot.vmp.service.ICloudRecordService; +import com.genersoft.iot.vmp.service.bean.CloudRecordItem; +import com.genersoft.iot.vmp.service.bean.DownloadFileInfo; +import com.genersoft.iot.vmp.service.bean.ErrorCallback; +import com.genersoft.iot.vmp.service.bean.InviteErrorCode; +import com.genersoft.iot.vmp.streamPush.bean.BatchRemoveParam; +import com.genersoft.iot.vmp.utils.DateUtil; +import com.genersoft.iot.vmp.utils.HttpUtils; +import com.genersoft.iot.vmp.vmanager.bean.ErrorCode; +import com.genersoft.iot.vmp.vmanager.bean.StreamContent; +import com.genersoft.iot.vmp.vmanager.bean.WVPResult; +import com.genersoft.iot.vmp.vmanager.cloudRecord.bean.CloudRecordUrl; +import com.github.pagehelper.PageInfo; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.ObjectUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.context.request.async.DeferredResult; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.List; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; + +@SuppressWarnings("rawtypes") +@Tag(name = "云端录像接口") +@Slf4j +@RestController +@RequestMapping("/api/cloud/record") +public class CloudRecordController { + + + @Autowired + private ICloudRecordService cloudRecordService; + + @Autowired + private IMediaServerService mediaServerService; + + @Autowired + private UserSetting userSetting; + + + @ResponseBody + @GetMapping("/date/list") + @Operation(summary = "查询存在云端录像的日期", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "app", description = "应用名", required = true) + @Parameter(name = "stream", description = "流ID", required = true) + @Parameter(name = "year", description = "年,置空则查询当年", required = false) + @Parameter(name = "month", description = "月,置空则查询当月", required = false) + @Parameter(name = "mediaServerId", description = "流媒体ID,置空则查询全部", required = false) + public List openRtpServer( + @RequestParam(required = true) String app, + @RequestParam(required = true) String stream, + @RequestParam(required = false) Integer year, + @RequestParam(required = false) Integer month, + @RequestParam(required = false) String mediaServerId + + ) { + log.info("[云端录像] 查询存在云端录像的日期 app->{}, stream->{}, mediaServerId->{}, year->{}, month->{}", app, stream, mediaServerId, year, month); + Calendar calendar = Calendar.getInstance(); + if (ObjectUtils.isEmpty(year)) { + year = calendar.get(Calendar.YEAR); + } + if (ObjectUtils.isEmpty(month)) { + month = calendar.get(Calendar.MONTH) + 1; + } + List mediaServers; + if (!ObjectUtils.isEmpty(mediaServerId)) { + mediaServers = new ArrayList<>(); + MediaServer mediaServer = mediaServerService.getOne(mediaServerId); + if (mediaServer == null) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "未找到流媒体: " + mediaServerId); + } + mediaServers.add(mediaServer); + } else { + mediaServers = mediaServerService.getAllOnlineList(); + } + if (mediaServers.isEmpty()) { + return new ArrayList<>(); + } + + return cloudRecordService.getDateList(app, stream, year, month, mediaServers); + } + + @ResponseBody + @GetMapping("/list") + @Operation(summary = "分页查询云端录像", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "query", description = "检索内容", required = false) + @Parameter(name = "app", description = "应用名", required = false) + @Parameter(name = "stream", description = "流ID", required = false) + @Parameter(name = "page", description = "当前页", required = true) + @Parameter(name = "count", description = "每页查询数量", required = true) + @Parameter(name = "startTime", description = "开始时间(yyyy-MM-dd HH:mm:ss)", required = false) + @Parameter(name = "endTime", description = "结束时间(yyyy-MM-dd HH:mm:ss)", required = false) + @Parameter(name = "mediaServerId", description = "流媒体ID,置空则查询全部流媒体", required = false) + @Parameter(name = "callId", description = "每次录像的唯一标识,置空则查询全部流媒体", required = false) + @Parameter(name = "ascOrder", description = "是否升序排序, 升序: true, 降序: false", required = false) + public PageInfo openRtpServer(@RequestParam(required = false) String query, + @RequestParam(required = false) String app, + @RequestParam(required = false) String stream, + @RequestParam int page, + @RequestParam int count, + @RequestParam(required = false) String startTime, + @RequestParam(required = false) String endTime, + @RequestParam(required = false) String mediaServerId, + @RequestParam(required = false) String callId, + @RequestParam(required = false) Boolean ascOrder + + ) { + log.info("[云端录像] 查询 app->{}, stream->{}, mediaServerId->{}, page->{}, count->{}, startTime->{}, endTime->{}, callId->{}", app, stream, mediaServerId, page, count, startTime, endTime, callId); + + List mediaServers; + if (!ObjectUtils.isEmpty(mediaServerId)) { + mediaServers = new ArrayList<>(); + MediaServer mediaServer = mediaServerService.getOne(mediaServerId); + if (mediaServer == null) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "未找到流媒体: " + mediaServerId); + } + mediaServers.add(mediaServer); + } else { + mediaServers = null; + } + if (query != null && ObjectUtils.isEmpty(query.trim())) { + query = null; + } + if (app != null && ObjectUtils.isEmpty(app.trim())) { + app = null; + } + if (stream != null && ObjectUtils.isEmpty(stream.trim())) { + stream = null; + } + if (startTime != null && ObjectUtils.isEmpty(startTime.trim())) { + startTime = null; + } + if (endTime != null && ObjectUtils.isEmpty(endTime.trim())) { + endTime = null; + } + if (callId != null && ObjectUtils.isEmpty(callId.trim())) { + callId = null; + } + return cloudRecordService.getList(page, count, query, app, stream, startTime, endTime, mediaServers, callId, ascOrder); + } + + @ResponseBody + @GetMapping("/task/add") + @Operation(summary = "添加合并任务") + @Parameter(name = "app", description = "应用名", required = false) + @Parameter(name = "stream", description = "流ID", required = false) + @Parameter(name = "mediaServerId", description = "流媒体ID", required = false) + @Parameter(name = "startTime", description = "鉴权ID", required = false) + @Parameter(name = "endTime", description = "鉴权ID", required = false) + @Parameter(name = "callId", description = "鉴权ID", required = false) + @Parameter(name = "remoteHost", description = "返回地址时的远程地址", required = false) + public String addTask(HttpServletRequest request, @RequestParam(required = false) String app, @RequestParam(required = false) String stream, @RequestParam(required = false) String mediaServerId, @RequestParam(required = false) String startTime, @RequestParam(required = false) String endTime, @RequestParam(required = false) String callId, @RequestParam(required = false) String remoteHost) { + MediaServer mediaServer; + if (mediaServerId == null) { + mediaServer = mediaServerService.getDefaultMediaServer(); + } else { + mediaServer = mediaServerService.getOne(mediaServerId); + } + if (mediaServer == null) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "未找到可用的流媒体"); + } else { + if (remoteHost == null) { + remoteHost = request.getScheme() + "://" + mediaServer.getIp() + ":" + mediaServer.getRecordAssistPort(); + } + } + return cloudRecordService.addTask(app, stream, mediaServer, startTime, endTime, callId, remoteHost, mediaServerId != null); + } + + @ResponseBody + @GetMapping("/task/list") + @Operation(summary = "查询合并任务") + @Parameter(name = "taskId", description = "任务Id", required = false) + @Parameter(name = "mediaServerId", description = "流媒体ID", required = false) + @Parameter(name = "isEnd", description = "是否结束", required = false) + public JSONArray queryTaskList(HttpServletRequest request, @RequestParam(required = false) String app, @RequestParam(required = false) String stream, @RequestParam(required = false) String callId, @RequestParam(required = false) String taskId, @RequestParam(required = false) String mediaServerId, @RequestParam(required = false) Boolean isEnd) { + if (ObjectUtils.isEmpty(mediaServerId)) { + mediaServerId = null; + } + + return cloudRecordService.queryTask(app, stream, callId, taskId, mediaServerId, isEnd, request.getScheme()); + } + + @ResponseBody + @GetMapping("/collect/add") + @Operation(summary = "添加收藏") + @Parameter(name = "app", description = "应用名", required = false) + @Parameter(name = "stream", description = "流ID", required = false) + @Parameter(name = "mediaServerId", description = "流媒体ID", required = false) + @Parameter(name = "startTime", description = "鉴权ID", required = false) + @Parameter(name = "endTime", description = "鉴权ID", required = false) + @Parameter(name = "callId", description = "鉴权ID", required = false) + @Parameter(name = "recordId", description = "录像记录的ID,用于精准收藏一个视频文件", required = false) + public int addCollect(@RequestParam(required = false) String app, @RequestParam(required = false) String stream, @RequestParam(required = false) String mediaServerId, @RequestParam(required = false) String startTime, @RequestParam(required = false) String endTime, @RequestParam(required = false) String callId, @RequestParam(required = false) Integer recordId) { + log.info("[云端录像] 添加收藏,app={},stream={},mediaServerId={},startTime={},endTime={},callId={},recordId={}", app, stream, mediaServerId, startTime, endTime, callId, recordId); + if (recordId != null) { + return cloudRecordService.changeCollectById(recordId, true); + } else { + return cloudRecordService.changeCollect(true, app, stream, mediaServerId, startTime, endTime, callId); + } + } + + @ResponseBody + @GetMapping("/collect/delete") + @Operation(summary = "移除收藏") + @Parameter(name = "app", description = "应用名", required = false) + @Parameter(name = "stream", description = "流ID", required = false) + @Parameter(name = "mediaServerId", description = "流媒体ID", required = false) + @Parameter(name = "startTime", description = "鉴权ID", required = false) + @Parameter(name = "endTime", description = "鉴权ID", required = false) + @Parameter(name = "callId", description = "鉴权ID", required = false) + @Parameter(name = "recordId", description = "录像记录的ID,用于精准精准移除一个视频文件的收藏", required = false) + public int deleteCollect(@RequestParam(required = false) String app, @RequestParam(required = false) String stream, @RequestParam(required = false) String mediaServerId, @RequestParam(required = false) String startTime, @RequestParam(required = false) String endTime, @RequestParam(required = false) String callId, @RequestParam(required = false) Integer recordId) { + log.info("[云端录像] 移除收藏,app={},stream={},mediaServerId={},startTime={},endTime={},callId={},recordId={}", app, stream, mediaServerId, startTime, endTime, callId, recordId); + if (recordId != null) { + return cloudRecordService.changeCollectById(recordId, false); + } else { + return cloudRecordService.changeCollect(false, app, stream, mediaServerId, startTime, endTime, callId); + } + } + + @ResponseBody + @GetMapping("/play/path") + @Operation(summary = "获取播放地址") + @Parameter(name = "recordId", description = "录像记录的ID", required = true) + public DownloadFileInfo getPlayUrlPath(@RequestParam(required = true) Integer recordId) { + return cloudRecordService.getPlayUrlPath(recordId); + } + + @ResponseBody + @GetMapping("/loadRecord") + @Operation(summary = "加载录像文件形成播放地址") + @Parameter(name = "app", description = "应用名", required = true) + @Parameter(name = "stream", description = "流ID", required = true) + @Parameter(name = "cloudRecordId", description = "云端录像ID", required = true) + public DeferredResult> loadRecord( + HttpServletRequest request, + @RequestParam(required = true) String app, + @RequestParam(required = true) String stream, + @RequestParam(required = true) int cloudRecordId + ) { + DeferredResult> result = new DeferredResult<>(); + + result.onTimeout(()->{ + log.info("[加载录像文件超时] app={}, stream={}, cloudRecordId={}", app, stream, cloudRecordId); + WVPResult wvpResult = new WVPResult<>(); + wvpResult.setCode(ErrorCode.ERROR100.getCode()); + wvpResult.setMsg("加载录像文件超时"); + result.setResult(wvpResult); + }); + + ErrorCallback callback = (code, msg, streamInfo) -> { + + WVPResult wvpResult = new WVPResult<>(); + if (code == InviteErrorCode.SUCCESS.getCode()) { + wvpResult.setCode(ErrorCode.SUCCESS.getCode()); + wvpResult.setMsg(ErrorCode.SUCCESS.getMsg()); + + if (streamInfo != null) { + if (userSetting.getUseSourceIpAsStreamIp()) { + streamInfo=streamInfo.clone();//深拷贝 + String host; + try { + URL url=new URL(request.getRequestURL().toString()); + host=url.getHost(); + } catch (MalformedURLException e) { + host=request.getLocalAddr(); + } + streamInfo.changeStreamIp(host); + } + if (!org.springframework.util.ObjectUtils.isEmpty(streamInfo.getMediaServer().getTranscodeSuffix()) && !"null".equalsIgnoreCase(streamInfo.getMediaServer().getTranscodeSuffix())) { + streamInfo.setStream(streamInfo.getStream() + "_" + streamInfo.getMediaServer().getTranscodeSuffix()); + } + wvpResult.setData(new StreamContent(streamInfo)); + }else { + wvpResult.setCode(code); + wvpResult.setMsg(msg); + } + }else { + wvpResult.setCode(code); + wvpResult.setMsg(msg); + } + result.setResult(wvpResult); + }; + + cloudRecordService.loadMP4File(app, stream, cloudRecordId, callback); + return result; + } + + @ResponseBody + @GetMapping("/seek") + @Operation(summary = "定位录像播放到制定位置") + @Parameter(name = "mediaServerId", description = "使用的节点Id", required = true) + @Parameter(name = "app", description = "应用名", required = true) + @Parameter(name = "stream", description = "流ID", required = true) + @Parameter(name = "seek", description = "要定位的时间位置,从录像开始的时间算起", required = true) + public void seekRecord( + @RequestParam(required = true) String mediaServerId, + @RequestParam(required = true) String app, + @RequestParam(required = true) String stream, + @RequestParam(required = true) Double seek, + @RequestParam(required = false) String schema + ) { + if (schema == null) { + schema = "ts"; + } + cloudRecordService.seekRecord(mediaServerId, app, stream, seek, schema); + } + + @ResponseBody + @GetMapping("/speed") + @Operation(summary = "设置录像播放速度") + @Parameter(name = "mediaServerId", description = "使用的节点Id", required = true) + @Parameter(name = "app", description = "应用名", required = true) + @Parameter(name = "stream", description = "流ID", required = true) + @Parameter(name = "speed", description = "要设置的录像倍速", required = true) + public void setRecordSpeed( + @RequestParam(required = true) String mediaServerId, + @RequestParam(required = true) String app, + @RequestParam(required = true) String stream, + @RequestParam(required = true) Integer speed, + @RequestParam(required = false) String schema + ) { + if (schema == null) { + schema = "ts"; + } + + cloudRecordService.setRecordSpeed(mediaServerId, app, stream, speed, schema); + } + + @ResponseBody + @DeleteMapping("/delete") + @Operation(summary = "删除录像文件") + @Parameter(name = "ids", description = "文件ID集合", required = true) + public void deleteFileByIds(@RequestBody BatchRemoveParam ids) { + cloudRecordService.deleteFileByIds(ids.getIds()); + } + + @ResponseBody + @GetMapping("/download/zip") + public void downloadZipFileFromUrl(HttpServletResponse response, Integer[] ids) { + log.info("[下载指定录像文件的压缩包] 查询 ids->{}", ids); + List arrayList = new ArrayList<>(List.of(ids)); + List cloudRecordItemList = cloudRecordService.getUrlListByIds(arrayList); + if (ObjectUtils.isEmpty(cloudRecordItemList)) { + log.warn("[下载指定录像文件的压缩包] 未找到录像文件,ids->{}", ids); + return; + } + + // 设置响应头 + response.setContentType("application/zip"); + response.setCharacterEncoding("UTF-8"); + response.setHeader("Content-Disposition", "attachment; filename=record_" + System.currentTimeMillis() + ".zip"); + + try (ZipOutputStream zos = new ZipOutputStream(response.getOutputStream())) { + for (CloudRecordUrl recordUrl : cloudRecordItemList) { + try { + zos.putNextEntry(new ZipEntry(recordUrl.getFileName())); + boolean downloadSuccess = HttpUtils.downLoadFile(recordUrl.getDownloadUrl(), zos); + if (!downloadSuccess) { + log.warn("[下载指定录像文件的压缩包] 下载文件失败: {}", recordUrl.getDownloadUrl()); + zos.closeEntry(); + continue; + } + +// try (FileInputStream fis = new FileInputStream(recordUrl.getFilePath())) { +// byte[] buf = new byte[8192]; // 8KB 缓冲区,提高性能 +// int len; +// while ((len = fis.read(buf)) != -1) { +// zos.write(buf, 0, len); +// } +// } + + zos.closeEntry(); + } catch (Exception e) { + log.error("[下载指定录像文件的压缩包] 处理文件失败: {}, 错误: {}", recordUrl.getFileName(), e.getMessage()); + // 继续处理下一个文件 + } + } + } catch (IOException e) { + log.error("[下载指定录像文件的压缩包] 创建压缩包失败,查询 ids->{}", ids, e); + } + } + + + + + + /************************* 以下这些接口只适合wvp和zlm部署在同一台服务器的情况,且wvp只有一个zlm节点的情况 ***************************************/ + + /** + * 下载指定录像文件的压缩包 + * @param query 检索内容 + * @param app 应用名 + * @param stream 流ID + * @param startTime 开始时间(yyyy-MM-dd HH:mm:ss) + * @param endTime 结束时间(yyyy-MM-dd HH:mm:ss) + * @param mediaServerId 流媒体ID,置空则查询全部流媒体 + * @param callId 每次录像的唯一标识,置空则查询全部流媒体 + * @param ids 指定的Id + */ + @ResponseBody + @GetMapping("/zip") + public void downloadZipFile(HttpServletResponse response, @RequestParam(required = false) String query, @RequestParam(required = false) String app, @RequestParam(required = false) String stream, @RequestParam(required = false) String startTime, @RequestParam(required = false) String endTime, @RequestParam(required = false) String mediaServerId, @RequestParam(required = false) String callId, @RequestParam(required = false) List ids + + ) { + log.info("[下载指定录像文件的压缩包] 查询 app->{}, stream->{}, mediaServerId->{}, startTime->{}, endTime->{}, callId->{}", app, stream, mediaServerId, startTime, endTime, callId); + + List mediaServers; + if (!ObjectUtils.isEmpty(mediaServerId)) { + mediaServers = new ArrayList<>(); + MediaServer mediaServer = mediaServerService.getOne(mediaServerId); + if (mediaServer == null) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "未找到流媒体: " + mediaServerId); + } + mediaServers.add(mediaServer); + } else { + mediaServers = mediaServerService.getAll(); + } + if (mediaServers.isEmpty()) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "当前无流媒体"); + } + if (query != null && ObjectUtils.isEmpty(query.trim())) { + query = null; + } + if (app != null && ObjectUtils.isEmpty(app.trim())) { + app = null; + } + if (stream != null && ObjectUtils.isEmpty(stream.trim())) { + stream = null; + } + if (startTime != null && ObjectUtils.isEmpty(startTime.trim())) { + startTime = null; + } + if (endTime != null && ObjectUtils.isEmpty(endTime.trim())) { + endTime = null; + } + if (callId != null && ObjectUtils.isEmpty(callId.trim())) { + callId = null; + } + // 设置响应头 + response.setContentType("application/zip"); + response.setCharacterEncoding("UTF-8"); + if (stream != null && callId != null) { + response.setHeader("Content-Disposition", "attachment;filename=" + stream + "_" + callId + ".zip"); + } else { + response.setHeader("Content-Disposition", "attachment;filename=cloud_record_" + System.currentTimeMillis() + ".zip"); + } + List cloudRecordItemList = cloudRecordService.getAllList(query, app, stream, startTime, endTime, mediaServers, callId, ids); + if (ObjectUtils.isEmpty(cloudRecordItemList)) { + return; + } + try (ZipOutputStream zos = new ZipOutputStream(response.getOutputStream())) { + for (CloudRecordItem cloudRecordItem : cloudRecordItemList) { + try { + String fileName = DateUtil.timestampMsToUrlToyyyy_MM_dd_HH_mm_ss((long)cloudRecordItem.getStartTime()) + ".mp4"; + zos.putNextEntry(new ZipEntry(fileName)); + + File file = new File(cloudRecordItem.getFilePath()); + if (!file.exists() || file.isDirectory()) { + log.warn("[下载指定录像文件的压缩包] 文件不存在或为目录: {}", cloudRecordItem.getFilePath()); + zos.closeEntry(); + continue; + } + + try (FileInputStream fis = new FileInputStream(cloudRecordItem.getFilePath())) { + byte[] buf = new byte[8192]; // 8KB 缓冲区,提高性能 + int len; + while ((len = fis.read(buf)) != -1) { + zos.write(buf, 0, len); + } + } + zos.closeEntry(); + log.debug("[下载指定录像文件的压缩包] 成功添加文件: {}", fileName); + } catch (Exception e) { + log.error("[下载指定录像文件的压缩包] 处理文件失败: {}, 错误: {}", cloudRecordItem.getFilePath(), e.getMessage()); + // 继续处理下一个文件 + } + } + } catch (IOException e) { + log.error("[下载指定录像文件的压缩包] 失败: 查询 app->{}, stream->{}, mediaServerId->{}, startTime->{}, endTime->{}, callId->{}", app, stream, mediaServerId, startTime, endTime, callId, e); + } + } + + /** + * + * @param query 检索内容 + * @param app 应用名 + * @param stream 流ID + * @param startTime 开始时间(yyyy-MM-dd HH:mm:ss) + * @param endTime 结束时间(yyyy-MM-dd HH:mm:ss) + * @param mediaServerId 流媒体ID,置空则查询全部流媒体 + * @param callId 每次录像的唯一标识,置空则查询全部流媒体 + * @param remoteHost 拼接播放地址时使用的远程地址 + */ + @ResponseBody + @GetMapping("/list-url") + @Operation(summary = "分页查询云端录像", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "query", description = "检索内容", required = false) + @Parameter(name = "app", description = "应用名", required = false) + @Parameter(name = "stream", description = "流ID", required = false) + @Parameter(name = "page", description = "当前页", required = true) + @Parameter(name = "count", description = "每页查询数量", required = true) + @Parameter(name = "startTime", description = "开始时间(yyyy-MM-dd HH:mm:ss)", required = false) + @Parameter(name = "endTime", description = "结束时间(yyyy-MM-dd HH:mm:ss)", required = false) + @Parameter(name = "mediaServerId", description = "流媒体ID,置空则查询全部流媒体", required = false) + @Parameter(name = "callId", description = "每次录像的唯一标识,置空则查询全部流媒体", required = false) + public PageInfo getListWithUrl(HttpServletRequest request, @RequestParam(required = false) String query, @RequestParam(required = false) String app, @RequestParam(required = false) String stream, @RequestParam int page, @RequestParam int count, @RequestParam(required = false) String startTime, @RequestParam(required = false) String endTime, @RequestParam(required = false) String mediaServerId, @RequestParam(required = false) String callId, @RequestParam(required = false) String remoteHost + + ) { + log.info("[云端录像] 查询URL app->{}, stream->{}, mediaServerId->{}, page->{}, count->{}, startTime->{}, endTime->{}, callId->{}", app, stream, mediaServerId, page, count, startTime, endTime, callId); + + List mediaServers; + if (!ObjectUtils.isEmpty(mediaServerId)) { + mediaServers = new ArrayList<>(); + MediaServer mediaServer = mediaServerService.getOne(mediaServerId); + if (mediaServer == null) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "未找到流媒体: " + mediaServerId); + } + mediaServers.add(mediaServer); + } else { + mediaServers = null; + } + if (query != null && ObjectUtils.isEmpty(query.trim())) { + query = null; + } + if (app != null && ObjectUtils.isEmpty(app.trim())) { + app = null; + } + if (stream != null && ObjectUtils.isEmpty(stream.trim())) { + stream = null; + } + if (startTime != null && ObjectUtils.isEmpty(startTime.trim())) { + startTime = null; + } + if (endTime != null && ObjectUtils.isEmpty(endTime.trim())) { + endTime = null; + } + if (callId != null && ObjectUtils.isEmpty(callId.trim())) { + callId = null; + } + MediaServer mediaServer = mediaServerService.getDefaultMediaServer(); + if (mediaServer == null) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "未找到流媒体节点"); + } + if (remoteHost == null) { + remoteHost = request.getScheme() + "://" + request.getLocalAddr() + ":" + (request.getScheme().equals("https") ? mediaServer.getHttpSSlPort() : mediaServer.getHttpPort()); + } + PageInfo cloudRecordItemPageInfo = cloudRecordService.getList(page, count, query, app, stream, startTime, endTime, mediaServers, callId, null); + PageInfo cloudRecordUrlPageInfo = new PageInfo<>(); + if (!ObjectUtils.isEmpty(cloudRecordItemPageInfo)) { + cloudRecordUrlPageInfo.setPageNum(cloudRecordItemPageInfo.getPageNum()); + cloudRecordUrlPageInfo.setPageSize(cloudRecordItemPageInfo.getPageSize()); + cloudRecordUrlPageInfo.setSize(cloudRecordItemPageInfo.getSize()); + cloudRecordUrlPageInfo.setEndRow(cloudRecordItemPageInfo.getEndRow()); + cloudRecordUrlPageInfo.setStartRow(cloudRecordItemPageInfo.getStartRow()); + cloudRecordUrlPageInfo.setPages(cloudRecordItemPageInfo.getPages()); + cloudRecordUrlPageInfo.setPrePage(cloudRecordItemPageInfo.getPrePage()); + cloudRecordUrlPageInfo.setNextPage(cloudRecordItemPageInfo.getNextPage()); + cloudRecordUrlPageInfo.setIsFirstPage(cloudRecordItemPageInfo.isIsFirstPage()); + cloudRecordUrlPageInfo.setIsLastPage(cloudRecordItemPageInfo.isIsLastPage()); + cloudRecordUrlPageInfo.setHasPreviousPage(cloudRecordItemPageInfo.isHasPreviousPage()); + cloudRecordUrlPageInfo.setHasNextPage(cloudRecordItemPageInfo.isHasNextPage()); + cloudRecordUrlPageInfo.setNavigatePages(cloudRecordItemPageInfo.getNavigatePages()); + cloudRecordUrlPageInfo.setNavigateFirstPage(cloudRecordItemPageInfo.getNavigateFirstPage()); + cloudRecordUrlPageInfo.setNavigateLastPage(cloudRecordItemPageInfo.getNavigateLastPage()); + cloudRecordUrlPageInfo.setNavigatepageNums(cloudRecordItemPageInfo.getNavigatepageNums()); + cloudRecordUrlPageInfo.setTotal(cloudRecordItemPageInfo.getTotal()); + List cloudRecordUrlList = new ArrayList<>(cloudRecordItemPageInfo.getList().size()); + List cloudRecordItemList = cloudRecordItemPageInfo.getList(); + for (CloudRecordItem cloudRecordItem : cloudRecordItemList) { + CloudRecordUrl cloudRecordUrl = new CloudRecordUrl(); + cloudRecordUrl.setId(cloudRecordItem.getId()); + cloudRecordUrl.setDownloadUrl(remoteHost + "/index/api/downloadFile?file_path=" + cloudRecordItem.getFilePath() + "&save_name=" + cloudRecordItem.getStream() + "_" + cloudRecordItem.getCallId() + "_" + DateUtil.timestampMsToUrlToyyyy_MM_dd_HH_mm_ss((long)cloudRecordItem.getStartTime())); + cloudRecordUrl.setPlayUrl(remoteHost + "/index/api/downloadFile?file_path=" + cloudRecordItem.getFilePath()); + cloudRecordUrlList.add(cloudRecordUrl); + } + cloudRecordUrlPageInfo.setList(cloudRecordUrlList); + } + return cloudRecordUrlPageInfo; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/vmanager/cloudRecord/bean/CloudRecordUrl.java b/src/main/java/com/genersoft/iot/vmp/vmanager/cloudRecord/bean/CloudRecordUrl.java new file mode 100644 index 0000000..bb75a09 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/vmanager/cloudRecord/bean/CloudRecordUrl.java @@ -0,0 +1,16 @@ +package com.genersoft.iot.vmp.vmanager.cloudRecord.bean; + +import lombok.Getter; +import lombok.Setter; + +@Setter +@Getter +public class CloudRecordUrl { + + private String filePath; + private String playUrl; + private String downloadUrl; + private String fileName; + private int id; + +} diff --git a/src/main/java/com/genersoft/iot/vmp/vmanager/log/LogController.java b/src/main/java/com/genersoft/iot/vmp/vmanager/log/LogController.java new file mode 100755 index 0000000..58f861a --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/vmanager/log/LogController.java @@ -0,0 +1,81 @@ +package com.genersoft.iot.vmp.vmanager.log; + +import com.genersoft.iot.vmp.conf.exception.ControllerException; +import com.genersoft.iot.vmp.conf.security.JwtUtils; +import com.genersoft.iot.vmp.service.ILogService; +import com.genersoft.iot.vmp.service.bean.LogFileInfo; +import com.genersoft.iot.vmp.vmanager.bean.ErrorCode; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.compress.utils.IOUtils; +import org.apache.commons.lang3.ObjectUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.*; + +import jakarta.servlet.ServletOutputStream; +import jakarta.servlet.http.HttpServletResponse; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.util.List; + +@SuppressWarnings("rawtypes") +@Tag(name = "日志文件查询接口") +@Slf4j +@RestController +@RequestMapping("/api/log") +public class LogController { + + @Autowired + private ILogService logService; + + + @ResponseBody + @GetMapping("/list") + @Operation(summary = "分页查询日志文件", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "query", description = "检索内容", required = false) + @Parameter(name = "startTime", description = "开始时间(yyyy-MM-dd HH:mm:ss)", required = false) + @Parameter(name = "endTime", description = "结束时间(yyyy-MM-dd HH:mm:ss)", required = false) + public List queryList(@RequestParam(required = false) String query, @RequestParam(required = false) String startTime, @RequestParam(required = false) String endTime + + ) { + if (ObjectUtils.isEmpty(query)) { + query = null; + } + if (ObjectUtils.isEmpty(startTime)) { + startTime = null; + } + if (ObjectUtils.isEmpty(endTime)) { + endTime = null; + } + return logService.queryList(query, startTime, endTime); + } + + /** + * 下载指定日志文件 + */ + @ResponseBody + @GetMapping("/file/{fileName}") + public void downloadFile(HttpServletResponse response, @PathVariable String fileName) { + try { + File file = logService.getFileByName(fileName); + if (file == null || !file.exists() || !file.isFile()) { + throw new ControllerException(ErrorCode.ERROR400); + } + final InputStream in = Files.newInputStream(file.toPath()); + response.setContentType(MediaType.TEXT_PLAIN_VALUE); + ServletOutputStream outputStream = response.getOutputStream(); + IOUtils.copy(in, response.getOutputStream()); + in.close(); + outputStream.close(); + } catch (IOException e) { + response.setStatus(HttpServletResponse.SC_NO_CONTENT); + } + } + +} diff --git a/src/main/java/com/genersoft/iot/vmp/vmanager/ps/PsController.java b/src/main/java/com/genersoft/iot/vmp/vmanager/ps/PsController.java new file mode 100755 index 0000000..efffbf9 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/vmanager/ps/PsController.java @@ -0,0 +1,281 @@ +package com.genersoft.iot.vmp.vmanager.ps; + +import com.genersoft.iot.vmp.common.VideoManagerConstants; +import com.genersoft.iot.vmp.conf.DynamicTask; +import com.genersoft.iot.vmp.conf.UserSetting; +import com.genersoft.iot.vmp.conf.exception.ControllerException; +import com.genersoft.iot.vmp.conf.security.JwtUtils; +import com.genersoft.iot.vmp.gb28181.bean.SendRtpInfo; +import com.genersoft.iot.vmp.media.bean.MediaServer; +import com.genersoft.iot.vmp.media.event.hook.Hook; +import com.genersoft.iot.vmp.media.event.hook.HookSubscribe; +import com.genersoft.iot.vmp.media.event.hook.HookType; +import com.genersoft.iot.vmp.media.service.IMediaServerService; +import com.genersoft.iot.vmp.service.ISendRtpServerService; +import com.genersoft.iot.vmp.service.bean.SSRCInfo; +import com.genersoft.iot.vmp.utils.redis.RedisUtil; +import com.genersoft.iot.vmp.vmanager.bean.ErrorCode; +import com.genersoft.iot.vmp.vmanager.bean.OtherPsSendInfo; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.extern.slf4j.Slf4j; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.web.bind.annotation.*; + +import java.io.IOException; +import java.util.List; +import java.util.UUID; +import java.util.concurrent.TimeUnit; + +@SuppressWarnings("rawtypes") +@Tag(name = "第三方PS服务对接") +@Slf4j +@RestController +@RequestMapping("/api/ps") +public class PsController { + + @Autowired + private HookSubscribe hookSubscribe; + + @Autowired + private IMediaServerService mediaServerService; + + @Autowired + private ISendRtpServerService sendRtpServerService; + + @Autowired + private UserSetting userSetting; + + @Autowired + private DynamicTask dynamicTask; + + + @Autowired + private RedisTemplate redisTemplate; + + + @GetMapping(value = "/receive/open") + @ResponseBody + @Operation(summary = "开启收流和获取发流信息", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "isSend", description = "是否发送,false时只开启收流, true同时返回推流信息", required = true) + @Parameter(name = "callId", description = "整个过程的唯一标识,为了与后续接口关联", required = true) + @Parameter(name = "ssrc", description = "来源流的SSRC,不传则不校验来源ssrc", required = false) + @Parameter(name = "stream", description = "形成的流的ID", required = true) + @Parameter(name = "tcpMode", description = "收流模式, 0为UDP, 1为TCP被动", required = true) + @Parameter(name = "callBack", description = "回调地址,如果收流超时会通道回调通知,回调为get请求,参数为callId", required = true) + public OtherPsSendInfo openRtpServer(Boolean isSend, @RequestParam(required = false)String ssrc, String callId, String stream, Integer tcpMode, String callBack) { + + log.info("[第三方PS服务对接->开启收流和获取发流信息] isSend->{}, ssrc->{}, callId->{}, stream->{}, tcpMode->{}, callBack->{}", + isSend, ssrc, callId, stream, tcpMode==0?"UDP":"TCP被动", callBack); + + MediaServer mediaServer = mediaServerService.getDefaultMediaServer(); + if (mediaServer == null) { + throw new ControllerException(ErrorCode.ERROR100.getCode(),"没有可用的MediaServer"); + } + if (stream == null) { + throw new ControllerException(ErrorCode.ERROR100.getCode(),"stream参数不可为空"); + } + if (isSend != null && isSend && callId == null) { + throw new ControllerException(ErrorCode.ERROR100.getCode(),"isSend为true时,CallID不能为空"); + } + long ssrcInt = 0; + if (ssrc != null) { + try { + ssrcInt = Long.parseLong(ssrc); + }catch (NumberFormatException e) { + throw new ControllerException(ErrorCode.ERROR100.getCode(),"ssrc格式错误"); + } + } + String receiveKey = VideoManagerConstants.WVP_OTHER_RECEIVE_PS_INFO + userSetting.getServerId() + "_" + callId + "_" + stream; + SSRCInfo ssrcInfo = mediaServerService.openRTPServer(mediaServer, stream, ssrcInt + "", false, false, null, false, false, false, tcpMode); + + if (ssrcInfo.getPort() == 0) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "获取端口失败"); + } + // 注册回调如果rtp收流超时则通过回调发送通知 + if (callBack != null) { + Hook hook = Hook.getInstance(HookType.on_rtp_server_timeout, "rtp", stream, mediaServer.getId()); + // 订阅 zlm启动事件, 新的zlm也会从这里进入系统 + hookSubscribe.addSubscribe(hook, + (hookData)->{ + if (stream.equals(hookData.getStream())) { + log.info("[第三方PS服务对接->开启收流和获取发流信息] 等待收流超时 callId->{}, 发送回调", callId); + // 将信息写入redis中,以备后用 + redisTemplate.delete(receiveKey); + OkHttpClient.Builder httpClientBuilder = new OkHttpClient.Builder(); + OkHttpClient client = httpClientBuilder.build(); + String url = callBack + "?callId=" + callId; + Request request = new Request.Builder().get().url(url).build(); + try { + client.newCall(request).execute(); + } catch (IOException e) { + log.error("[第三方PS服务对接->开启收流和获取发流信息] 等待收流超时 callId->{}, 发送回调失败", callId, e); + } + hookSubscribe.removeSubscribe(hook); + } + }); + } + OtherPsSendInfo otherPsSendInfo = new OtherPsSendInfo(); + otherPsSendInfo.setReceiveIp(mediaServer.getSdpIp()); + otherPsSendInfo.setReceivePort(ssrcInfo.getPort()); + otherPsSendInfo.setCallId(callId); + otherPsSendInfo.setStream(stream); + + // 将信息写入redis中,以备后用 + redisTemplate.opsForValue().set(receiveKey, otherPsSendInfo); + if (isSend != null && isSend) { + String key = VideoManagerConstants.WVP_OTHER_SEND_PS_INFO + userSetting.getServerId() + "_" + callId; + // 预创建发流信息 + int port = sendRtpServerService.getNextPort(mediaServer); + + otherPsSendInfo.setSendLocalIp(mediaServer.getSdpIp()); + otherPsSendInfo.setSendLocalPort(port); + // 将信息写入redis中,以备后用 + redisTemplate.opsForValue().set(key, otherPsSendInfo, 300, TimeUnit.SECONDS); + log.info("[第三方PS服务对接->开启收流和获取发流信息] 结果,callId->{}, {}", callId, otherPsSendInfo); + } + return otherPsSendInfo; + } + + @GetMapping(value = "/receive/close") + @ResponseBody + @Operation(summary = "关闭收流", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "stream", description = "流的ID", required = true) + public void closeRtpServer(String stream) { + log.info("[第三方PS服务对接->关闭收流] stream->{}", stream); + MediaServer mediaServerItem = mediaServerService.getDefaultMediaServer(); + mediaServerService.closeRTPServer(mediaServerItem, stream); + String receiveKey = VideoManagerConstants.WVP_OTHER_RECEIVE_PS_INFO + userSetting.getServerId() + "_*_" + stream; + List scan = RedisUtil.scan(redisTemplate, receiveKey); + if (!scan.isEmpty()) { + for (Object key : scan) { + // 将信息写入redis中,以备后用 + redisTemplate.delete(key); + } + } + } + + @GetMapping(value = "/send/start") + @ResponseBody + @Operation(summary = "发送流", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "ssrc", description = "发送流的SSRC", required = true) + @Parameter(name = "dstIp", description = "目标收流IP", required = true) + @Parameter(name = "dstPort", description = "目标收流端口", required = true) + @Parameter(name = "app", description = "待发送应用名", required = true) + @Parameter(name = "stream", description = "待发送流Id", required = true) + @Parameter(name = "callId", description = "整个过程的唯一标识,不传则使用随机端口发流", required = true) + @Parameter(name = "isUdp", description = "是否为UDP", required = true) + public void sendRTP(String ssrc, + String dstIp, + Integer dstPort, + String app, + String stream, + String callId, + Boolean isUdp + ) { + log.info("[第三方PS服务对接->发送流] " + + "ssrc->{}, \r\n" + + "dstIp->{}, \n" + + "dstPort->{}, \n" + + "app->{}, \n" + + "stream->{}, \n" + + "callId->{} \n", + ssrc, + dstIp, + dstPort, + app, + stream, + callId); + MediaServer mediaServer = mediaServerService.getDefaultMediaServer(); + String key = VideoManagerConstants.WVP_OTHER_SEND_PS_INFO + userSetting.getServerId() + "_" + callId; + OtherPsSendInfo sendInfo = (OtherPsSendInfo)redisTemplate.opsForValue().get(key); + if (sendInfo == null) { + sendInfo = new OtherPsSendInfo(); + } + sendInfo.setPushApp(app); + sendInfo.setPushStream(stream); + sendInfo.setPushSSRC(ssrc); + SendRtpInfo sendRtpItem = SendRtpInfo.getInstance(app, stream, ssrc, dstIp, dstPort, !isUdp, sendInfo.getSendLocalPort(), null); + Boolean streamReady = mediaServerService.isStreamReady(mediaServer, app, stream); + if (streamReady) { + mediaServerService.startSendRtp(mediaServer, sendRtpItem); + log.info("[第三方PS服务对接->发送流] 视频流发流成功,callId->{},param->{}", callId, sendRtpItem); + redisTemplate.opsForValue().set(key, sendInfo); + }else { + log.info("[第三方PS服务对接->发送流] 流不存在,等待流上线,callId->{}", callId); + String uuid = UUID.randomUUID().toString(); + Hook hook = Hook.getInstance(HookType.on_media_arrival, app, stream, mediaServer.getId()); + dynamicTask.startDelay(uuid, ()->{ + log.info("[第三方PS服务对接->发送流] 等待流上线超时 callId->{}", callId); + redisTemplate.delete(key); + hookSubscribe.removeSubscribe(hook); + }, 10000); + + // 订阅 zlm启动事件, 新的zlm也会从这里进入系统 + OtherPsSendInfo finalSendInfo = sendInfo; + hookSubscribe.removeSubscribe(hook); + hookSubscribe.addSubscribe(hook, + (hookData)->{ + dynamicTask.stop(uuid); + log.info("[第三方PS服务对接->发送流] 流上线,开始发流 callId->{}", callId); + try { + Thread.sleep(400); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + mediaServerService.startSendRtp(mediaServer, sendRtpItem); + log.info("[第三方PS服务对接->发送流] 视频流发流成功,callId->{},param->{}", callId, sendRtpItem); + redisTemplate.opsForValue().set(key, finalSendInfo); + hookSubscribe.removeSubscribe(hook); + }); + } + } + + @GetMapping(value = "/send/stop") + @ResponseBody + @Operation(summary = "关闭发送流") + @Parameter(name = "callId", description = "整个过程的唯一标识,不传则使用随机端口发流", required = true) + public void closeSendRTP(String callId) { + log.info("[第三方PS服务对接->关闭发送流] callId->{}", callId); + String key = VideoManagerConstants.WVP_OTHER_SEND_PS_INFO + userSetting.getServerId() + "_" + callId; + OtherPsSendInfo sendInfo = (OtherPsSendInfo)redisTemplate.opsForValue().get(key); + if (sendInfo == null){ + throw new ControllerException(ErrorCode.ERROR100.getCode(), "未开启发流"); + } + MediaServer mediaServerItem = mediaServerService.getDefaultMediaServer(); + boolean result = mediaServerService.stopSendRtp(mediaServerItem, sendInfo.getPushApp(), sendInfo.getStream(), sendInfo.getPushSSRC()); + if (!result) { + log.info("[第三方PS服务对接->关闭发送流] 失败 callId->{}", callId); + throw new ControllerException(ErrorCode.ERROR100.getCode(), "停止发流失败"); + }else { + log.info("[第三方PS服务对接->关闭发送流] 成功 callId->{}", callId); + } + redisTemplate.delete(key); + } + + + @GetMapping(value = "/getTestPort") + @ResponseBody + public int getTestPort() { + MediaServer defaultMediaServer = mediaServerService.getDefaultMediaServer(); + +// for (int i = 0; i <300; i++) { +// new Thread(() -> { +// int nextPort = sendRtpPortManager.getNextPort(defaultMediaServer); +// try { +// Thread.sleep((int)Math.random()*10); +// } catch (InterruptedException e) { +// throw new RuntimeException(e); +// } +// System.out.println(nextPort); +// }).start(); +// } + + return sendRtpServerService.getNextPort(defaultMediaServer); + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/vmanager/recordPlan/RecordPlanController.java b/src/main/java/com/genersoft/iot/vmp/vmanager/recordPlan/RecordPlanController.java new file mode 100644 index 0000000..8833326 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/vmanager/recordPlan/RecordPlanController.java @@ -0,0 +1,150 @@ +package com.genersoft.iot.vmp.vmanager.recordPlan; + +import com.genersoft.iot.vmp.conf.exception.ControllerException; +import com.genersoft.iot.vmp.conf.security.JwtUtils; +import com.genersoft.iot.vmp.gb28181.bean.CommonGBChannel; +import com.genersoft.iot.vmp.gb28181.service.IDeviceChannelService; +import com.genersoft.iot.vmp.service.IRecordPlanService; +import com.genersoft.iot.vmp.service.bean.RecordPlan; +import com.genersoft.iot.vmp.vmanager.bean.ErrorCode; +import com.genersoft.iot.vmp.vmanager.recordPlan.bean.RecordPlanParam; +import com.github.pagehelper.PageInfo; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.ObjectUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.util.Assert; +import org.springframework.web.bind.annotation.*; + +import java.util.ArrayList; +import java.util.List; + +@Tag(name = "录制计划") +@Slf4j +@RestController +@RequestMapping("/api/record/plan") +public class RecordPlanController { + + @Autowired + private IRecordPlanService recordPlanService; + + @Autowired + private IDeviceChannelService deviceChannelService; + + + @ResponseBody + @PostMapping("/add") + @Operation(summary = "添加录制计划", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "plan", description = "计划", required = true) + public void add(@RequestBody RecordPlan plan) { + if (plan.getPlanItemList() == null || plan.getPlanItemList().isEmpty()) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "添加录制计划时,录制计划不可为空"); + } + recordPlanService.add(plan); + } + + @ResponseBody + @PostMapping("/link") + @Operation(summary = "通道关联录制计划", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "param", description = "通道关联录制计划", required = true) + public void link(@RequestBody RecordPlanParam param) { + if (param.getAllLink() != null) { + if (param.getAllLink()) { + recordPlanService.linkAll(param.getPlanId()); + }else { + recordPlanService.cleanAll(param.getPlanId()); + } + return; + } + + if (param.getChannelIds() == null && param.getDeviceDbIds() == null) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "通道ID和国标设备ID不可都为NULL"); + } + + List channelIds = new ArrayList<>(); + if (param.getChannelIds() != null) { + channelIds.addAll(param.getChannelIds()); + }else { + List chanelIdList = deviceChannelService.queryChaneIdListByDeviceDbIds(param.getDeviceDbIds()); + if (chanelIdList != null && !chanelIdList.isEmpty()) { + channelIds = chanelIdList; + } + } + recordPlanService.link(channelIds, param.getPlanId()); + } + + @ResponseBody + @GetMapping("/get") + @Operation(summary = "查询录制计划", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "planId", description = "计划ID", required = true) + public RecordPlan get(Integer planId) { + if (planId == null) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "计划ID不可为NULL"); + } + return recordPlanService.get(planId); + } + + @ResponseBody + @GetMapping("/query") + @Operation(summary = "查询录制计划列表", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "query", description = "检索内容", required = false) + @Parameter(name = "page", description = "当前页", required = true) + @Parameter(name = "count", description = "每页查询数量", required = true) + public PageInfo query(@RequestParam(required = false) String query, @RequestParam Integer page, @RequestParam Integer count) { + if (query != null && ObjectUtils.isEmpty(query.trim())) { + query = null; + } + return recordPlanService.query(page, count, query); + } + + @Operation(summary = "分页查询录制计划关联的所有通道", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "page", description = "当前页", required = true) + @Parameter(name = "count", description = "每页条数", required = true) + @Parameter(name = "planId", description = "录制计划ID") + @Parameter(name = "channelType", description = "通道类型, 0:国标设备,1:推流设备,2:拉流代理") + @Parameter(name = "query", description = "查询内容") + @Parameter(name = "online", description = "是否在线") + @Parameter(name = "hasLink", description = "是否已经关联") + @GetMapping("/channel/list") + @ResponseBody + public PageInfo queryChannelList(int page, int count, + @RequestParam(required = false) Integer planId, + @RequestParam(required = false) String query, + @RequestParam(required = false) Integer channelType, + @RequestParam(required = false) Boolean online, + @RequestParam(required = false) Boolean hasLink) { + + Assert.notNull(planId, "录制计划ID不可为NULL"); + if (org.springframework.util.ObjectUtils.isEmpty(query)) { + query = null; + } + + return recordPlanService.queryChannelList(page, count, query, channelType, online, planId, hasLink); + } + + @ResponseBody + @PostMapping("/update") + @Operation(summary = "更新录制计划", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "plan", description = "计划", required = true) + public void update(@RequestBody RecordPlan plan) { + if (plan == null || plan.getId() == 0) { + throw new ControllerException(ErrorCode.ERROR400); + } + recordPlanService.update(plan); + } + + @ResponseBody + @DeleteMapping("/delete") + @Operation(summary = "删除录制计划", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "planId", description = "计划ID", required = true) + public void delete(Integer planId) { + if (planId == null) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "计划IDID不可为NULL"); + } + recordPlanService.delete(planId); + } + +} diff --git a/src/main/java/com/genersoft/iot/vmp/vmanager/recordPlan/bean/RecordPlanParam.java b/src/main/java/com/genersoft/iot/vmp/vmanager/recordPlan/bean/RecordPlanParam.java new file mode 100644 index 0000000..9f56a0f --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/vmanager/recordPlan/bean/RecordPlanParam.java @@ -0,0 +1,23 @@ +package com.genersoft.iot.vmp.vmanager.recordPlan.bean; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.util.List; + +@Data +@Schema(description = "录制计划-添加/编辑参数") +public class RecordPlanParam { + + @Schema(description = "关联的通道ID") + private List channelIds; + + @Schema(description = "关联的设备ID,会为设备下的所有通道关联此录制计划,channelId存在是此项不生效,") + private List deviceDbIds; + + @Schema(description = "全部关联/全部取消关联") + private Boolean allLink; + + @Schema(description = "录制计划ID, ID为空是删除关联的计划") + private Integer planId; +} diff --git a/src/main/java/com/genersoft/iot/vmp/vmanager/rtp/RtpController.java b/src/main/java/com/genersoft/iot/vmp/vmanager/rtp/RtpController.java new file mode 100755 index 0000000..b78e9a6 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/vmanager/rtp/RtpController.java @@ -0,0 +1,307 @@ +package com.genersoft.iot.vmp.vmanager.rtp; + +import com.genersoft.iot.vmp.common.VideoManagerConstants; +import com.genersoft.iot.vmp.conf.DynamicTask; +import com.genersoft.iot.vmp.conf.UserSetting; +import com.genersoft.iot.vmp.conf.exception.ControllerException; +import com.genersoft.iot.vmp.conf.security.JwtUtils; +import com.genersoft.iot.vmp.gb28181.bean.SendRtpInfo; +import com.genersoft.iot.vmp.media.bean.MediaServer; +import com.genersoft.iot.vmp.media.event.hook.Hook; +import com.genersoft.iot.vmp.media.event.hook.HookSubscribe; +import com.genersoft.iot.vmp.media.event.hook.HookType; +import com.genersoft.iot.vmp.media.service.IMediaServerService; +import com.genersoft.iot.vmp.service.ISendRtpServerService; +import com.genersoft.iot.vmp.service.bean.SSRCInfo; +import com.genersoft.iot.vmp.utils.redis.RedisUtil; +import com.genersoft.iot.vmp.vmanager.bean.ErrorCode; +import com.genersoft.iot.vmp.vmanager.bean.OtherRtpSendInfo; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.extern.slf4j.Slf4j; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.util.ObjectUtils; +import org.springframework.web.bind.annotation.*; + +import java.io.IOException; +import java.util.List; +import java.util.UUID; +import java.util.concurrent.TimeUnit; + +@SuppressWarnings("rawtypes") +@Tag(name = "第三方服务对接") +@Slf4j +@RestController +@RequestMapping("/api/rtp") +public class RtpController { + + @Autowired + private ISendRtpServerService sendRtpServerService; + + @Autowired + private HookSubscribe hookSubscribe; + + @Autowired + private IMediaServerService mediaServerService; + + @Autowired + private UserSetting userSetting; + + @Autowired + private DynamicTask dynamicTask; + + @Autowired + private RedisTemplate redisTemplate; + + + @GetMapping(value = "/receive/open") + @ResponseBody + @Operation(summary = "开启收流和获取发流信息", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "isSend", description = "是否发送,false时只开启收流, true同时返回推流信息", required = true) + @Parameter(name = "callId", description = "整个过程的唯一标识,为了与后续接口关联", required = true) + @Parameter(name = "ssrc", description = "来源流的SSRC,不传则不校验来源ssrc", required = false) + @Parameter(name = "stream", description = "形成的流的ID", required = true) + @Parameter(name = "tcpMode", description = "收流模式, 0为UDP, 1为TCP被动", required = true) + @Parameter(name = "callBack", description = "回调地址,如果收流超时会通道回调通知,回调为get请求,参数为callId", required = true) + public OtherRtpSendInfo openRtpServer(Boolean isSend, @RequestParam(required = false)String ssrc, String callId, String stream, Integer tcpMode, String callBack) { + + log.info("[第三方服务对接->开启收流和获取发流信息] isSend->{}, ssrc->{}, callId->{}, stream->{}, tcpMode->{}, callBack->{}", + isSend, ssrc, callId, stream, tcpMode==0?"UDP":"TCP被动", callBack); + + MediaServer mediaServer = mediaServerService.getDefaultMediaServer(); + if (mediaServer == null) { + throw new ControllerException(ErrorCode.ERROR100.getCode(),"没有可用的MediaServer"); + } + if (stream == null) { + throw new ControllerException(ErrorCode.ERROR100.getCode(),"stream参数不可为空"); + } + if (isSend != null && isSend && callId == null) { + throw new ControllerException(ErrorCode.ERROR100.getCode(),"isSend为true时,CallID不能为空"); + } + long ssrcInt = 0; + if (ssrc != null) { + try { + ssrcInt = Long.parseLong(ssrc); + }catch (NumberFormatException e) { + throw new ControllerException(ErrorCode.ERROR100.getCode(),"ssrc格式错误"); + } + } + String receiveKey = VideoManagerConstants.WVP_OTHER_RECEIVE_RTP_INFO + userSetting.getServerId() + "_" + callId + "_" + stream; + SSRCInfo ssrcInfoForVideo = mediaServerService.openRTPServer(mediaServer, stream, ssrcInt + "",false,false, null, false, false, false, tcpMode); + SSRCInfo ssrcInfoForAudio = mediaServerService.openRTPServer(mediaServer, stream + "_a", ssrcInt + "", false, false, null, false,false,false, tcpMode); + if (ssrcInfoForVideo.getPort() == 0 || ssrcInfoForAudio.getPort() == 0) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "获取端口失败"); + } + // 注册回调如果rtp收流超时则通过回调发送通知 + if (callBack != null) { + Hook hook = Hook.getInstance(HookType.on_rtp_server_timeout, "rtp", stream, mediaServer.getId()); + // 订阅 zlm启动事件, 新的zlm也会从这里进入系统 + hookSubscribe.addSubscribe(hook, + (hookData)->{ + if (stream.equals(hookData.getStream())) { + log.info("[开启收流和获取发流信息] 等待收流超时 callId->{}, 发送回调", callId); + OkHttpClient.Builder httpClientBuilder = new OkHttpClient.Builder(); + OkHttpClient client = httpClientBuilder.build(); + String url = callBack + "?callId=" + callId; + Request request = new Request.Builder().get().url(url).build(); + try { + client.newCall(request).execute(); + } catch (IOException e) { + log.error("[第三方服务对接->开启收流和获取发流信息] 等待收流超时 callId->{}, 发送回调失败", callId, e); + } + hookSubscribe.removeSubscribe(hook); + } + }); + } + String key = VideoManagerConstants.WVP_OTHER_SEND_RTP_INFO + userSetting.getServerId() + "_" + callId; + OtherRtpSendInfo otherRtpSendInfo = new OtherRtpSendInfo(); + otherRtpSendInfo.setReceiveIp(mediaServer.getSdpIp()); + otherRtpSendInfo.setReceivePortForVideo(ssrcInfoForVideo.getPort()); + otherRtpSendInfo.setReceivePortForAudio(ssrcInfoForAudio.getPort()); + otherRtpSendInfo.setCallId(callId); + otherRtpSendInfo.setStream(stream); + + // 将信息写入redis中,以备后用 + redisTemplate.opsForValue().set(receiveKey, otherRtpSendInfo); + if (isSend != null && isSend) { + // 预创建发流信息 + int portForVideo = sendRtpServerService.getNextPort(mediaServer); + int portForAudio = sendRtpServerService.getNextPort(mediaServer); + + otherRtpSendInfo.setSendLocalIp(mediaServer.getSdpIp()); + otherRtpSendInfo.setSendLocalPortForVideo(portForVideo); + otherRtpSendInfo.setSendLocalPortForAudio(portForAudio); + // 将信息写入redis中,以备后用 + redisTemplate.opsForValue().set(key, otherRtpSendInfo, 300, TimeUnit.SECONDS); + log.info("[第三方服务对接->开启收流和获取发流信息] 结果,callId->{}, {}", callId, otherRtpSendInfo); + } + // 将信息写入redis中,以备后用 + redisTemplate.opsForValue().set(key, otherRtpSendInfo, 300, TimeUnit.SECONDS); + return otherRtpSendInfo; + } + + @GetMapping(value = "/receive/close") + @ResponseBody + @Operation(summary = "关闭收流", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "stream", description = "流的ID", required = true) + public void closeRtpServer(String stream) { + log.info("[第三方服务对接->关闭收流] stream->{}", stream); + MediaServer mediaServerItem = mediaServerService.getDefaultMediaServer(); + mediaServerService.closeRTPServer(mediaServerItem, stream); + mediaServerService.closeRTPServer(mediaServerItem, stream+ "_a"); + String receiveKey = VideoManagerConstants.WVP_OTHER_RECEIVE_RTP_INFO + userSetting.getServerId() + "_*_" + stream; + List scan = RedisUtil.scan(redisTemplate, receiveKey); + if (scan.size() > 0) { + for (Object key : scan) { + // 将信息写入redis中,以备后用 + redisTemplate.delete(key); + } + } + } + + @GetMapping(value = "/send/start") + @ResponseBody + @Operation(summary = "发送流", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "ssrc", description = "发送流的SSRC", required = true) + @Parameter(name = "dstIpForAudio", description = "目标音频收流IP", required = false) + @Parameter(name = "dstIpForVideo", description = "目标视频收流IP", required = false) + @Parameter(name = "dstPortForAudio", description = "目标音频收流端口", required = false) + @Parameter(name = "dstPortForVideo", description = "目标视频收流端口", required = false) + @Parameter(name = "app", description = "待发送应用名", required = true) + @Parameter(name = "stream", description = "待发送流Id", required = true) + @Parameter(name = "callId", description = "整个过程的唯一标识,不传则使用随机端口发流", required = true) + @Parameter(name = "isUdp", description = "是否为UDP", required = true) + @Parameter(name = "ptForAudio", description = "rtp的音频pt", required = false) + @Parameter(name = "ptForVideo", description = "rtp的视频pt", required = false) + public void sendRTP(String ssrc, + @RequestParam(required = false)String dstIpForAudio, + @RequestParam(required = false)String dstIpForVideo, + @RequestParam(required = false)Integer dstPortForAudio, + @RequestParam(required = false)Integer dstPortForVideo, + String app, + String stream, + String callId, + Boolean isUdp, + @RequestParam(required = false)Integer ptForAudio, + @RequestParam(required = false)Integer ptForVideo + ) { + log.info("[第三方服务对接->发送流] " + + "ssrc->{}, \r\n" + + "dstIpForAudio->{}, \n" + + "dstIpForAudio->{}, \n" + + "dstPortForAudio->{}, \n" + + "dstPortForVideo->{}, \n" + + "app->{}, \n" + + "stream->{}, \n" + + "callId->{}, \n" + + "ptForAudio->{}, \n" + + "ptForVideo->{}", + ssrc, + dstIpForAudio, + dstIpForVideo, + dstPortForAudio, + dstPortForVideo, + app, + stream, + callId, + ptForAudio, + ptForVideo); + if (!((dstPortForAudio > 0 && !ObjectUtils.isEmpty(dstPortForAudio) || (dstPortForVideo > 0 && !ObjectUtils.isEmpty(dstIpForVideo))))) { + throw new ControllerException(ErrorCode.ERROR400.getCode(), "至少应该存在一组音频或视频发送参数"); + } + MediaServer mediaServer = mediaServerService.getDefaultMediaServer(); + String key = VideoManagerConstants.WVP_OTHER_SEND_RTP_INFO + userSetting.getServerId() + "_" + callId; + OtherRtpSendInfo sendInfo = (OtherRtpSendInfo)redisTemplate.opsForValue().get(key); + if (sendInfo == null) { + sendInfo = new OtherRtpSendInfo(); + } + sendInfo.setPushApp(app); + sendInfo.setPushStream(stream); + sendInfo.setPushSSRC(ssrc); + + + SendRtpInfo sendRtpItemForVideo; + SendRtpInfo sendRtpItemForAudio; + if (!ObjectUtils.isEmpty(dstIpForAudio) && dstPortForAudio > 0) { + sendRtpItemForAudio = SendRtpInfo.getInstance(app, stream, ssrc, dstIpForAudio, dstPortForAudio, !isUdp, sendInfo.getSendLocalPortForAudio(), ptForAudio); + } else { + sendRtpItemForAudio = null; + } + if (!ObjectUtils.isEmpty(dstIpForVideo) && dstPortForVideo > 0) { + sendRtpItemForVideo = SendRtpInfo.getInstance(app, stream, ssrc, dstIpForAudio, dstPortForAudio, !isUdp, sendInfo.getSendLocalPortForVideo(), ptForVideo); + } else { + sendRtpItemForVideo = null; + } + + Boolean streamReady = mediaServerService.isStreamReady(mediaServer, app, stream); + if (streamReady) { + if (sendRtpItemForVideo != null) { + mediaServerService.startSendRtp(mediaServer, sendRtpItemForVideo); + log.info("[第三方服务对接->发送流] 视频流发流成功,callId->{},param->{}", callId, sendRtpItemForVideo); + redisTemplate.opsForValue().set(key, sendInfo); + } + if(sendRtpItemForAudio != null) { + mediaServerService.startSendRtp(mediaServer, sendRtpItemForAudio); + log.info("[第三方服务对接->发送流] 音频流发流成功,callId->{},param->{}", callId, sendRtpItemForAudio); + redisTemplate.opsForValue().set(key, sendInfo); + } + }else { + log.info("[第三方服务对接->发送流] 流不存在,等待流上线,callId->{}", callId); + String uuid = UUID.randomUUID().toString(); + Hook hook = Hook.getInstance(HookType.on_media_arrival, app, stream, mediaServer.getId()); + dynamicTask.startDelay(uuid, ()->{ + log.info("[第三方服务对接->发送流] 等待流上线超时 callId->{}", callId); + redisTemplate.delete(key); + hookSubscribe.removeSubscribe(hook); + }, 10000); + + // 订阅 zlm启动事件, 新的zlm也会从这里进入系统 + hookSubscribe.removeSubscribe(hook); + OtherRtpSendInfo finalSendInfo = sendInfo; + hookSubscribe.addSubscribe(hook, + (hookData)->{ + dynamicTask.stop(uuid); + log.info("[第三方服务对接->发送流] 流上线,开始发流 callId->{}", callId); + try { + Thread.sleep(400); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + if (sendRtpItemForVideo != null) { + mediaServerService.startSendRtp(mediaServer, sendRtpItemForVideo); + log.info("[第三方服务对接->发送流] 视频流发流成功,callId->{},param->{}", callId, sendRtpItemForVideo); + redisTemplate.opsForValue().set(key, finalSendInfo); + } + if(sendRtpItemForAudio != null) { + mediaServerService.startSendRtp(mediaServer, sendRtpItemForAudio); + log.info("[第三方服务对接->发送流] 音频流发流成功,callId->{},param->{}", callId, sendRtpItemForAudio); + redisTemplate.opsForValue().set(key, finalSendInfo); + } + hookSubscribe.removeSubscribe(hook); + }); + } + } + + @GetMapping(value = "/send/stop") + @ResponseBody + @Operation(summary = "关闭发送流", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "callId", description = "整个过程的唯一标识,不传则使用随机端口发流", required = true) + public void closeSendRTP(String callId) { + log.info("[第三方服务对接->关闭发送流] callId->{}", callId); + String key = VideoManagerConstants.WVP_OTHER_SEND_RTP_INFO + userSetting.getServerId() + "_" + callId; + OtherRtpSendInfo sendInfo = (OtherRtpSendInfo)redisTemplate.opsForValue().get(key); + if (sendInfo == null){ + throw new ControllerException(ErrorCode.ERROR100.getCode(), "未开启发流"); + } + MediaServer mediaServerItem = mediaServerService.getDefaultMediaServer(); + mediaServerService.stopSendRtp(mediaServerItem, sendInfo.getPushApp(), sendInfo.getPushStream(), sendInfo.getPushSSRC()); + log.info("[第三方服务对接->关闭发送流] 成功 callId->{}", callId); + redisTemplate.delete(key); + } + +} diff --git a/src/main/java/com/genersoft/iot/vmp/vmanager/server/ServerController.java b/src/main/java/com/genersoft/iot/vmp/vmanager/server/ServerController.java new file mode 100755 index 0000000..2361573 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/vmanager/server/ServerController.java @@ -0,0 +1,384 @@ +package com.genersoft.iot.vmp.vmanager.server; + +import com.alibaba.fastjson2.JSON; +import com.alibaba.fastjson2.JSONObject; +import com.genersoft.iot.vmp.common.SystemAllInfo; +import com.genersoft.iot.vmp.common.VersionPo; +import com.genersoft.iot.vmp.conf.SipConfig; +import com.genersoft.iot.vmp.conf.UserSetting; +import com.genersoft.iot.vmp.conf.VersionInfo; +import com.genersoft.iot.vmp.conf.exception.ControllerException; +import com.genersoft.iot.vmp.conf.security.JwtUtils; +import com.genersoft.iot.vmp.gb28181.service.IDeviceChannelService; +import com.genersoft.iot.vmp.gb28181.service.IDeviceService; +import com.genersoft.iot.vmp.jt1078.config.JT1078Config; +import com.genersoft.iot.vmp.media.bean.MediaInfo; +import com.genersoft.iot.vmp.media.bean.MediaServer; +import com.genersoft.iot.vmp.media.event.mediaServer.MediaServerChangeEvent; +import com.genersoft.iot.vmp.media.service.IMediaServerService; +import com.genersoft.iot.vmp.service.IMapService; +import com.genersoft.iot.vmp.service.bean.MediaServerLoad; +import com.genersoft.iot.vmp.storager.IRedisCatchStorage; +import com.genersoft.iot.vmp.streamProxy.service.IStreamProxyService; +import com.genersoft.iot.vmp.streamPush.service.IStreamPushService; +import com.genersoft.iot.vmp.vmanager.bean.*; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.servlet.http.HttpServletRequest; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.util.ObjectUtils; +import org.springframework.web.bind.annotation.*; +import oshi.SystemInfo; +import oshi.hardware.CentralProcessor; +import oshi.hardware.GlobalMemory; +import oshi.hardware.HardwareAbstractionLayer; +import oshi.hardware.NetworkIF; +import oshi.software.os.OperatingSystem; + +import java.io.File; +import java.text.DecimalFormat; +import java.util.*; + +@SuppressWarnings("rawtypes") +@Tag(name = "服务控制") +@Slf4j +@RestController +@RequestMapping("/api/server") +public class ServerController { + + + @Autowired + private IMediaServerService mediaServerService; + + @Autowired + private VersionInfo versionInfo; + + @Autowired + private SipConfig sipConfig; + + @Autowired + private UserSetting userSetting; + + @Autowired + private JT1078Config jt1078Config; + + @Autowired + private IDeviceService deviceService; + + @Autowired + private IDeviceChannelService channelService; + + @Autowired + private IStreamPushService pushService; + + @Autowired + private IStreamProxyService proxyService; + + + @Autowired(required = false) + private IMapService mapService; + + @Value("${server.port}") + private int serverPort; + + + @Autowired + private IRedisCatchStorage redisCatchStorage; + + @Autowired + private ApplicationEventPublisher applicationEventPublisher; + + + @GetMapping(value = "/media_server/list") + @ResponseBody + @Operation(summary = "流媒体服务列表", security = @SecurityRequirement(name = JwtUtils.HEADER)) + public List getMediaServerList() { + return mediaServerService.getAll(); + } + + @GetMapping(value = "/media_server/online/list") + @ResponseBody + @Operation(summary = "在线流媒体服务列表", security = @SecurityRequirement(name = JwtUtils.HEADER)) + public List getOnlineMediaServerList() { + return mediaServerService.getAllOnline(); + } + + @GetMapping(value = "/media_server/one/{id}") + @ResponseBody + @Operation(summary = "停止视频回放", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "id", description = "流媒体服务ID", required = true) + public MediaServer getMediaServer(@PathVariable String id) { + return mediaServerService.getOne(id); + } + + @Operation(summary = "测试流媒体服务", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "ip", description = "流媒体服务IP", required = true) + @Parameter(name = "port", description = "流媒体服务HTT端口", required = true) + @Parameter(name = "secret", description = "流媒体服务secret", required = true) + @GetMapping(value = "/media_server/check") + @ResponseBody + public MediaServer checkMediaServer(@RequestParam String ip, @RequestParam int port, @RequestParam String secret, @RequestParam String type) { + return mediaServerService.checkMediaServer(ip, port, secret, type); + } + + @Operation(summary = "测试流媒体录像管理服务", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "ip", description = "流媒体服务IP", required = true) + @Parameter(name = "port", description = "流媒体服务HTT端口", required = true) + @GetMapping(value = "/media_server/record/check") + @ResponseBody + public void checkMediaRecordServer(@RequestParam String ip, @RequestParam int port) { + boolean checkResult = mediaServerService.checkMediaRecordServer(ip, port); + if (!checkResult) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "连接失败"); + } + } + + @Operation(summary = "保存流媒体服务", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "mediaServerItem", description = "流媒体信息", required = true) + @PostMapping(value = "/media_server/save") + @ResponseBody + public void saveMediaServer(@RequestBody MediaServer mediaServer) { + MediaServer mediaServerItemInDatabase = mediaServerService.getOneFromDatabase(mediaServer.getId()); + + if (mediaServerItemInDatabase != null) { + mediaServerService.update(mediaServer); + } else { + mediaServerService.add(mediaServer); + // 发送事件 + MediaServerChangeEvent event = new MediaServerChangeEvent(this); + event.setMediaServerItemList(mediaServer); + applicationEventPublisher.publishEvent(event); + } + } + + @Operation(summary = "移除流媒体服务", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "id", description = "流媒体ID", required = true) + @DeleteMapping(value = "/media_server/delete") + @ResponseBody + public void deleteMediaServer(@RequestParam String id) { + MediaServer mediaServer = mediaServerService.getOne(id); + if (mediaServer == null) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "流媒体不存在"); + } + mediaServerService.delete(mediaServer); + } + + @Operation(summary = "获取流信息", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "app", description = "应用名", required = true) + @Parameter(name = "stream", description = "流ID", required = true) + @Parameter(name = "mediaServerId", description = "流媒体ID", required = true) + @GetMapping(value = "/media_server/media_info") + @ResponseBody + public MediaInfo getMediaInfo(String app, String stream, String mediaServerId) { + MediaServer mediaServer = mediaServerService.getOne(mediaServerId); + if (mediaServer == null) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "流媒体不存在"); + } + return mediaServerService.getMediaInfo(mediaServer, app, stream); + } + + + @Operation(summary = "关闭服务", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @GetMapping(value = "/shutdown") + @ResponseBody + public void shutdown() { + log.info("正在关闭服务。。。"); + System.exit(1); + } + + @Operation(summary = "获取系统配置信息", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @GetMapping(value = "/system/configInfo") + @ResponseBody + public SystemConfigInfo getConfigInfo() { + SystemConfigInfo systemConfigInfo = new SystemConfigInfo(); + systemConfigInfo.setVersion(versionInfo.getVersion()); + systemConfigInfo.setSip(sipConfig); + systemConfigInfo.setAddOn(userSetting); + systemConfigInfo.setServerPort(serverPort); + systemConfigInfo.setJt1078Config(jt1078Config); + return systemConfigInfo; + } + + @Operation(summary = "获取版本信息", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @GetMapping(value = "/version") + @ResponseBody + public VersionPo VersionPogetVersion() { + return versionInfo.getVersion(); + } + + @GetMapping(value = "/config") + @Operation(summary = "获取配置信息", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "type", description = "配置类型(sip, base)", required = true) + @ResponseBody + public JSONObject getVersion(String type) { + + JSONObject jsonObject = new JSONObject(); + jsonObject.put("server.port", serverPort); + if (ObjectUtils.isEmpty(type)) { + jsonObject.put("sip", JSON.toJSON(sipConfig)); + jsonObject.put("base", JSON.toJSON(userSetting)); + } else { + switch (type) { + case "sip": + jsonObject.put("sip", sipConfig); + break; + case "base": + jsonObject.put("base", userSetting); + break; + default: + break; + } + } + return jsonObject; + } + + @GetMapping(value = "/system/info") + @ResponseBody + @Operation(summary = "获取系统信息", security = @SecurityRequirement(name = JwtUtils.HEADER)) + public SystemAllInfo getSystemInfo() { + SystemAllInfo systemAllInfo = redisCatchStorage.getSystemInfo(); + + return systemAllInfo; + } + + @GetMapping(value = "/media_server/load") + @ResponseBody + @Operation(summary = "获取负载信息", security = @SecurityRequirement(name = JwtUtils.HEADER)) + public List getMediaLoad() { + List result = new ArrayList<>(); + List allOnline = mediaServerService.getAllOnline(); + if (allOnline.isEmpty()) { + return result; + } else { + for (MediaServer mediaServerItem : allOnline) { + result.add(mediaServerService.getLoad(mediaServerItem)); + } + } + return result; + } + + @GetMapping(value = "/resource/info") + @ResponseBody + @Operation(summary = "获取负载信息", security = @SecurityRequirement(name = JwtUtils.HEADER)) + public ResourceInfo getResourceInfo() { + ResourceInfo result = new ResourceInfo(); + ResourceBaseInfo deviceInfo = deviceService.getOverview(); + result.setDevice(deviceInfo); + ResourceBaseInfo channelInfo = channelService.getOverview(); + result.setChannel(channelInfo); + ResourceBaseInfo pushInfo = pushService.getOverview(); + result.setPush(pushInfo); + ResourceBaseInfo proxyInfo = proxyService.getOverview(); + result.setProxy(proxyInfo); + + return result; + } + + @GetMapping(value = "/info") + @ResponseBody + @Operation(summary = "获取系统信息", security = @SecurityRequirement(name = JwtUtils.HEADER)) + public Map> getInfo(HttpServletRequest request) { + Map> result = new LinkedHashMap<>(); + Map hardwareMap = new LinkedHashMap<>(); + result.put("硬件信息", hardwareMap); + + SystemInfo systemInfo = new SystemInfo(); + HardwareAbstractionLayer hardware = systemInfo.getHardware(); + // 获取CPU信息 + CentralProcessor.ProcessorIdentifier processorIdentifier = hardware.getProcessor().getProcessorIdentifier(); + hardwareMap.put("CPU", processorIdentifier.getName()); + // 获取内存 + GlobalMemory memory = hardware.getMemory(); + hardwareMap.put("内存", formatByte(memory.getTotal() - memory.getAvailable()) + "/" + formatByte(memory.getTotal())); + hardwareMap.put("制造商", systemInfo.getHardware().getComputerSystem().getManufacturer()); + hardwareMap.put("产品名称", systemInfo.getHardware().getComputerSystem().getModel()); + // 网卡 + List networkIFs = hardware.getNetworkIFs(); + StringBuilder ips = new StringBuilder(); + for (int i = 0; i < networkIFs.size(); i++) { + NetworkIF networkIF = networkIFs.get(i); + String ipsStr = StringUtils.join(networkIF.getIPv4addr()); + if (ObjectUtils.isEmpty(ipsStr)) { + continue; + } + ips.append(ipsStr); + if (i < networkIFs.size() - 1) { + ips.append(","); + } + } + hardwareMap.put("网卡", ips.toString()); + + Map operatingSystemMap = new LinkedHashMap<>(); + result.put("操作系统", operatingSystemMap); + OperatingSystem operatingSystem = systemInfo.getOperatingSystem(); + operatingSystemMap.put("名称", operatingSystem.getFamily() + " " + operatingSystem.getVersionInfo().getVersion()); + operatingSystemMap.put("类型", operatingSystem.getManufacturer()); + + Map platformMap = new LinkedHashMap<>(); + result.put("平台信息", platformMap); + VersionPo version = versionInfo.getVersion(); + platformMap.put("版本", version.getVersion()); + platformMap.put("构建日期", version.getBUILD_DATE()); + platformMap.put("GIT分支", version.getGIT_BRANCH()); + platformMap.put("GIT地址", version.getGIT_URL()); + platformMap.put("GIT日期", version.getGIT_DATE()); + platformMap.put("GIT版本", version.getGIT_Revision_SHORT()); + platformMap.put("DOCKER环境", new File("/.dockerenv").exists()?"是":"否"); + + Map docmap = new LinkedHashMap<>(); + result.put("文档地址", docmap); + docmap.put("部署文档", "https://doc.wvp-pro.cn"); + docmap.put("接口文档", String.format("%s://%s:%s/doc.html", request.getScheme(), request.getServerName(), request.getServerPort())); + + + return result; + } + + /** + * 单位转换 + */ + private static String formatByte(long byteNumber) { + //换算单位 + double FORMAT = 1024.0; + double kbNumber = byteNumber / FORMAT; + if (kbNumber < FORMAT) { + return new DecimalFormat("#.##KB").format(kbNumber); + } + double mbNumber = kbNumber / FORMAT; + if (mbNumber < FORMAT) { + return new DecimalFormat("#.##MB").format(mbNumber); + } + double gbNumber = mbNumber / FORMAT; + if (gbNumber < FORMAT) { + return new DecimalFormat("#.##GB").format(gbNumber); + } + double tbNumber = gbNumber / FORMAT; + return new DecimalFormat("#.##TB").format(tbNumber); + } + + @GetMapping(value = "/map/config") + @ResponseBody + @Operation(summary = "获取地图配置", security = @SecurityRequirement(name = JwtUtils.HEADER)) + public List getMapConfig() { + if (mapService == null) { + return Collections.emptyList(); + } + return mapService.getConfig(); + } + + @GetMapping(value = "/map/model-icon/list") + @ResponseBody + @Operation(summary = "获取地图配置图标", security = @SecurityRequirement(name = JwtUtils.HEADER)) + public List getMapModelIconList() { + if (mapService == null) { + return Collections.emptyList(); + } + return mapService.getModelList(); + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/vmanager/user/RoleController.java b/src/main/java/com/genersoft/iot/vmp/vmanager/user/RoleController.java new file mode 100755 index 0000000..9969087 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/vmanager/user/RoleController.java @@ -0,0 +1,76 @@ +package com.genersoft.iot.vmp.vmanager.user; + +import com.genersoft.iot.vmp.conf.exception.ControllerException; +import com.genersoft.iot.vmp.conf.security.JwtUtils; +import com.genersoft.iot.vmp.conf.security.SecurityUtils; +import com.genersoft.iot.vmp.service.IRoleService; +import com.genersoft.iot.vmp.storager.dao.dto.Role; +import com.genersoft.iot.vmp.utils.DateUtil; +import com.genersoft.iot.vmp.vmanager.bean.ErrorCode; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@Tag(name = "角色管理") + +@RestController +@RequestMapping("/api/role") +public class RoleController { + + @Autowired + private IRoleService roleService; + + @PostMapping("/add") + @Operation(summary = "添加角色", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "name", description = "角色名", required = true) + @Parameter(name = "authority", description = "权限(自行定义内容,目前未使用)", required = true) + public void add(@RequestParam String name, + @RequestParam(required = false) String authority){ + // 获取当前登录用户id + int currenRoleId = SecurityUtils.getUserInfo().getRole().getId(); + if (currenRoleId != 1) { + // 只用角色id为0才可以删除和添加用户 + throw new ControllerException(ErrorCode.ERROR403); + } + + Role role = new Role(); + role.setName(name); + role.setAuthority(authority); + role.setCreateTime(DateUtil.getNow()); + role.setUpdateTime(DateUtil.getNow()); + + int addResult = roleService.add(role); + if (addResult <= 0) { + throw new ControllerException(ErrorCode.ERROR100); + } + } + + @DeleteMapping("/delete") + @Operation(summary = "删除角色", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "id", description = "用户Id", required = true) + public void delete(@RequestParam Integer id){ + // 获取当前登录用户id + int currenRoleId = SecurityUtils.getUserInfo().getRole().getId(); + if (currenRoleId != 1) { + // 只用角色id为0才可以删除和添加用户 + throw new ControllerException(ErrorCode.ERROR403); + } + int deleteResult = roleService.delete(id); + + if (deleteResult <= 0) { + throw new ControllerException(ErrorCode.ERROR100); + } + } + + @GetMapping("/all") + @Operation(summary = "查询角色", security = @SecurityRequirement(name = JwtUtils.HEADER)) + public List all(){ + // 获取当前登录用户id + return roleService.getAll(); + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/vmanager/user/UserApiKeyController.java b/src/main/java/com/genersoft/iot/vmp/vmanager/user/UserApiKeyController.java new file mode 100644 index 0000000..1f4566b --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/vmanager/user/UserApiKeyController.java @@ -0,0 +1,251 @@ +package com.genersoft.iot.vmp.vmanager.user; + +import com.genersoft.iot.vmp.conf.exception.ControllerException; +import com.genersoft.iot.vmp.conf.security.JwtUtils; +import com.genersoft.iot.vmp.conf.security.SecurityUtils; +import com.genersoft.iot.vmp.service.IUserApiKeyService; +import com.genersoft.iot.vmp.service.IUserService; +import com.genersoft.iot.vmp.storager.dao.dto.User; +import com.genersoft.iot.vmp.storager.dao.dto.UserApiKey; +import com.genersoft.iot.vmp.utils.DateUtil; +import com.genersoft.iot.vmp.vmanager.bean.ErrorCode; +import com.github.pagehelper.PageInfo; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.bind.annotation.*; + +import java.util.HashMap; +import java.util.Map; + +@Tag(name = "用户ApiKey管理") +@RestController +@RequestMapping("/api/userApiKey") +public class UserApiKeyController { + + public static final int EXPIRATION_TIME = Integer.MAX_VALUE; + @Autowired + private IUserService userService; + + @Autowired + private IUserApiKeyService userApiKeyService; + + /** + * 添加用户ApiKey + * + * @param userId + * @param app + * @param remark + * @param expiresAt + * @param enable + */ + @PostMapping("/add") + @Operation(summary = "添加用户ApiKey", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "userId", description = "用户Id", required = true) + @Parameter(name = "app", description = "应用名称", required = false) + @Parameter(name = "remark", description = "备注信息", required = false) + @Parameter(name = "expiredAt", description = "过期时间(不传代表永不过期)", required = false) + @Transactional + public synchronized void add( + @RequestParam(required = true) int userId, + @RequestParam(required = false) String app, + @RequestParam(required = false) String remark, + @RequestParam(required = false) String expiresAt, + @RequestParam(required = false) Boolean enable + ) { + User user = userService.getUserById(userId); + if (user == null) { + throw new ControllerException(ErrorCode.ERROR400.getCode(), "用户不存在"); + } + + Long expirationTime = null; + if (expiresAt != null) { + expirationTime = DateUtil.yyyy_MM_dd_HH_mm_ssToTimestampMs(expiresAt); + long difference = (expirationTime - System.currentTimeMillis()) / (60 * 1000); + if (difference < 0) { + throw new ControllerException(ErrorCode.ERROR400.getCode(), "过期时间不能早于当前时间"); + } + } + + UserApiKey userApiKey = new UserApiKey(); + userApiKey.setUserId(userId); + userApiKey.setApp(app); + userApiKey.setApiKey(null); + userApiKey.setRemark(remark); + userApiKey.setExpiredAt(expirationTime != null ? expirationTime : 0); + userApiKey.setEnable(enable != null ? enable : false); + userApiKey.setCreateTime(DateUtil.getNow()); + userApiKey.setUpdateTime(DateUtil.getNow()); + + int addResult = userApiKeyService.addApiKey(userApiKey); + + if (addResult <= 0) { + throw new ControllerException(ErrorCode.ERROR100); + } + + String apiKey; + do { + Map extra = new HashMap<>(1); + extra.put("apiKeyId", userApiKey.getId()); + apiKey = JwtUtils.createToken(user.getUsername(), expirationTime, extra); + } while (userApiKeyService.isApiKeyExists(apiKey)); + + int resetResult = userApiKeyService.reset(userApiKey.getId(), apiKey); + + if (resetResult <= 0) { + throw new ControllerException(ErrorCode.ERROR100); + } + } + + /** + * 分页查询ApiKey + * + * @param page 当前页 + * @param count 每页查询数量 + * @return 分页ApiKey列表 + */ + @GetMapping("/userApiKeys") + @Operation(summary = "分页查询用户", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "page", description = "当前页", required = true) + @Parameter(name = "count", description = "每页查询数量", required = true) + @Transactional + public PageInfo userApiKeys(@RequestParam(required = true) int page, @RequestParam(required = true) int count) { + return userApiKeyService.getUserApiKeys(page, count); + } + + @PostMapping("/enable") + @Operation(summary = "启用用户ApiKey", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "id", description = "用户ApiKeyId", required = true) + @Transactional + public void enable(@RequestParam(required = true) Integer id) { + // 获取当前登录用户id + int currenRoleId = SecurityUtils.getUserInfo().getRole().getId(); + if (currenRoleId != 1) { + // 只用角色id为1才可以管理UserApiKey + throw new ControllerException(ErrorCode.ERROR403); + } + UserApiKey userApiKey = userApiKeyService.getUserApiKeyById(id); + if (userApiKey == null) { + throw new ControllerException(ErrorCode.ERROR400.getCode(), "ApiKey不存在"); + } + + int enableResult = userApiKeyService.enable(id); + + if (enableResult <= 0) { + throw new ControllerException(ErrorCode.ERROR100); + } + } + + @PostMapping("/disable") + @Operation(summary = "停用用户ApiKey", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "id", description = "用户ApiKeyId", required = true) + @Transactional + public void disable(@RequestParam(required = true) Integer id) { + // 获取当前登录用户id + int currenRoleId = SecurityUtils.getUserInfo().getRole().getId(); + if (currenRoleId != 1) { + // 只用角色id为1才可以管理UserApiKey + throw new ControllerException(ErrorCode.ERROR403); + } + UserApiKey userApiKey = userApiKeyService.getUserApiKeyById(id); + if (userApiKey == null) { + throw new ControllerException(ErrorCode.ERROR400.getCode(), "ApiKey不存在"); + } + + int disableResult = userApiKeyService.disable(id); + + if (disableResult <= 0) { + throw new ControllerException(ErrorCode.ERROR100); + } + } + + @PostMapping("/reset") + @Operation(summary = "重置用户ApiKey", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "id", description = "用户ApiKeyId", required = true) + @Transactional + public void reset(@RequestParam(required = true) Integer id) { + // 获取当前登录用户id + int currenRoleId = SecurityUtils.getUserInfo().getRole().getId(); + if (currenRoleId != 1) { + // 只用角色id为1才可以管理UserApiKey + throw new ControllerException(ErrorCode.ERROR403); + } + UserApiKey userApiKey = userApiKeyService.getUserApiKeyById(id); + if (userApiKey == null) { + throw new ControllerException(ErrorCode.ERROR400.getCode(), "ApiKey不存在"); + } + User user = userService.getUserById(userApiKey.getUserId()); + if (user == null) { + throw new ControllerException(ErrorCode.ERROR400.getCode(), "用户不存在"); + } + Long expirationTime = null; + if (userApiKey.getExpiredAt() > 0) { + long timestamp = userApiKey.getExpiredAt(); + expirationTime = (timestamp - System.currentTimeMillis()) / (60 * 1000); + if (expirationTime < 0) { + throw new ControllerException(ErrorCode.ERROR400.getCode(), "ApiKey已失效"); + } + } + String apiKey; + do { + Map extra = new HashMap<>(1); + extra.put("apiKeyId", userApiKey.getId()); + apiKey = JwtUtils.createToken(user.getUsername(), expirationTime, extra); + } while (userApiKeyService.isApiKeyExists(apiKey)); + + int resetResult = userApiKeyService.reset(id, apiKey); + + if (resetResult <= 0) { + throw new ControllerException(ErrorCode.ERROR100); + } + } + + @PostMapping("/remark") + @Operation(summary = "备注用户ApiKey", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "id", description = "用户ApiKeyId", required = true) + @Parameter(name = "remark", description = "用户ApiKey备注", required = false) + @Transactional + public void remark(@RequestParam(required = true) Integer id, @RequestParam(required = false) String remark) { + // 获取当前登录用户id + int currenRoleId = SecurityUtils.getUserInfo().getRole().getId(); + if (currenRoleId != 1) { + // 只用角色id为1才可以管理UserApiKey + throw new ControllerException(ErrorCode.ERROR403); + } + UserApiKey userApiKey = userApiKeyService.getUserApiKeyById(id); + if (userApiKey == null) { + throw new ControllerException(ErrorCode.ERROR400.getCode(), "ApiKey不存在"); + } + int remarkResult = userApiKeyService.remark(id, remark); + + if (remarkResult <= 0) { + throw new ControllerException(ErrorCode.ERROR100); + } + } + + @DeleteMapping("/delete") + @Operation(summary = "删除用户ApiKey", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "id", description = "用户ApiKeyId", required = true) + @Transactional + public void delete(@RequestParam(required = true) Integer id) { + // 获取当前登录用户id + int currenRoleId = SecurityUtils.getUserInfo().getRole().getId(); + if (currenRoleId != 1) { + // 只用角色id为1才可以管理UserApiKey + throw new ControllerException(ErrorCode.ERROR403); + } + UserApiKey userApiKey = userApiKeyService.getUserApiKeyById(id); + if (userApiKey == null) { + throw new ControllerException(ErrorCode.ERROR400.getCode(), "ApiKey不存在"); + } + + int deleteResult = userApiKeyService.delete(id); + + if (deleteResult <= 0) { + throw new ControllerException(ErrorCode.ERROR100); + } + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/vmanager/user/UserController.java b/src/main/java/com/genersoft/iot/vmp/vmanager/user/UserController.java new file mode 100755 index 0000000..0525722 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/vmanager/user/UserController.java @@ -0,0 +1,228 @@ +package com.genersoft.iot.vmp.vmanager.user; + +import com.genersoft.iot.vmp.conf.UserSetting; +import com.genersoft.iot.vmp.conf.exception.ControllerException; +import com.genersoft.iot.vmp.conf.security.JwtUtils; +import com.genersoft.iot.vmp.conf.security.SecurityUtils; +import com.genersoft.iot.vmp.conf.security.dto.LoginUser; +import com.genersoft.iot.vmp.service.IRoleService; +import com.genersoft.iot.vmp.service.IUserService; +import com.genersoft.iot.vmp.storager.dao.dto.Role; +import com.genersoft.iot.vmp.storager.dao.dto.User; +import com.genersoft.iot.vmp.utils.DateUtil; +import com.genersoft.iot.vmp.vmanager.bean.ErrorCode; +import com.genersoft.iot.vmp.vmanager.bean.WVPResult; +import com.github.pagehelper.PageInfo; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.util.DigestUtils; +import org.springframework.util.ObjectUtils; +import org.springframework.web.bind.annotation.*; + +import javax.security.sasl.AuthenticationException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.time.LocalDateTime; +import java.util.List; + +@Tag(name = "用户管理") +@RestController +@RequestMapping("/api/user") +public class UserController { + + @Autowired + private AuthenticationManager authenticationManager; + + @Autowired + private IUserService userService; + + @Autowired + private IRoleService roleService; + + @Autowired + private UserSetting userSetting; + + @GetMapping("/login") + @PostMapping("/login") + @Operation(summary = "登录", description = "登录成功后返回AccessToken, 可以从返回值获取到也可以从响应头中获取到," + + "后续的请求需要添加请求头 'access-token'或者放在参数里") + + @Parameter(name = "username", description = "用户名", required = true) + @Parameter(name = "password", description = "密码(32位md5加密)", required = true) + public LoginUser login(HttpServletRequest request, HttpServletResponse response, @RequestParam String username, @RequestParam String password){ + LoginUser user; + try { + user = SecurityUtils.login(username, password, authenticationManager); + } catch (AuthenticationException e) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), e.getMessage()); + } + if (user == null) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "用户名或密码错误"); + }else { + String jwt = JwtUtils.createToken(username); + response.setHeader(JwtUtils.getHeader(), jwt); + user.setAccessToken(jwt); + user.setServerId(userSetting.getServerId()); + } + return user; + } + + + @PostMapping("/changePassword") + @Operation(summary = "修改密码", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "username", description = "用户名", required = true) + @Parameter(name = "oldpassword", description = "旧密码(已md5加密的密码)", required = true) + @Parameter(name = "password", description = "新密码(未md5加密的密码)", required = true) + public void changePassword(@RequestParam String oldPassword, @RequestParam String password){ + // 获取当前登录用户id + LoginUser userInfo = SecurityUtils.getUserInfo(); + if (userInfo== null) { + throw new ControllerException(ErrorCode.ERROR100); + } + String username = userInfo.getUsername(); + LoginUser user = null; + try { + user = SecurityUtils.login(username, oldPassword, authenticationManager); + if (user == null) { + throw new ControllerException(ErrorCode.ERROR100); + } + //int userId = SecurityUtils.getUserId(); + boolean result = userService.changePassword(user.getId(), DigestUtils.md5DigestAsHex(password.getBytes())); + if (!result) { + throw new ControllerException(ErrorCode.ERROR100); + } + } catch (AuthenticationException e) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), e.getMessage()); + } + } + + + @PostMapping("/add") + @Operation(summary = "添加用户", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "username", description = "用户名", required = true) + @Parameter(name = "password", description = "密码(未md5加密的密码)", required = true) + @Parameter(name = "roleId", description = "角色ID", required = true) + public void add(@RequestParam String username, + @RequestParam String password, + @RequestParam Integer roleId){ + if (ObjectUtils.isEmpty(username) || ObjectUtils.isEmpty(password) || roleId == null) { + throw new ControllerException(ErrorCode.ERROR400.getCode(), "参数不可为空"); + } + // 获取当前登录用户id + int currenRoleId = SecurityUtils.getUserInfo().getRole().getId(); + if (currenRoleId != 1) { + // 只用角色id为1才可以删除和添加用户 + throw new ControllerException(ErrorCode.ERROR400.getCode(), "用户无权限"); + } + User user = new User(); + user.setUsername(username); + user.setPassword(DigestUtils.md5DigestAsHex(password.getBytes())); + //新增用户的pushKey的生成规则为md5(时间戳+用户名) + user.setPushKey(DigestUtils.md5DigestAsHex((System.currentTimeMillis()+password).getBytes())); + Role role = roleService.getRoleById(roleId); + + if (role == null) { + throw new ControllerException(ErrorCode.ERROR400.getCode(), "角色不存在"); + } + user.setRole(role); + user.setCreateTime(DateUtil.getNow()); + user.setUpdateTime(DateUtil.getNow()); + int addResult = userService.addUser(user); + if (addResult <= 0) { + throw new ControllerException(ErrorCode.ERROR100); + } + } + + @DeleteMapping("/delete") + @Operation(summary = "删除用户", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "id", description = "用户Id", required = true) + public void delete(@RequestParam Integer id){ + // 获取当前登录用户id + int currenRoleId = SecurityUtils.getUserInfo().getRole().getId(); + if (currenRoleId != 1) { + // 只用角色id为0才可以删除和添加用户 + throw new ControllerException(ErrorCode.ERROR400.getCode(), "用户无权限"); + } + int deleteResult = userService.deleteUser(id); + if (deleteResult <= 0) { + throw new ControllerException(ErrorCode.ERROR100); + } + } + + @GetMapping("/all") + @Operation(summary = "查询用户", security = @SecurityRequirement(name = JwtUtils.HEADER)) + public List all(){ + // 获取当前登录用户id + return userService.getAllUsers(); + } + + /** + * 分页查询用户 + * + * @param page 当前页 + * @param count 每页查询数量 + * @return 分页用户列表 + */ + @GetMapping("/users") + @Operation(summary = "分页查询用户", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "page", description = "当前页", required = true) + @Parameter(name = "count", description = "每页查询数量", required = true) + public PageInfo users(int page, int count) { + return userService.getUsers(page, count); + } + + @RequestMapping("/changePushKey") + @Operation(summary = "修改pushkey", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "userId", description = "用户Id", required = true) + @Parameter(name = "pushKey", description = "新的pushKey", required = true) + public void changePushKey(@RequestParam Integer userId,@RequestParam String pushKey) { + // 获取当前登录用户id + int currenRoleId = SecurityUtils.getUserInfo().getRole().getId(); + WVPResult result = new WVPResult<>(); + if (currenRoleId != 1) { + // 只用角色id为0才可以删除和添加用户 + throw new ControllerException(ErrorCode.ERROR400.getCode(), "用户无权限"); + } + int resetPushKeyResult = userService.changePushKey(userId,pushKey); + if (resetPushKeyResult <= 0) { + throw new ControllerException(ErrorCode.ERROR100); + } + } + + @PostMapping("/changePasswordForAdmin") + @Operation(summary = "管理员修改普通用户密码", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "adminId", description = "管理员id", required = true) + @Parameter(name = "userId", description = "用户id", required = true) + @Parameter(name = "password", description = "新密码(未md5加密的密码)", required = true) + public void changePasswordForAdmin(@RequestParam int userId, @RequestParam String password) { + // 获取当前登录用户id + LoginUser userInfo = SecurityUtils.getUserInfo(); + if (userInfo == null) { + throw new ControllerException(ErrorCode.ERROR100); + } + Role role = userInfo.getRole(); + if (role != null && role.getId() == 1) { + boolean result = userService.changePassword(userId, DigestUtils.md5DigestAsHex(password.getBytes())); + if (!result) { + throw new ControllerException(ErrorCode.ERROR100); + } + } + } + + @PostMapping("/userInfo") + @Operation(summary = "管理员修改普通用户密码") + public LoginUser getUserInfo() { + // 获取当前登录用户id + LoginUser userInfo = SecurityUtils.getUserInfo(); + + if (userInfo == null) { + throw new ControllerException(ErrorCode.ERROR100); + } + User user = userService.getUser(userInfo.getUsername(), userInfo.getPassword()); + return new LoginUser(user, LocalDateTime.now()); + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/web/custom/CameraChannelController.java b/src/main/java/com/genersoft/iot/vmp/web/custom/CameraChannelController.java new file mode 100644 index 0000000..e6ffffd --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/web/custom/CameraChannelController.java @@ -0,0 +1,604 @@ +package com.genersoft.iot.vmp.web.custom; + +import com.genersoft.iot.vmp.common.StreamInfo; +import com.genersoft.iot.vmp.conf.DynamicTask; +import com.genersoft.iot.vmp.conf.UserSetting; +import com.genersoft.iot.vmp.conf.exception.ControllerException; +import com.genersoft.iot.vmp.conf.security.JwtUtils; +import com.genersoft.iot.vmp.gb28181.bean.CommonGBChannel; +import com.genersoft.iot.vmp.media.bean.MediaServer; +import com.genersoft.iot.vmp.media.service.IMediaServerService; +import com.genersoft.iot.vmp.media.zlm.dto.StreamAuthorityInfo; +import com.genersoft.iot.vmp.service.ICloudRecordService; +import com.genersoft.iot.vmp.service.bean.CloudRecordItem; +import com.genersoft.iot.vmp.service.bean.ErrorCallback; +import com.genersoft.iot.vmp.service.bean.InviteErrorCode; +import com.genersoft.iot.vmp.storager.IRedisCatchStorage; +import com.genersoft.iot.vmp.streamPush.bean.StreamPush; +import com.genersoft.iot.vmp.streamPush.service.IStreamPushPlayService; +import com.genersoft.iot.vmp.streamPush.service.IStreamPushService; +import com.genersoft.iot.vmp.utils.DateUtil; +import com.genersoft.iot.vmp.utils.HttpUtils; +import com.genersoft.iot.vmp.vmanager.bean.ErrorCode; +import com.genersoft.iot.vmp.vmanager.bean.StreamContent; +import com.genersoft.iot.vmp.vmanager.bean.WVPResult; +import com.genersoft.iot.vmp.vmanager.cloudRecord.bean.CloudRecordUrl; +import com.genersoft.iot.vmp.web.custom.bean.*; +import com.genersoft.iot.vmp.web.custom.service.CameraChannelService; +import com.github.pagehelper.PageInfo; +import io.swagger.v3.oas.annotations.Hidden; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.context.request.async.DeferredResult; + +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; + +@Tag(name = "第三方接口") +@Slf4j +@RestController +@RequestMapping(value = "/api/sy") +@ConditionalOnProperty(value = "sy.enable", havingValue = "true") +@Hidden +public class CameraChannelController { + + @Autowired + private CameraChannelService channelService; + + @Autowired + private UserSetting userSetting; + + @Autowired + private IRedisCatchStorage redisCatchStorage; + + @Autowired + private IMediaServerService mediaServerService; + + @Autowired + private ICloudRecordService cloudRecordService; + + @Autowired + private IStreamPushPlayService streamPushPlayService; + + @Autowired + private DynamicTask dynamicTask; + + @Autowired + private IStreamPushService streamPushService; + + @Value("${sy.ptz-control-time-interval}") + private int ptzControlTimeInterval = 300; + + @GetMapping(value = "/camera/list") + @ResponseBody + @Operation(summary = "查询摄像机列表, 只查询当前虚拟组织下的", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "page", description = "当前页") + @Parameter(name = "count", description = "每页查询数量") + @Parameter(name = "groupAlias", description = "分组别名") + @Parameter(name = "geoCoordSys", description = "坐标系类型:WGS84,GCJ02、BD09") + @Parameter(name = "status", description = "摄像头状态") + public PageInfo queryList(@RequestParam(required = false, value = "page", defaultValue = "1" )Integer page, + @RequestParam(required = false, value = "count", defaultValue = "100")Integer count, + String groupAlias, + @RequestParam(required = false) String geoCoordSys, + @RequestParam(required = false) Boolean status){ + + + return channelService.queryList(page, count, groupAlias, status, geoCoordSys); + } + + @GetMapping(value = "/camera/list-with-child") + @ResponseBody + @Operation(summary = "查询摄像机列表, 查询当前虚拟组织下以及全部子节点", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "page", description = "当前页") + @Parameter(name = "count", description = "每页查询数量") + @Parameter(name = "query", description = "查询内容") + @Parameter(name = "sortName", description = "排序字段名") + @Parameter(name = "order", description = "排序方式(true: 升序 或 false: 降序 )") + @Parameter(name = "groupAlias", description = "分组别名") + @Parameter(name = "geoCoordSys", description = "坐标系类型:WGS84,GCJ02、BD09") + @Parameter(name = "status", description = "摄像头状态") + public PageInfo queryListWithChild(@RequestParam(required = false, value = "page", defaultValue = "1" )Integer page, + + @RequestParam(required = false, value = "count", defaultValue = "100")Integer count, + @RequestParam(required = false) String query, + @RequestParam(required = false) String sortName, + @RequestParam(required = false) Boolean order, + @RequestParam(required = false) String groupAlias, + @RequestParam(required = false) String geoCoordSys, + @RequestParam(required = false) Boolean status){ + if (ObjectUtils.isEmpty(query)) { + query = null; + } + if (ObjectUtils.isEmpty(sortName)) { + sortName = null; + } + if (ObjectUtils.isEmpty(order)) { + order = null; + } + if (ObjectUtils.isEmpty(groupAlias)) { + groupAlias = null; + } + + return channelService.queryListWithChild(page, count, query, sortName, order, groupAlias, status, geoCoordSys); + } + + @GetMapping(value = "/camera/cont-with-child") + @ResponseBody + @Operation(summary = "查询摄像机列表的总数和在线数", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "groupAlias", description = "分组别名") + public List queryCountWithChild(String groupAlias){ + return channelService.queryCountWithChild(groupAlias); + } + + @GetMapping(value = "/camera/one") + @ResponseBody + @Operation(summary = "查询单个摄像头信息", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "deviceId", description = "通道编号") + @Parameter(name = "deviceCode", description = "摄像头设备国标编号, 对于非国标摄像头可以不设置此参数") + @Parameter(name = "geoCoordSys", description = "坐标系类型:WGS84,GCJ02、BD09") + public CameraChannel getOne(String deviceId, @RequestParam(required = false) String deviceCode, + @RequestParam(required = false) String geoCoordSys) { + return channelService.queryOne(deviceId, deviceCode, geoCoordSys); + } + + @GetMapping(value = "/camera/update") + @ResponseBody + @Operation(summary = "更新摄像头信息", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "deviceId", description = "通道编号") + @Parameter(name = "deviceCode", description = "摄像头设备国标编号, 对于非国标摄像头可以不设置此参数") + @Parameter(name = "name", description = "通道名称") + @Parameter(name = "longitude", description = "经度") + @Parameter(name = "latitude", description = "纬度") + @Parameter(name = "geoCoordSys", description = "坐标系类型:WGS84,GCJ02、BD09") + public void updateCamera(String deviceId, + @RequestParam(required = false) String deviceCode, + @RequestParam(required = false) String name, + @RequestParam(required = false) Double longitude, + @RequestParam(required = false) Double latitude, + @RequestParam(required = false) String geoCoordSys) { + channelService.updateCamera(deviceId, deviceCode, name, longitude, latitude, geoCoordSys); + } + + @PostMapping(value = "/camera/list/ids") + @ResponseBody + @Operation(summary = "根据编号查询多个摄像头信息", security = @SecurityRequirement(name = JwtUtils.HEADER)) + public List queryListByDeviceIds(@RequestBody IdsQueryParam param) { + return channelService.queryListByDeviceIds(param.getDeviceIds(), param.getGeoCoordSys()); + } + + @GetMapping(value = "/camera/list/box") + @ResponseBody + @Operation(summary = "根据矩形查询摄像头", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "minLongitude", description = "最小经度") + @Parameter(name = "maxLongitude", description = "最大经度") + @Parameter(name = "minLatitude", description = "最小纬度") + @Parameter(name = "maxLatitude", description = "最大纬度") + @Parameter(name = "level", description = "地图级别") + @Parameter(name = "groupAlias", description = "分组别名") + @Parameter(name = "geoCoordSys", description = "坐标系类型:WGS84,GCJ02、BD09") + public List queryListInBox(Double minLongitude, Double maxLongitude, + Double minLatitude, Double maxLatitude, + @RequestParam(required = false) Integer level, + String groupAlias, + @RequestParam(required = false) String geoCoordSys) { + return channelService.queryListInBox(minLongitude, maxLongitude, minLatitude, maxLatitude, level, groupAlias, geoCoordSys); + } + + @PostMapping(value = "/camera/list/polygon") + @ResponseBody + @Operation(summary = "根据多边形查询摄像头", security = @SecurityRequirement(name = JwtUtils.HEADER)) + public List queryListInPolygon(@RequestBody PolygonQueryParam param) { + return channelService.queryListInPolygon(param.getPosition(), param.getGroupAlias(), param.getLevel(), param.getGeoCoordSys()); + } + + @GetMapping(value = "/camera/list/circle") + @ResponseBody + @Operation(summary = "根据圆范围查询摄像头", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "centerLongitude", description = "圆心经度") + @Parameter(name = "centerLatitude", description = "圆心纬度") + @Parameter(name = "radius", description = "查询范围的半径,单位米") + @Parameter(name = "level", description = "地图级别") + @Parameter(name = "groupAlias", description = "分组别名") + @Parameter(name = "geoCoordSys", description = "坐标系类型:WGS84,GCJ02、BD09") + public List queryListInCircle(Double centerLongitude, Double centerLatitude, Double radius, String groupAlias, + @RequestParam(required = false) String geoCoordSys, @RequestParam(required = false) Integer level) { + return channelService.queryListInCircle(centerLongitude, centerLatitude, radius, level, groupAlias, geoCoordSys); + } + + @GetMapping(value = "/camera/list/address") + @ResponseBody + @Operation(summary = "根据安装地址和监视方位获取摄像头", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "address", description = "安装地址") + @Parameter(name = "directionType", description = "监视方位", required = false) + @Parameter(name = "geoCoordSys", description = "坐标系类型:WGS84,GCJ02、BD09") + public List queryListByAddressAndDirectionType(String address, @RequestParam(required = false) Integer directionType, @RequestParam(required = false) String geoCoordSys) { + return channelService.queryListByAddressAndDirectionType(address, directionType, geoCoordSys); + } + + @GetMapping(value = "/camera/control/play") + @ResponseBody + @Operation(summary = "播放摄像头", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "deviceId", description = "通道编号") + @Parameter(name = "deviceCode", description = "摄像头设备国标编号, 对于非国标摄像头可以不设置此参数") + public DeferredResult> play(HttpServletRequest request, String deviceId, @RequestParam(required = false) String deviceCode) { + + log.info("[SY-播放摄像头] API调用,deviceId:{} ,deviceCode:{} ",deviceId, deviceCode); + DeferredResult> result = new DeferredResult<>(userSetting.getPlayTimeout().longValue()); + + ErrorCallback callback = (code, msg, cameraStreamInfo) -> { + if (code == InviteErrorCode.SUCCESS.getCode()) { + StreamInfo streamInfo = cameraStreamInfo.getStreamInfo(); + CommonGBChannel channel = cameraStreamInfo.getChannel(); + WVPResult wvpResult = WVPResult.success(); + if (cameraStreamInfo.getStreamInfo() != null) { + if (userSetting.getUseSourceIpAsStreamIp()) { + streamInfo=streamInfo.clone();//深拷贝 + String host; + try { + URL url=new URL(request.getRequestURL().toString()); + host=url.getHost(); + } catch (MalformedURLException e) { + host=request.getLocalAddr(); + } + streamInfo.changeStreamIp(host); + } + if (!ObjectUtils.isEmpty(streamInfo.getMediaServer().getTranscodeSuffix()) + && !"null".equalsIgnoreCase(streamInfo.getMediaServer().getTranscodeSuffix())) { + streamInfo.setStream(streamInfo.getStream() + "_" + streamInfo.getMediaServer().getTranscodeSuffix()); + } + CameraStreamContent cameraStreamContent = new CameraStreamContent(streamInfo); + cameraStreamContent.setName(channel.getGbName()); + if (channel.getGbPtzType() != null) { + cameraStreamContent.setControlType( + (channel.getGbPtzType() == 1 || channel.getGbPtzType() == 4 || channel.getGbPtzType() == 5) ? 1 : 0); + }else { + cameraStreamContent.setControlType(0); + } + + wvpResult.setData(cameraStreamContent); + }else { + wvpResult.setCode(code); + wvpResult.setMsg(msg); + } + result.setResult(wvpResult); + }else { + result.setResult(WVPResult.fail(code, msg)); + } + }; + channelService.play(deviceId, deviceCode, callback); + return result; + } + + @GetMapping(value = "/camera/control/stop") + @ResponseBody + @Operation(summary = "停止播放摄像头", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "deviceId", description = "通道编号") + @Parameter(name = "deviceCode", description = "摄像头设备国标编号, 对于非国标摄像头可以不设置此参数") + public void stopPlay(String deviceId, @RequestParam(required = false) String deviceCode) { + log.info("[SY-停止播放摄像头] API调用,deviceId:{} ,deviceCode:{} ",deviceId, deviceCode); + channelService.stopPlay(deviceId, deviceCode); + } + + @Operation(summary = "云台控制", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "deviceId", description = "通道编号") + @Parameter(name = "deviceCode", description = "摄像头设备国标编号, 对于非国标摄像头可以不设置此参数") + @Parameter(name = "command", description = "控制指令,允许值: left, right, up, down, upleft, upright, downleft, downright, zoomin, zoomout, stop", required = true) + @Parameter(name = "speed", description = "速度(0-100)", required = true) + @GetMapping("/camera/control/ptz") + public DeferredResult> ptz(String deviceId, @RequestParam(required = false) String deviceCode, String command, Integer speed){ + + log.info("[SY-云台控制] API调用,deviceId:{} ,deviceCode:{} ,command:{} ,speed:{} ",deviceId, deviceCode, command, speed); + + DeferredResult> result = new DeferredResult<>(); + + result.onTimeout(()->{ + WVPResult wvpResult = WVPResult.fail(ErrorCode.ERROR100.getCode(), "请求超时"); + result.setResult(wvpResult); + }); + + channelService.ptz(deviceId, deviceCode, command, speed, (code, msg, data) -> { + WVPResult wvpResult = new WVPResult<>(); + wvpResult.setCode(code); + wvpResult.setMsg(msg); + wvpResult.setData(data); + result.setResult(wvpResult); + }); + // 设置时间间隔后自动发送停止 + if (!command.equalsIgnoreCase("stop")) { + dynamicTask.startDelay(UUID.randomUUID().toString(), () -> { + channelService.ptz(deviceId, deviceCode, "stop", speed, (code, msg, data) -> {}); + }, ptzControlTimeInterval); + } + return result; + } + + @GetMapping(value = "/camera/list-for-mobile") + @ResponseBody + @Operation(summary = "查询移动设备摄像机列表", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "page", description = "当前页") + @Parameter(name = "count", description = "每页查询数量") + @Parameter(name = "topGroupAlias", description = "分组别名") + public PageInfo queryListForMobile(@RequestParam(required = false, value = "page", defaultValue = "1" )Integer page, + @RequestParam(required = false, value = "count", defaultValue = "100")Integer count, + @RequestParam(required = false) String topGroupAlias){ + + return channelService.queryListForMobile(page, count, topGroupAlias); + } + + + @Operation(summary = "获取推流播放地址", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "app", description = "应用名", required = true) + @Parameter(name = "stream", description = "流id", required = true) + @Parameter(name = "callId", description = "推流时携带的自定义鉴权ID", required = true) + @GetMapping(value = "/push/play") + @ResponseBody + public DeferredResult> getStreamInfoByAppAndStream(HttpServletRequest request, + String app, + String stream, + String callId){ + StreamPush streamPush = streamPushService.getPush(app, stream); + Assert.notNull(streamPush, "地址不存在"); + + // 权限校验 + StreamAuthorityInfo streamAuthorityInfo = redisCatchStorage.getStreamAuthorityInfo(app, stream); + if (streamAuthorityInfo == null + || streamAuthorityInfo.getCallId() == null + || !streamAuthorityInfo.getCallId().equals(callId)) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "播放地址鉴权失败"); + } + + DeferredResult> result = new DeferredResult<>(userSetting.getPlayTimeout().longValue()); + result.onTimeout(()->{ + WVPResult fail = WVPResult.fail(ErrorCode.ERROR100.getCode(), "等待推流超时"); + result.setResult(fail); + }); + + streamPushPlayService.start(streamPush.getId(), (code, msg, streamInfo) -> { + if (code == 0 && streamInfo != null) { + streamInfo=streamInfo.clone();//深拷贝 + String host; + try { + URL url=new URL(request.getRequestURL().toString()); + host=url.getHost(); + } catch (MalformedURLException e) { + host=request.getLocalAddr(); + } + streamInfo.changeStreamIp(host); + WVPResult success = WVPResult.success(new StreamContent(streamInfo)); + result.setResult(success); + } + }, null, null); + return result; + } + + @ResponseBody + @GetMapping("/record/collect/add") + @Operation(summary = "添加收藏") + @Parameter(name = "app", description = "应用名", required = false) + @Parameter(name = "stream", description = "流ID", required = false) + @Parameter(name = "mediaServerId", description = "流媒体ID", required = false) + @Parameter(name = "startTime", description = "鉴权ID", required = false) + @Parameter(name = "endTime", description = "鉴权ID", required = false) + @Parameter(name = "callId", description = "鉴权ID", required = false) + @Parameter(name = "recordId", description = "录像记录的ID,用于精准收藏一个视频文件", required = false) + public int addCollect(@RequestParam(required = false) String app, @RequestParam(required = false) String stream, @RequestParam(required = false) String mediaServerId, @RequestParam(required = false) String startTime, @RequestParam(required = false) String endTime, @RequestParam(required = false) String callId, @RequestParam(required = false) Integer recordId) { + log.info("[云端录像] 添加收藏,app={},stream={},mediaServerId={},startTime={},endTime={},callId={},recordId={}", app, stream, mediaServerId, startTime, endTime, callId, recordId); + if (recordId != null) { + return cloudRecordService.changeCollectById(recordId, true); + } else { + return cloudRecordService.changeCollect(true, app, stream, mediaServerId, startTime, endTime, callId); + } + } + + @ResponseBody + @GetMapping("/record/collect/delete") + @Operation(summary = "移除收藏") + @Parameter(name = "app", description = "应用名", required = false) + @Parameter(name = "stream", description = "流ID", required = false) + @Parameter(name = "mediaServerId", description = "流媒体ID", required = false) + @Parameter(name = "startTime", description = "鉴权ID", required = false) + @Parameter(name = "endTime", description = "鉴权ID", required = false) + @Parameter(name = "callId", description = "鉴权ID", required = false) + @Parameter(name = "recordId", description = "录像记录的ID,用于精准精准移除一个视频文件的收藏", required = false) + public int deleteCollect(@RequestParam(required = false) String app, @RequestParam(required = false) String stream, @RequestParam(required = false) String mediaServerId, @RequestParam(required = false) String startTime, @RequestParam(required = false) String endTime, @RequestParam(required = false) String callId, @RequestParam(required = false) Integer recordId) { + log.info("[云端录像] 移除收藏,app={},stream={},mediaServerId={},startTime={},endTime={},callId={},recordId={}", app, stream, mediaServerId, startTime, endTime, callId, recordId); + if (recordId != null) { + return cloudRecordService.changeCollectById(recordId, false); + } else { + return cloudRecordService.changeCollect(false, app, stream, mediaServerId, startTime, endTime, callId); + } + } + + /************************* 以下这些接口只适合wvp和zlm部署在同一台服务器的情况,且wvp只有一个zlm节点的情况 ***************************************/ + + /** + * 下载指定录像文件的压缩包 + * @param app 应用名 + * @param stream 流ID + * @param callId 每次录像的唯一标识,置空则查询全部流媒体 + */ + @ResponseBody + @GetMapping("/record/zip") + public void downloadZipFile(HttpServletResponse response, + @RequestParam(required = false) String app, + @RequestParam(required = false) String stream, + @RequestParam(required = false) String callId + + ) { + log.info("[下载指定录像文件的压缩包] 查询 app->{}, stream->{}, callId->{}", app, stream, callId); + + if (app != null && ObjectUtils.isEmpty(app.trim())) { + app = null; + } + if (stream != null && ObjectUtils.isEmpty(stream.trim())) { + stream = null; + } + if (callId != null && ObjectUtils.isEmpty(callId.trim())) { + callId = null; + } + // 设置响应头 + response.setContentType("application/zip"); + response.setCharacterEncoding("UTF-8"); + if (stream != null && callId != null) { + response.addHeader("Content-Disposition", "attachment;filename=" + stream + "_" + callId + ".zip"); + } + List cloudRecordItemList = cloudRecordService.getUrlList(app, stream, callId); + if (ObjectUtils.isEmpty(cloudRecordItemList)) { + log.warn("[下载指定录像文件的压缩包] 未找到录像文件,app->{}, stream->{}, callId->{}", app, stream, callId); + return; + } + + try (ZipOutputStream zos = new ZipOutputStream(response.getOutputStream())) { + for (CloudRecordUrl recordUrl : cloudRecordItemList) { + try { + zos.putNextEntry(new ZipEntry(recordUrl.getFileName())); + boolean downloadSuccess = HttpUtils.downLoadFile(recordUrl.getDownloadUrl(), zos); + if (!downloadSuccess) { + log.warn("[下载指定录像文件的压缩包] 下载文件失败: {}", recordUrl.getDownloadUrl()); + zos.closeEntry(); + continue; + } + zos.closeEntry(); + } catch (Exception e) { + log.error("[下载指定录像文件的压缩包] 处理文件失败: {}, 错误: {}", recordUrl.getFileName(), e.getMessage()); + // 继续处理下一个文件 + } + } + } catch (IOException e) { + log.error("[下载指定录像文件的压缩包] 创建压缩包失败,查询 app->{}, stream->{}, callId->{}", app, stream, callId, e); + } + } + + /** + * + * @param query 检索内容 + * @param app 应用名 + * @param stream 流ID + * @param startTime 开始时间(yyyy-MM-dd HH:mm:ss) + * @param endTime 结束时间(yyyy-MM-dd HH:mm:ss) + * @param mediaServerId 流媒体ID,置空则查询全部流媒体 + * @param callId 每次录像的唯一标识,置空则查询全部流媒体 + * @param remoteHost 拼接播放地址时使用的远程地址 + */ + @ResponseBody + @GetMapping("/record/list-url") + @Operation(summary = "分页查询云端录像", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "query", description = "检索内容", required = false) + @Parameter(name = "app", description = "应用名", required = false) + @Parameter(name = "stream", description = "流ID", required = false) + @Parameter(name = "page", description = "当前页", required = true) + @Parameter(name = "count", description = "每页查询数量", required = true) + @Parameter(name = "startTime", description = "开始时间(yyyy-MM-dd HH:mm:ss)", required = false) + @Parameter(name = "endTime", description = "结束时间(yyyy-MM-dd HH:mm:ss)", required = false) + @Parameter(name = "mediaServerId", description = "流媒体ID,置空则查询全部流媒体", required = false) + @Parameter(name = "callId", description = "每次录像的唯一标识,置空则查询全部流媒体", required = false) + public PageInfo getListWithUrl(HttpServletRequest request, @RequestParam(required = false) String query, @RequestParam(required = false) String app, @RequestParam(required = false) String stream, @RequestParam int page, @RequestParam int count, @RequestParam(required = false) String startTime, @RequestParam(required = false) String endTime, @RequestParam(required = false) String mediaServerId, @RequestParam(required = false) String callId, @RequestParam(required = false) String remoteHost + + ) { + log.info("[云端录像] 查询URL app->{}, stream->{}, mediaServerId->{}, page->{}, count->{}, startTime->{}, endTime->{}, callId->{}", app, stream, mediaServerId, page, count, startTime, endTime, callId); + + List mediaServers; + if (!ObjectUtils.isEmpty(mediaServerId)) { + mediaServers = new ArrayList<>(); + MediaServer mediaServer = mediaServerService.getOne(mediaServerId); + if (mediaServer == null) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "未找到流媒体: " + mediaServerId); + } + mediaServers.add(mediaServer); + } else { + mediaServers = null; + } + if (query != null && ObjectUtils.isEmpty(query.trim())) { + query = null; + } + if (app != null && ObjectUtils.isEmpty(app.trim())) { + app = null; + } + if (stream != null && ObjectUtils.isEmpty(stream.trim())) { + stream = null; + } + if (startTime != null && ObjectUtils.isEmpty(startTime.trim())) { + startTime = null; + } + if (endTime != null && ObjectUtils.isEmpty(endTime.trim())) { + endTime = null; + } + if (callId != null && ObjectUtils.isEmpty(callId.trim())) { + callId = null; + } + MediaServer mediaServer = mediaServerService.getDefaultMediaServer(); + if (mediaServer == null) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "未找到流媒体节点"); + } + if (remoteHost == null) { + remoteHost = request.getScheme() + "://" + request.getLocalAddr() + ":" + (request.getScheme().equals("https") ? mediaServer.getHttpSSlPort() : mediaServer.getHttpPort()); + } + PageInfo cloudRecordItemPageInfo = cloudRecordService.getList(page, count, query, app, stream, startTime, endTime, mediaServers, callId, null); + PageInfo cloudRecordUrlPageInfo = new PageInfo<>(); + if (!ObjectUtils.isEmpty(cloudRecordItemPageInfo)) { + cloudRecordUrlPageInfo.setPageNum(cloudRecordItemPageInfo.getPageNum()); + cloudRecordUrlPageInfo.setPageSize(cloudRecordItemPageInfo.getPageSize()); + cloudRecordUrlPageInfo.setSize(cloudRecordItemPageInfo.getSize()); + cloudRecordUrlPageInfo.setEndRow(cloudRecordItemPageInfo.getEndRow()); + cloudRecordUrlPageInfo.setStartRow(cloudRecordItemPageInfo.getStartRow()); + cloudRecordUrlPageInfo.setPages(cloudRecordItemPageInfo.getPages()); + cloudRecordUrlPageInfo.setPrePage(cloudRecordItemPageInfo.getPrePage()); + cloudRecordUrlPageInfo.setNextPage(cloudRecordItemPageInfo.getNextPage()); + cloudRecordUrlPageInfo.setIsFirstPage(cloudRecordItemPageInfo.isIsFirstPage()); + cloudRecordUrlPageInfo.setIsLastPage(cloudRecordItemPageInfo.isIsLastPage()); + cloudRecordUrlPageInfo.setHasPreviousPage(cloudRecordItemPageInfo.isHasPreviousPage()); + cloudRecordUrlPageInfo.setHasNextPage(cloudRecordItemPageInfo.isHasNextPage()); + cloudRecordUrlPageInfo.setNavigatePages(cloudRecordItemPageInfo.getNavigatePages()); + cloudRecordUrlPageInfo.setNavigateFirstPage(cloudRecordItemPageInfo.getNavigateFirstPage()); + cloudRecordUrlPageInfo.setNavigateLastPage(cloudRecordItemPageInfo.getNavigateLastPage()); + cloudRecordUrlPageInfo.setNavigatepageNums(cloudRecordItemPageInfo.getNavigatepageNums()); + cloudRecordUrlPageInfo.setTotal(cloudRecordItemPageInfo.getTotal()); + List cloudRecordUrlList = new ArrayList<>(cloudRecordItemPageInfo.getList().size()); + List cloudRecordItemList = cloudRecordItemPageInfo.getList(); + for (CloudRecordItem cloudRecordItem : cloudRecordItemList) { + CloudRecordUrl cloudRecordUrl = new CloudRecordUrl(); + cloudRecordUrl.setId(cloudRecordItem.getId()); + cloudRecordUrl.setDownloadUrl(remoteHost + "/index/api/downloadFile?file_path=" + cloudRecordItem.getFilePath() + "&save_name=" + cloudRecordItem.getStream() + "_" + cloudRecordItem.getCallId() + "_" + DateUtil.timestampMsToUrlToyyyy_MM_dd_HH_mm_ss((long)cloudRecordItem.getStartTime())); + cloudRecordUrl.setPlayUrl(remoteHost + "/index/api/downloadFile?file_path=" + cloudRecordItem.getFilePath()); + cloudRecordUrlList.add(cloudRecordUrl); + } + cloudRecordUrlPageInfo.setList(cloudRecordUrlList); + } + return cloudRecordUrlPageInfo; + } + + @GetMapping(value = "/forceClose") + @ResponseBody + @Operation(summary = "强制停止推流", security = @SecurityRequirement(name = JwtUtils.HEADER)) + public void stop(String app, String stream){ + streamPushPlayService.stop(app, stream); + } + + @GetMapping(value = "/camera/meeting/list") + @ResponseBody + @Operation(summary = "查询会议设备", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @Parameter(name = "topGroupAlias", description = "分组别名") + public List queryMeetingChannelList(String topGroupAlias){ + return channelService.queryMeetingChannelList(topGroupAlias); + } + + +} diff --git a/src/main/java/com/genersoft/iot/vmp/web/custom/bean/CameraChannel.java b/src/main/java/com/genersoft/iot/vmp/web/custom/bean/CameraChannel.java new file mode 100644 index 0000000..647b094 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/web/custom/bean/CameraChannel.java @@ -0,0 +1,32 @@ +package com.genersoft.iot.vmp.web.custom.bean; + +import com.genersoft.iot.vmp.gb28181.bean.CommonGBChannel; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; +import lombok.Setter; + + +@Getter +@Setter +@Schema(description = "摄像头信息") +public class CameraChannel extends CommonGBChannel { + + @Schema(description = "摄像头设备国标编号") + private String deviceCode; + + + @Schema(description = "图标路径") + private String icon; + + /** + * 分组别名 + */ + @Schema(description = "所属组织结构别名") + private String groupAlias; + + /** + * 分组所属业务分组别名 + */ + @Schema(description = "所属业务分组别名") + private String topGroupGAlias; +} diff --git a/src/main/java/com/genersoft/iot/vmp/web/custom/bean/CameraCount.java b/src/main/java/com/genersoft/iot/vmp/web/custom/bean/CameraCount.java new file mode 100644 index 0000000..10187f7 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/web/custom/bean/CameraCount.java @@ -0,0 +1,13 @@ +package com.genersoft.iot.vmp.web.custom.bean; + +import lombok.Data; + +@Data +public class CameraCount { + + private String groupAlias; + private String deviceId; + private Long allCount; + private Long onlineCount; + +} diff --git a/src/main/java/com/genersoft/iot/vmp/web/custom/bean/CameraGroup.java b/src/main/java/com/genersoft/iot/vmp/web/custom/bean/CameraGroup.java new file mode 100644 index 0000000..182fa6d --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/web/custom/bean/CameraGroup.java @@ -0,0 +1,34 @@ +package com.genersoft.iot.vmp.web.custom.bean; + +import com.genersoft.iot.vmp.gb28181.bean.Group; +import lombok.Getter; + +import java.util.ArrayList; +import java.util.List; + +public class CameraGroup extends Group { + + @Getter + private CameraGroup parent; + + @Getter + private final List child = new ArrayList<>(); + + public void setParent(CameraGroup parent) { + if (parent == null) { + return; + } + this.parent = parent; + parent.addChild(this); + } + + public void addChild(CameraGroup child) { + if (child == null) { + return; + } + this.child.add(child); + if (this.parent != null) { + this.parent.addChild(child); + } + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/web/custom/bean/CameraStreamContent.java b/src/main/java/com/genersoft/iot/vmp/web/custom/bean/CameraStreamContent.java new file mode 100644 index 0000000..5327ef7 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/web/custom/bean/CameraStreamContent.java @@ -0,0 +1,22 @@ +package com.genersoft.iot.vmp.web.custom.bean; + +import com.genersoft.iot.vmp.common.StreamInfo; +import com.genersoft.iot.vmp.vmanager.bean.StreamContent; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class CameraStreamContent extends StreamContent { + + public CameraStreamContent(StreamInfo streamInfo) { + super(streamInfo); + } + + private String name; + + // 0不可动,1可动 + private Integer controlType; + + +} diff --git a/src/main/java/com/genersoft/iot/vmp/web/custom/bean/CameraStreamInfo.java b/src/main/java/com/genersoft/iot/vmp/web/custom/bean/CameraStreamInfo.java new file mode 100644 index 0000000..ca71a29 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/web/custom/bean/CameraStreamInfo.java @@ -0,0 +1,22 @@ +package com.genersoft.iot.vmp.web.custom.bean; + +import com.genersoft.iot.vmp.common.StreamInfo; +import com.genersoft.iot.vmp.gb28181.bean.CommonGBChannel; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class CameraStreamInfo { + + + private CommonGBChannel channel; + + + private StreamInfo streamInfo; + + public CameraStreamInfo(CommonGBChannel channel, StreamInfo streamInfo) { + this.channel = channel; + this.streamInfo = streamInfo; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/web/custom/bean/ChannelParam.java b/src/main/java/com/genersoft/iot/vmp/web/custom/bean/ChannelParam.java new file mode 100644 index 0000000..6c676ef --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/web/custom/bean/ChannelParam.java @@ -0,0 +1,15 @@ +package com.genersoft.iot.vmp.web.custom.bean; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Data +@Schema(description = "通道信息") +public class ChannelParam { + + @Schema(description = "摄像头设备国标编号, 对于非国标摄像头可以不设置此参数") + private String deviceCode; + + @Schema(description = "通道编号") + private String deviceId; +} diff --git a/src/main/java/com/genersoft/iot/vmp/web/custom/bean/IdsQueryParam.java b/src/main/java/com/genersoft/iot/vmp/web/custom/bean/IdsQueryParam.java new file mode 100644 index 0000000..dfa6303 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/web/custom/bean/IdsQueryParam.java @@ -0,0 +1,17 @@ +package com.genersoft.iot.vmp.web.custom.bean; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.util.List; + +@Data +@Schema(description = "根据多个ID获取摄像头列表") +public class IdsQueryParam { + + @Schema(description = "通道编号列表") + private List deviceIds; + + @Schema(description = "坐标系类型:WGS84,GCJ02、BD09") + private String geoCoordSys; +} diff --git a/src/main/java/com/genersoft/iot/vmp/web/custom/bean/Point.java b/src/main/java/com/genersoft/iot/vmp/web/custom/bean/Point.java new file mode 100644 index 0000000..0f0deb7 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/web/custom/bean/Point.java @@ -0,0 +1,13 @@ +package com.genersoft.iot.vmp.web.custom.bean; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Data +@Schema(description = "坐标") +public class Point { + + private double lng; + private double lat; + +} diff --git a/src/main/java/com/genersoft/iot/vmp/web/custom/bean/PolygonQueryParam.java b/src/main/java/com/genersoft/iot/vmp/web/custom/bean/PolygonQueryParam.java new file mode 100644 index 0000000..23613da --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/web/custom/bean/PolygonQueryParam.java @@ -0,0 +1,23 @@ +package com.genersoft.iot.vmp.web.custom.bean; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.util.List; + +@Data +@Schema(description = "多边形检索摄像头参数") +public class PolygonQueryParam { + + @Schema(description = "多边形位置,格式: [{'lng':116.32, 'lat': 39: 39.2}, {'lng':115.32, 'lat': 39: 38.2}, {'lng':125.32, 'lat': 39: 38.2}]") + private List position; + + @Schema(description = "地图级别") + private Integer level; + + @Schema(description = "分组别名") + private String groupAlias; + + @Schema(description = "坐标系类型:WGS84,GCJ02、BD09") + private String geoCoordSys; +} diff --git a/src/main/java/com/genersoft/iot/vmp/web/custom/conf/CachedBodyHttpServletRequest.java b/src/main/java/com/genersoft/iot/vmp/web/custom/conf/CachedBodyHttpServletRequest.java new file mode 100644 index 0000000..3a5a999 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/web/custom/conf/CachedBodyHttpServletRequest.java @@ -0,0 +1,125 @@ +package com.genersoft.iot.vmp.web.custom.conf; + +import jakarta.servlet.ReadListener; +import jakarta.servlet.ServletInputStream; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletRequestWrapper; +import lombok.extern.slf4j.Slf4j; + +import java.io.*; +import java.nio.charset.StandardCharsets; + +/** + * 自定义请求包装器,用于缓存请求体内容 + * 解决流只能读取一次的问题 + */ +@Slf4j +public class CachedBodyHttpServletRequest extends HttpServletRequestWrapper { + + private byte[] cachedBody; + private String cachedBodyString; + + public CachedBodyHttpServletRequest(HttpServletRequest request) { + super(request); + } + + @Override + public ServletInputStream getInputStream() throws IOException { + if (cachedBody == null) { + cacheInputStream(); + } + return new CachedBodyServletInputStream(cachedBody); + } + + @Override + public BufferedReader getReader() throws IOException { + if (cachedBodyString == null) { + if (cachedBody == null) { + cacheInputStream(); + } + cachedBodyString = new String(cachedBody, StandardCharsets.UTF_8); + } + return new BufferedReader(new StringReader(cachedBodyString)); + } + + /** + * 获取缓存的请求体内容 + */ + public String getCachedBody() { + if (cachedBodyString == null) { + if (cachedBody == null) { + try { + cacheInputStream(); + } catch (IOException e) { + log.warn("缓存请求体失败: {}", e.getMessage()); + return ""; + } + } + cachedBodyString = new String(cachedBody, StandardCharsets.UTF_8); + } + return cachedBodyString; + } + + /** + * 获取缓存的请求体字节数组 + */ + public byte[] getCachedBodyBytes() { + if (cachedBody == null) { + try { + cacheInputStream(); + } catch (IOException e) { + log.warn("缓存请求体失败: {}", e.getMessage()); + return new byte[0]; + } + } + return cachedBody; + } + + private void cacheInputStream() throws IOException { + try (ByteArrayOutputStream baos = new ByteArrayOutputStream(); + InputStream inputStream = super.getInputStream()) { + + byte[] buffer = new byte[1024]; + int bytesRead; + while ((bytesRead = inputStream.read(buffer)) != -1) { + baos.write(buffer, 0, bytesRead); + } + cachedBody = baos.toByteArray(); + log.debug("成功缓存请求体,长度: {}", cachedBody.length); + } + } + + /** + * 自定义 ServletInputStream 实现 + */ + private static class CachedBodyServletInputStream extends ServletInputStream { + private final ByteArrayInputStream inputStream; + + public CachedBodyServletInputStream(byte[] body) { + this.inputStream = new ByteArrayInputStream(body); + } + + @Override + public boolean isFinished() { + return inputStream.available() == 0; + } + + @Override + public boolean isReady() { + return true; + } + + @Override + public void setReadListener(ReadListener readListener) { + // 不需要实现 + } + + @Override + public int read() throws IOException { + return inputStream.read(); + } + } +} + + + diff --git a/src/main/java/com/genersoft/iot/vmp/web/custom/conf/SignAuthenticationFilter.java b/src/main/java/com/genersoft/iot/vmp/web/custom/conf/SignAuthenticationFilter.java new file mode 100644 index 0000000..b5c4bc0 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/web/custom/conf/SignAuthenticationFilter.java @@ -0,0 +1,174 @@ +package com.genersoft.iot.vmp.web.custom.conf; + +import cn.hutool.core.util.CharsetUtil; +import cn.hutool.core.util.HexUtil; +import cn.hutool.crypto.SmUtil; +import cn.hutool.crypto.symmetric.SM4; +import com.alibaba.fastjson2.JSON; +import com.alibaba.fastjson2.JSONObject; +import com.genersoft.iot.vmp.vmanager.bean.WVPResult; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Component; +import org.springframework.util.ObjectUtils; +import org.springframework.web.filter.OncePerRequestFilter; + +import javax.sip.message.Response; +import java.io.IOException; +import java.io.PrintWriter; +import java.util.Map; +import java.util.Set; +import java.util.TreeSet; + +/** + * sign token 过滤器 + */ + +@Slf4j +@Component +@ConditionalOnProperty(value = "sy.enable", havingValue = "true") +public class SignAuthenticationFilter extends OncePerRequestFilter { + + + @Override + protected void doFilterInternal(HttpServletRequest servletRequest, HttpServletResponse response, FilterChain chain) throws IOException, ServletException { + // 忽略登录请求的token验证 + String requestURI = servletRequest.getRequestURI(); + // 包装原始请求,缓存请求体 + CachedBodyHttpServletRequest request = new CachedBodyHttpServletRequest(servletRequest); + if (!requestURI.startsWith("/api/sy")) { + chain.doFilter(request, response); + return; + } +// if (request.getParameter("ccerty") != null) { +// chain.doFilter(request, response); +// return; +// } + // 设置响应内容类型 + response.setContentType("application/json;charset=UTF-8"); + + try { + String sign = request.getParameter("sign"); + String appKey = request.getParameter("appKey"); + String accessToken = request.getParameter("accessToken"); + String timestampStr = request.getParameter("timestamp"); + + if (sign == null || appKey == null || accessToken == null || timestampStr == null) { + log.info("[SY-接口验签] 缺少关键参数:sign/appKey/accessToken/timestamp, 请求地址: {} ", requestURI); + response.setStatus(Response.OK); + PrintWriter out = response.getWriter(); + out.println(getErrorResult(1, "参数非法")); + out.close(); + return; + } + if (SyTokenManager.INSTANCE.appMap.get(appKey) == null) { + log.info("[SY-接口验签] appKey {} 对应的 secret 不存在, 请求地址: {} ", appKey, requestURI); + response.setStatus(Response.OK); + PrintWriter out = response.getWriter(); + out.println(getErrorResult(1, "参数非法")); + out.close(); + return; + } + + Map parameterMap = request.getParameterMap(); + // 参数排序 + Set paramKeys = new TreeSet<>(parameterMap.keySet()); + + // 拼接签名信息 + // 参数拼接 + StringBuilder beforeSign = new StringBuilder(); + for (String paramKey : paramKeys) { + if (paramKey.equals("sign")) { + continue; + } + beforeSign.append(paramKey).append(parameterMap.get(paramKey)[0]); + } + // 如果是post请求的json消息,拼接body字符串 + if (request.getContentLength() > 0 + && request.getMethod().equalsIgnoreCase("POST") + && request.getContentType().equalsIgnoreCase(MediaType.APPLICATION_JSON_VALUE)) { + // 读取body内容 - 使用自定义缓存机制 + String requestBody = request.getCachedBody(); + if (!ObjectUtils.isEmpty(requestBody)) { + beforeSign.append(requestBody); + log.debug("[SY-接口验签] 读取到请求体内容,长度: {}", requestBody.length()); + } else { + log.warn("[SY-接口验签] 请求体内容为空"); + } + } + beforeSign.append(SyTokenManager.INSTANCE.appMap.get(appKey)); + // 生成签名 + String buildSign = SmUtil.sm3(beforeSign.toString()); + if (!buildSign.equals(sign)) { + log.info("[SY-接口验签] 失败,加密前内容: {}, 请求地址: {} ", beforeSign, requestURI); + response.setStatus(Response.OK); + PrintWriter out = response.getWriter(); + out.println(getErrorResult(2, "签名错误")); + out.close(); + return; + } + // 验证请求时间戳 + long timestamp = Long.parseLong(timestampStr); + long currentTimeMillis = System.currentTimeMillis(); + if (currentTimeMillis > SyTokenManager.INSTANCE.expires * 60 * 1000 + timestamp ) { + log.info("[SY-接口验签] 时间戳已经过期, 请求时间戳:{}, 当前时间: {}, 过期时间: {}, 请求地址: {} ", timestamp, currentTimeMillis, timestamp + SyTokenManager.INSTANCE.expires * 60 * 1000, requestURI); + response.setStatus(Response.OK); + PrintWriter out = response.getWriter(); + out.println(getErrorResult(3, "接口己过期")); + out.close(); + return; + } + // accessToken校验 + if (accessToken.equals(SyTokenManager.INSTANCE.adminToken)) { + log.info("[SY-接口验签] adminToken已经默认放行, 请求地址: {} ", requestURI); + chain.doFilter(request, response); + return; + }else { + // 对token进行解密 + SM4 sm4 = SmUtil.sm4(HexUtil.decodeHex(SyTokenManager.INSTANCE.sm4Key)); + String decryptStr = sm4.decryptStr(accessToken, CharsetUtil.CHARSET_UTF_8); + if (decryptStr == null) { + log.info("[SY-接口验签] accessToken解密失败, 请求地址: {} ", requestURI); + response.setStatus(Response.OK); + PrintWriter out = response.getWriter(); + out.println(getErrorResult(2, "签名错误")); + out.close(); + return; + } + JSONObject jsonObject = JSON.parseObject(decryptStr); + Long expirationTime = jsonObject.getLong("expirationTime"); + if (expirationTime < System.currentTimeMillis()) { + log.info("[SY-接口验签] accessToken 已经过期, 请求地址: {} ", requestURI); + response.setStatus(Response.OK); + PrintWriter out = response.getWriter(); + out.println(getErrorResult(4, "token已过期或错误")); + out.close(); + return; + } + } + }catch (Exception e) { + log.info("[SY-接口验签] 读取body失败, 请求地址: {} ", requestURI, e); + response.setStatus(Response.OK); + if (!response.isCommitted()) { + PrintWriter out = response.getWriter(); + out.println(getErrorResult(2, "签名错误")); + out.close(); + } + return; + } + chain.doFilter(request, response); + } + + private String getErrorResult(Integer code, String message) { + WVPResult wvpResult = new WVPResult<>(); + wvpResult.setCode(code); + wvpResult.setMsg(message); + return JSON.toJSONString(wvpResult); + } + +} diff --git a/src/main/java/com/genersoft/iot/vmp/web/custom/conf/SyTokenManager.java b/src/main/java/com/genersoft/iot/vmp/web/custom/conf/SyTokenManager.java new file mode 100644 index 0000000..0d98244 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/web/custom/conf/SyTokenManager.java @@ -0,0 +1,31 @@ +package com.genersoft.iot.vmp.web.custom.conf; + +import java.util.HashMap; +import java.util.Map; + +public enum SyTokenManager { + INSTANCE; + + /** + * 普通用户 app Key 和 secret + */ + public final Map appMap = new HashMap<>(); + + + /** + * 管理员专属token + */ + public String adminToken; + + /** + * sm4密钥 + */ + public String sm4Key; + + /** + * 接口有效时长,单位分钟 + */ + public Long expires; + + +} diff --git a/src/main/java/com/genersoft/iot/vmp/web/custom/service/CameraChannelService.java b/src/main/java/com/genersoft/iot/vmp/web/custom/service/CameraChannelService.java new file mode 100644 index 0000000..9bf7018 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/web/custom/service/CameraChannelService.java @@ -0,0 +1,654 @@ +package com.genersoft.iot.vmp.web.custom.service; + +import com.alibaba.fastjson2.JSONArray; +import com.alibaba.fastjson2.JSONObject; +import com.genersoft.iot.vmp.conf.DynamicTask; +import com.genersoft.iot.vmp.conf.UserSetting; +import com.genersoft.iot.vmp.conf.exception.ControllerException; +import com.genersoft.iot.vmp.gb28181.bean.CommonGBChannel; +import com.genersoft.iot.vmp.gb28181.bean.FrontEndControlCodeForPTZ; +import com.genersoft.iot.vmp.gb28181.bean.Group; +import com.genersoft.iot.vmp.gb28181.bean.MobilePosition; +import com.genersoft.iot.vmp.gb28181.dao.CommonGBChannelMapper; +import com.genersoft.iot.vmp.gb28181.dao.GroupMapper; +import com.genersoft.iot.vmp.gb28181.event.channel.ChannelEvent; +import com.genersoft.iot.vmp.gb28181.event.subscribe.mobilePosition.MobilePositionEvent; +import com.genersoft.iot.vmp.gb28181.service.IGbChannelControlService; +import com.genersoft.iot.vmp.gb28181.service.IGbChannelPlayService; +import com.genersoft.iot.vmp.service.bean.ErrorCallback; +import com.genersoft.iot.vmp.utils.Coordtransform; +import com.genersoft.iot.vmp.vmanager.bean.ErrorCode; +import com.genersoft.iot.vmp.web.custom.bean.*; +import com.genersoft.iot.vmp.web.custom.conf.SyTokenManager; +import com.github.pagehelper.PageHelper; +import com.github.pagehelper.PageInfo; +import com.github.xiaoymin.knife4j.core.util.Assert; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.CommandLineRunner; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.event.EventListener; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Service; + +import java.util.*; + +@Slf4j +@Service +@ConditionalOnProperty(value = "sy.enable", havingValue = "true") +public class CameraChannelService implements CommandLineRunner { + + private final String REDIS_GPS_MESSAGE = "VM_MSG_MOBILE_GPS"; + private final String REDIS_CHANNEL_MESSAGE = "VM_MSG_MOBILE_CHANNEL"; + + @Autowired + private CommonGBChannelMapper channelMapper; + + @Autowired + private GroupMapper groupMapper; + + @Autowired + private RedisTemplate redisTemplate; + + @Autowired + private RedisTemplate redisTemplateForString; + + @Autowired + private IGbChannelPlayService channelPlayService; + + @Autowired + private IGbChannelControlService channelControlService; + + @Autowired + private UserSetting userSetting; + + @Autowired + private DynamicTask dynamicTask; + + @Override + public void run(String... args) { + // 启动时获取全局token + String taskKey = UUID.randomUUID().toString(); + if (!refreshToken()) { + log.info("[SY-读取Token]失败,30秒后重试"); + dynamicTask.startDelay(taskKey, ()->{ + this.run(args); + }, 30000); + }else { + log.info("[SY-读取Token] 成功"); + } + } + + private boolean refreshToken() { + String adminToken = redisTemplateForString.opsForValue().get("SYSTEM_ACCESS_TOKEN"); + if (adminToken == null) { + log.warn("[SY读取TOKEN] SYSTEM_ACCESS_TOKEN 读取失败"); + return false; + } + SyTokenManager.INSTANCE.adminToken = adminToken; + + String sm4Key = redisTemplateForString.opsForValue().get("SYSTEM_SM4_KEY"); + if (sm4Key == null) { + log.warn("[SY读取TOKEN] SYSTEM_SM4_KEY 读取失败"); + return false; + } + SyTokenManager.INSTANCE.sm4Key = sm4Key; + + JSONObject appJson = (JSONObject)redisTemplate.opsForValue().get("SYSTEM_APPKEY"); + if (appJson == null) { + log.warn("[SY读取TOKEN] SYSTEM_APPKEY 读取失败"); + return false; + } + SyTokenManager.INSTANCE.appMap.put(appJson.getString("appKey"), appJson.getString("appSecret")); + + JSONObject timeJson = (JSONObject)redisTemplate.opsForValue().get("sys_INTERFACE_VALID_TIME"); + if (timeJson == null) { + log.warn("[SY读取TOKEN] sys_INTERFACE_VALID_TIME 读取失败"); + return false; + } + SyTokenManager.INSTANCE.expires = timeJson.getLong("systemValue"); + + return true; + } + + // 监听通道变化,如果是移动设备则发送redis消息 + @EventListener + public void onApplicationEvent(ChannelEvent event) { + List channels = event.getChannels(); + if (channels.isEmpty()) { + return; + } + List resultListForAdd = new ArrayList<>(); + List resultListForDelete = new ArrayList<>(); + List resultListForUpdate = new ArrayList<>(); + List resultListForOnline = new ArrayList<>(); + List resultListForOffline = new ArrayList<>(); + + switch (event.getMessageType()) { + case UPDATE: + List oldChannelList = event.getOldChannels(); + List channelList = event.getChannels(); + // 更新操作 + if (oldChannelList == null || oldChannelList.isEmpty()) { + // 无旧设备则不需要判断, 目前只有分组或行政区划转换为通道信息时没有旧的通道信息,这两个类型也是不需要发送通知的,直接忽略即可 + break; + } + // 需要比对旧数据,看看是否是新增的移动设备或者取消的移动设备 + // 将 channelList 转为以 gbDeviceId 为 key 的 Map + Map oldChannelMap = new HashMap<>(); + for (CommonGBChannel channel : oldChannelList) { + if (channel != null && channel.getGbDeviceId() != null) { + oldChannelMap.put(channel.getGbDeviceId(), channel); + } + } + for (CommonGBChannel channel : channelList) { + if (channel.getGbPtzType() != null && channel.getGbPtzType() == 99) { + CommonGBChannel oldChannel = oldChannelMap.get(channel.getGbDeviceId()); + if (channel.getGbStatus() == null) { + channel.setGbStatus(oldChannel.getGbStatus()); + } + if (oldChannel != null) { + if (oldChannel.getGbPtzType() != null && oldChannel.getGbPtzType() == 99) { + resultListForUpdate.add(channel); + }else { + resultListForAdd.add(channel); + } + }else { + resultListForAdd.add(channel); + } + }else { + CommonGBChannel oldChannel = oldChannelMap.get(channel.getGbDeviceId()); + if (oldChannel != null && oldChannel.getGbPtzType() != null && oldChannel.getGbPtzType() == 99) { + CameraChannel cameraChannel = new CameraChannel(); + cameraChannel.setGbDeviceId(channel.getGbDeviceId()); + resultListForDelete.add(cameraChannel); + } + } + } + + break; + case DEL: + for (CommonGBChannel channel : channels) { + if (channel.getGbPtzType() != null && channel.getGbPtzType() == 99) { + CameraChannel cameraChannel = new CameraChannel(); + cameraChannel.setGbDeviceId(channel.getGbDeviceId()); + resultListForDelete.add(cameraChannel); + } + } + break; + case ON: + case OFF: + case DEFECT: + case VLOST: + for (CommonGBChannel channel : channels) { + if (channel.getGbPtzType() != null && channel.getGbPtzType() == 99) { + if (event.getMessageType() == ChannelEvent.ChannelEventMessageType.ON) { + resultListForOnline.add(channel); + }else { + resultListForOffline.add(channel); + } + + } + } + break; + case ADD: + for (CommonGBChannel channel : channels) { + if (channel.getGbPtzType() != null && channel.getGbPtzType() == 99) { + resultListForAdd.add(channel); + } + } + break; + } + if (!resultListForDelete.isEmpty()) { + JSONObject jsonObject = new JSONObject(); + jsonObject.put("type", ChannelEvent.ChannelEventMessageType.DEL); + jsonObject.put("list", resultListForDelete); + log.info("[SY-redis发送通知-DEL] 发送 通道信息变化 {}: {}", REDIS_CHANNEL_MESSAGE, jsonObject.toString()); + redisTemplate.convertAndSend(REDIS_CHANNEL_MESSAGE, jsonObject); + } + if (!resultListForAdd.isEmpty()) { + sendChannelMessage(resultListForAdd, ChannelEvent.ChannelEventMessageType.ADD); + } + if (!resultListForUpdate.isEmpty()) { + sendChannelMessage(resultListForUpdate, ChannelEvent.ChannelEventMessageType.UPDATE); + } + if (!resultListForOnline.isEmpty()) { + sendChannelMessage(resultListForOnline, ChannelEvent.ChannelEventMessageType.ON); + } + if (!resultListForOffline.isEmpty()) { + sendChannelMessage(resultListForOffline, ChannelEvent.ChannelEventMessageType.OFF); + } + } + + private void sendChannelMessage(List channelList, ChannelEvent.ChannelEventMessageType type) { + if (channelList.isEmpty()) { + return; + } + List cameraChannelList = channelMapper.queryCameraChannelByIds(channelList); + JSONObject jsonObject = new JSONObject(); + jsonObject.put("type", type); + jsonObject.put("list", cameraChannelList); + log.info("[SY-redis发送通知-{}] 发送 通道信息变化 {}: {}", type, REDIS_CHANNEL_MESSAGE, jsonObject.toString()); + redisTemplate.convertAndSend(REDIS_CHANNEL_MESSAGE, jsonObject); + } + + // 监听GPS消息,如果是移动设备则发送redis消息 + @EventListener + public void onApplicationEvent(MobilePositionEvent event) { + MobilePosition mobilePosition = event.getMobilePosition(); + Integer channelId = mobilePosition.getChannelId(); + CameraChannel cameraChannel = channelMapper.queryCameraChannelById(channelId); + + // 非移动设备类型 不发送 + if (cameraChannel == null || cameraChannel.getGbPtzType() == null || cameraChannel.getGbPtzType() != 99) { + return; + } + // 发送redis消息 + JSONObject jsonObject = new JSONObject(); + jsonObject.put("time", mobilePosition.getTime()); + jsonObject.put("deviceId", mobilePosition.getChannelDeviceId()); + jsonObject.put("longitude", mobilePosition.getLongitude()); + jsonObject.put("latitude", mobilePosition.getLatitude()); + jsonObject.put("altitude", mobilePosition.getAltitude()); + jsonObject.put("direction", mobilePosition.getDirection()); + jsonObject.put("speed", mobilePosition.getSpeed()); + jsonObject.put("topGroupGAlias", cameraChannel.getTopGroupGAlias()); + jsonObject.put("groupAlias", cameraChannel.getGroupAlias()); + log.info("[SY-redis发送通知] 发送 移动设备位置信息移动位置 {}: {}", REDIS_GPS_MESSAGE, jsonObject.toString()); + redisTemplate.convertAndSend(REDIS_GPS_MESSAGE, jsonObject); + } + + + public PageInfo queryList(Integer page, Integer count, String groupAlias, Boolean status, String geoCoordSys) { + // 构建组织结构信息 + Group group = groupMapper.queryGroupByAlias(groupAlias); + Assert.notNull(group, "组织结构不存在"); + String groupDeviceId = group.getDeviceId(); + + // 构建分页 + PageHelper.startPage(page, count); + + List all = channelMapper.queryListForSy(groupDeviceId, status); + PageInfo groupPageInfo = new PageInfo<>(all); + List list = addIconPathAndPositionForCameraChannelList(groupPageInfo.getList(), geoCoordSys); + groupPageInfo.setList(list); + return groupPageInfo; + } + + public PageInfo queryListWithChild(Integer page, Integer count, String query, String sortName, Boolean order, String groupAlias, Boolean status, String geoCoordSys) { + + List groupList = null; + // 构建组织结构信息 + if (groupAlias != null) { + CameraGroup group = groupMapper.queryGroupByAlias(groupAlias); + Assert.notNull(group, "组织结构不存在"); + String groupDeviceId = group.getDeviceId(); + // 获取所有子节点 + groupList = queryAllGroupChildren(group.getId(), group.getBusinessGroup()); + groupList.add(group); + } + + // 构建分页 + PageHelper.startPage(page, count); + if (query != null) { + query = query.replaceAll("/", "//") + .replaceAll("%", "/%") + .replaceAll("_", "/_"); + } + if (order == null) { + order = true; + } + List all = channelMapper.queryListWithChildForSy(query, sortName, order, groupList, status); + PageInfo groupPageInfo = new PageInfo<>(all); + List list = addIconPathAndPositionForCameraChannelList(groupPageInfo.getList(), geoCoordSys); + groupPageInfo.setList(list); + return groupPageInfo; + } + + // 获取所有子节点 + private List queryAllGroupChildren(int groupId, String businessGroup) { + Map groupMap = groupMapper.queryByBusinessGroupForMap(businessGroup); + for (CameraGroup cameraGroup : groupMap.values()) { + cameraGroup.setParent(groupMap.get(cameraGroup.getParentId())); + } + CameraGroup cameraGroup = groupMap.get(groupId); + if (cameraGroup == null) { + return Collections.emptyList(); + }else { + return cameraGroup.getChild(); + } + } + + public List queryCountWithChild(String groupAlias) { + // 构建组织结构信息 + CameraGroup group = groupMapper.queryGroupByAlias(groupAlias); + Assert.notNull(group, "组织结构不存在"); + // 获取所有子节点 + List groupList = queryAllGroupChildren(group.getId(), group.getBusinessGroup()); + groupList.add(group); + + // TODO 此处整理可优化,尽量让sql直接返回对应的结构 无需二次整理 + List cameraCounts = groupMapper.queryCountWithChild(groupList); + if (cameraCounts.isEmpty()) { + return Collections.emptyList(); + }else { + Map cameraGroupMap = new HashMap<>(); + for (CameraGroup cameraGroup : groupList) { + cameraGroupMap.put(cameraGroup.getDeviceId(), cameraGroup.getAlias()); + } + List result = new ArrayList<>(); + for (CameraCount cameraCount : cameraCounts) { + String alias = cameraGroupMap.get(cameraCount.getDeviceId()); + if (alias == null) { + continue; + } + cameraCount.setGroupAlias(alias); + result.add(cameraCount); + } + return result; + } + } + + /** + * 为通道增加图片信息和转换坐标系 + */ + private List addIconPathAndPositionForCameraChannelList(List channels, String geoCoordSys) { + // 读取redis 图标信息 + /* + { + "brand": "WVP", + "createdTime": 1715845840000, + "displayInSelect": true, + "id": 12, + "imagesPath": "images/lt132", + "machineName": "图传对讲单兵", + "machineType": "LT132" + }, + */ + JSONArray jsonArray = (JSONArray) redisTemplate.opsForValue().get("machineInfo"); + Map pathMap = new HashMap<>(); + if (jsonArray != null && !jsonArray.isEmpty()) { + for (int i = 0; i < jsonArray.size(); i++) { + JSONObject jsonObject = jsonArray.getJSONObject(i); + String machineType = jsonObject.getString("machineType"); + String imagesPath = jsonObject.getString("imagesPath"); + if (machineType != null && imagesPath != null) { + pathMap.put(machineType, imagesPath); + } + } + }else { + log.warn("[读取通道图标信息失败]"); + } + for (CameraChannel channel : channels) { + if (channel.getGbModel() != null && pathMap.get(channel.getGbModel()) != null) { + channel.setIcon(pathMap.get(channel.getGbModel())); + } + // 坐标系转换 + if (geoCoordSys != null && channel.getGbLongitude() != null && channel.getGbLatitude() != null + && channel.getGbLongitude() > 0 && channel.getGbLatitude() > 0) { + if (geoCoordSys.equalsIgnoreCase("GCJ02")) { + Double[] position = Coordtransform.WGS84ToGCJ02(channel.getGbLongitude(), channel.getGbLatitude()); + channel.setGbLongitude(position[0]); + channel.setGbLatitude(position[1]); + }else if (geoCoordSys.equalsIgnoreCase("BD09")) { + Double[] gcj02Position = Coordtransform.WGS84ToGCJ02(channel.getGbLongitude(), channel.getGbLatitude()); + Double[] position = Coordtransform.GCJ02ToBD09(gcj02Position[0], gcj02Position[1]); + channel.setGbLongitude(position[0]); + channel.setGbLatitude(position[1]); + } + } + } + return channels; + } + + public CameraChannel queryOne(String deviceId, String deviceCode, String geoCoordSys) { + List cameraChannels = channelMapper.queryGbChannelByChannelDeviceIdAndGbDeviceId(deviceId, deviceCode); + Assert.isTrue(cameraChannels.isEmpty(), "通道不存在"); + List channels = addIconPathAndPositionForCameraChannelList(cameraChannels, geoCoordSys); + CameraChannel channel = channels.get(0); + if (deviceCode != null) { + channel.setDeviceCode(deviceCode); + } + + return channel; + } + + /** + * 播放通道 + * @param deviceId 通道编号 + * @param deviceCode 通道对应的国标设备的编号 + * @param callback 点播结果的回放 + */ + public void play(String deviceId, String deviceCode, ErrorCallback callback) { + List cameraChannels = channelMapper.queryGbChannelByChannelDeviceIdAndGbDeviceId(deviceId, deviceCode); + Assert.isTrue(cameraChannels.isEmpty(), "通道不存在"); + CameraChannel channel = cameraChannels.get(0); + channelPlayService.play(channel, null, userSetting.getRecordSip(), (code, msg, data) -> { + callback.run(code, msg, new CameraStreamInfo(channel, data)); + }); + } + + /** + * 停止播放通道 + * @param deviceId 通道编号 + * @param deviceCode 通道对应的国标设备的编号 + */ + public void stopPlay(String deviceId, String deviceCode) { + List cameraChannels = channelMapper.queryGbChannelByChannelDeviceIdAndGbDeviceId(deviceId, deviceCode); + Assert.isTrue(cameraChannels.isEmpty(), "通道不存在"); + CameraChannel channel = cameraChannels.get(0); + channelPlayService.stopPlay(channel); + } + + public void ptz(String deviceId, String deviceCode, String command, Integer speed, ErrorCallback callback) { + List cameraChannels = channelMapper.queryGbChannelByChannelDeviceIdAndGbDeviceId(deviceId, deviceCode); + Assert.isTrue(cameraChannels.isEmpty(), "通道不存在"); + CameraChannel channel = cameraChannels.get(0); + + if (speed == null) { + speed = 50; + }else if (speed < 0 || speed > 100) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "panSpeed 为 0-100的数字"); + } + + FrontEndControlCodeForPTZ controlCode = new FrontEndControlCodeForPTZ(); + controlCode.setPanSpeed(speed); + controlCode.setTiltSpeed(speed); + controlCode.setZoomSpeed(speed); + switch (command){ + case "left": + controlCode.setPan(0); + break; + case "right": + controlCode.setPan(1); + break; + case "up": + controlCode.setTilt(0); + break; + case "down": + controlCode.setTilt(1); + break; + case "upleft": + controlCode.setPan(0); + controlCode.setTilt(0); + break; + case "upright": + controlCode.setTilt(0); + controlCode.setPan(1); + break; + case "downleft": + controlCode.setPan(0); + controlCode.setTilt(1); + break; + case "downright": + controlCode.setTilt(1); + controlCode.setPan(1); + break; + case "zoomin": + controlCode.setZoom(1); + break; + case "zoomout": + controlCode.setZoom(0); + break; + default: + break; + } + + channelControlService.ptz(channel, controlCode, callback); + } + + public void updateCamera(String deviceId, String deviceCode, String name, Double longitude, Double latitude, String geoCoordSys) { + List cameraChannels = channelMapper.queryGbChannelByChannelDeviceIdAndGbDeviceId(deviceId, deviceCode); + Assert.isTrue(cameraChannels.isEmpty(), "通道不存在"); + CameraChannel commonGBChannel = cameraChannels.get(0); + commonGBChannel.setGbName(name); + if (geoCoordSys != null && longitude != null && latitude != null + && longitude > 0 && latitude > 0) { + if (geoCoordSys.equalsIgnoreCase("GCJ02")) { + Double[] position = Coordtransform.GCJ02ToWGS84(longitude, latitude); + commonGBChannel.setGbLongitude(position[0]); + commonGBChannel.setGbLatitude(position[1]); + }else if (geoCoordSys.equalsIgnoreCase("BD09")) { + Double[] gcj02Position = Coordtransform.BD09ToGCJ02(longitude, latitude); + Double[] position = Coordtransform.GCJ02ToWGS84(gcj02Position[0], gcj02Position[1]); + commonGBChannel.setGbLongitude(position[0]); + commonGBChannel.setGbLatitude(position[1]); + }else { + commonGBChannel.setGbLongitude(longitude); + commonGBChannel.setGbLatitude(latitude); + } + }else { + commonGBChannel.setGbLongitude(longitude); + commonGBChannel.setGbLatitude(latitude); + } + channelMapper.update(commonGBChannel); + } + + public List queryListByDeviceIds(List deviceIds, String geoCoordSys) { + List cameraChannels = channelMapper.queryListByDeviceIds(deviceIds); + return addIconPathAndPositionForCameraChannelList(cameraChannels, geoCoordSys); + } + + public List queryListByAddressAndDirectionType(String address, Integer directionType, String geoCoordSys) { + List cameraChannels = channelMapper.queryListByAddressAndDirectionType(address, directionType); + return addIconPathAndPositionForCameraChannelList(cameraChannels, geoCoordSys); + } + + + public List queryListInBox(Double minLongitude, Double maxLongitude, Double minLatitude, Double maxLatitude, Integer level, String groupAlias, String geoCoordSys) { + // 构建组织结构信息 + CameraGroup group = groupMapper.queryGroupByAlias(groupAlias); + Assert.notNull(group, "组织结构不存在"); + // 获取所有子节点 + List groupList = queryAllGroupChildren(group.getId(), group.getBusinessGroup()); + groupList.add(group); + // 参数坐标系列转换 + if (geoCoordSys != null) { + if (geoCoordSys.equalsIgnoreCase("GCJ02")) { + Double[] minPosition = Coordtransform.GCJ02ToWGS84(minLongitude, minLatitude); + minLongitude = minPosition[0]; + minLatitude = minPosition[1]; + + Double[] maxPosition = Coordtransform.GCJ02ToWGS84(maxLongitude, maxLatitude); + maxLongitude = maxPosition[0]; + maxLatitude = maxPosition[1]; + }else if (geoCoordSys.equalsIgnoreCase("BD09")) { + Double[] gcj02MinPosition = Coordtransform.BD09ToGCJ02(minLongitude, minLatitude); + Double[] minPosition = Coordtransform.GCJ02ToWGS84(gcj02MinPosition[0], gcj02MinPosition[1]); + minLongitude = minPosition[0]; + minLatitude = minPosition[1]; + + Double[] gcj02MaxPosition = Coordtransform.BD09ToGCJ02(maxLongitude, maxLatitude); + Double[] maxPosition = Coordtransform.GCJ02ToWGS84(gcj02MaxPosition[0], gcj02MaxPosition[1]); + maxLongitude = maxPosition[0]; + maxLatitude = maxPosition[1]; + } + } + + List all = channelMapper.queryListInBox(minLongitude, maxLongitude, minLatitude, maxLatitude, level, groupList); + return addIconPathAndPositionForCameraChannelList(all, geoCoordSys); + } + + public List queryListInCircle(Double centerLongitude, Double centerLatitude, Double radius, Integer level, String groupAlias, String geoCoordSys) { + // 构建组织结构信息 + CameraGroup group = groupMapper.queryGroupByAlias(groupAlias); + Assert.notNull(group, "组织结构不存在"); + // 获取所有子节点 + List groupList = queryAllGroupChildren(group.getId(), group.getBusinessGroup()); + groupList.add(group); + + // 参数坐标系列转换 + if (geoCoordSys != null) { + if (geoCoordSys.equalsIgnoreCase("GCJ02")) { + Double[] position = Coordtransform.GCJ02ToWGS84(centerLongitude, centerLatitude); + centerLongitude = position[0]; + centerLatitude = position[1]; + + }else if (geoCoordSys.equalsIgnoreCase("BD09")) { + Double[] gcj02Position = Coordtransform.BD09ToGCJ02(centerLongitude, centerLatitude); + Double[] position = Coordtransform.GCJ02ToWGS84(gcj02Position[0], gcj02Position[1]); + centerLongitude = position[0]; + centerLatitude = position[1]; + } + } + + List all = channelMapper.queryListInCircle(centerLongitude, centerLatitude, radius, level, groupList); + return addIconPathAndPositionForCameraChannelList(all, geoCoordSys); + } + + public List queryListInPolygon(List pointList, String groupAlias, Integer level, String geoCoordSys) { + // 构建组织结构信息 + CameraGroup group = groupMapper.queryGroupByAlias(groupAlias); + Assert.notNull(group, "组织结构不存在"); + // 获取所有子节点 + List groupList = queryAllGroupChildren(group.getId(), group.getBusinessGroup()); + groupList.add(group); + + // 参数坐标系列转换 + if (geoCoordSys != null) { + for (Point point : pointList) { + if (geoCoordSys.equalsIgnoreCase("GCJ02")) { + Double[] position = Coordtransform.GCJ02ToWGS84(point.getLng(), point.getLat()); + point.setLng(position[0]); + point.setLat(position[1]); + }else if (geoCoordSys.equalsIgnoreCase("BD09")) { + Double[] gcj02Position = Coordtransform.BD09ToGCJ02(point.getLng(), point.getLat()); + Double[] position = Coordtransform.GCJ02ToWGS84(gcj02Position[0], gcj02Position[1]); + point.setLng(position[0]); + point.setLat(position[1]); + } + } + } + + List all = channelMapper.queryListInPolygon(pointList, level, groupList); + return addIconPathAndPositionForCameraChannelList(all, geoCoordSys); + } + + public PageInfo queryListForMobile(Integer page, Integer count, String topGroupAlias) { + + CameraGroup cameraGroup = groupMapper.queryGroupByAlias(topGroupAlias); + + String business = null; + if (cameraGroup != null) { + business = cameraGroup.getDeviceId(); + } + // 构建分页 + PageHelper.startPage(page, count); + List all = channelMapper.queryListForSyMobile(business); + + PageInfo groupPageInfo = new PageInfo<>(all); + List list = addIconPathAndPositionForCameraChannelList(groupPageInfo.getList(), null); + groupPageInfo.setList(list); + return groupPageInfo; + } + + + public List queryMeetingChannelList(String topGroupAlias) { + CameraGroup cameraGroup = groupMapper.queryGroupByAlias(topGroupAlias); + Assert.notNull(cameraGroup, "域不存在"); + String business = cameraGroup.getDeviceId(); + Assert.notNull(business, "域不存在"); + + return channelMapper.queryMeetingChannelList(business); + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/web/custom/service/SyServiceImpl.java b/src/main/java/com/genersoft/iot/vmp/web/custom/service/SyServiceImpl.java new file mode 100644 index 0000000..0950768 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/web/custom/service/SyServiceImpl.java @@ -0,0 +1,109 @@ +package com.genersoft.iot.vmp.web.custom.service; + +import com.alibaba.fastjson2.JSONArray; +import com.alibaba.fastjson2.JSONObject; +import com.genersoft.iot.vmp.service.IMapService; +import com.genersoft.iot.vmp.vmanager.bean.MapConfig; +import com.genersoft.iot.vmp.vmanager.bean.MapModelIcon; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.List; + +/** + * 第三方平台适配 + */ +@Slf4j +@Service +@ConditionalOnProperty(value = "sy.enable", havingValue = "true") +public class SyServiceImpl implements IMapService { + + @Autowired + private RedisTemplate redisTemplate; + + @Override + public List getConfig() { + List configList = new ArrayList<>(); + JSONObject configObject = (JSONObject)redisTemplate.opsForValue().get("interfaceConfig1"); + if (configObject == null) { + return configList; + } + // 浅色地图 + MapConfig mapConfigForDefault = readConfig("FRAGMENTIMG_SERVER", configObject); + if (mapConfigForDefault != null) { + mapConfigForDefault.setName("浅色地图"); + configList.add(mapConfigForDefault); + } + + // 深色地图 + MapConfig mapConfigForDark = readConfig("POLARNIGHTBLUE_FRAGMENTIMG_SERVER", configObject); + if (mapConfigForDark != null) { + mapConfigForDark.setName("深色地图"); + configList.add(mapConfigForDark); + } + + // 卫星地图 + MapConfig mapConfigForSatellited = readConfig("SATELLITE_FRAGMENTIMG_SERVER", configObject); + if (mapConfigForSatellited != null) { + mapConfigForSatellited.setName("卫星地图"); + configList.add(mapConfigForSatellited); + } + return configList; + } + + private MapConfig readConfig(String key, JSONObject jsonObject) { + JSONArray fragmentimgServerArray = jsonObject.getJSONArray(key); + if (fragmentimgServerArray == null || fragmentimgServerArray.isEmpty()) { + return null; + } + JSONObject fragmentimgServer = fragmentimgServerArray.getJSONObject(0); + // 坐标系 + String geoCoordSys = fragmentimgServer.getString("csysType").toUpperCase(); + // 获取地址 + String path = fragmentimgServer.getString("path"); + String ip = fragmentimgServer.getString("ip"); + JSONObject portJson = fragmentimgServer.getJSONObject("port"); + JSONObject httpPortJson = portJson.getJSONObject("httpPort"); + String protocol = httpPortJson.getString("portType"); + Integer port = httpPortJson.getInteger("port"); + String tileUrl = String.format("%s://%s:%s%s", protocol, ip, port, path); + MapConfig mapConfig = new MapConfig(); + mapConfig.setCoordinateSystem(geoCoordSys); + mapConfig.setTilesUrl(tileUrl); + return mapConfig; + + } + + @Override + public List getModelList() { + // 读取redis 图标信息 + /* + { + "brand": "WVP", + "createdTime": 1715845840000, + "displayInSelect": true, + "id": 12, + "imagesPath": "images/lt132", + "machineName": "图传对讲单兵", + "machineType": "LT132" + }, + */ + List mapModelIconList = new ArrayList<>(); + JSONArray jsonArray = (JSONArray) redisTemplate.opsForValue().get("machineInfo"); + if (jsonArray != null && !jsonArray.isEmpty()) { + for (int i = 0; i < jsonArray.size(); i++) { + JSONObject jsonObject = jsonArray.getJSONObject(i); + String machineType = jsonObject.getString("machineType"); + String machineName = jsonObject.getString("machineName"); + String imagesPath = jsonObject.getString("imagesPath"); + + mapModelIconList.add(MapModelIcon.getInstance(machineType, machineName, imagesPath)); + } + } + return mapModelIconList; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/web/gb28181/ApiControlController.java b/src/main/java/com/genersoft/iot/vmp/web/gb28181/ApiControlController.java new file mode 100644 index 0000000..529aac9 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/web/gb28181/ApiControlController.java @@ -0,0 +1,156 @@ +package com.genersoft.iot.vmp.web.gb28181; + +import com.genersoft.iot.vmp.conf.exception.ControllerException; +import com.genersoft.iot.vmp.gb28181.bean.Device; +import com.genersoft.iot.vmp.gb28181.transmit.cmd.impl.SIPCommander; +import com.genersoft.iot.vmp.gb28181.service.IDeviceService; +import com.genersoft.iot.vmp.vmanager.bean.ErrorCode; +import io.swagger.v3.oas.annotations.Hidden; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import javax.sip.InvalidArgumentException; +import javax.sip.SipException; +import java.text.ParseException; + +/** + * API兼容:设备控制 + */ +@Slf4j +@RestController +@RequestMapping(value = "/api/v1/control") +@Hidden +public class ApiControlController { + + @Autowired + private SIPCommander cmder; + + @Autowired + private IDeviceService deviceService; + + /** + * 设备控制 - 云台控制 + * @param serial 设备编号 + * @param command 控制指令 允许值: left, right, up, down, upleft, upright, downleft, downright, zoomin, zoomout, stop + * @param channel 通道序号 + * @param code 通道编号 + * @param speed 速度(0~255) 默认值: 129 + */ + @GetMapping(value = "/ptz") + private void ptz(String serial,String command, + @RequestParam(required = false)Integer channel, + @RequestParam(required = false)String code, + @RequestParam(required = false)Integer speed){ + + if (log.isDebugEnabled()) { + log.debug("模拟接口> 设备云台控制 API调用,deviceId:{} ,channelId:{} ,command:{} ,speed:{} ", + serial, code, command, speed); + } + if (channel == null) {channel = 0;} + if (speed == null) {speed = 0;} + Device device = deviceService.getDeviceByDeviceId(serial); + if (device == null) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "device[ " + serial + " ]未找到"); + } + int cmdCode = -1; + switch (command){ + case "left": + cmdCode = 2; + break; + case "right": + cmdCode = 1; + break; + case "up": + cmdCode = 8; + break; + case "down": + cmdCode = 4; + break; + case "upleft": + cmdCode = 10; + break; + case "upright": + cmdCode = 9; + break; + case "downleft": + cmdCode = 6; + break; + case "downright": + cmdCode = 5; + break; + case "zoomin": + cmdCode = 16; + break; + case "zoomout": + cmdCode = 32; + break; + case "stop": + cmdCode = 0; + break; + default: + break; + } + if (cmdCode == -1) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "未识别的指令:" + command); + } + // 默认值 50 + try { + cmder.frontEndCmd(device, code, cmdCode, speed, speed, speed); + } catch (SipException | InvalidArgumentException | ParseException e) { + log.error("[命令发送失败] 云台控制: {}", e.getMessage()); + throw new ControllerException(ErrorCode.ERROR100.getCode(), "命令发送失败: " + e.getMessage()); + } + } + + /** + * 设备控制 - 预置位控制 + * @param serial 设备编号 + * @param code 通道编号,通过 /api/v1/device/channellist 获取的 ChannelList.ID, 该参数和 channel 二选一传递即可 + * @param channel 通道序号, 默认值: 1 + * @param command 控制指令 允许值: set, goto, remove + * @param preset 预置位编号(1~255) + * @param name 预置位名称, command=set 时有效 + */ + @GetMapping(value = "/preset") + private void list(String serial,String command, + @RequestParam(required = false)Integer channel, + @RequestParam(required = false)String code, + @RequestParam(required = false)String name, + @RequestParam(required = false)Integer preset){ + + if (log.isDebugEnabled()) { + log.debug("模拟接口> 预置位控制 API调用,deviceId:{} ,channelId:{} ,command:{} ,name:{} ,preset:{} ", + serial, code, command, name, preset); + } + + if (channel == null) {channel = 0;} + Device device = deviceService.getDeviceByDeviceId(serial); + if (device == null) { + throw new ControllerException(ErrorCode.ERROR100.getCode(), "device[ " + serial + " ]未找到"); + } + int cmdCode = 0; + switch (command){ + case "set": + cmdCode = 129; + break; + case "goto": + cmdCode = 130; + break; + case "remove": + cmdCode = 131; + break; + default: + break; + } + try { + cmder.frontEndCmd(device, code, cmdCode, 0, preset, 0); + } catch (SipException | InvalidArgumentException | ParseException e) { + log.error("[命令发送失败] 预置位控制: {}", e.getMessage()); + throw new ControllerException(ErrorCode.ERROR100.getCode(), "命令发送失败: " + e.getMessage()); + } + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/web/gb28181/ApiController.java b/src/main/java/com/genersoft/iot/vmp/web/gb28181/ApiController.java new file mode 100644 index 0000000..cb3fdbe --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/web/gb28181/ApiController.java @@ -0,0 +1,106 @@ +package com.genersoft.iot.vmp.web.gb28181; + +import com.alibaba.fastjson2.JSONObject; +import com.genersoft.iot.vmp.conf.SipConfig; +import io.swagger.v3.oas.annotations.Hidden; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.ResponseBody; + +/** + * API兼容:系统接口 + */ +@Controller +@Slf4j +@RequestMapping(value = "/api/v1") +@Hidden +public class ApiController { + + @Autowired + private SipConfig sipConfig; + + + @GetMapping("/getserverinfo") + private JSONObject getserverinfo(){ + JSONObject result = new JSONObject(); + result.put("Authorization","ceshi"); + result.put("Hardware",""); + result.put("InterfaceVersion","2.5.5"); + result.put("IsDemo",""); + result.put("Hardware","false"); + result.put("APIAuth","false"); + result.put("RemainDays","永久"); + result.put("RunningTime",""); + result.put("ServerTime","2020-09-02 17:11"); + result.put("StartUpTime","2020-09-02 17:11"); + result.put("Server",""); + result.put("SIPSerial", sipConfig.getId()); + result.put("SIPRealm", sipConfig.getDomain()); + result.put("SIPHost", sipConfig.getShowIp()); + result.put("SIPPort", sipConfig.getPort()); + result.put("ChannelCount","1000"); + result.put("VersionType",""); + result.put("LogoMiniText",""); + result.put("LogoText",""); + result.put("CopyrightText",""); + + return result; + } + + @GetMapping(value = "/userinfo") + private JSONObject userinfo(){ +// JSONObject result = new JSONObject(); +// result.put("ID","ceshi"); +// result.put("Hardware",""); +// result.put("InterfaceVersion","2.5.5"); +// result.put("IsDemo",""); +// result.put("Hardware","false"); +// result.put("APIAuth","false"); +// result.put("RemainDays","永久"); +// result.put("RunningTime",""); +// result.put("ServerTime","2020-09-02 17:11"); +// result.put("StartUpTime","2020-09-02 17:11"); +// result.put("Server",""); +// result.put("SIPSerial", sipConfig.getId()); +// result.put("SIPRealm", sipConfig.getDomain()); +// result.put("SIPHost", sipConfig.getIp()); +// result.put("SIPPort", sipConfig.getPort()); +// result.put("ChannelCount","1000"); +// result.put("VersionType",""); +// result.put("LogoMiniText",""); +// result.put("LogoText",""); +// result.put("CopyrightText",""); + + return null; + } + + /** + * 系统接口 - 登录 + * @param username 用户名 + * @param password 密码(经过md5加密,32位长度,不带中划线,不区分大小写) + * @return + */ + @GetMapping(value = "/login") + @ResponseBody + private JSONObject login(String username,String password ){ + if (log.isDebugEnabled()) { + log.debug(String.format("模拟接口> 登录 API调用,username:%s ,password:%s ", + username, password)); + } + + JSONObject result = new JSONObject(); + result.put("CookieToken","ynBDDiKMg"); + result.put("URLToken","MOBkORkqnrnoVGcKIAHXppgfkNWRdV7utZSkDrI448Q.oxNjAxNTM4NDk3LCJwIjoiZGJjODg5NzliNzVj" + + "Nzc2YmU5MzBjM2JjNjg1ZWFiNGI5ZjhhN2Y0N2RlZjg3NWUyOTJkY2VkYjkwYmEwMTA0NyIsInQiOjE2MDA5MzM2OTcsInUiOiI" + + "4ODlkZDYyM2ViIn0eyJlIj.GciOiJIUzI1NiIsInR5cCI6IkpXVCJ9eyJhb"); + result.put("TokenTimeout",604800); + result.put("AuthToken","MOBkORkqnrnoVGcKIAHXppgfkNWRdV7utZSkDrI448Q.oxNjAxNTM4NDk3LCJwIjoiZGJjODg5NzliNzVj" + + "Nzc2YmU5MzBjM2JjNjg1ZWFiNGI5ZjhhN2Y0N2RlZjg3NWUyOTJkY2VkYjkwYmEwMTA0NyIsInQiOjE2MDA5MzM2OTcsInUiOiI" + + "4ODlkZDYyM2ViIn0eyJlIj.GciOiJIUzI1NiIsInR5cCI6IkpXVCJ9eyJhb"); + result.put("Token","ynBDDiKMg"); + return result; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/web/gb28181/ApiDeviceController.java b/src/main/java/com/genersoft/iot/vmp/web/gb28181/ApiDeviceController.java new file mode 100644 index 0000000..3b9ba81 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/web/gb28181/ApiDeviceController.java @@ -0,0 +1,228 @@ +package com.genersoft.iot.vmp.web.gb28181; + +import com.alibaba.fastjson2.JSONArray; +import com.alibaba.fastjson2.JSONObject; +import com.genersoft.iot.vmp.gb28181.bean.Device; +import com.genersoft.iot.vmp.gb28181.bean.Preset; +import com.genersoft.iot.vmp.gb28181.service.IDeviceChannelService; +import com.genersoft.iot.vmp.gb28181.service.IDeviceService; +import com.genersoft.iot.vmp.gb28181.transmit.callback.DeferredResultHolder; +import com.genersoft.iot.vmp.gb28181.transmit.cmd.impl.SIPCommander; +import com.genersoft.iot.vmp.vmanager.bean.ErrorCode; +import com.genersoft.iot.vmp.vmanager.bean.WVPResult; +import com.genersoft.iot.vmp.web.gb28181.dto.DeviceChannelExtend; +import com.github.pagehelper.PageInfo; +import io.swagger.v3.oas.annotations.Hidden; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.context.request.async.DeferredResult; + +import java.util.*; + +/** + * API兼容:设备信息 + */ +@SuppressWarnings("unchecked") +@Slf4j +@RestController +@RequestMapping(value = "/api/v1/device") +@Hidden +public class ApiDeviceController { + + @Autowired + private SIPCommander cmder; + @Autowired + private IDeviceChannelService channelService; + + @Autowired + private DeferredResultHolder resultHolder; + + @Autowired + private IDeviceService deviceService; + + + /** + * 分页获取设备列表 现在直接返回,尚未实现分页 + * @param start + * @param limit + * @param q + * @param online + * @return + */ + @GetMapping(value = "/list") + public JSONObject list( @RequestParam(required = false)Integer start, + @RequestParam(required = false)Integer limit, + @RequestParam(required = false)String q, + @RequestParam(required = false)Boolean online ){ + +// if (logger.isDebugEnabled()) { +// logger.debug("查询所有视频设备API调用"); +// } + + JSONObject result = new JSONObject(); + List devices; + if (start == null || limit ==null) { + devices = deviceService.getAllByStatus(online); + result.put("DeviceCount", devices.size()); + }else { + PageInfo deviceList = deviceService.getAll(start/limit, limit,null, online); + result.put("DeviceCount", deviceList.getTotal()); + devices = deviceList.getList(); + } + + JSONArray deviceJSONList = new JSONArray(); + devices.stream().forEach(device -> { + JSONObject deviceJsonObject = new JSONObject(); + deviceJsonObject.put("ID", device.getDeviceId()); + deviceJsonObject.put("Name", device.getName()); + deviceJsonObject.put("Type", "GB"); + deviceJsonObject.put("ChannelCount", device.getChannelCount()); + deviceJsonObject.put("RecvStreamIP", ""); + deviceJsonObject.put("CatalogInterval", 3600); // 通道目录抓取周期 + deviceJsonObject.put("SubscribeInterval", device.getSubscribeCycleForCatalog()); // 订阅周期(秒), 0 表示后台不周期订阅 + deviceJsonObject.put("Online", device.isOnLine()); + deviceJsonObject.put("Password", ""); + deviceJsonObject.put("MediaTransport", device.getTransport()); + deviceJsonObject.put("RemoteIP", device.getIp()); + deviceJsonObject.put("RemotePort", device.getPort()); + deviceJsonObject.put("LastRegisterAt", ""); + deviceJsonObject.put("LastKeepaliveAt", ""); + deviceJsonObject.put("UpdatedAt", ""); + deviceJsonObject.put("CreatedAt", ""); + deviceJSONList.add(deviceJsonObject); + }); + result.put("DeviceList",deviceJSONList); + return result; + } + + @GetMapping(value = "/channellist") + public JSONObject channellist( String serial, + @RequestParam(required = false)String channel_type, + @RequestParam(required = false)String code , + @RequestParam(required = false)String dir_serial , + @RequestParam(required = false)Integer start, + @RequestParam(required = false)Integer limit, + @RequestParam(required = false)String q, + @RequestParam(required = false)Boolean online ){ + + JSONObject result = new JSONObject(); + List deviceChannels; + List channelIds = null; + if (!ObjectUtils.isEmpty(code)) { + String[] split = code.trim().split(","); + channelIds = Arrays.asList(split); + } + List allDeviceChannelList = channelService.queryChannelExtendsByDeviceId(serial,channelIds,online); + if (start == null || limit ==null) { + deviceChannels = allDeviceChannelList; + result.put("ChannelCount", deviceChannels.size()); + }else { + if (start > allDeviceChannelList.size()) { + deviceChannels = new ArrayList<>(); + }else { + if (start + limit < allDeviceChannelList.size()) { + deviceChannels = allDeviceChannelList.subList(start, start + limit); + }else { + deviceChannels = allDeviceChannelList.subList(start, allDeviceChannelList.size()); + } + } + result.put("ChannelCount", allDeviceChannelList.size()); + } + JSONArray channleJSONList = new JSONArray(); + deviceChannels.stream().forEach(deviceChannelExtend -> { + JSONObject deviceJOSNChannel = new JSONObject(); + deviceJOSNChannel.put("ID", deviceChannelExtend.getChannelId()); + deviceJOSNChannel.put("DeviceID", deviceChannelExtend.getDeviceId()); + deviceJOSNChannel.put("DeviceName", deviceChannelExtend.getDeviceName()); + deviceJOSNChannel.put("DeviceOnline", deviceChannelExtend.isDeviceOnline()); + deviceJOSNChannel.put("Channel", 0); // TODO 自定义序号 + deviceJOSNChannel.put("Name", deviceChannelExtend.getName()); + deviceJOSNChannel.put("Custom", false); + deviceJOSNChannel.put("CustomName", ""); + deviceJOSNChannel.put("SubCount", deviceChannelExtend.getSubCount()); // TODO ? 子节点数, SubCount > 0 表示该通道为子目录 + deviceJOSNChannel.put("SnapURL", ""); + deviceJOSNChannel.put("Manufacturer ", deviceChannelExtend.getManufacture()); + deviceJOSNChannel.put("Model", deviceChannelExtend.getModel()); + deviceJOSNChannel.put("Owner", deviceChannelExtend.getOwner()); + deviceJOSNChannel.put("CivilCode", deviceChannelExtend.getCivilCode()); + deviceJOSNChannel.put("Address", deviceChannelExtend.getAddress()); + deviceJOSNChannel.put("Parental", deviceChannelExtend.getParental()); // 当为通道设备时, 是否有通道子设备, 1-有,0-没有 + deviceJOSNChannel.put("ParentID", deviceChannelExtend.getParentId()); // 直接上级编号 + deviceJOSNChannel.put("Secrecy", deviceChannelExtend.getSecrecy()); + deviceJOSNChannel.put("RegisterWay", 1); // 注册方式, 缺省为1, 允许值: 1, 2, 3 + // 1-IETF RFC3261, + // 2-基于口令的双向认证, + // 3-基于数字证书的双向认证 + deviceJOSNChannel.put("Status", deviceChannelExtend.getStatus()); + deviceJOSNChannel.put("Longitude", deviceChannelExtend.getLongitude()); + deviceJOSNChannel.put("Latitude", deviceChannelExtend.getLatitude()); + deviceJOSNChannel.put("PTZType ", deviceChannelExtend.getPTZType()); // 云台类型, 0 - 未知, 1 - 球机, 2 - 半球, + // 3 - 固定枪机, 4 - 遥控枪机 + deviceJOSNChannel.put("CustomPTZType", ""); + deviceJOSNChannel.put("StreamID", deviceChannelExtend.getStreamId()); // StreamID 直播流ID, 有值表示正在直播 + deviceJOSNChannel.put("NumOutputs ", -1); // 直播在线人数 + channleJSONList.add(deviceJOSNChannel); + }); + result.put("ChannelList", channleJSONList); + return result; + } + + /** + * 设备信息 - 获取下级通道预置位 + * @param serial 设备编号 + * @param code 通道编号,通过 /api/v1/device/channellist 获取的 ChannelList.ID, 该参数和 channel 二选一传递即可 + * @param channel 通道序号, 默认值: 1 + * @param fill 是否填充空置预置位,当下级返回预置位,但不够255个时,自动填充空置预置位到255个, 默认值: true, 允许值: true, false + * @param timeout 超时时间(秒) 默认值: 15 + * @return + */ + @GetMapping(value = "/fetchpreset") + private DeferredResult> list(String serial, + @RequestParam(required = false)Integer channel, + @RequestParam(required = false)String code, + @RequestParam(required = false)Boolean fill, + @RequestParam(required = false)Integer timeout){ + + if (log.isDebugEnabled()) { + log.debug("<模拟接口> 获取下级通道预置位 API调用,deviceId:{} ,channel:{} ,code:{} ,fill:{} ,timeout:{} ", + serial, channel, code, fill, timeout); + } + + Device device = deviceService.getDeviceByDeviceId(serial); + Assert.notNull(device, "设备不存在"); + DeferredResult> deferredResult = new DeferredResult<> (timeout * 1000L); + deviceService.queryPreset(device, code, (resultCode, msg, data) -> { + if (resultCode == ErrorCode.SUCCESS.getCode()) { + List presetQuerySipReqList = (List)data; + HashMap resultMap = new HashMap<>(); + resultMap.put("DeviceID", code); + resultMap.put("Result", "OK"); + resultMap.put("SumNum", presetQuerySipReqList.size()); + ArrayList> presetItemList = new ArrayList<>(presetQuerySipReqList.size()); + for (Preset presetQuerySipReq : presetQuerySipReqList) { + Map item = new HashMap<>(); + item.put("PresetID", presetQuerySipReq.getPresetId()); + item.put("PresetName", presetQuerySipReq.getPresetName()); + item.put("PresetEnable", true); + presetItemList.add(item); + } + resultMap.put("PresetItemList",presetItemList ); + deferredResult.setResult(new WVPResult<>(resultCode, msg, resultMap)); + }else { + deferredResult.setResult(new WVPResult<>(resultCode, msg, null)); + } + }); + + deferredResult.onTimeout(()->{ + log.warn("[获取设备预置位] 超时, {}", device.getDeviceId()); + deferredResult.setResult(WVPResult.fail(ErrorCode.ERROR100.getCode(), "wait for presetquery timeout["+timeout+"s]")); + }); + return deferredResult; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/web/gb28181/ApiStreamController.java b/src/main/java/com/genersoft/iot/vmp/web/gb28181/ApiStreamController.java new file mode 100644 index 0000000..a867315 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/web/gb28181/ApiStreamController.java @@ -0,0 +1,278 @@ +package com.genersoft.iot.vmp.web.gb28181; + +import com.alibaba.fastjson2.JSONObject; +import com.genersoft.iot.vmp.common.InviteInfo; +import com.genersoft.iot.vmp.common.InviteSessionType; +import com.genersoft.iot.vmp.common.StreamInfo; +import com.genersoft.iot.vmp.conf.UserSetting; +import com.genersoft.iot.vmp.conf.exception.SsrcTransactionNotFoundException; +import com.genersoft.iot.vmp.gb28181.bean.Device; +import com.genersoft.iot.vmp.gb28181.bean.DeviceChannel; +import com.genersoft.iot.vmp.gb28181.service.IDeviceChannelService; +import com.genersoft.iot.vmp.gb28181.service.IDeviceService; +import com.genersoft.iot.vmp.gb28181.service.IInviteStreamService; +import com.genersoft.iot.vmp.gb28181.service.IPlayService; +import com.genersoft.iot.vmp.gb28181.transmit.cmd.impl.SIPCommander; +import com.genersoft.iot.vmp.media.bean.MediaServer; +import com.genersoft.iot.vmp.service.bean.InviteErrorCode; +import io.swagger.v3.oas.annotations.Hidden; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.context.request.async.DeferredResult; + +import javax.sip.InvalidArgumentException; +import javax.sip.SipException; +import java.text.ParseException; + +/** + * API兼容:实时直播 + */ +@SuppressWarnings(value = {"rawtypes", "unchecked"}) + +@Slf4j +@RestController +@RequestMapping(value = "/api/v1/stream") +@Hidden +public class ApiStreamController { + + @Autowired + private SIPCommander cmder; + + @Autowired + private UserSetting userSetting; + + @Autowired + private IDeviceService deviceService; + + @Autowired + private IDeviceChannelService deviceChannelService; + + @Autowired + private IPlayService playService; + + @Autowired + private IInviteStreamService inviteStreamService; + + /** + * 实时直播 - 开始直播 + * @param serial 设备编号 + * @param channel 通道序号 默认值: 1 + * @param code 通道编号,通过 /api/v1/device/channellist 获取的 ChannelList.ID, 该参数和 channel 二选一传递即可 + * @param cdn 转推 CDN 地址, 形如: [rtmp|rtsp]://xxx, encodeURIComponent + * @param audio 是否开启音频, 默认 开启 + * @param transport 流传输模式, 默认 UDP + * @param checkchannelstatus 是否检查通道状态, 默认 false, 表示 拉流前不检查通道状态是否在线 + * @param transportmode 当 transport=TCP 时有效, 指示流传输主被动模式, 默认被动 + * @param timeout 拉流超时(秒), + * @return + */ + @GetMapping("/start") + private DeferredResult start(String serial , + @RequestParam(required = false)Integer channel , + @RequestParam(required = false)String code, + @RequestParam(required = false)String cdn, + @RequestParam(required = false)String audio, + @RequestParam(required = false)String transport, + @RequestParam(required = false)String checkchannelstatus , + @RequestParam(required = false)String transportmode, + @RequestParam(required = false)String timeout + + ){ + DeferredResult result = new DeferredResult<>(userSetting.getPlayTimeout().longValue() + 10); + Device device = deviceService.getDeviceByDeviceId(serial); + if (device == null ) { + JSONObject resultJSON = new JSONObject(); + resultJSON.put("error","device[ " + serial + " ]未找到"); + result.setResult(resultJSON); + return result; + }else if (!device.isOnLine()) { + JSONObject resultJSON = new JSONObject(); + resultJSON.put("error","device[ " + code + " ]offline"); + result.setResult(resultJSON); + return result; + } + + + DeviceChannel deviceChannel = deviceChannelService.getOne(serial, code); + if (deviceChannel == null) { + JSONObject resultJSON = new JSONObject(); + resultJSON.put("error","channel[ " + code + " ]未找到"); + result.setResult(resultJSON); + return result; + }else if (!deviceChannel.getStatus().equalsIgnoreCase("ON")) { + JSONObject resultJSON = new JSONObject(); + resultJSON.put("error","channel[ " + code + " ]offline"); + result.setResult(resultJSON); + return result; + } + + result.onTimeout(()->{ + log.info("播放等待超时"); + JSONObject resultJSON = new JSONObject(); + resultJSON.put("error","timeout"); + result.setResult(resultJSON); + inviteStreamService.removeInviteInfoByDeviceAndChannel(InviteSessionType.PLAY, deviceChannel.getId()); + deviceChannelService.stopPlay(deviceChannel.getId()); + // 清理RTP server + }); + + MediaServer newMediaServerItem = playService.getNewMediaServerItem(device); + + playService.play(newMediaServerItem, serial, code, null, (errorCode, msg, data) -> { + if (errorCode == InviteErrorCode.SUCCESS.getCode()) { + if (data != null) { + StreamInfo streamInfo = (StreamInfo)data; + JSONObject resultJjson = new JSONObject(); + resultJjson.put("StreamID", streamInfo.getStream()); + resultJjson.put("DeviceID", serial); + resultJjson.put("ChannelID", code); + resultJjson.put("ChannelName", deviceChannel.getName()); + resultJjson.put("ChannelCustomName", ""); + if (streamInfo.getTranscodeStream() != null) { + resultJjson.put("FLV", streamInfo.getTranscodeStream().getFlv().getUrl()); + }else { + resultJjson.put("FLV", streamInfo.getFlv().getUrl()); + + } + if(streamInfo.getHttps_flv() != null) { + if (streamInfo.getTranscodeStream() != null) { + resultJjson.put("HTTPS_FLV", streamInfo.getTranscodeStream().getHttps_flv().getUrl()); + }else { + resultJjson.put("HTTPS_FLV", streamInfo.getHttps_flv().getUrl()); + } + } + + if (streamInfo.getTranscodeStream() != null) { + resultJjson.put("WS_FLV", streamInfo.getTranscodeStream().getWs_flv().getUrl()); + }else { + resultJjson.put("WS_FLV", streamInfo.getWs_flv().getUrl()); + } + + if(streamInfo.getWss_flv() != null) { + if (streamInfo.getTranscodeStream() != null) { + resultJjson.put("WSS_FLV", streamInfo.getTranscodeStream().getWss_flv().getUrl()); + }else { + resultJjson.put("WSS_FLV", streamInfo.getWss_flv().getUrl()); + } + } + resultJjson.put("RTMP", streamInfo.getRtmp().getUrl()); + if (streamInfo.getRtmps() != null) { + resultJjson.put("RTMPS", streamInfo.getRtmps().getUrl()); + } + resultJjson.put("HLS", streamInfo.getHls().getUrl()); + if (streamInfo.getHttps_hls() != null) { + resultJjson.put("HTTPS_HLS", streamInfo.getHttps_hls().getUrl()); + } + resultJjson.put("RTSP", streamInfo.getRtsp().getUrl()); + if (streamInfo.getRtsps() != null) { + resultJjson.put("RTSPS", streamInfo.getRtsps().getUrl()); + } + resultJjson.put("WEBRTC", streamInfo.getRtc().getUrl()); + if (streamInfo.getRtcs() != null) { + resultJjson.put("HTTPS_WEBRTC", streamInfo.getRtcs().getUrl()); + } + resultJjson.put("CDN", ""); + resultJjson.put("SnapURL", ""); + resultJjson.put("Transport", device.getTransport()); + resultJjson.put("StartAt", ""); + resultJjson.put("Duration", ""); + resultJjson.put("SourceVideoCodecName", ""); + resultJjson.put("SourceVideoWidth", ""); + resultJjson.put("SourceVideoHeight", ""); + resultJjson.put("SourceVideoFrameRate", ""); + resultJjson.put("SourceAudioCodecName", ""); + resultJjson.put("SourceAudioSampleRate", ""); + resultJjson.put("AudioEnable", ""); + resultJjson.put("Ondemand", ""); + resultJjson.put("InBytes", ""); + resultJjson.put("InBitRate", ""); + resultJjson.put("OutBytes", ""); + resultJjson.put("NumOutputs", ""); + resultJjson.put("CascadeSize", ""); + resultJjson.put("RelaySize", ""); + resultJjson.put("ChannelPTZType", "0"); + result.setResult(resultJjson); + }else { + JSONObject resultJjson = new JSONObject(); + resultJjson.put("error", "channel[ " + code + " ] " + msg); + result.setResult(resultJjson); + } + }else { + JSONObject resultJjson = new JSONObject(); + resultJjson.put("error", "channel[ " + code + " ] " + msg); + result.setResult(resultJjson); + } + }); + + return result; + } + + /** + * 实时直播 - 直播流停止 + * @param serial 设备编号 + * @param channel 通道序号 + * @param code 通道国标编号 + * @param check_outputs + * @return + */ + @GetMapping("/stop") + @ResponseBody + private JSONObject stop(String serial , + @RequestParam(required = false)Integer channel , + @RequestParam(required = false)String code, + @RequestParam(required = false)String check_outputs + + ){ + + + Device device = deviceService.getDeviceByDeviceId(serial); + if (device == null) { + JSONObject result = new JSONObject(); + result.put("error","未找到设备"); + return result; + } + DeviceChannel deviceChannel = deviceChannelService.getOne(serial, code); + if (deviceChannel == null) { + JSONObject result = new JSONObject(); + result.put("error","未找到通道"); + return result; + } + InviteInfo inviteInfo = inviteStreamService.getInviteInfoByDeviceAndChannel(InviteSessionType.PLAY, deviceChannel.getId()); + if (inviteInfo == null) { + JSONObject result = new JSONObject(); + result.put("error","未找到流信息"); + return result; + } + + try { + cmder.streamByeCmd(device, code, "rtp", inviteInfo.getStream(), null, null); + } catch (InvalidArgumentException | ParseException | SipException | SsrcTransactionNotFoundException e) { + JSONObject result = new JSONObject(); + result.put("error","发送BYE失败:" + e.getMessage()); + return result; + } + inviteStreamService.removeInviteInfo(inviteInfo); + deviceChannelService.stopPlay(inviteInfo.getChannelId()); + return null; + } + + /** + * 实时直播 - 直播流保活 + * @param serial 设备编号 + * @param channel 通道序号 + * @param code 通道国标编号 + * @return + */ + @GetMapping("/touch") + @ResponseBody + private JSONObject touch(String serial ,String t, + @RequestParam(required = false)Integer channel , + @RequestParam(required = false)String code, + @RequestParam(required = false)String autorestart, + @RequestParam(required = false)String audio, + @RequestParam(required = false)String cdn + ){ + return null; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/web/gb28181/AuthController.java b/src/main/java/com/genersoft/iot/vmp/web/gb28181/AuthController.java new file mode 100644 index 0000000..caed2ad --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/web/gb28181/AuthController.java @@ -0,0 +1,27 @@ +package com.genersoft.iot.vmp.web.gb28181; + +import com.genersoft.iot.vmp.service.IUserService; +import com.genersoft.iot.vmp.storager.dao.dto.User; +import io.swagger.v3.oas.annotations.Hidden; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + + +@RestController +@RequestMapping(value = "/auth") +@Hidden +public class AuthController { + + @Autowired + private IUserService userService; + + @GetMapping("/login") + public String devices(String name, String passwd){ + User user = userService.getUser(name, passwd); + if (user != null) { + return "success"; + }else { + return "fail"; + } + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/web/gb28181/dto/DeviceChannelExtend.java b/src/main/java/com/genersoft/iot/vmp/web/gb28181/dto/DeviceChannelExtend.java new file mode 100644 index 0000000..e963075 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/web/gb28181/dto/DeviceChannelExtend.java @@ -0,0 +1,235 @@ +package com.genersoft.iot.vmp.web.gb28181.dto; + +import lombok.Data; + +@Data +public class DeviceChannelExtend { + + + /** + * 数据库自增ID + */ + private int id; + + /** + * 通道id + */ + private String channelId; + + /** + * 设备id + */ + private String deviceId; + + /** + * 通道名 + */ + private String name; + + private String deviceName; + + private boolean deviceOnline; + + /** + * 生产厂商 + */ + private String manufacture; + + /** + * 型号 + */ + private String model; + + /** + * 设备归属 + */ + private String owner; + + /** + * 行政区域 + */ + private String civilCode; + + /** + * 警区 + */ + private String block; + + /** + * 安装地址 + */ + private String address; + + /** + * 是否有子设备 1有, 0没有 + */ + private int parental; + + /** + * 父级id + */ + private String parentId; + + /** + * 信令安全模式 缺省为0; 0:不采用; 2: S/MIME签名方式; 3: S/ MIME加密签名同时采用方式; 4:数字摘要方式 + */ + private int safetyWay; + + /** + * 注册方式 缺省为1;1:符合IETFRFC3261标准的认证注册模 式; 2:基于口令的双向认证注册模式; 3:基于数字证书的双向认证注册模式 + */ + private int registerWay; + + /** + * 证书序列号 + */ + private String certNum; + + /** + * 证书有效标识 缺省为0;证书有效标识:0:无效1: 有效 + */ + private int certifiable; + + /** + * 证书无效原因码 + */ + private int errCode; + + /** + * 证书终止有效期 + */ + private String endTime; + + /** + * 保密属性 缺省为0; 0:不涉密, 1:涉密 + */ + private String secrecy; + + /** + * IP地址 + */ + private String ipAddress; + + /** + * 端口号 + */ + private int port; + + /** + * 密码 + */ + private String password; + + /** + * 云台类型 + */ + private int PTZType; + + /** + * 云台类型描述字符串 + */ + private String PTZTypeText; + + /** + * 创建时间 + */ + private String createTime; + + /** + * 更新时间 + */ + private String updateTime; + + /** + * 在线/离线 + * 1在线,0离线 + * 默认在线 + * 信令: + * ON + * OFF + * 遇到过NVR下的IPC下发信令可以推流, 但是 Status 响应 OFF + */ + private String status; + + /** + * 经度 + */ + private double longitude; + + /** + * 纬度 + */ + private double latitude; + + /** + * 经度 GCJ02 + */ + private double longitudeGcj02; + + /** + * 纬度 GCJ02 + */ + private double latitudeGcj02; + + /** + * 经度 WGS84 + */ + private double longitudeWgs84; + + /** + * 纬度 WGS84 + */ + private double latitudeWgs84; + + /** + * 子设备数 + */ + private int subCount; + + /** + * 流唯一编号,存在表示正在直播 + */ + private String streamId; + + /** + * 是否含有音频 + */ + private boolean hasAudio; + + /** + * 标记通道的类型,0->国标通道 1->直播流通道 2->业务分组/虚拟组织/行政区划 + */ + private int channelType; + + /** + * 业务分组 + */ + private String businessGroupId; + + /** + * GPS的更新时间 + */ + private String gpsTime; + + + public void setPTZType(int PTZType) { + this.PTZType = PTZType; + switch (PTZType) { + case 0: + this.PTZTypeText = "未知"; + break; + case 1: + this.PTZTypeText = "球机"; + break; + case 2: + this.PTZTypeText = "半球"; + break; + case 3: + this.PTZTypeText = "固定枪机"; + break; + case 4: + this.PTZTypeText = "遥控枪机"; + break; + } + } +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml new file mode 100644 index 0000000..d85c017 --- /dev/null +++ b/src/main/resources/application.yml @@ -0,0 +1,150 @@ +spring: + application: + name: wvp + cache: + type: redis + thymeleaf: + cache: false + # 设置接口超时时间 + mvc: + async: + request-timeout: 20000 + # [可选]上传文件大小限制 + servlet: + multipart: + max-file-size: 10MB + max-request-size: 100MB + # REDIS数据库配置 + data: + redis: + # Redis服务器IP (Docker内部服务名) + host: ${REDIS_HOST:polaris-redis} + # 端口号 + port: ${REDIS_PORT:6379} + # [可选] 数据库 DB + database: 1 + # [可选] 访问密码,若你的redis服务器没有设置密码,就不需要用密码去连接 + password: + # [可选] 超时时间 + timeout: 10000 + ## [可选] 一个pool最多可分配多少个jedis实例 + #poolMaxTotal: 1000 + ## [可选] 一个pool最多有多少个状态为idle(空闲)的jedis实例 + #poolMaxIdle: 500 + ## [可选] 最大的等待时间(秒) + #poolMaxWait: 5 + # [必选] jdbc数据库配置 + datasource: + # mysql数据源 + type: com.zaxxer.hikari.HikariDataSource + driver-class-name: com.mysql.cj.jdbc.Driver + # 数据库连接地址 (Docker内部服务名: polaris-mysql) + url: jdbc:mysql://${DATABASE_HOST:polaris-mysql}:${DATABASE_PORT:3306}/wvp?useUnicode=true&characterEncoding=UTF8&rewriteBatchedStatements=true&serverTimezone=PRC&useSSL=false&allowMultiQueries=true&allowPublicKeyRetrieval=true + # 数据库用户名 (来自 docker-compose.yml 配置) + username: ${DATABASE_USER:wvp_user} + # 数据库密码 (来自 docker-compose.yml 配置) + password: ${DATABASE_PASSWORD:wvp_password} + +#[可选] 监听的HTTP端口, 网页和接口调用都是这个端口 +server: + port: 18978 + ssl: + # [可选] 是否开启HTTPS访问 + # docker里运行,内部不需要HTTPS + enabled: false +# 作为28181服务器的配置 +sip: + # [必须修改] 本机的IP,对应你的网卡,监听什么ip就是使用什么网卡, + # 如果要监听多张网卡,可以使用逗号分隔多个IP, 例如: 192.168.1.4,10.0.0.4 + # 如果不明白,就使用0.0.0.0,大部分情况都是可以的 + # 请不要使用127.0.0.1,任何包括localhost在内的域名都是不可以的。 + ip: 0.0.0.0 + # 前端展示的IP (来自 .env: SIP_ShowIP) + show-ip: ${SIP_ShowIP:127.0.0.1} + # SIP端口 (来自 .env: SIP_Port) + port: ${SIP_Port:8160} + # 根据国标6.1.2中规定,domain宜采用ID统一编码的前十位编码。国标附录D中定义前8位为中心编码(由省级、市级、区级、基层编号组成,参照GB/T 2260-2007) + # 后两位为行业编码,定义参照附录D.3 + # SIP域 (来自 .env: SIP_Domain) + domain: ${SIP_Domain:3502000000} + # SIP ID (来自 .env: SIP_Id) + id: ${SIP_Id:35020000002000000001} + # 默认设备认证密码 (来自 .env: SIP_Password) + password: ${SIP_Password:wvp_sip_password} + # [可选] 国标级联注册失败,再次发起注册的时间间隔。 默认60秒 + register-time-interval: 60 + # [可选] 云台控制速度 + ptz-speed: 50 + # TODO [可选] 收到心跳后自动上线, 重启服务后会将所有设备置为离线,默认false,等待注册后上线。设置为true则收到心跳设置为上线。 + # keepalliveToOnline: false + # 是否存储alarm信息 + alarm: true + # 命令发送等待回复的超时时间, 单位:毫秒 + timeout: 1000 + +# 默认服务器配置 +media: + id: polaris + # ZLM 内网IP (Docker内部服务名) + ip: ${ZLM_HOST:polaris-media} + http-port: 8778 + # 返回流地址时的ip (来自 .env: Stream_IP) + stream-ip: ${Stream_IP:127.0.0.1} + # wvp在国标信令中使用的ip (来自 .env: SDP_IP) + sdp-ip: ${SDP_IP:127.0.0.1} + # zlm服务器访问WVP所使用的IP (Docker内部服务名) + hook-ip: ${ZLM_HOOK_HOST:polaris-wvp} + # [可选] sslport + http-ssl-port: 0 + # 流媒体端口配置 (来自 .env: WebHttp) + flv-port: ${MediaHttp:-8080} + flv-ssl-port: ${MediaHttps:} + ws-flv-port: ${MediaHttp:-8080} + ws-flv-ssl-port: ${MediaHttps:} + # RTP端口 (来自 .env: MediaRtp) + rtp-proxy-port: ${MediaRtp:-10003} + # RTMP端口 (来自 .env: MediaRtmp) + rtmp-port: ${MediaRtmp:-10001} + rtmp-ssl-port: 0 + # RTSP端口 (来自 .env: MediaRtsp) + rtsp-port: ${MediaRtsp:-10002} + rtsp-ssl-port: 0 + # [可选] 是否自动配置ZLM, 如果希望手动配置ZLM, 可以设为false, 不建议新接触的用户修改 + auto-config: true + # ZLM密钥 (来自 docker-compose.yml: ZLM_SERCERT) + secret: ${ZLM_SERCERT:su6TiedN2rVAmBbIDX0aa0QTiBJLBdcf} + # 启用多端口模式, 多端口模式使用端口区分每路流,兼容性更好。 单端口使用流的ssrc区分, 点播超时建议使用多端口测试 + rtp: + # [可选] 是否启用多端口模式, 开启后会在portRange范围内选择端口用于媒体流传输 + enable: true + # [可选] + port-range: 30000,30500 + # [可选] + send-port-range: 50502,50506 + + record-path: /opt/media/bin/www/record/ + record-day: 7 + record-assist-port: 0 +user-settings: + auto-apply-play: true + play-timeout: 30000 + wait-track: false + # 录像推流 (来自 .env: RecordPushLive, 默认为空) + record-push-live: ${RecordPushLive:false} + # 录像SIP (来自 .env: RecordSip) + record-sip: ${RecordSip:false} + stream-on-demand: true + interface-authentication: true + broadcast-for-platform: TCP-PASSIVE + push-stream-after-ack: true + send-to-platforms-when-id-lost: true + interface-authentication-excludes: + # - /api/** + push-authority: true + # allowed-origins: + # - http://localhost:8080 + # - http://127.0.0.1:8080 + # - http://0.0.0.0:8080 + # - ${NGINX_HOST} +logging: + config: classpath:logback-spring.xml \ No newline at end of file diff --git a/src/main/resources/banner.txt b/src/main/resources/banner.txt new file mode 100644 index 0000000..7a75e1c --- /dev/null +++ b/src/main/resources/banner.txt @@ -0,0 +1,7 @@ + ___ __ ___ ___ ________ ________ ________ ________ +|\ \ |\ \|\ \ / /|\ __ \ |\ __ \|\ __ \|\ __ \ +\ \ \ \ \ \ \ \ / / | \ \|\ \ ____________\ \ \|\ \ \ \|\ \ \ \|\ \ + \ \ \ __\ \ \ \ \/ / / \ \ ____\\____________\ \ ____\ \ _ _\ \ \\\ \ + \ \ \|\__\_\ \ \ / / \ \ \___\|____________|\ \ \___|\ \ \\ \\ \ \\\ \ + \ \____________\ \__/ / \ \__\ \ \__\ \ \__\\ _\\ \_______\ + \|____________|\|__|/ \|__| \|__| \|__|\|__|\|_______| diff --git a/src/main/resources/civilCode.csv b/src/main/resources/civilCode.csv new file mode 100644 index 0000000..6c2284a --- /dev/null +++ b/src/main/resources/civilCode.csv @@ -0,0 +1,3224 @@ +编号,名称,上级 +11,北京市, +1101,市辖区,11 +110101,东城区,1101 +110102,西城区,1101 +110105,朝阳区,1101 +110106,丰台区,1101 +110107,石景山区,1101 +110108,海淀区,1101 +110109,门头沟区,1101 +110111,房山区,1101 +110112,通州区,1101 +110113,顺义区,1101 +110114,昌平区,1101 +110115,大兴区,1101 +110116,怀柔区,1101 +110117,平谷区,1101 +110118,密云区,1101 +110119,延庆区,1101 +12,天津市, +1201,市辖区,12 +120101,和平区,1201 +120102,河东区,1201 +120103,河西区,1201 +120104,南开区,1201 +120105,河北区,1201 +120106,红桥区,1201 +120110,东丽区,1201 +120111,西青区,1201 +120112,津南区,1201 +120113,北辰区,1201 +120114,武清区,1201 +120115,宝坻区,1201 +120116,滨海新区,1201 +120117,宁河区,1201 +120118,静海区,1201 +120119,蓟州区,1201 +13,河北省, +1301,石家庄市,13 +130102,长安区,1301 +130104,桥西区,1301 +130105,新华区,1301 +130107,井陉矿区,1301 +130108,裕华区,1301 +130109,藁城区,1301 +130110,鹿泉区,1301 +130111,栾城区,1301 +130121,井陉县,1301 +130123,正定县,1301 +130125,行唐县,1301 +130126,灵寿县,1301 +130127,高邑县,1301 +130128,深泽县,1301 +130129,赞皇县,1301 +130130,无极县,1301 +130131,平山县,1301 +130132,元氏县,1301 +130133,赵县,1301 +130181,辛集市,1301 +130183,晋州市,1301 +130184,新乐市,1301 +1302,唐山市,13 +130202,路南区,1302 +130203,路北区,1302 +130204,古冶区,1302 +130205,开平区,1302 +130207,丰南区,1302 +130208,丰润区,1302 +130209,曹妃甸区,1302 +130224,滦南县,1302 +130225,乐亭县,1302 +130227,迁西县,1302 +130229,玉田县,1302 +130281,遵化市,1302 +130283,迁安市,1302 +130284,滦州市,1302 +1303,秦皇岛市,13 +130302,海港区,1303 +130303,山海关区,1303 +130304,北戴河区,1303 +130306,抚宁区,1303 +130321,青龙满族自治县,1303 +130322,昌黎县,1303 +130324,卢龙县,1303 +1304,邯郸市,13 +130402,邯山区,1304 +130403,丛台区,1304 +130404,复兴区,1304 +130406,峰峰矿区,1304 +130407,肥乡区,1304 +130408,永年区,1304 +130423,临漳县,1304 +130424,成安县,1304 +130425,大名县,1304 +130426,涉县,1304 +130427,磁县,1304 +130430,邱县,1304 +130431,鸡泽县,1304 +130432,广平县,1304 +130433,馆陶县,1304 +130434,魏县,1304 +130435,曲周县,1304 +130481,武安市,1304 +1305,邢台市,13 +130502,桥东区,1305 +130503,桥西区,1305 +130521,邢台县,1305 +130522,临城县,1305 +130523,内丘县,1305 +130524,柏乡县,1305 +130525,隆尧县,1305 +130526,任县,1305 +130527,南和县,1305 +130528,宁晋县,1305 +130529,巨鹿县,1305 +130530,新河县,1305 +130531,广宗县,1305 +130532,平乡县,1305 +130533,威县,1305 +130534,清河县,1305 +130535,临西县,1305 +130581,南宫市,1305 +130582,沙河市,1305 +1306,保定市,13 +130602,竞秀区,1306 +130606,莲池区,1306 +130607,满城区,1306 +130608,清苑区,1306 +130609,徐水区,1306 +130623,涞水县,1306 +130624,阜平县,1306 +130626,定兴县,1306 +130627,唐县,1306 +130628,高阳县,1306 +130629,容城县,1306 +130630,涞源县,1306 +130631,望都县,1306 +130632,安新县,1306 +130633,易县,1306 +130634,曲阳县,1306 +130635,蠡县,1306 +130636,顺平县,1306 +130637,博野县,1306 +130638,雄县,1306 +130681,涿州市,1306 +130682,定州市,1306 +130683,安国市,1306 +130684,高碑店市,1306 +1307,张家口市,13 +130702,桥东区,1307 +130703,桥西区,1307 +130705,宣化区,1307 +130706,下花园区,1307 +130708,万全区,1307 +130709,崇礼区,1307 +130722,张北县,1307 +130723,康保县,1307 +130724,沽源县,1307 +130725,尚义县,1307 +130726,蔚县,1307 +130727,阳原县,1307 +130728,怀安县,1307 +130730,怀来县,1307 +130731,涿鹿县,1307 +130732,赤城县,1307 +1308,承德市,13 +130802,双桥区,1308 +130803,双滦区,1308 +130804,鹰手营子矿区,1308 +130821,承德县,1308 +130822,兴隆县,1308 +130824,滦平县,1308 +130825,隆化县,1308 +130826,丰宁满族自治县,1308 +130827,宽城满族自治县,1308 +130828,围场满族蒙古族自治县,1308 +130881,平泉市,1308 +1309,沧州市,13 +130902,新华区,1309 +130903,运河区,1309 +130921,沧县,1309 +130922,青县,1309 +130923,东光县,1309 +130924,海兴县,1309 +130925,盐山县,1309 +130926,肃宁县,1309 +130927,南皮县,1309 +130928,吴桥县,1309 +130929,献县,1309 +130930,孟村回族自治县,1309 +130981,泊头市,1309 +130982,任丘市,1309 +130983,黄骅市,1309 +130984,河间市,1309 +1310,廊坊市,13 +131002,安次区,1310 +131003,广阳区,1310 +131022,固安县,1310 +131023,永清县,1310 +131024,香河县,1310 +131025,大城县,1310 +131026,文安县,1310 +131028,大厂回族自治县,1310 +131081,霸州市,1310 +131082,三河市,1310 +1311,衡水市,13 +131102,桃城区,1311 +131103,冀州区,1311 +131121,枣强县,1311 +131122,武邑县,1311 +131123,武强县,1311 +131124,饶阳县,1311 +131125,安平县,1311 +131126,故城县,1311 +131127,景县,1311 +131128,阜城县,1311 +131182,深州市,1311 +14,山西省, +1401,太原市,14 +140105,小店区,1401 +140106,迎泽区,1401 +140107,杏花岭区,1401 +140108,尖草坪区,1401 +140109,万柏林区,1401 +140110,晋源区,1401 +140121,清徐县,1401 +140122,阳曲县,1401 +140123,娄烦县,1401 +140181,古交市,1401 +1402,大同市,14 +140212,新荣区,1402 +140213,平城区,1402 +140214,云冈区,1402 +140215,云州区,1402 +140221,阳高县,1402 +140222,天镇县,1402 +140223,广灵县,1402 +140224,灵丘县,1402 +140225,浑源县,1402 +140226,左云县,1402 +1403,阳泉市,14 +140302,城区,1403 +140303,矿区,1403 +140311,郊区,1403 +140321,平定县,1403 +140322,盂县,1403 +1404,长治市,14 +140403,潞州区,1404 +140404,上党区,1404 +140405,屯留区,1404 +140406,潞城区,1404 +140423,襄垣县,1404 +140425,平顺县,1404 +140426,黎城县,1404 +140427,壶关县,1404 +140428,长子县,1404 +140429,武乡县,1404 +140430,沁县,1404 +140431,沁源县,1404 +1405,晋城市,14 +140502,城区,1405 +140521,沁水县,1405 +140522,阳城县,1405 +140524,陵川县,1405 +140525,泽州县,1405 +140581,高平市,1405 +1406,朔州市,14 +140602,朔城区,1406 +140603,平鲁区,1406 +140621,山阴县,1406 +140622,应县,1406 +140623,右玉县,1406 +140681,怀仁市,1406 +1407,晋中市,14 +140702,榆次区,1407 +140721,榆社县,1407 +140722,左权县,1407 +140723,和顺县,1407 +140724,昔阳县,1407 +140725,寿阳县,1407 +140726,太谷县,1407 +140727,祁县,1407 +140728,平遥县,1407 +140729,灵石县,1407 +140781,介休市,1407 +1408,运城市,14 +140802,盐湖区,1408 +140821,临猗县,1408 +140822,万荣县,1408 +140823,闻喜县,1408 +140824,稷山县,1408 +140825,新绛县,1408 +140826,绛县,1408 +140827,垣曲县,1408 +140828,夏县,1408 +140829,平陆县,1408 +140830,芮城县,1408 +140881,永济市,1408 +140882,河津市,1408 +1409,忻州市,14 +140902,忻府区,1409 +140921,定襄县,1409 +140922,五台县,1409 +140923,代县,1409 +140924,繁峙县,1409 +140925,宁武县,1409 +140926,静乐县,1409 +140927,神池县,1409 +140928,五寨县,1409 +140929,岢岚县,1409 +140930,河曲县,1409 +140931,保德县,1409 +140932,偏关县,1409 +140981,原平市,1409 +1410,临汾市,14 +141002,尧都区,1410 +141021,曲沃县,1410 +141022,翼城县,1410 +141023,襄汾县,1410 +141024,洪洞县,1410 +141025,古县,1410 +141026,安泽县,1410 +141027,浮山县,1410 +141028,吉县,1410 +141029,乡宁县,1410 +141030,大宁县,1410 +141031,隰县,1410 +141032,永和县,1410 +141033,蒲县,1410 +141034,汾西县,1410 +141081,侯马市,1410 +141082,霍州市,1410 +1411,吕梁市,14 +141102,离石区,1411 +141121,文水县,1411 +141122,交城县,1411 +141123,兴县,1411 +141124,临县,1411 +141125,柳林县,1411 +141126,石楼县,1411 +141127,岚县,1411 +141128,方山县,1411 +141129,中阳县,1411 +141130,交口县,1411 +141181,孝义市,1411 +141182,汾阳市,1411 +15,内蒙古自治区, +1501,呼和浩特市,15 +150102,新城区,1501 +150103,回民区,1501 +150104,玉泉区,1501 +150105,赛罕区,1501 +150121,土默特左旗,1501 +150122,托克托县,1501 +150123,和林格尔县,1501 +150124,清水河县,1501 +150125,武川县,1501 +1502,包头市,15 +150202,东河区,1502 +150203,昆都仑区,1502 +150204,青山区,1502 +150205,石拐区,1502 +150206,白云鄂博矿区,1502 +150207,九原区,1502 +150221,土默特右旗,1502 +150222,固阳县,1502 +150223,达尔罕茂明安联合旗,1502 +1503,乌海市,15 +150302,海勃湾区,1503 +150303,海南区,1503 +150304,乌达区,1503 +1504,赤峰市,15 +150402,红山区,1504 +150403,元宝山区,1504 +150404,松山区,1504 +150421,阿鲁科尔沁旗,1504 +150422,巴林左旗,1504 +150423,巴林右旗,1504 +150424,林西县,1504 +150425,克什克腾旗,1504 +150426,翁牛特旗,1504 +150428,喀喇沁旗,1504 +150429,宁城县,1504 +150430,敖汉旗,1504 +1505,通辽市,15 +150502,科尔沁区,1505 +150521,科尔沁左翼中旗,1505 +150522,科尔沁左翼后旗,1505 +150523,开鲁县,1505 +150524,库伦旗,1505 +150525,奈曼旗,1505 +150526,扎鲁特旗,1505 +150581,霍林郭勒市,1505 +1506,鄂尔多斯市,15 +150602,东胜区,1506 +150603,康巴什区,1506 +150621,达拉特旗,1506 +150622,准格尔旗,1506 +150623,鄂托克前旗,1506 +150624,鄂托克旗,1506 +150625,杭锦旗,1506 +150626,乌审旗,1506 +150627,伊金霍洛旗,1506 +1507,呼伦贝尔市,15 +150702,海拉尔区,1507 +150703,扎赉诺尔区,1507 +150721,阿荣旗,1507 +150722,莫力达瓦达斡尔族自治旗,1507 +150723,鄂伦春自治旗,1507 +150724,鄂温克族自治旗,1507 +150725,陈巴尔虎旗,1507 +150726,新巴尔虎左旗,1507 +150727,新巴尔虎右旗,1507 +150781,满洲里市,1507 +150782,牙克石市,1507 +150783,扎兰屯市,1507 +150784,额尔古纳市,1507 +150785,根河市,1507 +1508,巴彦淖尔市,15 +150802,临河区,1508 +150821,五原县,1508 +150822,磴口县,1508 +150823,乌拉特前旗,1508 +150824,乌拉特中旗,1508 +150825,乌拉特后旗,1508 +150826,杭锦后旗,1508 +1509,乌兰察布市,15 +150902,集宁区,1509 +150921,卓资县,1509 +150922,化德县,1509 +150923,商都县,1509 +150924,兴和县,1509 +150925,凉城县,1509 +150926,察哈尔右翼前旗,1509 +150927,察哈尔右翼中旗,1509 +150928,察哈尔右翼后旗,1509 +150929,四子王旗,1509 +150981,丰镇市,1509 +1522,兴安盟,15 +152201,乌兰浩特市,1522 +152202,阿尔山市,1522 +152221,科尔沁右翼前旗,1522 +152222,科尔沁右翼中旗,1522 +152223,扎赉特旗,1522 +152224,突泉县,1522 +1525,锡林郭勒盟,15 +152501,二连浩特市,1525 +152502,锡林浩特市,1525 +152522,阿巴嘎旗,1525 +152523,苏尼特左旗,1525 +152524,苏尼特右旗,1525 +152525,东乌珠穆沁旗,1525 +152526,西乌珠穆沁旗,1525 +152527,太仆寺旗,1525 +152528,镶黄旗,1525 +152529,正镶白旗,1525 +152530,正蓝旗,1525 +152531,多伦县,1525 +1529,阿拉善盟,15 +152921,阿拉善左旗,1529 +152922,阿拉善右旗,1529 +152923,额济纳旗,1529 +21,辽宁省, +2101,沈阳市,21 +210102,和平区,2101 +210103,沈河区,2101 +210104,大东区,2101 +210105,皇姑区,2101 +210106,铁西区,2101 +210111,苏家屯区,2101 +210112,浑南区,2101 +210113,沈北新区,2101 +210114,于洪区,2101 +210115,辽中区,2101 +210123,康平县,2101 +210124,法库县,2101 +210181,新民市,2101 +2102,大连市,21 +210202,中山区,2102 +210203,西岗区,2102 +210204,沙河口区,2102 +210211,甘井子区,2102 +210212,旅顺口区,2102 +210213,金州区,2102 +210214,普兰店区,2102 +210224,长海县,2102 +210281,瓦房店市,2102 +210283,庄河市,2102 +2103,鞍山市,21 +210302,铁东区,2103 +210303,铁西区,2103 +210304,立山区,2103 +210311,千山区,2103 +210321,台安县,2103 +210323,岫岩满族自治县,2103 +210381,海城市,2103 +2104,抚顺市,21 +210402,新抚区,2104 +210403,东洲区,2104 +210404,望花区,2104 +210411,顺城区,2104 +210421,抚顺县,2104 +210422,新宾满族自治县,2104 +210423,清原满族自治县,2104 +2105,本溪市,21 +210502,平山区,2105 +210503,溪湖区,2105 +210504,明山区,2105 +210505,南芬区,2105 +210521,本溪满族自治县,2105 +210522,桓仁满族自治县,2105 +2106,丹东市,21 +210602,元宝区,2106 +210603,振兴区,2106 +210604,振安区,2106 +210624,宽甸满族自治县,2106 +210681,东港市,2106 +210682,凤城市,2106 +2107,锦州市,21 +210702,古塔区,2107 +210703,凌河区,2107 +210711,太和区,2107 +210726,黑山县,2107 +210727,义县,2107 +210781,凌海市,2107 +210782,北镇市,2107 +2108,营口市,21 +210802,站前区,2108 +210803,西市区,2108 +210804,鲅鱼圈区,2108 +210811,老边区,2108 +210881,盖州市,2108 +210882,大石桥市,2108 +2109,阜新市,21 +210902,海州区,2109 +210903,新邱区,2109 +210904,太平区,2109 +210905,清河门区,2109 +210911,细河区,2109 +210921,阜新蒙古族自治县,2109 +210922,彰武县,2109 +2110,辽阳市,21 +211002,白塔区,2110 +211003,文圣区,2110 +211004,宏伟区,2110 +211005,弓长岭区,2110 +211011,太子河区,2110 +211021,辽阳县,2110 +211081,灯塔市,2110 +2111,盘锦市,21 +211102,双台子区,2111 +211103,兴隆台区,2111 +211104,大洼区,2111 +211122,盘山县,2111 +2112,铁岭市,21 +211202,银州区,2112 +211204,清河区,2112 +211221,铁岭县,2112 +211223,西丰县,2112 +211224,昌图县,2112 +211281,调兵山市,2112 +211282,开原市,2112 +2113,朝阳市,21 +211302,双塔区,2113 +211303,龙城区,2113 +211321,朝阳县,2113 +211322,建平县,2113 +211324,喀喇沁左翼蒙古族自治县,2113 +211381,北票市,2113 +211382,凌源市,2113 +2114,葫芦岛市,21 +211402,连山区,2114 +211403,龙港区,2114 +211404,南票区,2114 +211421,绥中县,2114 +211422,建昌县,2114 +211481,兴城市,2114 +22,吉林省, +2201,长春市,22 +220102,南关区,2201 +220103,宽城区,2201 +220104,朝阳区,2201 +220105,二道区,2201 +220106,绿园区,2201 +220112,双阳区,2201 +220113,九台区,2201 +220122,农安县,2201 +220182,榆树市,2201 +220183,德惠市,2201 +2202,吉林市,22 +220202,昌邑区,2202 +220203,龙潭区,2202 +220204,船营区,2202 +220211,丰满区,2202 +220221,永吉县,2202 +220281,蛟河市,2202 +220282,桦甸市,2202 +220283,舒兰市,2202 +220284,磐石市,2202 +2203,四平市,22 +220302,铁西区,2203 +220303,铁东区,2203 +220322,梨树县,2203 +220323,伊通满族自治县,2203 +220381,公主岭市,2203 +220382,双辽市,2203 +2204,辽源市,22 +220402,龙山区,2204 +220403,西安区,2204 +220421,东丰县,2204 +220422,东辽县,2204 +2205,通化市,22 +220502,东昌区,2205 +220503,二道江区,2205 +220521,通化县,2205 +220523,辉南县,2205 +220524,柳河县,2205 +220581,梅河口市,2205 +220582,集安市,2205 +2206,白山市,22 +220602,浑江区,2206 +220605,江源区,2206 +220621,抚松县,2206 +220622,靖宇县,2206 +220623,长白朝鲜族自治县,2206 +220681,临江市,2206 +2207,松原市,22 +220702,宁江区,2207 +220721,前郭尔罗斯蒙古族自治县,2207 +220722,长岭县,2207 +220723,乾安县,2207 +220781,扶余市,2207 +2208,白城市,22 +220802,洮北区,2208 +220821,镇赉县,2208 +220822,通榆县,2208 +220881,洮南市,2208 +220882,大安市,2208 +2224,延边朝鲜族自治州,22 +222401,延吉市,2224 +222402,图们市,2224 +222403,敦化市,2224 +222404,珲春市,2224 +222405,龙井市,2224 +222406,和龙市,2224 +222424,汪清县,2224 +222426,安图县,2224 +23,黑龙江省, +2301,哈尔滨市,23 +230102,道里区,2301 +230103,南岗区,2301 +230104,道外区,2301 +230108,平房区,2301 +230109,松北区,2301 +230110,香坊区,2301 +230111,呼兰区,2301 +230112,阿城区,2301 +230113,双城区,2301 +230123,依兰县,2301 +230124,方正县,2301 +230125,宾县,2301 +230126,巴彦县,2301 +230127,木兰县,2301 +230128,通河县,2301 +230129,延寿县,2301 +230183,尚志市,2301 +230184,五常市,2301 +2302,齐齐哈尔市,23 +230202,龙沙区,2302 +230203,建华区,2302 +230204,铁锋区,2302 +230205,昂昂溪区,2302 +230206,富拉尔基区,2302 +230207,碾子山区,2302 +230208,梅里斯达斡尔族区,2302 +230221,龙江县,2302 +230223,依安县,2302 +230224,泰来县,2302 +230225,甘南县,2302 +230227,富裕县,2302 +230229,克山县,2302 +230230,克东县,2302 +230231,拜泉县,2302 +230281,讷河市,2302 +2303,鸡西市,23 +230302,鸡冠区,2303 +230303,恒山区,2303 +230304,滴道区,2303 +230305,梨树区,2303 +230306,城子河区,2303 +230307,麻山区,2303 +230321,鸡东县,2303 +230381,虎林市,2303 +230382,密山市,2303 +2304,鹤岗市,23 +230402,向阳区,2304 +230403,工农区,2304 +230404,南山区,2304 +230405,兴安区,2304 +230406,东山区,2304 +230407,兴山区,2304 +230421,萝北县,2304 +230422,绥滨县,2304 +2305,双鸭山市,23 +230502,尖山区,2305 +230503,岭东区,2305 +230505,四方台区,2305 +230506,宝山区,2305 +230521,集贤县,2305 +230522,友谊县,2305 +230523,宝清县,2305 +230524,饶河县,2305 +2306,大庆市,23 +230602,萨尔图区,2306 +230603,龙凤区,2306 +230604,让胡路区,2306 +230605,红岗区,2306 +230606,大同区,2306 +230621,肇州县,2306 +230622,肇源县,2306 +230623,林甸县,2306 +230624,杜尔伯特蒙古族自治县,2306 +2307,伊春市,23 +230702,伊春区,2307 +230703,南岔区,2307 +230704,友好区,2307 +230705,西林区,2307 +230706,翠峦区,2307 +230707,新青区,2307 +230708,美溪区,2307 +230709,金山屯区,2307 +230710,五营区,2307 +230711,乌马河区,2307 +230712,汤旺河区,2307 +230713,带岭区,2307 +230714,乌伊岭区,2307 +230715,红星区,2307 +230716,上甘岭区,2307 +230722,嘉荫县,2307 +230781,铁力市,2307 +2308,佳木斯市,23 +230803,向阳区,2308 +230804,前进区,2308 +230805,东风区,2308 +230811,郊区,2308 +230822,桦南县,2308 +230826,桦川县,2308 +230828,汤原县,2308 +230881,同江市,2308 +230882,富锦市,2308 +230883,抚远市,2308 +2309,七台河市,23 +230902,新兴区,2309 +230903,桃山区,2309 +230904,茄子河区,2309 +230921,勃利县,2309 +2310,牡丹江市,23 +231002,东安区,2310 +231003,阳明区,2310 +231004,爱民区,2310 +231005,西安区,2310 +231025,林口县,2310 +231081,绥芬河市,2310 +231083,海林市,2310 +231084,宁安市,2310 +231085,穆棱市,2310 +231086,东宁市,2310 +2311,黑河市,23 +231102,爱辉区,2311 +231121,嫩江县,2311 +231123,逊克县,2311 +231124,孙吴县,2311 +231181,北安市,2311 +231182,五大连池市,2311 +2312,绥化市,23 +231202,北林区,2312 +231221,望奎县,2312 +231222,兰西县,2312 +231223,青冈县,2312 +231224,庆安县,2312 +231225,明水县,2312 +231226,绥棱县,2312 +231281,安达市,2312 +231282,肇东市,2312 +231283,海伦市,2312 +2327,大兴安岭地区,23 +232701,漠河市,2327 +232721,呼玛县,2327 +232722,塔河县,2327 +31,上海市, +3101,市辖区,31 +310101,黄浦区,3101 +310104,徐汇区,3101 +310105,长宁区,3101 +310106,静安区,3101 +310107,普陀区,3101 +310109,虹口区,3101 +310110,杨浦区,3101 +310112,闵行区,3101 +310113,宝山区,3101 +310114,嘉定区,3101 +310115,浦东新区,3101 +310116,金山区,3101 +310117,松江区,3101 +310118,青浦区,3101 +310120,奉贤区,3101 +310151,崇明区,3101 +32,江苏省, +3201,南京市,32 +320102,玄武区,3201 +320104,秦淮区,3201 +320105,建邺区,3201 +320106,鼓楼区,3201 +320111,浦口区,3201 +320113,栖霞区,3201 +320114,雨花台区,3201 +320115,江宁区,3201 +320116,六合区,3201 +320117,溧水区,3201 +320118,高淳区,3201 +3202,无锡市,32 +320205,锡山区,3202 +320206,惠山区,3202 +320211,滨湖区,3202 +320213,梁溪区,3202 +320214,新吴区,3202 +320281,江阴市,3202 +320282,宜兴市,3202 +3203,徐州市,32 +320302,鼓楼区,3203 +320303,云龙区,3203 +320305,贾汪区,3203 +320311,泉山区,3203 +320312,铜山区,3203 +320321,丰县,3203 +320322,沛县,3203 +320324,睢宁县,3203 +320381,新沂市,3203 +320382,邳州市,3203 +3204,常州市,32 +320402,天宁区,3204 +320404,钟楼区,3204 +320411,新北区,3204 +320412,武进区,3204 +320413,金坛区,3204 +320481,溧阳市,3204 +3205,苏州市,32 +320505,虎丘区,3205 +320506,吴中区,3205 +320507,相城区,3205 +320508,姑苏区,3205 +320509,吴江区,3205 +320581,常熟市,3205 +320582,张家港市,3205 +320583,昆山市,3205 +320585,太仓市,3205 +3206,南通市,32 +320602,崇川区,3206 +320611,港闸区,3206 +320612,通州区,3206 +320623,如东县,3206 +320681,启东市,3206 +320682,如皋市,3206 +320684,海门市,3206 +320685,海安市,3206 +3207,连云港市,32 +320703,连云区,3207 +320706,海州区,3207 +320707,赣榆区,3207 +320722,东海县,3207 +320723,灌云县,3207 +320724,灌南县,3207 +3208,淮安市,32 +320803,淮安区,3208 +320804,淮阴区,3208 +320812,清江浦区,3208 +320813,洪泽区,3208 +320826,涟水县,3208 +320830,盱眙县,3208 +320831,金湖县,3208 +3209,盐城市,32 +320902,亭湖区,3209 +320903,盐都区,3209 +320904,大丰区,3209 +320921,响水县,3209 +320922,滨海县,3209 +320923,阜宁县,3209 +320924,射阳县,3209 +320925,建湖县,3209 +320981,东台市,3209 +3210,扬州市,32 +321002,广陵区,3210 +321003,邗江区,3210 +321012,江都区,3210 +321023,宝应县,3210 +321081,仪征市,3210 +321084,高邮市,3210 +3211,镇江市,32 +321102,京口区,3211 +321111,润州区,3211 +321112,丹徒区,3211 +321181,丹阳市,3211 +321182,扬中市,3211 +321183,句容市,3211 +3212,泰州市,32 +321202,海陵区,3212 +321203,高港区,3212 +321204,姜堰区,3212 +321281,兴化市,3212 +321282,靖江市,3212 +321283,泰兴市,3212 +3213,宿迁市,32 +321302,宿城区,3213 +321311,宿豫区,3213 +321322,沭阳县,3213 +321323,泗阳县,3213 +321324,泗洪县,3213 +33,浙江省, +3301,杭州市,33 +330102,上城区,3301 +330103,下城区,3301 +330104,江干区,3301 +330105,拱墅区,3301 +330106,西湖区,3301 +330108,滨江区,3301 +330109,萧山区,3301 +330110,余杭区,3301 +330111,富阳区,3301 +330112,临安区,3301 +330122,桐庐县,3301 +330127,淳安县,3301 +330182,建德市,3301 +3302,宁波市,33 +330203,海曙区,3302 +330205,江北区,3302 +330206,北仑区,3302 +330211,镇海区,3302 +330212,鄞州区,3302 +330213,奉化区,3302 +330225,象山县,3302 +330226,宁海县,3302 +330281,余姚市,3302 +330282,慈溪市,3302 +3303,温州市,33 +330302,鹿城区,3303 +330303,龙湾区,3303 +330304,瓯海区,3303 +330305,洞头区,3303 +330324,永嘉县,3303 +330326,平阳县,3303 +330327,苍南县,3303 +330328,文成县,3303 +330329,泰顺县,3303 +330381,瑞安市,3303 +330382,乐清市,3303 +3304,嘉兴市,33 +330402,南湖区,3304 +330411,秀洲区,3304 +330421,嘉善县,3304 +330424,海盐县,3304 +330481,海宁市,3304 +330482,平湖市,3304 +330483,桐乡市,3304 +3305,湖州市,33 +330502,吴兴区,3305 +330503,南浔区,3305 +330521,德清县,3305 +330522,长兴县,3305 +330523,安吉县,3305 +3306,绍兴市,33 +330602,越城区,3306 +330603,柯桥区,3306 +330604,上虞区,3306 +330624,新昌县,3306 +330681,诸暨市,3306 +330683,嵊州市,3306 +3307,金华市,33 +330702,婺城区,3307 +330703,金东区,3307 +330723,武义县,3307 +330726,浦江县,3307 +330727,磐安县,3307 +330781,兰溪市,3307 +330782,义乌市,3307 +330783,东阳市,3307 +330784,永康市,3307 +3308,衢州市,33 +330802,柯城区,3308 +330803,衢江区,3308 +330822,常山县,3308 +330824,开化县,3308 +330825,龙游县,3308 +330881,江山市,3308 +3309,舟山市,33 +330902,定海区,3309 +330903,普陀区,3309 +330921,岱山县,3309 +330922,嵊泗县,3309 +3310,台州市,33 +331002,椒江区,3310 +331003,黄岩区,3310 +331004,路桥区,3310 +331022,三门县,3310 +331023,天台县,3310 +331024,仙居县,3310 +331081,温岭市,3310 +331082,临海市,3310 +331083,玉环市,3310 +3311,丽水市,33 +331102,莲都区,3311 +331121,青田县,3311 +331122,缙云县,3311 +331123,遂昌县,3311 +331124,松阳县,3311 +331125,云和县,3311 +331126,庆元县,3311 +331127,景宁畲族自治县,3311 +331181,龙泉市,3311 +34,安徽省, +3401,合肥市,34 +340102,瑶海区,3401 +340103,庐阳区,3401 +340104,蜀山区,3401 +340111,包河区,3401 +340121,长丰县,3401 +340122,肥东县,3401 +340123,肥西县,3401 +340124,庐江县,3401 +340181,巢湖市,3401 +3402,芜湖市,34 +340202,镜湖区,3402 +340203,弋江区,3402 +340207,鸠江区,3402 +340208,三山区,3402 +340221,芜湖县,3402 +340222,繁昌县,3402 +340223,南陵县,3402 +340225,无为县,3402 +3403,蚌埠市,34 +340302,龙子湖区,3403 +340303,蚌山区,3403 +340304,禹会区,3403 +340311,淮上区,3403 +340321,怀远县,3403 +340322,五河县,3403 +340323,固镇县,3403 +3404,淮南市,34 +340402,大通区,3404 +340403,田家庵区,3404 +340404,谢家集区,3404 +340405,八公山区,3404 +340406,潘集区,3404 +340421,凤台县,3404 +340422,寿县,3404 +3405,马鞍山市,34 +340503,花山区,3405 +340504,雨山区,3405 +340506,博望区,3405 +340521,当涂县,3405 +340522,含山县,3405 +340523,和县,3405 +3406,淮北市,34 +340602,杜集区,3406 +340603,相山区,3406 +340604,烈山区,3406 +340621,濉溪县,3406 +3407,铜陵市,34 +340705,铜官区,3407 +340706,义安区,3407 +340711,郊区,3407 +340722,枞阳县,3407 +3408,安庆市,34 +340802,迎江区,3408 +340803,大观区,3408 +340811,宜秀区,3408 +340822,怀宁县,3408 +340825,太湖县,3408 +340826,宿松县,3408 +340827,望江县,3408 +340828,岳西县,3408 +340881,桐城市,3408 +340882,潜山市,3408 +3410,黄山市,34 +341002,屯溪区,3410 +341003,黄山区,3410 +341004,徽州区,3410 +341021,歙县,3410 +341022,休宁县,3410 +341023,黟县,3410 +341024,祁门县,3410 +3411,滁州市,34 +341102,琅琊区,3411 +341103,南谯区,3411 +341122,来安县,3411 +341124,全椒县,3411 +341125,定远县,3411 +341126,凤阳县,3411 +341181,天长市,3411 +341182,明光市,3411 +3412,阜阳市,34 +341202,颍州区,3412 +341203,颍东区,3412 +341204,颍泉区,3412 +341221,临泉县,3412 +341222,太和县,3412 +341225,阜南县,3412 +341226,颍上县,3412 +341282,界首市,3412 +3413,宿州市,34 +341302,埇桥区,3413 +341321,砀山县,3413 +341322,萧县,3413 +341323,灵璧县,3413 +341324,泗县,3413 +3415,六安市,34 +341502,金安区,3415 +341503,裕安区,3415 +341504,叶集区,3415 +341522,霍邱县,3415 +341523,舒城县,3415 +341524,金寨县,3415 +341525,霍山县,3415 +3416,亳州市,34 +341602,谯城区,3416 +341621,涡阳县,3416 +341622,蒙城县,3416 +341623,利辛县,3416 +3417,池州市,34 +341702,贵池区,3417 +341721,东至县,3417 +341722,石台县,3417 +341723,青阳县,3417 +3418,宣城市,34 +341802,宣州区,3418 +341821,郎溪县,3418 +341822,广德县,3418 +341823,泾县,3418 +341824,绩溪县,3418 +341825,旌德县,3418 +341881,宁国市,3418 +35,福建省, +3501,福州市,35 +350102,鼓楼区,3501 +350103,台江区,3501 +350104,仓山区,3501 +350105,马尾区,3501 +350111,晋安区,3501 +350112,长乐区,3501 +350121,闽侯县,3501 +350122,连江县,3501 +350123,罗源县,3501 +350124,闽清县,3501 +350125,永泰县,3501 +350128,平潭县,3501 +350181,福清市,3501 +3502,厦门市,35 +350203,思明区,3502 +350205,海沧区,3502 +350206,湖里区,3502 +350211,集美区,3502 +350212,同安区,3502 +350213,翔安区,3502 +3503,莆田市,35 +350302,城厢区,3503 +350303,涵江区,3503 +350304,荔城区,3503 +350305,秀屿区,3503 +350322,仙游县,3503 +3504,三明市,35 +350402,梅列区,3504 +350403,三元区,3504 +350421,明溪县,3504 +350423,清流县,3504 +350424,宁化县,3504 +350425,大田县,3504 +350426,尤溪县,3504 +350427,沙县,3504 +350428,将乐县,3504 +350429,泰宁县,3504 +350430,建宁县,3504 +350481,永安市,3504 +3505,泉州市,35 +350502,鲤城区,3505 +350503,丰泽区,3505 +350504,洛江区,3505 +350505,泉港区,3505 +350521,惠安县,3505 +350524,安溪县,3505 +350525,永春县,3505 +350526,德化县,3505 +350527,金门县,3505 +350581,石狮市,3505 +350582,晋江市,3505 +350583,南安市,3505 +3506,漳州市,35 +350602,芗城区,3506 +350603,龙文区,3506 +350622,云霄县,3506 +350623,漳浦县,3506 +350624,诏安县,3506 +350625,长泰县,3506 +350626,东山县,3506 +350627,南靖县,3506 +350628,平和县,3506 +350629,华安县,3506 +350681,龙海市,3506 +3507,南平市,35 +350702,延平区,3507 +350703,建阳区,3507 +350721,顺昌县,3507 +350722,浦城县,3507 +350723,光泽县,3507 +350724,松溪县,3507 +350725,政和县,3507 +350781,邵武市,3507 +350782,武夷山市,3507 +350783,建瓯市,3507 +3508,龙岩市,35 +350802,新罗区,3508 +350803,永定区,3508 +350821,长汀县,3508 +350823,上杭县,3508 +350824,武平县,3508 +350825,连城县,3508 +350881,漳平市,3508 +3509,宁德市,35 +350902,蕉城区,3509 +350921,霞浦县,3509 +350922,古田县,3509 +350923,屏南县,3509 +350924,寿宁县,3509 +350925,周宁县,3509 +350926,柘荣县,3509 +350981,福安市,3509 +350982,福鼎市,3509 +36,江西省, +3601,南昌市,36 +360102,东湖区,3601 +360103,西湖区,3601 +360104,青云谱区,3601 +360105,湾里区,3601 +360111,青山湖区,3601 +360112,新建区,3601 +360121,南昌县,3601 +360123,安义县,3601 +360124,进贤县,3601 +3602,景德镇市,36 +360202,昌江区,3602 +360203,珠山区,3602 +360222,浮梁县,3602 +360281,乐平市,3602 +3603,萍乡市,36 +360302,安源区,3603 +360313,湘东区,3603 +360321,莲花县,3603 +360322,上栗县,3603 +360323,芦溪县,3603 +3604,九江市,36 +360402,濂溪区,3604 +360403,浔阳区,3604 +360404,柴桑区,3604 +360423,武宁县,3604 +360424,修水县,3604 +360425,永修县,3604 +360426,德安县,3604 +360428,都昌县,3604 +360429,湖口县,3604 +360430,彭泽县,3604 +360481,瑞昌市,3604 +360482,共青城市,3604 +360483,庐山市,3604 +3605,新余市,36 +360502,渝水区,3605 +360521,分宜县,3605 +3606,鹰潭市,36 +360602,月湖区,3606 +360603,余江区,3606 +360681,贵溪市,3606 +3607,赣州市,36 +360702,章贡区,3607 +360703,南康区,3607 +360704,赣县区,3607 +360722,信丰县,3607 +360723,大余县,3607 +360724,上犹县,3607 +360725,崇义县,3607 +360726,安远县,3607 +360727,龙南县,3607 +360728,定南县,3607 +360729,全南县,3607 +360730,宁都县,3607 +360731,于都县,3607 +360732,兴国县,3607 +360733,会昌县,3607 +360734,寻乌县,3607 +360735,石城县,3607 +360781,瑞金市,3607 +3608,吉安市,36 +360802,吉州区,3608 +360803,青原区,3608 +360821,吉安县,3608 +360822,吉水县,3608 +360823,峡江县,3608 +360824,新干县,3608 +360825,永丰县,3608 +360826,泰和县,3608 +360827,遂川县,3608 +360828,万安县,3608 +360829,安福县,3608 +360830,永新县,3608 +360881,井冈山市,3608 +3609,宜春市,36 +360902,袁州区,3609 +360921,奉新县,3609 +360922,万载县,3609 +360923,上高县,3609 +360924,宜丰县,3609 +360925,靖安县,3609 +360926,铜鼓县,3609 +360981,丰城市,3609 +360982,樟树市,3609 +360983,高安市,3609 +3610,抚州市,36 +361002,临川区,3610 +361003,东乡区,3610 +361021,南城县,3610 +361022,黎川县,3610 +361023,南丰县,3610 +361024,崇仁县,3610 +361025,乐安县,3610 +361026,宜黄县,3610 +361027,金溪县,3610 +361028,资溪县,3610 +361030,广昌县,3610 +3611,上饶市,36 +361102,信州区,3611 +361103,广丰区,3611 +361121,上饶县,3611 +361123,玉山县,3611 +361124,铅山县,3611 +361125,横峰县,3611 +361126,弋阳县,3611 +361127,余干县,3611 +361128,鄱阳县,3611 +361129,万年县,3611 +361130,婺源县,3611 +361181,德兴市,3611 +37,山东省, +3701,济南市,37 +370102,历下区,3701 +370103,市中区,3701 +370104,槐荫区,3701 +370105,天桥区,3701 +370112,历城区,3701 +370113,长清区,3701 +370114,章丘区,3701 +370115,济阳区,3701 +370124,平阴县,3701 +370126,商河县,3701 +3702,青岛市,37 +370202,市南区,3702 +370203,市北区,3702 +370211,黄岛区,3702 +370212,崂山区,3702 +370213,李沧区,3702 +370214,城阳区,3702 +370215,即墨区,3702 +370281,胶州市,3702 +370283,平度市,3702 +370285,莱西市,3702 +3703,淄博市,37 +370302,淄川区,3703 +370303,张店区,3703 +370304,博山区,3703 +370305,临淄区,3703 +370306,周村区,3703 +370321,桓台县,3703 +370322,高青县,3703 +370323,沂源县,3703 +3704,枣庄市,37 +370402,市中区,3704 +370403,薛城区,3704 +370404,峄城区,3704 +370405,台儿庄区,3704 +370406,山亭区,3704 +370481,滕州市,3704 +3705,东营市,37 +370502,东营区,3705 +370503,河口区,3705 +370505,垦利区,3705 +370522,利津县,3705 +370523,广饶县,3705 +3706,烟台市,37 +370602,芝罘区,3706 +370611,福山区,3706 +370612,牟平区,3706 +370613,莱山区,3706 +370634,长岛县,3706 +370681,龙口市,3706 +370682,莱阳市,3706 +370683,莱州市,3706 +370684,蓬莱市,3706 +370685,招远市,3706 +370686,栖霞市,3706 +370687,海阳市,3706 +3707,潍坊市,37 +370702,潍城区,3707 +370703,寒亭区,3707 +370704,坊子区,3707 +370705,奎文区,3707 +370724,临朐县,3707 +370725,昌乐县,3707 +370781,青州市,3707 +370782,诸城市,3707 +370783,寿光市,3707 +370784,安丘市,3707 +370785,高密市,3707 +370786,昌邑市,3707 +3708,济宁市,37 +370811,任城区,3708 +370812,兖州区,3708 +370826,微山县,3708 +370827,鱼台县,3708 +370828,金乡县,3708 +370829,嘉祥县,3708 +370830,汶上县,3708 +370831,泗水县,3708 +370832,梁山县,3708 +370881,曲阜市,3708 +370883,邹城市,3708 +3709,泰安市,37 +370902,泰山区,3709 +370911,岱岳区,3709 +370921,宁阳县,3709 +370923,东平县,3709 +370982,新泰市,3709 +370983,肥城市,3709 +3710,威海市,37 +371002,环翠区,3710 +371003,文登区,3710 +371082,荣成市,3710 +371083,乳山市,3710 +3711,日照市,37 +371102,东港区,3711 +371103,岚山区,3711 +371121,五莲县,3711 +371122,莒县,3711 +3712,莱芜市,37 +371202,莱城区,3712 +371203,钢城区,3712 +3713,临沂市,37 +371302,兰山区,3713 +371311,罗庄区,3713 +371312,河东区,3713 +371321,沂南县,3713 +371322,郯城县,3713 +371323,沂水县,3713 +371324,兰陵县,3713 +371325,费县,3713 +371326,平邑县,3713 +371327,莒南县,3713 +371328,蒙阴县,3713 +371329,临沭县,3713 +3714,德州市,37 +371402,德城区,3714 +371403,陵城区,3714 +371422,宁津县,3714 +371423,庆云县,3714 +371424,临邑县,3714 +371425,齐河县,3714 +371426,平原县,3714 +371427,夏津县,3714 +371428,武城县,3714 +371481,乐陵市,3714 +371482,禹城市,3714 +3715,聊城市,37 +371502,东昌府区,3715 +371521,阳谷县,3715 +371522,莘县,3715 +371523,茌平县,3715 +371524,东阿县,3715 +371525,冠县,3715 +371526,高唐县,3715 +371581,临清市,3715 +3716,滨州市,37 +371602,滨城区,3716 +371603,沾化区,3716 +371621,惠民县,3716 +371622,阳信县,3716 +371623,无棣县,3716 +371625,博兴县,3716 +371681,邹平市,3716 +3717,菏泽市,37 +371702,牡丹区,3717 +371703,定陶区,3717 +371721,曹县,3717 +371722,单县,3717 +371723,成武县,3717 +371724,巨野县,3717 +371725,郓城县,3717 +371726,鄄城县,3717 +371728,东明县,3717 +41,河南省, +4101,郑州市,41 +410102,中原区,4101 +410103,二七区,4101 +410104,管城回族区,4101 +410105,金水区,4101 +410106,上街区,4101 +410108,惠济区,4101 +410122,中牟县,4101 +410181,巩义市,4101 +410182,荥阳市,4101 +410183,新密市,4101 +410184,新郑市,4101 +410185,登封市,4101 +4102,开封市,41 +410202,龙亭区,4102 +410203,顺河回族区,4102 +410204,鼓楼区,4102 +410205,禹王台区,4102 +410212,祥符区,4102 +410221,杞县,4102 +410222,通许县,4102 +410223,尉氏县,4102 +410225,兰考县,4102 +4103,洛阳市,41 +410302,老城区,4103 +410303,西工区,4103 +410304,瀍河回族区,4103 +410305,涧西区,4103 +410306,吉利区,4103 +410311,洛龙区,4103 +410322,孟津县,4103 +410323,新安县,4103 +410324,栾川县,4103 +410325,嵩县,4103 +410326,汝阳县,4103 +410327,宜阳县,4103 +410328,洛宁县,4103 +410329,伊川县,4103 +410381,偃师市,4103 +4104,平顶山市,41 +410402,新华区,4104 +410403,卫东区,4104 +410404,石龙区,4104 +410411,湛河区,4104 +410421,宝丰县,4104 +410422,叶县,4104 +410423,鲁山县,4104 +410425,郏县,4104 +410481,舞钢市,4104 +410482,汝州市,4104 +4105,安阳市,41 +410502,文峰区,4105 +410503,北关区,4105 +410505,殷都区,4105 +410506,龙安区,4105 +410522,安阳县,4105 +410523,汤阴县,4105 +410526,滑县,4105 +410527,内黄县,4105 +410581,林州市,4105 +4106,鹤壁市,41 +410602,鹤山区,4106 +410603,山城区,4106 +410611,淇滨区,4106 +410621,浚县,4106 +410622,淇县,4106 +4107,新乡市,41 +410702,红旗区,4107 +410703,卫滨区,4107 +410704,凤泉区,4107 +410711,牧野区,4107 +410721,新乡县,4107 +410724,获嘉县,4107 +410725,原阳县,4107 +410726,延津县,4107 +410727,封丘县,4107 +410728,长垣县,4107 +410781,卫辉市,4107 +410782,辉县市,4107 +4108,焦作市,41 +410802,解放区,4108 +410803,中站区,4108 +410804,马村区,4108 +410811,山阳区,4108 +410821,修武县,4108 +410822,博爱县,4108 +410823,武陟县,4108 +410825,温县,4108 +410882,沁阳市,4108 +410883,孟州市,4108 +4109,濮阳市,41 +410902,华龙区,4109 +410922,清丰县,4109 +410923,南乐县,4109 +410926,范县,4109 +410927,台前县,4109 +410928,濮阳县,4109 +4110,许昌市,41 +411002,魏都区,4110 +411003,建安区,4110 +411024,鄢陵县,4110 +411025,襄城县,4110 +411081,禹州市,4110 +411082,长葛市,4110 +4111,漯河市,41 +411102,源汇区,4111 +411103,郾城区,4111 +411104,召陵区,4111 +411121,舞阳县,4111 +411122,临颍县,4111 +4112,三门峡市,41 +411202,湖滨区,4112 +411203,陕州区,4112 +411221,渑池县,4112 +411224,卢氏县,4112 +411281,义马市,4112 +411282,灵宝市,4112 +4113,南阳市,41 +411302,宛城区,4113 +411303,卧龙区,4113 +411321,南召县,4113 +411322,方城县,4113 +411323,西峡县,4113 +411324,镇平县,4113 +411325,内乡县,4113 +411326,淅川县,4113 +411327,社旗县,4113 +411328,唐河县,4113 +411329,新野县,4113 +411330,桐柏县,4113 +411381,邓州市,4113 +4114,商丘市,41 +411402,梁园区,4114 +411403,睢阳区,4114 +411421,民权县,4114 +411422,睢县,4114 +411423,宁陵县,4114 +411424,柘城县,4114 +411425,虞城县,4114 +411426,夏邑县,4114 +411481,永城市,4114 +4115,信阳市,41 +411502,浉河区,4115 +411503,平桥区,4115 +411521,罗山县,4115 +411522,光山县,4115 +411523,新县,4115 +411524,商城县,4115 +411525,固始县,4115 +411526,潢川县,4115 +411527,淮滨县,4115 +411528,息县,4115 +4116,周口市,41 +411602,川汇区,4116 +411621,扶沟县,4116 +411622,西华县,4116 +411623,商水县,4116 +411624,沈丘县,4116 +411625,郸城县,4116 +411626,淮阳县,4116 +411627,太康县,4116 +411628,鹿邑县,4116 +411681,项城市,4116 +4117,驻马店市,41 +411702,驿城区,4117 +411721,西平县,4117 +411722,上蔡县,4117 +411723,平舆县,4117 +411724,正阳县,4117 +411725,确山县,4117 +411726,泌阳县,4117 +411727,汝南县,4117 +411728,遂平县,4117 +411729,新蔡县,4117 +419001,济源市,41 +42,湖北省, +4201,武汉市,42 +420102,江岸区,4201 +420103,江汉区,4201 +420104,硚口区,4201 +420105,汉阳区,4201 +420106,武昌区,4201 +420107,青山区,4201 +420111,洪山区,4201 +420112,东西湖区,4201 +420113,汉南区,4201 +420114,蔡甸区,4201 +420115,江夏区,4201 +420116,黄陂区,4201 +420117,新洲区,4201 +4202,黄石市,42 +420202,黄石港区,4202 +420203,西塞山区,4202 +420204,下陆区,4202 +420205,铁山区,4202 +420222,阳新县,4202 +420281,大冶市,4202 +4203,十堰市,42 +420302,茅箭区,4203 +420303,张湾区,4203 +420304,郧阳区,4203 +420322,郧西县,4203 +420323,竹山县,4203 +420324,竹溪县,4203 +420325,房县,4203 +420381,丹江口市,4203 +4205,宜昌市,42 +420502,西陵区,4205 +420503,伍家岗区,4205 +420504,点军区,4205 +420505,猇亭区,4205 +420506,夷陵区,4205 +420525,远安县,4205 +420526,兴山县,4205 +420527,秭归县,4205 +420528,长阳土家族自治县,4205 +420529,五峰土家族自治县,4205 +420581,宜都市,4205 +420582,当阳市,4205 +420583,枝江市,4205 +4206,襄阳市,42 +420602,襄城区,4206 +420606,樊城区,4206 +420607,襄州区,4206 +420624,南漳县,4206 +420625,谷城县,4206 +420626,保康县,4206 +420682,老河口市,4206 +420683,枣阳市,4206 +420684,宜城市,4206 +4207,鄂州市,42 +420702,梁子湖区,4207 +420703,华容区,4207 +420704,鄂城区,4207 +4208,荆门市,42 +420802,东宝区,4208 +420804,掇刀区,4208 +420822,沙洋县,4208 +420881,钟祥市,4208 +420882,京山市,4208 +4209,孝感市,42 +420902,孝南区,4209 +420921,孝昌县,4209 +420922,大悟县,4209 +420923,云梦县,4209 +420981,应城市,4209 +420982,安陆市,4209 +420984,汉川市,4209 +4210,荆州市,42 +421002,沙市区,4210 +421003,荆州区,4210 +421022,公安县,4210 +421023,监利县,4210 +421024,江陵县,4210 +421081,石首市,4210 +421083,洪湖市,4210 +421087,松滋市,4210 +4211,黄冈市,42 +421102,黄州区,4211 +421121,团风县,4211 +421122,红安县,4211 +421123,罗田县,4211 +421124,英山县,4211 +421125,浠水县,4211 +421126,蕲春县,4211 +421127,黄梅县,4211 +421181,麻城市,4211 +421182,武穴市,4211 +4212,咸宁市,42 +421202,咸安区,4212 +421221,嘉鱼县,4212 +421222,通城县,4212 +421223,崇阳县,4212 +421224,通山县,4212 +421281,赤壁市,4212 +4213,随州市,42 +421303,曾都区,4213 +421321,随县,4213 +421381,广水市,4213 +4228,恩施土家族苗族自治州,42 +422801,恩施市,4228 +422802,利川市,4228 +422822,建始县,4228 +422823,巴东县,4228 +422825,宣恩县,4228 +422826,咸丰县,4228 +422827,来凤县,4228 +422828,鹤峰县,4228 +429004,仙桃市,42 +429005,潜江市,42 +429006,天门市,42 +429021,神农架林区,42 +43,湖南省, +4301,长沙市,43 +430102,芙蓉区,4301 +430103,天心区,4301 +430104,岳麓区,4301 +430105,开福区,4301 +430111,雨花区,4301 +430112,望城区,4301 +430121,长沙县,4301 +430181,浏阳市,4301 +430182,宁乡市,4301 +4302,株洲市,43 +430202,荷塘区,4302 +430203,芦淞区,4302 +430204,石峰区,4302 +430211,天元区,4302 +430212,渌口区,4302 +430223,攸县,4302 +430224,茶陵县,4302 +430225,炎陵县,4302 +430281,醴陵市,4302 +4303,湘潭市,43 +430302,雨湖区,4303 +430304,岳塘区,4303 +430321,湘潭县,4303 +430381,湘乡市,4303 +430382,韶山市,4303 +4304,衡阳市,43 +430405,珠晖区,4304 +430406,雁峰区,4304 +430407,石鼓区,4304 +430408,蒸湘区,4304 +430412,南岳区,4304 +430421,衡阳县,4304 +430422,衡南县,4304 +430423,衡山县,4304 +430424,衡东县,4304 +430426,祁东县,4304 +430481,耒阳市,4304 +430482,常宁市,4304 +4305,邵阳市,43 +430502,双清区,4305 +430503,大祥区,4305 +430511,北塔区,4305 +430521,邵东县,4305 +430522,新邵县,4305 +430523,邵阳县,4305 +430524,隆回县,4305 +430525,洞口县,4305 +430527,绥宁县,4305 +430528,新宁县,4305 +430529,城步苗族自治县,4305 +430581,武冈市,4305 +4306,岳阳市,43 +430602,岳阳楼区,4306 +430603,云溪区,4306 +430611,君山区,4306 +430621,岳阳县,4306 +430623,华容县,4306 +430624,湘阴县,4306 +430626,平江县,4306 +430681,汨罗市,4306 +430682,临湘市,4306 +4307,常德市,43 +430702,武陵区,4307 +430703,鼎城区,4307 +430721,安乡县,4307 +430722,汉寿县,4307 +430723,澧县,4307 +430724,临澧县,4307 +430725,桃源县,4307 +430726,石门县,4307 +430781,津市市,4307 +4308,张家界市,43 +430802,永定区,4308 +430811,武陵源区,4308 +430821,慈利县,4308 +430822,桑植县,4308 +4309,益阳市,43 +430902,资阳区,4309 +430903,赫山区,4309 +430921,南县,4309 +430922,桃江县,4309 +430923,安化县,4309 +430981,沅江市,4309 +4310,郴州市,43 +431002,北湖区,4310 +431003,苏仙区,4310 +431021,桂阳县,4310 +431022,宜章县,4310 +431023,永兴县,4310 +431024,嘉禾县,4310 +431025,临武县,4310 +431026,汝城县,4310 +431027,桂东县,4310 +431028,安仁县,4310 +431081,资兴市,4310 +4311,永州市,43 +431102,零陵区,4311 +431103,冷水滩区,4311 +431121,祁阳县,4311 +431122,东安县,4311 +431123,双牌县,4311 +431124,道县,4311 +431125,江永县,4311 +431126,宁远县,4311 +431127,蓝山县,4311 +431128,新田县,4311 +431129,江华瑶族自治县,4311 +4312,怀化市,43 +431202,鹤城区,4312 +431221,中方县,4312 +431222,沅陵县,4312 +431223,辰溪县,4312 +431224,溆浦县,4312 +431225,会同县,4312 +431226,麻阳苗族自治县,4312 +431227,新晃侗族自治县,4312 +431228,芷江侗族自治县,4312 +431229,靖州苗族侗族自治县,4312 +431230,通道侗族自治县,4312 +431281,洪江市,4312 +4313,娄底市,43 +431302,娄星区,4313 +431321,双峰县,4313 +431322,新化县,4313 +431381,冷水江市,4313 +431382,涟源市,4313 +4331,湘西土家族苗族自治州,43 +433101,吉首市,4331 +433122,泸溪县,4331 +433123,凤凰县,4331 +433124,花垣县,4331 +433125,保靖县,4331 +433126,古丈县,4331 +433127,永顺县,4331 +433130,龙山县,4331 +44,广东省, +4401,广州市,44 +440103,荔湾区,4401 +440104,越秀区,4401 +440105,海珠区,4401 +440106,天河区,4401 +440111,白云区,4401 +440112,黄埔区,4401 +440113,番禺区,4401 +440114,花都区,4401 +440115,南沙区,4401 +440117,从化区,4401 +440118,增城区,4401 +4402,韶关市,44 +440203,武江区,4402 +440204,浈江区,4402 +440205,曲江区,4402 +440222,始兴县,4402 +440224,仁化县,4402 +440229,翁源县,4402 +440232,乳源瑶族自治县,4402 +440233,新丰县,4402 +440281,乐昌市,4402 +440282,南雄市,4402 +4403,深圳市,44 +440303,罗湖区,4403 +440304,福田区,4403 +440305,南山区,4403 +440306,宝安区,4403 +440307,龙岗区,4403 +440308,盐田区,4403 +440309,龙华区,4403 +440310,坪山区,4403 +440311,光明区,4403 +4404,珠海市,44 +440402,香洲区,4404 +440403,斗门区,4404 +440404,金湾区,4404 +4405,汕头市,44 +440507,龙湖区,4405 +440511,金平区,4405 +440512,濠江区,4405 +440513,潮阳区,4405 +440514,潮南区,4405 +440515,澄海区,4405 +440523,南澳县,4405 +4406,佛山市,44 +440604,禅城区,4406 +440605,南海区,4406 +440606,顺德区,4406 +440607,三水区,4406 +440608,高明区,4406 +4407,江门市,44 +440703,蓬江区,4407 +440704,江海区,4407 +440705,新会区,4407 +440781,台山市,4407 +440783,开平市,4407 +440784,鹤山市,4407 +440785,恩平市,4407 +4408,湛江市,44 +440802,赤坎区,4408 +440803,霞山区,4408 +440804,坡头区,4408 +440811,麻章区,4408 +440823,遂溪县,4408 +440825,徐闻县,4408 +440881,廉江市,4408 +440882,雷州市,4408 +440883,吴川市,4408 +4409,茂名市,44 +440902,茂南区,4409 +440904,电白区,4409 +440981,高州市,4409 +440982,化州市,4409 +440983,信宜市,4409 +4412,肇庆市,44 +441202,端州区,4412 +441203,鼎湖区,4412 +441204,高要区,4412 +441223,广宁县,4412 +441224,怀集县,4412 +441225,封开县,4412 +441226,德庆县,4412 +441284,四会市,4412 +4413,惠州市,44 +441302,惠城区,4413 +441303,惠阳区,4413 +441322,博罗县,4413 +441323,惠东县,4413 +441324,龙门县,4413 +4414,梅州市,44 +441402,梅江区,4414 +441403,梅县区,4414 +441422,大埔县,4414 +441423,丰顺县,4414 +441424,五华县,4414 +441426,平远县,4414 +441427,蕉岭县,4414 +441481,兴宁市,4414 +4415,汕尾市,44 +441502,城区,4415 +441521,海丰县,4415 +441523,陆河县,4415 +441581,陆丰市,4415 +4416,河源市,44 +441602,源城区,4416 +441621,紫金县,4416 +441622,龙川县,4416 +441623,连平县,4416 +441624,和平县,4416 +441625,东源县,4416 +4417,阳江市,44 +441702,江城区,4417 +441704,阳东区,4417 +441721,阳西县,4417 +441781,阳春市,4417 +4418,清远市,44 +441802,清城区,4418 +441803,清新区,4418 +441821,佛冈县,4418 +441823,阳山县,4418 +441825,连山壮族瑶族自治县,4418 +441826,连南瑶族自治县,4418 +441881,英德市,4418 +441882,连州市,4418 +4419,东莞市,44 +4420,中山市,44 +4451,潮州市,44 +445102,湘桥区,4451 +445103,潮安区,4451 +445122,饶平县,4451 +4452,揭阳市,44 +445202,榕城区,4452 +445203,揭东区,4452 +445222,揭西县,4452 +445224,惠来县,4452 +445281,普宁市,4452 +4453,云浮市,44 +445302,云城区,4453 +445303,云安区,4453 +445321,新兴县,4453 +445322,郁南县,4453 +445381,罗定市,4453 +45,广西壮族自治区, +4501,南宁市,45 +450102,兴宁区,4501 +450103,青秀区,4501 +450105,江南区,4501 +450107,西乡塘区,4501 +450108,良庆区,4501 +450109,邕宁区,4501 +450110,武鸣区,4501 +450123,隆安县,4501 +450124,马山县,4501 +450125,上林县,4501 +450126,宾阳县,4501 +450127,横县,4501 +4502,柳州市,45 +450202,城中区,4502 +450203,鱼峰区,4502 +450204,柳南区,4502 +450205,柳北区,4502 +450206,柳江区,4502 +450222,柳城县,4502 +450223,鹿寨县,4502 +450224,融安县,4502 +450225,融水苗族自治县,4502 +450226,三江侗族自治县,4502 +4503,桂林市,45 +450302,秀峰区,4503 +450303,叠彩区,4503 +450304,象山区,4503 +450305,七星区,4503 +450311,雁山区,4503 +450312,临桂区,4503 +450321,阳朔县,4503 +450323,灵川县,4503 +450324,全州县,4503 +450325,兴安县,4503 +450326,永福县,4503 +450327,灌阳县,4503 +450328,龙胜各族自治县,4503 +450329,资源县,4503 +450330,平乐县,4503 +450332,恭城瑶族自治县,4503 +450381,荔浦市,4503 +4504,梧州市,45 +450403,万秀区,4504 +450405,长洲区,4504 +450406,龙圩区,4504 +450421,苍梧县,4504 +450422,藤县,4504 +450423,蒙山县,4504 +450481,岑溪市,4504 +4505,北海市,45 +450502,海城区,4505 +450503,银海区,4505 +450512,铁山港区,4505 +450521,合浦县,4505 +4506,防城港市,45 +450602,港口区,4506 +450603,防城区,4506 +450621,上思县,4506 +450681,东兴市,4506 +4507,钦州市,45 +450702,钦南区,4507 +450703,钦北区,4507 +450721,灵山县,4507 +450722,浦北县,4507 +4508,贵港市,45 +450802,港北区,4508 +450803,港南区,4508 +450804,覃塘区,4508 +450821,平南县,4508 +450881,桂平市,4508 +4509,玉林市,45 +450902,玉州区,4509 +450903,福绵区,4509 +450921,容县,4509 +450922,陆川县,4509 +450923,博白县,4509 +450924,兴业县,4509 +450981,北流市,4509 +4510,百色市,45 +451002,右江区,4510 +451021,田阳县,4510 +451022,田东县,4510 +451023,平果县,4510 +451024,德保县,4510 +451026,那坡县,4510 +451027,凌云县,4510 +451028,乐业县,4510 +451029,田林县,4510 +451030,西林县,4510 +451031,隆林各族自治县,4510 +451081,靖西市,4510 +4511,贺州市,45 +451102,八步区,4511 +451103,平桂区,4511 +451121,昭平县,4511 +451122,钟山县,4511 +451123,富川瑶族自治县,4511 +4512,河池市,45 +451202,金城江区,4512 +451203,宜州区,4512 +451221,南丹县,4512 +451222,天峨县,4512 +451223,凤山县,4512 +451224,东兰县,4512 +451225,罗城仫佬族自治县,4512 +451226,环江毛南族自治县,4512 +451227,巴马瑶族自治县,4512 +451228,都安瑶族自治县,4512 +451229,大化瑶族自治县,4512 +4513,来宾市,45 +451302,兴宾区,4513 +451321,忻城县,4513 +451322,象州县,4513 +451323,武宣县,4513 +451324,金秀瑶族自治县,4513 +451381,合山市,4513 +4514,崇左市,45 +451402,江州区,4514 +451421,扶绥县,4514 +451422,宁明县,4514 +451423,龙州县,4514 +451424,大新县,4514 +451425,天等县,4514 +451481,凭祥市,4514 +46,海南省, +4601,海口市,46 +460105,秀英区,4601 +460106,龙华区,4601 +460107,琼山区,4601 +460108,美兰区,4601 +4602,三亚市,46 +460202,海棠区,4602 +460203,吉阳区,4602 +460204,天涯区,4602 +460205,崖州区,4602 +4603,三沙市,46 +4604,儋州市,46 +469001,五指山市,46 +469002,琼海市,46 +469005,文昌市,46 +469006,万宁市,46 +469007,东方市,46 +469021,定安县,46 +469022,屯昌县,46 +469023,澄迈县,46 +469024,临高县,46 +469025,白沙黎族自治县,46 +469026,昌江黎族自治县,46 +469027,乐东黎族自治县,46 +469028,陵水黎族自治县,46 +469029,保亭黎族苗族自治县,46 +469030,琼中黎族苗族自治县,46 +50,重庆市, +5001,市辖区,50 +500101,万州区,5001 +500102,涪陵区,5001 +500103,渝中区,5001 +500104,大渡口区,5001 +500105,江北区,5001 +500106,沙坪坝区,5001 +500107,九龙坡区,5001 +500108,南岸区,5001 +500109,北碚区,5001 +500110,綦江区,5001 +500111,大足区,5001 +500112,渝北区,5001 +500113,巴南区,5001 +500114,黔江区,5001 +500115,长寿区,5001 +500116,江津区,5001 +500117,合川区,5001 +500118,永川区,5001 +500119,南川区,5001 +500120,璧山区,5001 +500151,铜梁区,5001 +500152,潼南区,5001 +500153,荣昌区,5001 +500154,开州区,5001 +500155,梁平区,5001 +500156,武隆区,5001 +500229,城口县,5001 +500230,丰都县,5001 +500231,垫江县,5001 +500233,忠县,5001 +500235,云阳县,5001 +500236,奉节县,5001 +500237,巫山县,5001 +500238,巫溪县,5001 +500240,石柱土家族自治县,5001 +500241,秀山土家族苗族自治县,5001 +500242,酉阳土家族苗族自治县,5001 +500243,彭水苗族土家族自治县,5001 +51,四川省, +5101,成都市,51 +510104,锦江区,5101 +510105,青羊区,5101 +510106,金牛区,5101 +510107,武侯区,5101 +510108,成华区,5101 +510112,龙泉驿区,5101 +510113,青白江区,5101 +510114,新都区,5101 +510115,温江区,5101 +510116,双流区,5101 +510117,郫都区,5101 +510121,金堂县,5101 +510129,大邑县,5101 +510131,蒲江县,5101 +510132,新津县,5101 +510181,都江堰市,5101 +510182,彭州市,5101 +510183,邛崃市,5101 +510184,崇州市,5101 +510185,简阳市,5101 +5103,自贡市,51 +510302,自流井区,5103 +510303,贡井区,5103 +510304,大安区,5103 +510311,沿滩区,5103 +510321,荣县,5103 +510322,富顺县,5103 +5104,攀枝花市,51 +510402,东区,5104 +510403,西区,5104 +510411,仁和区,5104 +510421,米易县,5104 +510422,盐边县,5104 +5105,泸州市,51 +510502,江阳区,5105 +510503,纳溪区,5105 +510504,龙马潭区,5105 +510521,泸县,5105 +510522,合江县,5105 +510524,叙永县,5105 +510525,古蔺县,5105 +5106,德阳市,51 +510603,旌阳区,5106 +510604,罗江区,5106 +510623,中江县,5106 +510681,广汉市,5106 +510682,什邡市,5106 +510683,绵竹市,5106 +5107,绵阳市,51 +510703,涪城区,5107 +510704,游仙区,5107 +510705,安州区,5107 +510722,三台县,5107 +510723,盐亭县,5107 +510725,梓潼县,5107 +510726,北川羌族自治县,5107 +510727,平武县,5107 +510781,江油市,5107 +5108,广元市,51 +510802,利州区,5108 +510811,昭化区,5108 +510812,朝天区,5108 +510821,旺苍县,5108 +510822,青川县,5108 +510823,剑阁县,5108 +510824,苍溪县,5108 +5109,遂宁市,51 +510903,船山区,5109 +510904,安居区,5109 +510921,蓬溪县,5109 +510922,射洪县,5109 +510923,大英县,5109 +5110,内江市,51 +511002,市中区,5110 +511011,东兴区,5110 +511024,威远县,5110 +511025,资中县,5110 +511083,隆昌市,5110 +5111,乐山市,51 +511102,市中区,5111 +511111,沙湾区,5111 +511112,五通桥区,5111 +511113,金口河区,5111 +511123,犍为县,5111 +511124,井研县,5111 +511126,夹江县,5111 +511129,沐川县,5111 +511132,峨边彝族自治县,5111 +511133,马边彝族自治县,5111 +511181,峨眉山市,5111 +5113,南充市,51 +511302,顺庆区,5113 +511303,高坪区,5113 +511304,嘉陵区,5113 +511321,南部县,5113 +511322,营山县,5113 +511323,蓬安县,5113 +511324,仪陇县,5113 +511325,西充县,5113 +511381,阆中市,5113 +5114,眉山市,51 +511402,东坡区,5114 +511403,彭山区,5114 +511421,仁寿县,5114 +511423,洪雅县,5114 +511424,丹棱县,5114 +511425,青神县,5114 +5115,宜宾市,51 +511502,翠屏区,5115 +511503,南溪区,5115 +511504,叙州区,5115 +511523,江安县,5115 +511524,长宁县,5115 +511525,高县,5115 +511526,珙县,5115 +511527,筠连县,5115 +511528,兴文县,5115 +511529,屏山县,5115 +5116,广安市,51 +511602,广安区,5116 +511603,前锋区,5116 +511621,岳池县,5116 +511622,武胜县,5116 +511623,邻水县,5116 +511681,华蓥市,5116 +5117,达州市,51 +511702,通川区,5117 +511703,达川区,5117 +511722,宣汉县,5117 +511723,开江县,5117 +511724,大竹县,5117 +511725,渠县,5117 +511781,万源市,5117 +5118,雅安市,51 +511802,雨城区,5118 +511803,名山区,5118 +511822,荥经县,5118 +511823,汉源县,5118 +511824,石棉县,5118 +511825,天全县,5118 +511826,芦山县,5118 +511827,宝兴县,5118 +5119,巴中市,51 +511902,巴州区,5119 +511903,恩阳区,5119 +511921,通江县,5119 +511922,南江县,5119 +511923,平昌县,5119 +5120,资阳市,51 +512002,雁江区,5120 +512021,安岳县,5120 +512022,乐至县,5120 +5132,阿坝藏族羌族自治州,51 +513201,马尔康市,5132 +513221,汶川县,5132 +513222,理县,5132 +513223,茂县,5132 +513224,松潘县,5132 +513225,九寨沟县,5132 +513226,金川县,5132 +513227,小金县,5132 +513228,黑水县,5132 +513230,壤塘县,5132 +513231,阿坝县,5132 +513232,若尔盖县,5132 +513233,红原县,5132 +5133,甘孜藏族自治州,51 +513301,康定市,5133 +513322,泸定县,5133 +513323,丹巴县,5133 +513324,九龙县,5133 +513325,雅江县,5133 +513326,道孚县,5133 +513327,炉霍县,5133 +513328,甘孜县,5133 +513329,新龙县,5133 +513330,德格县,5133 +513331,白玉县,5133 +513332,石渠县,5133 +513333,色达县,5133 +513334,理塘县,5133 +513335,巴塘县,5133 +513336,乡城县,5133 +513337,稻城县,5133 +513338,得荣县,5133 +5134,凉山彝族自治州,51 +513401,西昌市,5134 +513422,木里藏族自治县,5134 +513423,盐源县,5134 +513424,德昌县,5134 +513425,会理县,5134 +513426,会东县,5134 +513427,宁南县,5134 +513428,普格县,5134 +513429,布拖县,5134 +513430,金阳县,5134 +513431,昭觉县,5134 +513432,喜德县,5134 +513433,冕宁县,5134 +513434,越西县,5134 +513435,甘洛县,5134 +513436,美姑县,5134 +513437,雷波县,5134 +52,贵州省, +5201,贵阳市,52 +520102,南明区,5201 +520103,云岩区,5201 +520111,花溪区,5201 +520112,乌当区,5201 +520113,白云区,5201 +520115,观山湖区,5201 +520121,开阳县,5201 +520122,息烽县,5201 +520123,修文县,5201 +520181,清镇市,5201 +5202,六盘水市,52 +520201,钟山区,5202 +520203,六枝特区,5202 +520221,水城县,5202 +520281,盘州市,5202 +5203,遵义市,52 +520302,红花岗区,5203 +520303,汇川区,5203 +520304,播州区,5203 +520322,桐梓县,5203 +520323,绥阳县,5203 +520324,正安县,5203 +520325,道真仡佬族苗族自治县,5203 +520326,务川仡佬族苗族自治县,5203 +520327,凤冈县,5203 +520328,湄潭县,5203 +520329,余庆县,5203 +520330,习水县,5203 +520381,赤水市,5203 +520382,仁怀市,5203 +5204,安顺市,52 +520402,西秀区,5204 +520403,平坝区,5204 +520422,普定县,5204 +520423,镇宁布依族苗族自治县,5204 +520424,关岭布依族苗族自治县,5204 +520425,紫云苗族布依族自治县,5204 +5205,毕节市,52 +520502,七星关区,5205 +520521,大方县,5205 +520522,黔西县,5205 +520523,金沙县,5205 +520524,织金县,5205 +520525,纳雍县,5205 +520526,威宁彝族回族苗族自治县,5205 +520527,赫章县,5205 +5206,铜仁市,52 +520602,碧江区,5206 +520603,万山区,5206 +520621,江口县,5206 +520622,玉屏侗族自治县,5206 +520623,石阡县,5206 +520624,思南县,5206 +520625,印江土家族苗族自治县,5206 +520626,德江县,5206 +520627,沿河土家族自治县,5206 +520628,松桃苗族自治县,5206 +5223,黔西南布依族苗族自治州,52 +522301,兴义市,5223 +522302,兴仁市,5223 +522323,普安县,5223 +522324,晴隆县,5223 +522325,贞丰县,5223 +522326,望谟县,5223 +522327,册亨县,5223 +522328,安龙县,5223 +5226,黔东南苗族侗族自治州,52 +522601,凯里市,5226 +522622,黄平县,5226 +522623,施秉县,5226 +522624,三穗县,5226 +522625,镇远县,5226 +522626,岑巩县,5226 +522627,天柱县,5226 +522628,锦屏县,5226 +522629,剑河县,5226 +522630,台江县,5226 +522631,黎平县,5226 +522632,榕江县,5226 +522633,从江县,5226 +522634,雷山县,5226 +522635,麻江县,5226 +522636,丹寨县,5226 +5227,黔南布依族苗族自治州,52 +522701,都匀市,5227 +522702,福泉市,5227 +522722,荔波县,5227 +522723,贵定县,5227 +522725,瓮安县,5227 +522726,独山县,5227 +522727,平塘县,5227 +522728,罗甸县,5227 +522729,长顺县,5227 +522730,龙里县,5227 +522731,惠水县,5227 +522732,三都水族自治县,5227 +53,云南省, +5301,昆明市,53 +530102,五华区,5301 +530103,盘龙区,5301 +530111,官渡区,5301 +530112,西山区,5301 +530113,东川区,5301 +530114,呈贡区,5301 +530115,晋宁区,5301 +530124,富民县,5301 +530125,宜良县,5301 +530126,石林彝族自治县,5301 +530127,嵩明县,5301 +530128,禄劝彝族苗族自治县,5301 +530129,寻甸回族彝族自治县,5301 +530181,安宁市,5301 +5303,曲靖市,53 +530302,麒麟区,5303 +530303,沾益区,5303 +530304,马龙区,5303 +530322,陆良县,5303 +530323,师宗县,5303 +530324,罗平县,5303 +530325,富源县,5303 +530326,会泽县,5303 +530381,宣威市,5303 +5304,玉溪市,53 +530402,红塔区,5304 +530403,江川区,5304 +530422,澄江县,5304 +530423,通海县,5304 +530424,华宁县,5304 +530425,易门县,5304 +530426,峨山彝族自治县,5304 +530427,新平彝族傣族自治县,5304 +530428,元江哈尼族彝族傣族自治县,5304 +5305,保山市,53 +530502,隆阳区,5305 +530521,施甸县,5305 +530523,龙陵县,5305 +530524,昌宁县,5305 +530581,腾冲市,5305 +5306,昭通市,53 +530602,昭阳区,5306 +530621,鲁甸县,5306 +530622,巧家县,5306 +530623,盐津县,5306 +530624,大关县,5306 +530625,永善县,5306 +530626,绥江县,5306 +530627,镇雄县,5306 +530628,彝良县,5306 +530629,威信县,5306 +530681,水富市,5306 +5307,丽江市,53 +530702,古城区,5307 +530721,玉龙纳西族自治县,5307 +530722,永胜县,5307 +530723,华坪县,5307 +530724,宁蒗彝族自治县,5307 +5308,普洱市,53 +530802,思茅区,5308 +530821,宁洱哈尼族彝族自治县,5308 +530822,墨江哈尼族自治县,5308 +530823,景东彝族自治县,5308 +530824,景谷傣族彝族自治县,5308 +530825,镇沅彝族哈尼族拉祜族自治县,5308 +530826,江城哈尼族彝族自治县,5308 +530827,孟连傣族拉祜族佤族自治县,5308 +530828,澜沧拉祜族自治县,5308 +530829,西盟佤族自治县,5308 +5309,临沧市,53 +530902,临翔区,5309 +530921,凤庆县,5309 +530922,云县,5309 +530923,永德县,5309 +530924,镇康县,5309 +530925,双江拉祜族佤族布朗族傣族自治县,5309 +530926,耿马傣族佤族自治县,5309 +530927,沧源佤族自治县,5309 +5323,楚雄彝族自治州,53 +532301,楚雄市,5323 +532322,双柏县,5323 +532323,牟定县,5323 +532324,南华县,5323 +532325,姚安县,5323 +532326,大姚县,5323 +532327,永仁县,5323 +532328,元谋县,5323 +532329,武定县,5323 +532331,禄丰县,5323 +5325,红河哈尼族彝族自治州,53 +532501,个旧市,5325 +532502,开远市,5325 +532503,蒙自市,5325 +532504,弥勒市,5325 +532523,屏边苗族自治县,5325 +532524,建水县,5325 +532525,石屏县,5325 +532527,泸西县,5325 +532528,元阳县,5325 +532529,红河县,5325 +532530,金平苗族瑶族傣族自治县,5325 +532531,绿春县,5325 +532532,河口瑶族自治县,5325 +5326,文山壮族苗族自治州,53 +532601,文山市,5326 +532622,砚山县,5326 +532623,西畴县,5326 +532624,麻栗坡县,5326 +532625,马关县,5326 +532626,丘北县,5326 +532627,广南县,5326 +532628,富宁县,5326 +5328,西双版纳傣族自治州,53 +532801,景洪市,5328 +532822,勐海县,5328 +532823,勐腊县,5328 +5329,大理白族自治州,53 +532901,大理市,5329 +532922,漾濞彝族自治县,5329 +532923,祥云县,5329 +532924,宾川县,5329 +532925,弥渡县,5329 +532926,南涧彝族自治县,5329 +532927,巍山彝族回族自治县,5329 +532928,永平县,5329 +532929,云龙县,5329 +532930,洱源县,5329 +532931,剑川县,5329 +532932,鹤庆县,5329 +5331,德宏傣族景颇族自治州,53 +533102,瑞丽市,5331 +533103,芒市,5331 +533122,梁河县,5331 +533123,盈江县,5331 +533124,陇川县,5331 +5333,怒江傈僳族自治州,53 +533301,泸水市,5333 +533323,福贡县,5333 +533324,贡山独龙族怒族自治县,5333 +533325,兰坪白族普米族自治县,5333 +5334,迪庆藏族自治州,53 +533401,香格里拉市,5334 +533422,德钦县,5334 +533423,维西傈僳族自治县,5334 +54,西藏自治区, +5401,拉萨市,54 +540102,城关区,5401 +540103,堆龙德庆区,5401 +540104,达孜区,5401 +540121,林周县,5401 +540122,当雄县,5401 +540123,尼木县,5401 +540124,曲水县,5401 +540127,墨竹工卡县,5401 +5402,日喀则市,54 +540202,桑珠孜区,5402 +540221,南木林县,5402 +540222,江孜县,5402 +540223,定日县,5402 +540224,萨迦县,5402 +540225,拉孜县,5402 +540226,昂仁县,5402 +540227,谢通门县,5402 +540228,白朗县,5402 +540229,仁布县,5402 +540230,康马县,5402 +540231,定结县,5402 +540232,仲巴县,5402 +540233,亚东县,5402 +540234,吉隆县,5402 +540235,聂拉木县,5402 +540236,萨嘎县,5402 +540237,岗巴县,5402 +5403,昌都市,54 +540302,卡若区,5403 +540321,江达县,5403 +540322,贡觉县,5403 +540323,类乌齐县,5403 +540324,丁青县,5403 +540325,察雅县,5403 +540326,八宿县,5403 +540327,左贡县,5403 +540328,芒康县,5403 +540329,洛隆县,5403 +540330,边坝县,5403 +5404,林芝市,54 +540402,巴宜区,5404 +540421,工布江达县,5404 +540422,米林县,5404 +540423,墨脱县,5404 +540424,波密县,5404 +540425,察隅县,5404 +540426,朗县,5404 +5405,山南市,54 +540502,乃东区,5405 +540521,扎囊县,5405 +540522,贡嘎县,5405 +540523,桑日县,5405 +540524,琼结县,5405 +540525,曲松县,5405 +540526,措美县,5405 +540527,洛扎县,5405 +540528,加查县,5405 +540529,隆子县,5405 +540530,错那县,5405 +540531,浪卡子县,5405 +5406,那曲市,54 +540602,色尼区,5406 +540621,嘉黎县,5406 +540622,比如县,5406 +540623,聂荣县,5406 +540624,安多县,5406 +540625,申扎县,5406 +540626,索县,5406 +540627,班戈县,5406 +540628,巴青县,5406 +540629,尼玛县,5406 +540630,双湖县,5406 +5425,阿里地区,54 +542521,普兰县,5425 +542522,札达县,5425 +542523,噶尔县,5425 +542524,日土县,5425 +542525,革吉县,5425 +542526,改则县,5425 +542527,措勤县,5425 +61,陕西省, +6101,西安市,61 +610102,新城区,6101 +610103,碑林区,6101 +610104,莲湖区,6101 +610111,灞桥区,6101 +610112,未央区,6101 +610113,雁塔区,6101 +610114,阎良区,6101 +610115,临潼区,6101 +610116,长安区,6101 +610117,高陵区,6101 +610118,鄠邑区,6101 +610122,蓝田县,6101 +610124,周至县,6101 +6102,铜川市,61 +610202,王益区,6102 +610203,印台区,6102 +610204,耀州区,6102 +610222,宜君县,6102 +6103,宝鸡市,61 +610302,渭滨区,6103 +610303,金台区,6103 +610304,陈仓区,6103 +610322,凤翔县,6103 +610323,岐山县,6103 +610324,扶风县,6103 +610326,眉县,6103 +610327,陇县,6103 +610328,千阳县,6103 +610329,麟游县,6103 +610330,凤县,6103 +610331,太白县,6103 +6104,咸阳市,61 +610402,秦都区,6104 +610403,杨陵区,6104 +610404,渭城区,6104 +610422,三原县,6104 +610423,泾阳县,6104 +610424,乾县,6104 +610425,礼泉县,6104 +610426,永寿县,6104 +610428,长武县,6104 +610429,旬邑县,6104 +610430,淳化县,6104 +610431,武功县,6104 +610481,兴平市,6104 +610482,彬州市,6104 +6105,渭南市,61 +610502,临渭区,6105 +610503,华州区,6105 +610522,潼关县,6105 +610523,大荔县,6105 +610524,合阳县,6105 +610525,澄城县,6105 +610526,蒲城县,6105 +610527,白水县,6105 +610528,富平县,6105 +610581,韩城市,6105 +610582,华阴市,6105 +6106,延安市,61 +610602,宝塔区,6106 +610603,安塞区,6106 +610621,延长县,6106 +610622,延川县,6106 +610623,子长县,6106 +610625,志丹县,6106 +610626,吴起县,6106 +610627,甘泉县,6106 +610628,富县,6106 +610629,洛川县,6106 +610630,宜川县,6106 +610631,黄龙县,6106 +610632,黄陵县,6106 +6107,汉中市,61 +610702,汉台区,6107 +610703,南郑区,6107 +610722,城固县,6107 +610723,洋县,6107 +610724,西乡县,6107 +610725,勉县,6107 +610726,宁强县,6107 +610727,略阳县,6107 +610728,镇巴县,6107 +610729,留坝县,6107 +610730,佛坪县,6107 +6108,榆林市,61 +610802,榆阳区,6108 +610803,横山区,6108 +610822,府谷县,6108 +610824,靖边县,6108 +610825,定边县,6108 +610826,绥德县,6108 +610827,米脂县,6108 +610828,佳县,6108 +610829,吴堡县,6108 +610830,清涧县,6108 +610831,子洲县,6108 +610881,神木市,6108 +6109,安康市,61 +610902,汉滨区,6109 +610921,汉阴县,6109 +610922,石泉县,6109 +610923,宁陕县,6109 +610924,紫阳县,6109 +610925,岚皋县,6109 +610926,平利县,6109 +610927,镇坪县,6109 +610928,旬阳县,6109 +610929,白河县,6109 +6110,商洛市,61 +611002,商州区,6110 +611021,洛南县,6110 +611022,丹凤县,6110 +611023,商南县,6110 +611024,山阳县,6110 +611025,镇安县,6110 +611026,柞水县,6110 +62,甘肃省, +6201,兰州市,62 +620102,城关区,6201 +620103,七里河区,6201 +620104,西固区,6201 +620105,安宁区,6201 +620111,红古区,6201 +620121,永登县,6201 +620122,皋兰县,6201 +620123,榆中县,6201 +6202,嘉峪关市,62 +6203,金昌市,62 +620302,金川区,6203 +620321,永昌县,6203 +6204,白银市,62 +620402,白银区,6204 +620403,平川区,6204 +620421,靖远县,6204 +620422,会宁县,6204 +620423,景泰县,6204 +6205,天水市,62 +620502,秦州区,6205 +620503,麦积区,6205 +620521,清水县,6205 +620522,秦安县,6205 +620523,甘谷县,6205 +620524,武山县,6205 +620525,张家川回族自治县,6205 +6206,武威市,62 +620602,凉州区,6206 +620621,民勤县,6206 +620622,古浪县,6206 +620623,天祝藏族自治县,6206 +6207,张掖市,62 +620702,甘州区,6207 +620721,肃南裕固族自治县,6207 +620722,民乐县,6207 +620723,临泽县,6207 +620724,高台县,6207 +620725,山丹县,6207 +6208,平凉市,62 +620802,崆峒区,6208 +620821,泾川县,6208 +620822,灵台县,6208 +620823,崇信县,6208 +620825,庄浪县,6208 +620826,静宁县,6208 +620881,华亭市,6208 +6209,酒泉市,62 +620902,肃州区,6209 +620921,金塔县,6209 +620922,瓜州县,6209 +620923,肃北蒙古族自治县,6209 +620924,阿克塞哈萨克族自治县,6209 +620981,玉门市,6209 +620982,敦煌市,6209 +6210,庆阳市,62 +621002,西峰区,6210 +621021,庆城县,6210 +621022,环县,6210 +621023,华池县,6210 +621024,合水县,6210 +621025,正宁县,6210 +621026,宁县,6210 +621027,镇原县,6210 +6211,定西市,62 +621102,安定区,6211 +621121,通渭县,6211 +621122,陇西县,6211 +621123,渭源县,6211 +621124,临洮县,6211 +621125,漳县,6211 +621126,岷县,6211 +6212,陇南市,62 +621202,武都区,6212 +621221,成县,6212 +621222,文县,6212 +621223,宕昌县,6212 +621224,康县,6212 +621225,西和县,6212 +621226,礼县,6212 +621227,徽县,6212 +621228,两当县,6212 +6229,临夏回族自治州,62 +622901,临夏市,6229 +622921,临夏县,6229 +622922,康乐县,6229 +622923,永靖县,6229 +622924,广河县,6229 +622925,和政县,6229 +622926,东乡族自治县,6229 +622927,积石山保安族东乡族撒拉族自治县,6229 +6230,甘南藏族自治州,62 +623001,合作市,6230 +623021,临潭县,6230 +623022,卓尼县,6230 +623023,舟曲县,6230 +623024,迭部县,6230 +623025,玛曲县,6230 +623026,碌曲县,6230 +623027,夏河县,6230 +63,青海省, +6301,西宁市,63 +630102,城东区,6301 +630103,城中区,6301 +630104,城西区,6301 +630105,城北区,6301 +630121,大通回族土族自治县,6301 +630122,湟中县,6301 +630123,湟源县,6301 +6302,海东市,63 +630202,乐都区,6302 +630203,平安区,6302 +630222,民和回族土族自治县,6302 +630223,互助土族自治县,6302 +630224,化隆回族自治县,6302 +630225,循化撒拉族自治县,6302 +6322,海北藏族自治州,63 +632221,门源回族自治县,6322 +632222,祁连县,6322 +632223,海晏县,6322 +632224,刚察县,6322 +6323,黄南藏族自治州,63 +632321,同仁县,6323 +632322,尖扎县,6323 +632323,泽库县,6323 +632324,河南蒙古族自治县,6323 +6325,海南藏族自治州,63 +632521,共和县,6325 +632522,同德县,6325 +632523,贵德县,6325 +632524,兴海县,6325 +632525,贵南县,6325 +6326,果洛藏族自治州,63 +632621,玛沁县,6326 +632622,班玛县,6326 +632623,甘德县,6326 +632624,达日县,6326 +632625,久治县,6326 +632626,玛多县,6326 +6327,玉树藏族自治州,63 +632701,玉树市,6327 +632722,杂多县,6327 +632723,称多县,6327 +632724,治多县,6327 +632725,囊谦县,6327 +632726,曲麻莱县,6327 +6328,海西蒙古族藏族自治州,63 +632801,格尔木市,6328 +632802,德令哈市,6328 +632803,茫崖市,6328 +632821,乌兰县,6328 +632822,都兰县,6328 +632823,天峻县,6328 +64,宁夏回族自治区, +6401,银川市,64 +640104,兴庆区,6401 +640105,西夏区,6401 +640106,金凤区,6401 +640121,永宁县,6401 +640122,贺兰县,6401 +640181,灵武市,6401 +6402,石嘴山市,64 +640202,大武口区,6402 +640205,惠农区,6402 +640221,平罗县,6402 +6403,吴忠市,64 +640302,利通区,6403 +640303,红寺堡区,6403 +640323,盐池县,6403 +640324,同心县,6403 +640381,青铜峡市,6403 +6404,固原市,64 +640402,原州区,6404 +640422,西吉县,6404 +640423,隆德县,6404 +640424,泾源县,6404 +640425,彭阳县,6404 +6405,中卫市,64 +640502,沙坡头区,6405 +640521,中宁县,6405 +640522,海原县,6405 +65,新疆维吾尔自治区, +6501,乌鲁木齐市,65 +650102,天山区,6501 +650103,沙依巴克区,6501 +650104,新市区,6501 +650105,水磨沟区,6501 +650106,头屯河区,6501 +650107,达坂城区,6501 +650109,米东区,6501 +650121,乌鲁木齐县,6501 +6502,克拉玛依市,65 +650202,独山子区,6502 +650203,克拉玛依区,6502 +650204,白碱滩区,6502 +650205,乌尔禾区,6502 +6504,吐鲁番市,65 +650402,高昌区,6504 +650421,鄯善县,6504 +650422,托克逊县,6504 +6505,哈密市,65 +650502,伊州区,6505 +650521,巴里坤哈萨克自治县,6505 +650522,伊吾县,6505 +6523,昌吉回族自治州,65 +652301,昌吉市,6523 +652302,阜康市,6523 +652323,呼图壁县,6523 +652324,玛纳斯县,6523 +652325,奇台县,6523 +652327,吉木萨尔县,6523 +652328,木垒哈萨克自治县,6523 +6527,博尔塔拉蒙古自治州,65 +652701,博乐市,6527 +652702,阿拉山口市,6527 +652722,精河县,6527 +652723,温泉县,6527 +6528,巴音郭楞蒙古自治州,65 +652801,库尔勒市,6528 +652822,轮台县,6528 +652823,尉犁县,6528 +652824,若羌县,6528 +652825,且末县,6528 +652826,焉耆回族自治县,6528 +652827,和静县,6528 +652828,和硕县,6528 +652829,博湖县,6528 +6529,阿克苏地区,65 +652901,阿克苏市,6529 +652922,温宿县,6529 +652923,库车县,6529 +652924,沙雅县,6529 +652925,新和县,6529 +652926,拜城县,6529 +652927,乌什县,6529 +652928,阿瓦提县,6529 +652929,柯坪县,6529 +6530,克孜勒苏柯尔克孜自治州,65 +653001,阿图什市,6530 +653022,阿克陶县,6530 +653023,阿合奇县,6530 +653024,乌恰县,6530 +6531,喀什地区,65 +653101,喀什市,6531 +653121,疏附县,6531 +653122,疏勒县,6531 +653123,英吉沙县,6531 +653124,泽普县,6531 +653125,莎车县,6531 +653126,叶城县,6531 +653127,麦盖提县,6531 +653128,岳普湖县,6531 +653129,伽师县,6531 +653130,巴楚县,6531 +653131,塔什库尔干塔吉克自治县,6531 +6532,和田地区,65 +653201,和田市,6532 +653221,和田县,6532 +653222,墨玉县,6532 +653223,皮山县,6532 +653224,洛浦县,6532 +653225,策勒县,6532 +653226,于田县,6532 +653227,民丰县,6532 +6540,伊犁哈萨克自治州,65 +654002,伊宁市,6540 +654003,奎屯市,6540 +654004,霍尔果斯市,6540 +654021,伊宁县,6540 +654022,察布查尔锡伯自治县,6540 +654023,霍城县,6540 +654024,巩留县,6540 +654025,新源县,6540 +654026,昭苏县,6540 +654027,特克斯县,6540 +654028,尼勒克县,6540 +6542,塔城地区,65 +654201,塔城市,6542 +654202,乌苏市,6542 +654221,额敏县,6542 +654223,沙湾县,6542 +654224,托里县,6542 +654225,裕民县,6542 +654226,和布克赛尔蒙古自治县,6542 +6543,阿勒泰地区,65 +654301,阿勒泰市,6543 +654321,布尔津县,6543 +654322,富蕴县,6543 +654323,福海县,6543 +654324,哈巴河县,6543 +654325,青河县,6543 +654326,吉木乃县,6543 +659001,石河子市,65 +659002,阿拉尔市,65 +659003,图木舒克市,65 +659004,五家渠市,65 +659005,北屯市,65 +659006,铁门关市,65 +659007,双河市,65 +659008,可克达拉市,65 +659009,昆玉市,65 +71,台湾省, +81,香港特别行政区, +82,澳门特别行政区, diff --git a/src/main/resources/jwk.json b/src/main/resources/jwk.json new file mode 100644 index 0000000..912b8f3 --- /dev/null +++ b/src/main/resources/jwk.json @@ -0,0 +1 @@ +{"keys":[{"kty":"RSA","kid":"3e79646c4dbc408383a9eed09f2b85ae","n":"rThRAlbMRceko3NkymeSoN2ICVaDlNBLWv3cyLUeixjWcmuhnPv2JpXmgoxezKZfhH_0sChBof--BaaqSUukl9wWMW1bWCyFyU5qNczhQk3ANlhaLiSgXsqD-NKI3ObJjB-26fnOZb9QskCqrPW1lEtwgb9-skMAfGlh5kaDOKjYKI64DPSMMXpSiJEDM-7DK-TFfm0QfPcoH-k-1C02NHlGWehVUn9FUJ0TAiDxpKj28qOmYh7s1M7OU_h-Sso7LM-5zbftpcO6SINe81Gw9JPd7rKPCRxkw8ROSCCq-JH_zshM80kTK2nWcseGvhQ_4vKQIBp9PrAgCrGJHM160w","e":"AQAB","d":"AwS2NKo6iQS_k7GREg3X-kGh-zest00h4wYFcOHnFFlsczX47PlfArEeASxdAofrpi1soB0zd5UzRHnxAbH1vkexg076hoDQG__nzeQyEKu2K7xCZgdxW_V_cziH9gF3hZ-P2mfl9tPsng6OatElRt5BqaEingyY15ImiJK1-qi_LTx4gfwRfquKLbUgqJR4Tf6eKlwOzEo41Ilo26gnojNzWryB_XHG7lj6SngPDBJp7ty32je4Fv3A3hXt7JHDwloww6-xiRtUflDpSec4A-o-PHgbfoYLyM7mM4BDt4PM54EHm4u8WzypG0wNKDTiq4KSapei5xDbiG3RpngvAQ","p":"5kUHkGxnZvZT762Ex-0De2nYodAbbZNVR-eIPx2ng2VZmEbAU3cp_DxigpXWyQ0FwJ2Me8GvxnlbxJ7k7d-4AV2X8q6Q-UqXajHdudRU_QX05kPEgZ3xtPk5ekI0-u1BEQT7pY_gxlZC2mzXAcVLd-LwbVPuQEba5S4JMsjcHUE","q":"wJNa06-qZ2tWncGl7cfJdO-SJ_H3taowMhh-RsJmeVefjjN3pfVjjE0wG_rIP-BjjCB9OhvSnI8LDjoNu8uIg090DYnA6IUfZpWo3zjgedeyqQyXFVjjVQkn98zgp5NFLpuitZsl9-EHhh7JaZDCwaJ527MN3VCoQxeI75ggjxM","dp":"HQTH_kBbC5OxYjwIxrUswinFnia-viFaFvSrq-CN0rY8Az-vTxVuWhY2B-TgK3gTqIFyScpP34A9u1qW2Q9fffSQiInNRU1MJZrhKWED0NsmULprkjYYVsktoCWlzZWGpKFvIR8voW8Pf71FnziA2TvlNrHkDX-gaE9T422Cp8E","dq":"owJYqMWS1dYLTKBlx0ANbHl6W2u7xb_Y6h7HjTfzLBWazvEL_6QW7uVLqvN-XGuheDTsK6rvfWyr7BACHgvsc1JnJyqK64f8C4b1mnZ3tUt7RROONBi43ftRJLX9GHxV3F0LvvQkkI2gI8ydq0lJQkU5J1qKiuNCewBJ_p3kOZc","qi":"hNAZV6aWEEWfB1HkrfdtO6sjq9ceEod55ez82I1ZNgoKle8gpRkh3vw2EIJ_5lcw57s5rw8G-sCQPG1AQSZ6u9aURwHkIXjpIhLAlv6gvKkCh0smPPvnSiltJKOJsuHkrD6rGkV1f-MlCS51lKlk9xShQzkRidkNd4BUh0a7ktA"}]} \ No newline at end of file diff --git a/src/main/resources/logback-spring.xml b/src/main/resources/logback-spring.xml new file mode 100644 index 0000000..308f148 --- /dev/null +++ b/src/main/resources/logback-spring.xml @@ -0,0 +1,86 @@ + + + + + + + + + + + + + + + + ${log.pattern} + UTF-8 + + + + + DEBUG + + + + + + + ${log.pattern} + UTF-8 + + + + + + + + + + ${LOG_HOME}/wvp-%d{yyyy-MM-dd}.%i.log + + 30 + 20MB + + + + %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50}:%L - %msg%n + UTF-8 + + + + + DEBUG + + + + + + + + ${LOG_HOME}/sip-%d{yyyy-MM-dd}.%i.log + + 30 + 50MB + + + + %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50}:%L - %msg%n + UTF-8 + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/test/java/com/genersoft/iot/vmp/jt1078/JT1078ServerTest.java b/src/test/java/com/genersoft/iot/vmp/jt1078/JT1078ServerTest.java new file mode 100644 index 0000000..2a48961 --- /dev/null +++ b/src/test/java/com/genersoft/iot/vmp/jt1078/JT1078ServerTest.java @@ -0,0 +1,103 @@ +package com.genersoft.iot.vmp.jt1078; + +import com.genersoft.iot.vmp.jt1078.cmd.JT1078Template; +import com.genersoft.iot.vmp.jt1078.codec.netty.TcpServer; +import com.genersoft.iot.vmp.jt1078.proc.response.J9102; +import com.genersoft.iot.vmp.jt1078.proc.response.J9201; +import com.genersoft.iot.vmp.jt1078.proc.response.J9202; +import com.genersoft.iot.vmp.jt1078.proc.response.J9205; + +import java.util.Scanner; + +/** + * @author QingtaiJiang + * @date 2023/4/28 14:22 + * @email qingtaij@163.com + */ +public class JT1078ServerTest { + + private static final JT1078Template jt1078Template = new JT1078Template(); + + public static void main(String[] args) { + System.out.println("Starting jt1078 server..."); + TcpServer tcpServer = new TcpServer(21078, null, null); + tcpServer.start(); + System.out.println("Start jt1078 server success!"); + + + Scanner s = new Scanner(System.in); + while (true) { + String code = s.nextLine(); + switch (code) { + case "1": + test9102(); + break; + case "2": + test9201(); + break; + case "3": + test9202(); + break; + case "4": + test9205(); + break; + default: + break; + } + } + } + + private static void test9102() { + J9102 j9102 = new J9102(); + j9102.setChannel(1); + j9102.setCommand(0); + j9102.setCloseType(0); + j9102.setStreamType(0); + + Object s = jt1078Template.stopLive("18864197066", j9102, 6); + System.out.println(s); + } + + private static void test9201() { + J9201 j9201 = new J9201(); + j9201.setIp("192.168.1.1"); + j9201.setChannel(1); + j9201.setTcpPort(7618); + j9201.setUdpPort(7618); + j9201.setType(0); + j9201.setRate(0); + j9201.setStorageType(0); + j9201.setPlaybackType(0); + j9201.setPlaybackSpeed(0); + j9201.setStartTime("230428134100"); + j9201.setEndTime("230428134200"); + + Object s = jt1078Template.startBackLive("18864197066", j9201, 6); + System.out.println(s); + } + + private static void test9202() { + J9202 j9202 = new J9202(); + + j9202.setChannel(1); + j9202.setPlaybackType(2); + j9202.setPlaybackSpeed(0); + j9202.setPlaybackTime("230428134100"); + + Object s = jt1078Template.controlBackLive("18864197066", j9202, 6); + System.out.println(s); + } + + private static void test9205() { + J9205 j9205 = new J9205(); + j9205.setChannelId(1); + j9205.setStartTime("230428134100"); + j9205.setEndTime("230428134100"); + j9205.setMediaType(0); + j9205.setStreamType(0); + j9205.setStorageType(0); + + Object s = jt1078Template.queryBackTime("18864197066", j9205, 6); + System.out.println(s); + } +}