提交初始版本
This commit is contained in:
commit
a101fd28cc
|
|
@ -0,0 +1,199 @@
|
|||
# 海康威视视频应用服务 API — Spring Boot 集成
|
||||
|
||||
## 功能概述
|
||||
|
||||
| 接口 | 说明 |
|
||||
|------|------|
|
||||
| `GET /api/cameras` | 分页获取摄像头资源列表 |
|
||||
| `GET /api/cameras/{cameraIndexCode}` | 查询摄像头详情 |
|
||||
| `GET /api/cameras/{cameraIndexCode}/preview-url` | 快捷获取实时预览播放地址 |
|
||||
| `POST /api/cameras/preview-url` | 获取实时预览播放地址(完整参数) |
|
||||
| `POST /api/cameras/playback-url` | 获取录像回放播放地址 |
|
||||
|
||||
---
|
||||
|
||||
## Web 监控界面
|
||||
|
||||
启动后访问 `http://localhost:8080` 即可打开视频监控界面:
|
||||
|
||||
- 左侧配置平台 IP / AppKey / AppSecret,点击「连接插件 & 初始化」
|
||||
- 自动加载摄像头列表,点击任意摄像头即可播放实时视频
|
||||
- 支持**实时预览** / **录像回放**切换
|
||||
- 支持 1/4/9 分屏显示
|
||||
- 抓图、本地录像等快捷操作
|
||||
|
||||
> **前提:** 在 Windows 客户端安装海康官方 `VideoWebPlugin.exe`,
|
||||
> 并将 JS 插件文件放入 `src/main/resources/static/js/` 目录。
|
||||
|
||||
---
|
||||
|
||||
## 快速开始
|
||||
|
||||
### 1. 配置 application.yml
|
||||
|
||||
```yaml
|
||||
hikvision:
|
||||
api:
|
||||
host: https://192.168.1.100 # 平台IP或域名
|
||||
app-key: your_app_key # 开放平台应用AppKey
|
||||
app-secret: your_app_secret # 开放平台应用AppSecret
|
||||
```
|
||||
|
||||
> AppKey/AppSecret 在海康 iSecure Center 开放平台 → 应用管理 → 创建应用 后获取。
|
||||
|
||||
### 2. 编译运行
|
||||
|
||||
```bash
|
||||
mvn clean package -DskipTests
|
||||
java -jar target/hikvision-video-api-1.0.0.jar
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 接口调用示例
|
||||
|
||||
### 获取摄像头列表
|
||||
|
||||
```bash
|
||||
curl "http://localhost:8080/api/cameras?pageNo=1&pageSize=20"
|
||||
```
|
||||
|
||||
响应:
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"msg": "success",
|
||||
"data": {
|
||||
"total": 100,
|
||||
"pageNo": 1,
|
||||
"pageSize": 20,
|
||||
"totalPage": 5,
|
||||
"list": [
|
||||
{
|
||||
"cameraIndexCode": "abc123",
|
||||
"cameraName": "1号楼入口",
|
||||
"onlineStatus": "1",
|
||||
"installPlace": "1号楼大门"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 获取实时预览播放地址(RTSP)
|
||||
|
||||
```bash
|
||||
curl "http://localhost:8080/api/cameras/abc123/preview-url?protocol=1"
|
||||
```
|
||||
|
||||
响应:
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"data": {
|
||||
"url": "rtsp://192.168.1.100:554/openUrl/xxxtoken"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 获取实时预览播放地址(HLS)
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:8080/api/cameras/preview-url \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"cameraIndexCode": "abc123",
|
||||
"streamType": 0,
|
||||
"protocol": 3,
|
||||
"transmode": 1
|
||||
}'
|
||||
```
|
||||
|
||||
响应:
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"data": {
|
||||
"url": "https://192.168.1.100/openUrl/xxxtoken/index.m3u8"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 获取录像回放播放地址
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:8080/api/cameras/playback-url \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"cameraIndexCode": "abc123",
|
||||
"beginTime": "2024-01-01T00:00:00.000+08:00",
|
||||
"endTime": "2024-01-01T01:00:00.000+08:00",
|
||||
"protocol": 1,
|
||||
"recordType": 0
|
||||
}'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 协议类型说明
|
||||
|
||||
| protocol值 | 协议 | 说明 |
|
||||
|------------|------|------|
|
||||
| 1 | RTSP | 适合 VLC、ffmpeg 等本地播放 |
|
||||
| 2 | RTMP | 适合 Flash/流媒体服务器转发 |
|
||||
| 3 | HLS | 适合 Web 页面播放(video.js、hls.js) |
|
||||
| 4 | FLV (HTTP-FLV) | 适合 Web 低延时播放(flv.js) |
|
||||
|
||||
---
|
||||
|
||||
## 项目结构
|
||||
|
||||
```
|
||||
src/main/java/com/hikvision/video/
|
||||
├── HikvisionVideoApplication.java # 启动类
|
||||
├── config/
|
||||
│ └── HikvisionProperties.java # 配置读取
|
||||
├── controller/
|
||||
│ └── VideoController.java # REST接口
|
||||
├── model/
|
||||
│ └── HikModels.java # 请求/响应模型
|
||||
├── service/
|
||||
│ └── HikvisionVideoService.java # 业务逻辑 & API调用
|
||||
└── util/
|
||||
├── HikvisionSignUtil.java # HMAC-SHA256签名
|
||||
└── HttpUtil.java # HTTP请求工具
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 签名说明
|
||||
|
||||
海康 API 采用 **HMAC-SHA256** 签名鉴权,签名字符串格式:
|
||||
|
||||
```
|
||||
{METHOD}\n
|
||||
{Accept}\n
|
||||
{Content-Type}\n
|
||||
x-ca-key:{AppKey}\n
|
||||
x-ca-nonce:{随机UUID}\n
|
||||
x-ca-timestamp:{毫秒时间戳}\n
|
||||
{请求路径}
|
||||
```
|
||||
|
||||
签名结果 Base64 编码后放入请求头 `X-Ca-Signature`。
|
||||
|
||||
---
|
||||
|
||||
## 常见错误码
|
||||
|
||||
| code | 含义 | 解决方案 |
|
||||
|------|------|---------|
|
||||
| 0 | 成功 | — |
|
||||
| 401 | 鉴权失败 | 检查 AppKey/AppSecret 配置 |
|
||||
| 1001 | 参数错误 | 检查 cameraIndexCode 是否存在 |
|
||||
| 17004 | 摄像头离线 | 检查设备在线状态 |
|
||||
| 10001 | 无权限 | 检查应用是否有摄像头访问权限 |
|
||||
|
|
@ -0,0 +1,89 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
|
||||
https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-parent</artifactId>
|
||||
<version>3.2.0</version>
|
||||
<relativePath/>
|
||||
</parent>
|
||||
|
||||
<groupId>com.hikvision</groupId>
|
||||
<artifactId>hikvision-video-api</artifactId>
|
||||
<version>1.0.0</version>
|
||||
<name>hikvision-video-api</name>
|
||||
<description>海康威视视频应用服务 API 集成</description>
|
||||
|
||||
<properties>
|
||||
<java.version>17</java.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<!-- Spring Boot Web -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Spring Boot Validation -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-validation</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Lombok -->
|
||||
<dependency>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
|
||||
<!-- Jackson (JSON) -->
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.core</groupId>
|
||||
<artifactId>jackson-databind</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Apache HttpClient 5 (用于调用海康API) -->
|
||||
<dependency>
|
||||
<groupId>org.apache.httpcomponents.client5</groupId>
|
||||
<artifactId>httpclient5</artifactId>
|
||||
<version>5.3.1</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Commons Codec (HMAC-SHA256签名) -->
|
||||
<dependency>
|
||||
<groupId>commons-codec</groupId>
|
||||
<artifactId>commons-codec</artifactId>
|
||||
<version>1.16.0</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Spring Boot Test -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
<configuration>
|
||||
<excludes>
|
||||
<exclude>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
</exclude>
|
||||
</excludes>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
package com.hikvision.video;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
|
||||
/**
|
||||
* 海康威视视频 API 集成 - Spring Boot 启动类
|
||||
*/
|
||||
@SpringBootApplication
|
||||
@EnableConfigurationProperties
|
||||
public class HikvisionVideoApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(HikvisionVideoApplication.class, args);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
package com.hikvision.video.config;
|
||||
|
||||
import lombok.Data;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* 海康威视 API 配置属性
|
||||
*/
|
||||
@Data
|
||||
@Component
|
||||
@ConfigurationProperties(prefix = "hikvision.api")
|
||||
public class HikvisionProperties {
|
||||
|
||||
/** 平台地址,例如 https://192.168.1.1 */
|
||||
private String host;
|
||||
|
||||
/** AppKey */
|
||||
private String appKey;
|
||||
|
||||
/** AppSecret */
|
||||
private String appSecret;
|
||||
}
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
package com.hikvision.video.config;
|
||||
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.web.servlet.config.annotation.*;
|
||||
|
||||
/**
|
||||
* Web MVC 配置:静态资源 + 跨域
|
||||
*/
|
||||
@Configuration
|
||||
public class WebMvcConfig implements WebMvcConfigurer {
|
||||
|
||||
/**
|
||||
* 静态资源映射
|
||||
* 将 /js/** /css/** 映射到 classpath:/static/js/ 等目录
|
||||
* (Spring Boot 默认已映射 /static/,此处显式声明便于理解)
|
||||
*/
|
||||
@Override
|
||||
public void addResourceHandlers(ResourceHandlerRegistry registry) {
|
||||
registry.addResourceHandler("/js/**")
|
||||
.addResourceLocations("classpath:/static/js/");
|
||||
registry.addResourceHandler("/css/**")
|
||||
.addResourceLocations("classpath:/static/css/");
|
||||
registry.addResourceHandler("/**")
|
||||
.addResourceLocations("classpath:/static/");
|
||||
}
|
||||
|
||||
/**
|
||||
* 跨域配置(开发阶段允许所有来源,生产环境请限制)
|
||||
*/
|
||||
@Override
|
||||
public void addCorsMappings(CorsRegistry registry) {
|
||||
registry.addMapping("/api/**")
|
||||
.allowedOriginPatterns("*")
|
||||
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
|
||||
.allowedHeaders("*")
|
||||
.allowCredentials(false)
|
||||
.maxAge(3600);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,223 @@
|
|||
package com.hikvision.video.controller;
|
||||
|
||||
import com.hikvision.video.model.CaptureResult;
|
||||
import com.hikvision.video.service.HikvisionVideoService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.core.io.FileSystemResource;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.nio.file.attribute.BasicFileAttributes;
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneId;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
/**
|
||||
* 抓图管理 Controller
|
||||
* <p>
|
||||
* POST /api/captures/{cameraIndexCode} - 触发抓图并保存
|
||||
* GET /api/captures - 获取本地抓图列表
|
||||
* GET /api/captures/image/{filename} - 获取图片文件(内联展示)
|
||||
* DELETE /api/captures/{filename} - 删除指定抓图
|
||||
* </p>
|
||||
*/
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping("/api/captures")
|
||||
@RequiredArgsConstructor
|
||||
public class CaptureController {
|
||||
|
||||
private final HikvisionVideoService hikvisionVideoService;
|
||||
|
||||
@Value("${app.capture.save-path:./captures}")
|
||||
private String capturesSavePath;
|
||||
|
||||
private static final DateTimeFormatter DISPLAY_FMT =
|
||||
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
|
||||
|
||||
// ==================== 触发抓图 ====================
|
||||
|
||||
/**
|
||||
* 触发抓图并保存到本地
|
||||
*
|
||||
* @param cameraIndexCode 监控点编码
|
||||
* @param cameraName 监控点名称(可选,用于展示)
|
||||
*/
|
||||
@PostMapping("/{cameraIndexCode}")
|
||||
public ResponseEntity<Map<String, Object>> capture(
|
||||
@PathVariable String cameraIndexCode,
|
||||
@RequestParam(required = false) String cameraName) {
|
||||
|
||||
log.info("[Capture] 触发抓图 camera={}", cameraIndexCode);
|
||||
CaptureResult result = hikvisionVideoService.captureImage(cameraIndexCode, cameraName);
|
||||
return ok(result);
|
||||
}
|
||||
|
||||
// ==================== 抓图列表 ====================
|
||||
|
||||
/**
|
||||
* 获取本地 captures 目录下所有图片的列表(按时间倒序)
|
||||
*/
|
||||
@GetMapping
|
||||
public ResponseEntity<Map<String, Object>> listCaptures() {
|
||||
try {
|
||||
Path dir = Paths.get(capturesSavePath);
|
||||
if (!Files.exists(dir)) {
|
||||
return ok(Collections.emptyList());
|
||||
}
|
||||
|
||||
try (Stream<Path> stream = Files.list(dir)) {
|
||||
List<Map<String, Object>> list = stream
|
||||
.filter(p -> p.getFileName().toString().toLowerCase().endsWith(".jpg")
|
||||
|| p.getFileName().toString().toLowerCase().endsWith(".jpeg")
|
||||
|| p.getFileName().toString().toLowerCase().endsWith(".png"))
|
||||
.sorted(Comparator.comparing(p -> {
|
||||
try { return Files.getLastModifiedTime(p).toInstant(); }
|
||||
catch (IOException e) { return Instant.EPOCH; }
|
||||
}, Comparator.reverseOrder()))
|
||||
.map(p -> {
|
||||
Map<String, Object> item = new LinkedHashMap<>();
|
||||
String filename = p.getFileName().toString();
|
||||
item.put("filename", filename);
|
||||
item.put("localUrl", "/api/captures/image/" + filename);
|
||||
item.put("fileSizeBytes", fileSize(p));
|
||||
item.put("captureTime", lastModifiedStr(p));
|
||||
// 从文件名解析 cameraIndexCode(格式:{code}_{timestamp}.jpg)
|
||||
int sep = filename.lastIndexOf('_');
|
||||
if (sep > 0) {
|
||||
// 文件名格式:cameraIndexCode_yyyyMMdd_HHmmss.jpg
|
||||
// 最后两段是日期和时间,取前面部分作为 cameraIndexCode
|
||||
String withoutExt = filename.replaceAll("\\.[^.]+$", "");
|
||||
String[] parts = withoutExt.split("_");
|
||||
if (parts.length >= 3) {
|
||||
// cameraIndexCode 可能本身含下划线,取除最后两段外的部分
|
||||
String code = String.join("_",
|
||||
Arrays.copyOfRange(parts, 0, parts.length - 2));
|
||||
item.put("cameraIndexCode", code);
|
||||
} else {
|
||||
item.put("cameraIndexCode", withoutExt);
|
||||
}
|
||||
} else {
|
||||
item.put("cameraIndexCode", filename);
|
||||
}
|
||||
return item;
|
||||
})
|
||||
.collect(Collectors.toList());
|
||||
|
||||
return ok(list);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("[Capture] 列出抓图失败: {}", e.getMessage(), e);
|
||||
return err("列出抓图失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 提供图片文件 ====================
|
||||
|
||||
/**
|
||||
* 提供图片文件(内联展示,浏览器可直接显示)
|
||||
*
|
||||
* @param filename 文件名
|
||||
*/
|
||||
@GetMapping("/image/{filename}")
|
||||
public ResponseEntity<Resource> getImage(@PathVariable String filename) {
|
||||
// 安全校验:防止路径穿越
|
||||
if (filename.contains("..") || filename.contains("/") || filename.contains("\\")) {
|
||||
return ResponseEntity.badRequest().build();
|
||||
}
|
||||
|
||||
Path filePath = Paths.get(capturesSavePath).resolve(filename);
|
||||
if (!Files.exists(filePath) || !Files.isRegularFile(filePath)) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
|
||||
Resource resource = new FileSystemResource(filePath);
|
||||
String contentType = filename.toLowerCase().endsWith(".png") ? "image/png" : "image/jpeg";
|
||||
|
||||
return ResponseEntity.ok()
|
||||
.header(HttpHeaders.CONTENT_DISPOSITION, "inline; filename=\"" + filename + "\"")
|
||||
.contentType(MediaType.parseMediaType(contentType))
|
||||
.body(resource);
|
||||
}
|
||||
|
||||
// ==================== 删除抓图 ====================
|
||||
|
||||
/**
|
||||
* 删除指定抓图文件
|
||||
*
|
||||
* @param filename 文件名
|
||||
*/
|
||||
@DeleteMapping("/{filename}")
|
||||
public ResponseEntity<Map<String, Object>> deleteCapture(@PathVariable String filename) {
|
||||
if (filename.contains("..") || filename.contains("/") || filename.contains("\\")) {
|
||||
return ResponseEntity.badRequest().body(fail("非法文件名"));
|
||||
}
|
||||
try {
|
||||
Path filePath = Paths.get(capturesSavePath).resolve(filename);
|
||||
if (!Files.exists(filePath)) {
|
||||
return ResponseEntity.ok(fail("文件不存在: " + filename));
|
||||
}
|
||||
Files.delete(filePath);
|
||||
log.info("[Capture] 已删除抓图: {}", filename);
|
||||
return ok("已删除: " + filename);
|
||||
} catch (Exception e) {
|
||||
log.error("[Capture] 删除失败: {}", e.getMessage(), e);
|
||||
return err("删除失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 工具方法 ====================
|
||||
|
||||
private long fileSize(Path p) {
|
||||
try { return Files.size(p); } catch (IOException e) { return 0; }
|
||||
}
|
||||
|
||||
private String lastModifiedStr(Path p) {
|
||||
try {
|
||||
BasicFileAttributes attr = Files.readAttributes(p, BasicFileAttributes.class);
|
||||
return LocalDateTime.ofInstant(attr.lastModifiedTime().toInstant(), ZoneId.systemDefault())
|
||||
.format(DISPLAY_FMT);
|
||||
} catch (IOException e) { return ""; }
|
||||
}
|
||||
|
||||
private ResponseEntity<Map<String, Object>> ok(Object data) {
|
||||
Map<String, Object> res = new LinkedHashMap<>();
|
||||
res.put("code", 200);
|
||||
res.put("msg", "success");
|
||||
res.put("data", data);
|
||||
return ResponseEntity.ok(res);
|
||||
}
|
||||
|
||||
private ResponseEntity<Map<String, Object>> err(String msg) {
|
||||
Map<String, Object> res = new LinkedHashMap<>();
|
||||
res.put("code", 500);
|
||||
res.put("msg", msg);
|
||||
return ResponseEntity.internalServerError().body(res);
|
||||
}
|
||||
|
||||
private Map<String, Object> fail(String msg) {
|
||||
Map<String, Object> res = new LinkedHashMap<>();
|
||||
res.put("code", 400);
|
||||
res.put("msg", msg);
|
||||
return res;
|
||||
}
|
||||
|
||||
@ExceptionHandler(RuntimeException.class)
|
||||
public ResponseEntity<Map<String, Object>> handleException(RuntimeException e) {
|
||||
log.error("[Capture] 接口异常: {}", e.getMessage(), e);
|
||||
return err(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,111 @@
|
|||
package com.hikvision.video.controller;
|
||||
|
||||
import com.hikvision.video.model.CaptureTask;
|
||||
import com.hikvision.video.service.CaptureTaskService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 定时抓图任务管理 Controller
|
||||
* <p>
|
||||
* POST /api/capture-tasks - 创建任务
|
||||
* GET /api/capture-tasks - 获取任务列表
|
||||
* GET /api/capture-tasks/{id} - 获取单个任务
|
||||
* PUT /api/capture-tasks/{id}/start - 启动任务
|
||||
* PUT /api/capture-tasks/{id}/stop - 停止任务
|
||||
* DELETE /api/capture-tasks/{id} - 删除任务
|
||||
* </p>
|
||||
*/
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping("/api/capture-tasks")
|
||||
@RequiredArgsConstructor
|
||||
public class CaptureTaskController {
|
||||
|
||||
private final CaptureTaskService captureTaskService;
|
||||
|
||||
/**
|
||||
* 创建定时抓图任务
|
||||
* <pre>
|
||||
* {
|
||||
* "cameraIndexCode": "xxx",
|
||||
* "cameraName": "摄像头名称",
|
||||
* "intervalSeconds": 60,
|
||||
* "maxCaptures": 0
|
||||
* }
|
||||
* </pre>
|
||||
*/
|
||||
@PostMapping
|
||||
public ResponseEntity<Map<String, Object>> createTask(
|
||||
@RequestBody Map<String, Object> body) {
|
||||
|
||||
String cameraIndexCode = (String) body.get("cameraIndexCode");
|
||||
String cameraName = (String) body.getOrDefault("cameraName", "");
|
||||
int intervalSeconds = ((Number) body.getOrDefault("intervalSeconds", 60)).intValue();
|
||||
int maxCaptures = ((Number) body.getOrDefault("maxCaptures", 0)).intValue();
|
||||
|
||||
CaptureTask task = captureTaskService.createTask(
|
||||
cameraIndexCode, cameraName, intervalSeconds, maxCaptures);
|
||||
return ok(task);
|
||||
}
|
||||
|
||||
/** 获取所有任务 */
|
||||
@GetMapping
|
||||
public ResponseEntity<Map<String, Object>> listTasks() {
|
||||
List<CaptureTask> tasks = captureTaskService.listTasks();
|
||||
return ok(tasks);
|
||||
}
|
||||
|
||||
/** 获取单个任务详情 */
|
||||
@GetMapping("/{id}")
|
||||
public ResponseEntity<Map<String, Object>> getTask(@PathVariable String id) {
|
||||
return ok(captureTaskService.getTask(id));
|
||||
}
|
||||
|
||||
/** 启动任务 */
|
||||
@PutMapping("/{id}/start")
|
||||
public ResponseEntity<Map<String, Object>> startTask(@PathVariable String id) {
|
||||
log.info("[CaptureTask] 启动任务请求 id={}", id);
|
||||
return ok(captureTaskService.startTask(id));
|
||||
}
|
||||
|
||||
/** 停止任务 */
|
||||
@PutMapping("/{id}/stop")
|
||||
public ResponseEntity<Map<String, Object>> stopTask(@PathVariable String id) {
|
||||
log.info("[CaptureTask] 停止任务请求 id={}", id);
|
||||
return ok(captureTaskService.stopTask(id));
|
||||
}
|
||||
|
||||
/** 删除任务 */
|
||||
@DeleteMapping("/{id}")
|
||||
public ResponseEntity<Map<String, Object>> deleteTask(@PathVariable String id) {
|
||||
log.info("[CaptureTask] 删除任务请求 id={}", id);
|
||||
captureTaskService.deleteTask(id);
|
||||
return ok("已删除任务: " + id);
|
||||
}
|
||||
|
||||
// ==================== 统一响应 ====================
|
||||
|
||||
private ResponseEntity<Map<String, Object>> ok(Object data) {
|
||||
Map<String, Object> res = new LinkedHashMap<>();
|
||||
res.put("code", 200);
|
||||
res.put("msg", "success");
|
||||
res.put("data", data);
|
||||
return ResponseEntity.ok(res);
|
||||
}
|
||||
|
||||
@ExceptionHandler(RuntimeException.class)
|
||||
public ResponseEntity<Map<String, Object>> handleException(RuntimeException e) {
|
||||
log.error("[CaptureTask] 接口异常: {}", e.getMessage(), e);
|
||||
Map<String, Object> res = new LinkedHashMap<>();
|
||||
res.put("code", 500);
|
||||
res.put("msg", e.getMessage());
|
||||
return ResponseEntity.internalServerError().body(res);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,171 @@
|
|||
package com.hikvision.video.controller;
|
||||
|
||||
import com.hikvision.video.model.*;
|
||||
import com.hikvision.video.service.RegionChannelService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 设备管理 Controller
|
||||
* <p>
|
||||
* GET /api/devices - 分页查询设备列表
|
||||
* POST /api/devices/page - 查询设备列表(完整参数)
|
||||
* GET /api/devices/{deviceIndexCode} - 查询设备详情
|
||||
* GET /api/devices/{deviceIndexCode}/channels - 查询设备下的视频通道
|
||||
* </p>
|
||||
*/
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping("/api/devices")
|
||||
@RequiredArgsConstructor
|
||||
public class DeviceController {
|
||||
|
||||
private final RegionChannelService regionChannelService;
|
||||
|
||||
// ==================== 设备列表 ====================
|
||||
|
||||
/**
|
||||
* 分页查询设备列表(GET 快捷方式)
|
||||
*
|
||||
* @param regionIndexCode 区域编码(可选)
|
||||
* @param cascadeFlag 是否包含子区域:0-否, 1-是(默认1)
|
||||
* @param onlineStatus 在线状态:1-在线, 0-离线(可选)
|
||||
* @param deviceType 设备类型:IPC/DVR/NVR(可选)
|
||||
* @param pageNo 页码(默认1)
|
||||
* @param pageSize 每页数量(默认50)
|
||||
*/
|
||||
@GetMapping
|
||||
public ResponseEntity<Map<String, Object>> getDevicePage(
|
||||
@RequestParam(required = false) String regionIndexCode,
|
||||
@RequestParam(defaultValue = "1") Integer cascadeFlag,
|
||||
@RequestParam(required = false) String onlineStatus,
|
||||
@RequestParam(required = false) String deviceType,
|
||||
@RequestParam(defaultValue = "1") Integer pageNo,
|
||||
@RequestParam(defaultValue = "50") Integer pageSize) {
|
||||
|
||||
DevicePageRequest req = new DevicePageRequest();
|
||||
req.setRegionIndexCode(regionIndexCode);
|
||||
req.setCascadeFlag(cascadeFlag);
|
||||
req.setOnlineStatus(onlineStatus);
|
||||
req.setDeviceType(deviceType);
|
||||
req.setPageNo(pageNo);
|
||||
req.setPageSize(pageSize);
|
||||
|
||||
PageData<DeviceInfo> data = regionChannelService.getDevicePage(req);
|
||||
return ok(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页查询设备列表(POST 完整参数方式)
|
||||
* <p>
|
||||
* 请求体示例:
|
||||
* <pre>
|
||||
* {
|
||||
* "regionIndexCode": "root000000",
|
||||
* "cascadeFlag": 1,
|
||||
* "onlineStatus": "1",
|
||||
* "deviceType": "IPC",
|
||||
* "pageNo": 1,
|
||||
* "pageSize": 50
|
||||
* }
|
||||
* </pre>
|
||||
*/
|
||||
@PostMapping("/page")
|
||||
public ResponseEntity<Map<String, Object>> getDevicePagePost(
|
||||
@RequestBody DevicePageRequest request) {
|
||||
|
||||
PageData<DeviceInfo> data = regionChannelService.getDevicePage(request);
|
||||
return ok(data);
|
||||
}
|
||||
|
||||
// ==================== 设备详情 ====================
|
||||
|
||||
/**
|
||||
* 查询单个设备详情
|
||||
*
|
||||
* @param deviceIndexCode 设备编码
|
||||
*/
|
||||
@GetMapping("/{deviceIndexCode}")
|
||||
public ResponseEntity<Map<String, Object>> getDeviceDetail(
|
||||
@PathVariable String deviceIndexCode) {
|
||||
|
||||
log.info("[Device] 查询设备详情 code={}", deviceIndexCode);
|
||||
List<DeviceInfo> list = regionChannelService.getDeviceDetail(deviceIndexCode);
|
||||
Object data = (list != null && !list.isEmpty()) ? list.get(0) : null;
|
||||
return ok(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量查询设备详情(POST)
|
||||
* <p>
|
||||
* 请求体示例:
|
||||
* <pre>
|
||||
* {
|
||||
* "deviceIndexCodes": ["code1", "code2", "code3"]
|
||||
* }
|
||||
* </pre>
|
||||
*/
|
||||
@PostMapping("/detail")
|
||||
public ResponseEntity<Map<String, Object>> getDeviceDetailBatch(
|
||||
@RequestBody Map<String, List<String>> body) {
|
||||
|
||||
List<String> codes = body.get("deviceIndexCodes");
|
||||
if (codes == null || codes.isEmpty()) {
|
||||
return badRequest("deviceIndexCodes 不能为空");
|
||||
}
|
||||
List<DeviceInfo> list = regionChannelService.getDeviceDetail(codes.toArray(new String[0]));
|
||||
return ok(list);
|
||||
}
|
||||
|
||||
// ==================== 设备下通道 ====================
|
||||
|
||||
/**
|
||||
* 查询设备下属视频通道列表
|
||||
*
|
||||
* @param deviceIndexCode 设备编码
|
||||
* @param pageNo 页码(默认1)
|
||||
* @param pageSize 每页数量(默认100)
|
||||
*/
|
||||
@GetMapping("/{deviceIndexCode}/channels")
|
||||
public ResponseEntity<Map<String, Object>> getChannelsByDevice(
|
||||
@PathVariable String deviceIndexCode,
|
||||
@RequestParam(defaultValue = "1") Integer pageNo,
|
||||
@RequestParam(defaultValue = "100") Integer pageSize) {
|
||||
|
||||
log.info("[Device] 查询设备通道 device={} page={}/{}", deviceIndexCode, pageNo, pageSize);
|
||||
PageData<ChannelInfo> data = regionChannelService.getChannelsByDevice(deviceIndexCode, pageNo, pageSize);
|
||||
return ok(data);
|
||||
}
|
||||
|
||||
// ==================== 统一响应 ====================
|
||||
|
||||
private ResponseEntity<Map<String, Object>> ok(Object data) {
|
||||
Map<String, Object> res = new HashMap<>();
|
||||
res.put("code", 200);
|
||||
res.put("msg", "success");
|
||||
res.put("data", data);
|
||||
return ResponseEntity.ok(res);
|
||||
}
|
||||
|
||||
private ResponseEntity<Map<String, Object>> badRequest(String msg) {
|
||||
Map<String, Object> res = new HashMap<>();
|
||||
res.put("code", 400);
|
||||
res.put("msg", msg);
|
||||
return ResponseEntity.badRequest().body(res);
|
||||
}
|
||||
|
||||
@ExceptionHandler(RuntimeException.class)
|
||||
public ResponseEntity<Map<String, Object>> handleException(RuntimeException e) {
|
||||
log.error("[Device] 接口异常: {}", e.getMessage(), e);
|
||||
Map<String, Object> res = new HashMap<>();
|
||||
res.put("code", 500);
|
||||
res.put("msg", e.getMessage());
|
||||
return ResponseEntity.internalServerError().body(res);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,277 @@
|
|||
package com.hikvision.video.controller;
|
||||
|
||||
import com.hikvision.video.model.*;
|
||||
import com.hikvision.video.service.RegionChannelService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 区域管理 Controller
|
||||
* <p>
|
||||
* GET /api/regions - 分页查询区域列表
|
||||
* GET /api/regions/tree - 查询区域树(一级或全递归)
|
||||
* GET /api/regions/{regionIndexCode}/channels - 查询区域下的视频通道
|
||||
* POST /api/regions/channels - 查询区域通道(完整参数)
|
||||
* GET /api/regions/channel-tree - 区域+通道聚合树
|
||||
* </p>
|
||||
*/
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping("/api/regions")
|
||||
@RequiredArgsConstructor
|
||||
public class RegionController {
|
||||
|
||||
private final RegionChannelService regionChannelService;
|
||||
|
||||
// ==================== 区域接口 ====================
|
||||
|
||||
/**
|
||||
* 分页查询区域列表
|
||||
*
|
||||
* @param parentCode 父区域编码(可选,不传查根节点)
|
||||
* @param cascadeFlag 是否递归子区域:0-否, 1-是(默认0)
|
||||
* @param pageNo 页码(默认1)
|
||||
* @param pageSize 每页数量(默认50)
|
||||
*/
|
||||
@GetMapping
|
||||
public ResponseEntity<Map<String, Object>> getRegionPage(
|
||||
@RequestParam(required = false) String parentCode,
|
||||
@RequestParam(defaultValue = "0") Integer cascadeFlag,
|
||||
@RequestParam(defaultValue = "1") Integer pageNo,
|
||||
@RequestParam(defaultValue = "50") Integer pageSize) {
|
||||
|
||||
RegionTreeRequest req = new RegionTreeRequest();
|
||||
req.setRegionIndexCode(parentCode);
|
||||
req.setCascadeFlag(cascadeFlag);
|
||||
req.setPageNo(pageNo);
|
||||
req.setPageSize(pageSize);
|
||||
|
||||
PageData<RegionInfo> data = regionChannelService.getRegionPage(req);
|
||||
return ok(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* POST 方式查询区域列表(完整参数)
|
||||
*/
|
||||
@PostMapping("/page")
|
||||
public ResponseEntity<Map<String, Object>> getRegionPagePost(
|
||||
@RequestBody RegionTreeRequest request) {
|
||||
|
||||
PageData<RegionInfo> data = regionChannelService.getRegionPage(request);
|
||||
return ok(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询区域树
|
||||
*
|
||||
* @param parentCode 父区域编码(可选,不传从根节点展开)
|
||||
*/
|
||||
@GetMapping("/tree")
|
||||
public ResponseEntity<Map<String, Object>> getRegionTree(
|
||||
@RequestParam(required = false) String parentCode) {
|
||||
|
||||
List<RegionInfo> tree = regionChannelService.getRegionTree(parentCode);
|
||||
return ok(tree);
|
||||
}
|
||||
|
||||
/**
|
||||
* 区域+通道聚合树(递归,支持任意层级)
|
||||
* <p>
|
||||
* 从 rootCode 出发递归展开所有子区域,并可选同步加载每个区域的摄像头列表。
|
||||
* 数据量较大时建议:cascadeCamera=false(前端懒加载通道)+ maxDepth 控制深度。
|
||||
* </p>
|
||||
*
|
||||
* @param rootCode 根区域编码(可选,不传则从平台根节点)
|
||||
* @param cascadeCamera 是否同步加载每区域的摄像头(默认 false,避免超时)
|
||||
* @param maxDepth 最大递归深度(默认 10,最大不超过服务端限制)
|
||||
*/
|
||||
@GetMapping("/channel-tree")
|
||||
public ResponseEntity<Map<String, Object>> getRegionChannelTree(
|
||||
@RequestParam(required = false) String rootCode,
|
||||
@RequestParam(defaultValue = "false") boolean cascadeCamera,
|
||||
@RequestParam(defaultValue = "10") int maxDepth) {
|
||||
|
||||
List<RegionChannelTree> tree = regionChannelService.buildRegionChannelTree(rootCode, cascadeCamera, maxDepth);
|
||||
return ok(tree);
|
||||
}
|
||||
|
||||
// ==================== v2 区域接口 ====================
|
||||
|
||||
/**
|
||||
* v2:获取平台根区域 indexCode
|
||||
* <p>
|
||||
* GET /api/regions/v2/root
|
||||
* </p>
|
||||
*/
|
||||
@GetMapping("/v2/root")
|
||||
public ResponseEntity<Map<String, Object>> getRootRegion() {
|
||||
String indexCode = regionChannelService.getRootRegionIndexCode();
|
||||
return ok(indexCode);
|
||||
}
|
||||
|
||||
/**
|
||||
* v2:分页查询子区域列表
|
||||
* <p>
|
||||
* GET /api/regions/v2/sub?parentIndexCode=xxx&pageNo=1&pageSize=200
|
||||
* </p>
|
||||
*
|
||||
* @param parentIndexCode 父区域编码(必填)
|
||||
* @param pageNo 页码(默认 1)
|
||||
* @param pageSize 每页数量(默认 200)
|
||||
*/
|
||||
@GetMapping("/v2/sub")
|
||||
public ResponseEntity<Map<String, Object>> getSubRegionsV2(
|
||||
@RequestParam String parentIndexCode,
|
||||
@RequestParam(defaultValue = "1") Integer pageNo,
|
||||
@RequestParam(defaultValue = "200") Integer pageSize) {
|
||||
|
||||
if (parentIndexCode == null || parentIndexCode.isBlank()) {
|
||||
return badRequest("parentIndexCode 不能为空");
|
||||
}
|
||||
PageData<SubRegionInfo> data = regionChannelService.getSubRegionsV2(parentIndexCode, pageNo, pageSize);
|
||||
return ok(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* v2:按区域编码集合搜索摄像头
|
||||
* <p>
|
||||
* POST /api/regions/v2/cameras
|
||||
* 请求体示例:
|
||||
* <pre>
|
||||
* {
|
||||
* "regionIndexCodes": ["xxx", "yyy"],
|
||||
* "isSubRegion": false,
|
||||
* "pageNo": 1,
|
||||
* "pageSize": 200
|
||||
* }
|
||||
* </pre>
|
||||
* </p>
|
||||
*/
|
||||
@PostMapping("/v2/cameras")
|
||||
public ResponseEntity<Map<String, Object>> searchCamerasV2(
|
||||
@RequestBody Map<String, Object> body) {
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
List<String> codes = (List<String>) body.getOrDefault("regionIndexCodes", new ArrayList<>());
|
||||
if (codes == null || codes.isEmpty()) {
|
||||
return badRequest("regionIndexCodes 不能为空");
|
||||
}
|
||||
boolean isSubRegion = Boolean.TRUE.equals(body.get("isSubRegion"))
|
||||
|| "1".equals(String.valueOf(body.getOrDefault("isSubRegion", "0")));
|
||||
int pageNo = ((Number) body.getOrDefault("pageNo", 1)).intValue();
|
||||
int pageSize = ((Number) body.getOrDefault("pageSize", 200)).intValue();
|
||||
|
||||
PageData<CameraInfoV2> data = regionChannelService.searchCamerasV2(codes, isSubRegion, pageNo, pageSize);
|
||||
return ok(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* v2:构建完整区域通道聚合树
|
||||
* <p>
|
||||
* GET /api/regions/v2/channel-tree?cascadeCamera=false&maxDepth=5
|
||||
* </p>
|
||||
*
|
||||
* @param cascadeCamera 是否同步加载每区域的摄像头(默认 false,避免超时)
|
||||
* @param maxDepth 最大递归深度(默认 5)
|
||||
*/
|
||||
@GetMapping("/v2/channel-tree")
|
||||
public ResponseEntity<Map<String, Object>> buildV2RegionTree(
|
||||
@RequestParam(defaultValue = "false") boolean cascadeCamera,
|
||||
@RequestParam(defaultValue = "5") int maxDepth) {
|
||||
|
||||
List<RegionV2Tree> tree = regionChannelService.buildV2RegionTree(cascadeCamera, maxDepth);
|
||||
return ok(tree);
|
||||
}
|
||||
|
||||
// ==================== 区域下通道接口 ====================
|
||||
|
||||
/**
|
||||
* 查询指定区域下的视频通道(GET 快捷方式)
|
||||
*
|
||||
* @param regionIndexCode 区域编码
|
||||
* @param cascadeFlag 是否包含子区域:0-否, 1-是(默认1)
|
||||
* @param onlineStatus 在线状态过滤:1-在线, 0-离线(可选)
|
||||
* @param channelType 通道类型过滤(可选)
|
||||
* @param pageNo 页码
|
||||
* @param pageSize 每页数量
|
||||
*/
|
||||
@GetMapping("/{regionIndexCode}/channels")
|
||||
public ResponseEntity<Map<String, Object>> getChannelsByRegion(
|
||||
@PathVariable String regionIndexCode,
|
||||
@RequestParam(defaultValue = "1") Integer cascadeFlag,
|
||||
@RequestParam(required = false) String onlineStatus,
|
||||
@RequestParam(required = false) String channelType,
|
||||
@RequestParam(defaultValue = "1") Integer pageNo,
|
||||
@RequestParam(defaultValue = "100") Integer pageSize) {
|
||||
|
||||
ChannelPageRequest req = new ChannelPageRequest();
|
||||
req.setRegionIndexCode(regionIndexCode);
|
||||
req.setCascadeFlag(cascadeFlag);
|
||||
req.setOnlineStatus(onlineStatus);
|
||||
req.setChannelType(channelType);
|
||||
req.setPageNo(pageNo);
|
||||
req.setPageSize(pageSize);
|
||||
|
||||
PageData<ChannelInfo> data = regionChannelService.getChannelsByRegion(req);
|
||||
return ok(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询区域下视频通道(POST 完整参数方式)
|
||||
* <p>
|
||||
* 请求体示例:
|
||||
* <pre>
|
||||
* {
|
||||
* "regionIndexCode": "root000000",
|
||||
* "cascadeFlag": 1,
|
||||
* "onlineStatus": "1",
|
||||
* "channelType": "1",
|
||||
* "pageNo": 1,
|
||||
* "pageSize": 100
|
||||
* }
|
||||
* </pre>
|
||||
*/
|
||||
@PostMapping("/channels")
|
||||
public ResponseEntity<Map<String, Object>> getChannelsByRegionPost(
|
||||
@RequestBody ChannelPageRequest request) {
|
||||
|
||||
if (request.getRegionIndexCode() == null || request.getRegionIndexCode().isBlank()) {
|
||||
return badRequest("regionIndexCode 不能为空");
|
||||
}
|
||||
PageData<ChannelInfo> data = regionChannelService.getChannelsByRegion(request);
|
||||
return ok(data);
|
||||
}
|
||||
|
||||
// ==================== 统一响应 ====================
|
||||
|
||||
private ResponseEntity<Map<String, Object>> ok(Object data) {
|
||||
Map<String, Object> res = new HashMap<>();
|
||||
res.put("code", 200);
|
||||
res.put("msg", "success");
|
||||
res.put("data", data);
|
||||
return ResponseEntity.ok(res);
|
||||
}
|
||||
|
||||
private ResponseEntity<Map<String, Object>> badRequest(String msg) {
|
||||
Map<String, Object> res = new HashMap<>();
|
||||
res.put("code", 400);
|
||||
res.put("msg", msg);
|
||||
return ResponseEntity.badRequest().body(res);
|
||||
}
|
||||
|
||||
@ExceptionHandler(RuntimeException.class)
|
||||
public ResponseEntity<Map<String, Object>> handleException(RuntimeException e) {
|
||||
log.error("[Region] 接口异常: {}", e.getMessage(), e);
|
||||
Map<String, Object> res = new HashMap<>();
|
||||
res.put("code", 500);
|
||||
res.put("msg", e.getMessage());
|
||||
return ResponseEntity.internalServerError().body(res);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
package com.hikvision.video.controller;
|
||||
|
||||
import com.hikvision.video.sse.SseEmitterManager;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.http.MediaType;
|
||||
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.servlet.mvc.method.annotation.SseEmitter;
|
||||
|
||||
/**
|
||||
* SSE 订阅端点
|
||||
* <p>
|
||||
* 前端通过 {@code GET /api/events} 建立长连接,
|
||||
* 后端每次从海康 API 抓取数据后实时推送 {@code fetch} 事件。
|
||||
* </p>
|
||||
*
|
||||
* <pre>
|
||||
* EventSource 使用示例(前端):
|
||||
* const es = new EventSource('/api/events');
|
||||
* es.addEventListener('fetch', e => console.log(JSON.parse(e.data)));
|
||||
* </pre>
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/api/events")
|
||||
@RequiredArgsConstructor
|
||||
public class SseController {
|
||||
|
||||
private final SseEmitterManager sseEmitterManager;
|
||||
|
||||
/**
|
||||
* 订阅数据抓取事件流
|
||||
*
|
||||
* @return SSE 长连接,推送类型为 {@code fetch} 的事件
|
||||
*/
|
||||
@GetMapping(produces = MediaType.TEXT_EVENT_STREAM_VALUE)
|
||||
public SseEmitter subscribe() {
|
||||
return sseEmitterManager.connect();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,253 @@
|
|||
package com.hikvision.video.controller;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.hikvision.video.model.*;
|
||||
import com.hikvision.video.model.PreviewUrlV2Request;
|
||||
import com.hikvision.video.service.HikvisionVideoService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 视频资源控制器
|
||||
* <p>
|
||||
* 提供的接口:
|
||||
* GET /api/cameras - 分页获取摄像头列表
|
||||
* GET /api/cameras/{cameraIndexCode} - 获取摄像头详情
|
||||
* POST /api/cameras/preview-url - 获取实时预览播放地址
|
||||
* POST /api/cameras/playback-url - 获取录像回放播放地址
|
||||
* </p>
|
||||
*/
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping("/api/cameras")
|
||||
@RequiredArgsConstructor
|
||||
public class VideoController {
|
||||
|
||||
private final HikvisionVideoService hikvisionVideoService;
|
||||
|
||||
// ==================== 摄像头资源列表 ====================
|
||||
|
||||
/**
|
||||
* 分页获取摄像头资源列表
|
||||
*
|
||||
* @param pageNo 页码(默认1)
|
||||
* @param pageSize 每页数量(默认20,最大1000)
|
||||
* @param orgIndexCode 组织编码(可选)
|
||||
* @param onlineStatus 在线状态(可选,1在线/0离线)
|
||||
*/
|
||||
@GetMapping
|
||||
public ResponseEntity<Map<String, Object>> getCameraList(
|
||||
@RequestParam(defaultValue = "1") Integer pageNo,
|
||||
@RequestParam(defaultValue = "20") Integer pageSize,
|
||||
@RequestParam(required = false) String orgIndexCode,
|
||||
@RequestParam(required = false) String onlineStatus) {
|
||||
|
||||
log.info("查询摄像头列表 page={}/{} org={} status={}", pageNo, pageSize, orgIndexCode, onlineStatus);
|
||||
|
||||
CameraPageRequest req = new CameraPageRequest();
|
||||
req.setPageNo(pageNo);
|
||||
req.setPageSize(pageSize);
|
||||
req.setOrgIndexCode(orgIndexCode);
|
||||
req.setOnlineStatus(onlineStatus);
|
||||
|
||||
PageData<CameraInfo> pageData = hikvisionVideoService.getCameraPage(req);
|
||||
return ResponseEntity.ok(success(pageData));
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取摄像头详情
|
||||
*
|
||||
* @param cameraIndexCode 摄像头唯一编码
|
||||
*/
|
||||
@GetMapping("/{cameraIndexCode}")
|
||||
public ResponseEntity<Map<String, Object>> getCameraDetail(
|
||||
@PathVariable String cameraIndexCode) {
|
||||
|
||||
log.info("查询摄像头详情 cameraIndexCode={}", cameraIndexCode);
|
||||
JsonNode detail = hikvisionVideoService.getCameraDetail(cameraIndexCode);
|
||||
return ResponseEntity.ok(success(detail));
|
||||
}
|
||||
|
||||
// ==================== 播放地址 ====================
|
||||
|
||||
/**
|
||||
* 获取实时预览播放地址
|
||||
* <p>
|
||||
* 请求体示例:
|
||||
* <pre>
|
||||
* {
|
||||
* "cameraIndexCode": "xxx",
|
||||
* "streamType": 0, // 0-主码流 1-子码流
|
||||
* "protocol": 1, // 1-RTSP 2-RTMP 3-HLS 4-FLV
|
||||
* "transmode": 1 // 0-UDP 1-TCP
|
||||
* }
|
||||
* </pre>
|
||||
*/
|
||||
@PostMapping("/preview-url")
|
||||
public ResponseEntity<Map<String, Object>> getPreviewUrl(
|
||||
@RequestBody PreviewUrlRequest request) {
|
||||
|
||||
log.info("获取预览地址 cameraIndexCode={}", request.getCameraIndexCode());
|
||||
VideoUrlResult result = hikvisionVideoService.getPreviewUrl(request);
|
||||
return ResponseEntity.ok(success(result));
|
||||
}
|
||||
|
||||
/**
|
||||
* 快捷获取预览地址(GET方式,使用默认参数)
|
||||
*
|
||||
* @param cameraIndexCode 摄像头编码
|
||||
* @param protocol 协议:1-RTSP(默认), 2-RTMP, 3-HLS, 4-FLV
|
||||
*/
|
||||
@GetMapping("/{cameraIndexCode}/preview-url")
|
||||
public ResponseEntity<Map<String, Object>> getPreviewUrlSimple(
|
||||
@PathVariable String cameraIndexCode,
|
||||
@RequestParam(defaultValue = "1") Integer protocol) {
|
||||
|
||||
PreviewUrlRequest req = new PreviewUrlRequest();
|
||||
req.setCameraIndexCode(cameraIndexCode);
|
||||
req.setProtocol(protocol);
|
||||
req.setStreamType(0);
|
||||
req.setTransmode(1);
|
||||
|
||||
VideoUrlResult result = hikvisionVideoService.getPreviewUrl(req);
|
||||
return ResponseEntity.ok(success(result));
|
||||
}
|
||||
|
||||
/**
|
||||
* v2:获取实时视频预览地址
|
||||
* <p>
|
||||
* GET /api/cameras/{cameraIndexCode}/preview-url/v2?protocol=rtsp&streamType=0&transmode=1
|
||||
* </p>
|
||||
*
|
||||
* @param cameraIndexCode 监控点编码
|
||||
* @param protocol 协议:rtsp(默认)/ rtmp / hls / flv / ws / wss
|
||||
* @param streamType 码流:0-主码流(默认), 1-子码流
|
||||
* @param transmode 传输:0-UDP, 1-TCP(默认)
|
||||
*/
|
||||
@GetMapping("/{cameraIndexCode}/preview-url/v2")
|
||||
public ResponseEntity<Map<String, Object>> getPreviewUrlV2(
|
||||
@PathVariable String cameraIndexCode,
|
||||
@RequestParam(defaultValue = "rtsp") String protocol,
|
||||
@RequestParam(defaultValue = "0") Integer streamType,
|
||||
@RequestParam(defaultValue = "1") Integer transmode) {
|
||||
|
||||
log.info("[v2] 获取预览地址 cameraIndexCode={} protocol={}", cameraIndexCode, protocol);
|
||||
VideoUrlResult result = hikvisionVideoService.getPreviewUrlV2(
|
||||
cameraIndexCode, streamType, protocol, transmode);
|
||||
return ResponseEntity.ok(success(result));
|
||||
}
|
||||
|
||||
/**
|
||||
* v2:获取实时视频预览地址(POST 完整参数方式)
|
||||
* <p>
|
||||
* POST /api/cameras/preview-url/v2
|
||||
* 请求体示例:
|
||||
* <pre>
|
||||
* {
|
||||
* "cameraIndexCode": "xxx",
|
||||
* "streamType": 0,
|
||||
* "protocol": "rtsp",
|
||||
* "transmode": 1,
|
||||
* "expand": "transcode=0",
|
||||
* "streamform": "ps"
|
||||
* }
|
||||
* </pre>
|
||||
*/
|
||||
@PostMapping("/preview-url/v2")
|
||||
public ResponseEntity<Map<String, Object>> getPreviewUrlV2Post(
|
||||
@RequestBody PreviewUrlV2Request request) {
|
||||
|
||||
log.info("[v2] 获取预览地址(POST) cameraIndexCode={}", request.getCameraIndexCode());
|
||||
VideoUrlResult result = hikvisionVideoService.getPreviewUrlV2(
|
||||
request.getCameraIndexCode(),
|
||||
request.getStreamType(),
|
||||
request.getProtocol(),
|
||||
request.getTransmode());
|
||||
return ResponseEntity.ok(success(result));
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取录像回放播放地址(POST 完整参数)
|
||||
* <p>
|
||||
* 请求体示例:
|
||||
* <pre>
|
||||
* {
|
||||
* "cameraIndexCode": "xxx",
|
||||
* "beginTime": "2024-01-01T00:00:00.000+08:00",
|
||||
* "endTime": "2024-01-01T01:00:00.000+08:00",
|
||||
* "recordLocation": "0",
|
||||
* "protocol": "rtsp",
|
||||
* "transmode": 0
|
||||
* }
|
||||
* </pre>
|
||||
*/
|
||||
@PostMapping("/playback-url")
|
||||
public ResponseEntity<Map<String, Object>> getPlaybackUrl(
|
||||
@RequestBody PlaybackUrlRequest request) {
|
||||
|
||||
log.info("[v2] 获取回放地址 cameraIndexCode={} beginTime={}",
|
||||
request.getCameraIndexCode(), request.getBeginTime());
|
||||
|
||||
VideoUrlResult result = hikvisionVideoService.getPlaybackUrl(request);
|
||||
return ResponseEntity.ok(success(result));
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取录像回放播放地址(GET 快捷方式,供前端直接调用)
|
||||
* <p>
|
||||
* GET /api/cameras/{cameraIndexCode}/playback-url
|
||||
* ?beginTime=2024-01-01T00:00:00.000%2B08:00
|
||||
* &endTime=2024-01-01T01:00:00.000%2B08:00
|
||||
* &recordLocation=0
|
||||
* </p>
|
||||
*/
|
||||
@GetMapping("/{cameraIndexCode}/playback-url")
|
||||
public ResponseEntity<Map<String, Object>> getPlaybackUrlSimple(
|
||||
@PathVariable String cameraIndexCode,
|
||||
@RequestParam String beginTime,
|
||||
@RequestParam String endTime,
|
||||
@RequestParam(defaultValue = "0") String recordLocation,
|
||||
@RequestParam(defaultValue = "rtsp") String protocol,
|
||||
@RequestParam(defaultValue = "0") Integer transmode) {
|
||||
|
||||
log.info("[v2] GET 回放地址 cameraIndexCode={} begin={} end={}",
|
||||
cameraIndexCode, beginTime, endTime);
|
||||
|
||||
PlaybackUrlRequest req = new PlaybackUrlRequest();
|
||||
req.setCameraIndexCode(cameraIndexCode);
|
||||
req.setBeginTime(beginTime);
|
||||
req.setEndTime(endTime);
|
||||
req.setRecordLocation(recordLocation);
|
||||
req.setProtocol(protocol);
|
||||
req.setTransmode(transmode);
|
||||
|
||||
VideoUrlResult result = hikvisionVideoService.getPlaybackUrl(req);
|
||||
return ResponseEntity.ok(success(result));
|
||||
}
|
||||
|
||||
// ==================== 统一响应格式 ====================
|
||||
|
||||
private Map<String, Object> success(Object data) {
|
||||
Map<String, Object> res = new HashMap<>();
|
||||
res.put("code", 200);
|
||||
res.put("msg", "success");
|
||||
res.put("data", data);
|
||||
return res;
|
||||
}
|
||||
|
||||
// ==================== 全局异常处理 ====================
|
||||
|
||||
@ExceptionHandler(RuntimeException.class)
|
||||
public ResponseEntity<Map<String, Object>> handleException(RuntimeException e) {
|
||||
log.error("API异常: {}", e.getMessage(), e);
|
||||
Map<String, Object> res = new HashMap<>();
|
||||
res.put("code", 500);
|
||||
res.put("msg", e.getMessage());
|
||||
return ResponseEntity.internalServerError().body(res);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
package com.hikvision.video.model;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonAlias;
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 摄像头信息
|
||||
* <p>
|
||||
* 兼容 v1(cameraIndexCode / cameraName)与 v2(indexCode / name)字段名差异。
|
||||
* </p>
|
||||
*/
|
||||
@Data
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public class CameraInfo {
|
||||
/** 摄像头唯一编码(v1: cameraIndexCode, v2 可能返回 indexCode) */
|
||||
@JsonAlias("indexCode")
|
||||
private String cameraIndexCode;
|
||||
/** 摄像头名称(v1: cameraName, v2 可能返回 name) */
|
||||
@JsonAlias("name")
|
||||
private String cameraName;
|
||||
/** 摄像头类型:1-枪机, 2-半球, 3-球机, 4-云台 */
|
||||
private String cameraType;
|
||||
/** 摄像头类型描述 */
|
||||
private String cameraTypeName;
|
||||
/** 在线状态:0-离线, 1-在线 */
|
||||
private String onlineStatus;
|
||||
/** 设备序列号 */
|
||||
private String deviceSerial;
|
||||
/** 所属组织编号 */
|
||||
private String regionIndexCode;
|
||||
/** 所属组织名称 */
|
||||
private String regionName;
|
||||
/** 安装地址 */
|
||||
private String installPlace;
|
||||
/** 能力集(录像、预览等)*/
|
||||
private String capability;
|
||||
/** 像素 */
|
||||
private String pixel;
|
||||
/** 经度 */
|
||||
private String longitude;
|
||||
/** 纬度 */
|
||||
private String latitude;
|
||||
}
|
||||
|
|
@ -0,0 +1,80 @@
|
|||
package com.hikvision.video.model;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonAlias;
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* v2 摄像头搜索结果
|
||||
* <p>
|
||||
* 对应接口:POST /artemis/api/resource/v2/camera/search
|
||||
* 实际返回字段映射:
|
||||
* indexCode → cameraIndexCode
|
||||
* name → cameraName
|
||||
* channelType → cameraType
|
||||
* installLocation → installPlace
|
||||
* chanNum → channelNo
|
||||
* dacIndexCode → deviceIndexCode
|
||||
* </p>
|
||||
*/
|
||||
@Data
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public class CameraInfoV2 {
|
||||
|
||||
/** 监控点唯一编码(v2 返回 indexCode) */
|
||||
@JsonAlias("indexCode")
|
||||
private String cameraIndexCode;
|
||||
|
||||
/** 监控点名称(v2 返回 name) */
|
||||
@JsonAlias("name")
|
||||
private String cameraName;
|
||||
|
||||
/** 通道类型(v2 返回 channelType,如 "analog") */
|
||||
@JsonAlias("channelType")
|
||||
private String cameraType;
|
||||
|
||||
/** 监控点类型描述 */
|
||||
private String cameraTypeName;
|
||||
|
||||
/** 在线状态:0-离线, 1-在线(v2 可能不返回此字段) */
|
||||
private String onlineStatus;
|
||||
|
||||
/** 所属区域编码 */
|
||||
private String regionIndexCode;
|
||||
|
||||
/** 所属区域名称 */
|
||||
private String regionName;
|
||||
|
||||
/** 区域路径名称 */
|
||||
private String regionPathName;
|
||||
|
||||
/** 设备编码(v2 返回 dacIndexCode) */
|
||||
@JsonAlias("dacIndexCode")
|
||||
private String deviceSerial;
|
||||
|
||||
/** 安装地点(v2 返回 installLocation) */
|
||||
@JsonAlias("installLocation")
|
||||
private String installPlace;
|
||||
|
||||
/** 能力集 */
|
||||
private String capability;
|
||||
|
||||
/** 通道号(v2 返回 chanNum) */
|
||||
@JsonAlias("chanNum")
|
||||
private Integer channelNo;
|
||||
|
||||
/** 经度 */
|
||||
private String longitude;
|
||||
|
||||
/** 纬度 */
|
||||
private String latitude;
|
||||
|
||||
/** 像素 */
|
||||
private String pixel;
|
||||
|
||||
/** 创建时间 */
|
||||
private String createTime;
|
||||
|
||||
/** 更新时间 */
|
||||
private String updateTime;
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
package com.hikvision.video.model;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 摄像头分页请求
|
||||
*/
|
||||
@Data
|
||||
public class CameraPageRequest {
|
||||
/** 页码,从1开始 */
|
||||
private Integer pageNo = 1;
|
||||
/** 每页数量,最大1000 */
|
||||
private Integer pageSize = 100;
|
||||
/** 组织编号(可选,按组织筛选) */
|
||||
private String orgIndexCode;
|
||||
/** 是否在线:1在线 0离线,不传查全部 */
|
||||
private String onlineStatus;
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
package com.hikvision.video.model;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 抓图结果
|
||||
*/
|
||||
@Data
|
||||
public class CaptureResult {
|
||||
|
||||
/** 摄像头编码 */
|
||||
private String cameraIndexCode;
|
||||
|
||||
/** 摄像头名称(可选,由调用方传入) */
|
||||
private String cameraName;
|
||||
|
||||
/** 本地保存文件名(如 abc123_20260523_173045.jpg) */
|
||||
private String filename;
|
||||
|
||||
/** 本地访问 URL(如 /api/captures/image/abc123_20260523_173045.jpg) */
|
||||
private String localUrl;
|
||||
|
||||
/** 抓图时间(ISO8601) */
|
||||
private String captureTime;
|
||||
|
||||
/** 文件大小(字节) */
|
||||
private long fileSizeBytes;
|
||||
|
||||
/** 海康返回的原始抓图 URL(临时,不保证长期有效) */
|
||||
private String sourceUrl;
|
||||
}
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
package com.hikvision.video.model;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 定时抓图任务配置
|
||||
*/
|
||||
@Data
|
||||
public class CaptureTask {
|
||||
|
||||
/** 任务唯一ID(UUID) */
|
||||
private String id;
|
||||
|
||||
/** 监控点编码 */
|
||||
private String cameraIndexCode;
|
||||
|
||||
/** 监控点名称(展示用) */
|
||||
private String cameraName;
|
||||
|
||||
/**
|
||||
* 抓图间隔(秒),最小 10
|
||||
*/
|
||||
private int intervalSeconds = 60;
|
||||
|
||||
/**
|
||||
* 最大抓图次数,0 = 无限
|
||||
*/
|
||||
private int maxCaptures = 0;
|
||||
|
||||
/**
|
||||
* 任务状态
|
||||
* STOPPED - 已停止 / 未启动
|
||||
* RUNNING - 运行中
|
||||
* ERROR - 上次执行出错(仍在运行)
|
||||
*/
|
||||
private String status = "STOPPED";
|
||||
|
||||
/** 任务创建时间 */
|
||||
private String createTime;
|
||||
|
||||
/** 最近一次抓图时间 */
|
||||
private String lastCaptureTime;
|
||||
|
||||
/** 累计抓图次数 */
|
||||
private int captureCount = 0;
|
||||
|
||||
/** 最近一次错误信息 */
|
||||
private String lastError;
|
||||
|
||||
/** 最近一次成功抓图的本地 URL */
|
||||
private String lastCaptureUrl;
|
||||
}
|
||||
|
|
@ -0,0 +1,82 @@
|
|||
package com.hikvision.video.model;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonAlias;
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 视频通道信息
|
||||
* <p>
|
||||
* 兼容 v2 实际返回字段:
|
||||
* indexCode → cameraIndexCode
|
||||
* name → cameraName
|
||||
* installLocation → installPlace
|
||||
* channelType → cameraType(v2 返回字符串如 "analog")
|
||||
* </p>
|
||||
*/
|
||||
@Data
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public class ChannelInfo {
|
||||
/** 通道唯一编码(v2 返回 indexCode) */
|
||||
@JsonAlias("indexCode")
|
||||
private String cameraIndexCode;
|
||||
|
||||
/** 通道名称(v2 返回 name) */
|
||||
@JsonAlias("name")
|
||||
private String cameraName;
|
||||
|
||||
/** 通道类型(v2 返回 channelType,如 "analog") */
|
||||
@JsonAlias("channelType")
|
||||
private String cameraType;
|
||||
|
||||
/** 通道类型名称 */
|
||||
private String cameraTypeName;
|
||||
|
||||
/** 在线状态:0-离线, 1-在线(v2 可能不返回此字段) */
|
||||
private String onlineStatus;
|
||||
|
||||
/** 所属设备序列号 */
|
||||
private String deviceSerial;
|
||||
|
||||
/** 设备编码(v2 返回 dacIndexCode) */
|
||||
@JsonAlias("dacIndexCode")
|
||||
private String deviceIndexCode;
|
||||
|
||||
/** 设备名称 */
|
||||
private String deviceName;
|
||||
|
||||
/** 通道序号 */
|
||||
@JsonAlias("chanNum")
|
||||
private Integer channelNo;
|
||||
|
||||
/** 所属区域编码 */
|
||||
private String regionIndexCode;
|
||||
|
||||
/** 所属区域名称 */
|
||||
private String regionName;
|
||||
|
||||
/** 区域路径名称(如 贵州消防总队/黔西南消防支队/...) */
|
||||
private String regionPathName;
|
||||
|
||||
/** 安装地址(v2 返回 installLocation) */
|
||||
@JsonAlias("installLocation")
|
||||
private String installPlace;
|
||||
|
||||
/** 能力集 */
|
||||
private String capability;
|
||||
|
||||
/** 分辨率 / 像素 */
|
||||
private String pixel;
|
||||
|
||||
/** 经度 */
|
||||
private String longitude;
|
||||
|
||||
/** 纬度 */
|
||||
private String latitude;
|
||||
|
||||
/** 创建时间 */
|
||||
private String createTime;
|
||||
|
||||
/** 更新时间 */
|
||||
private String updateTime;
|
||||
}
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
package com.hikvision.video.model;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 视频通道分页查询请求(按区域)
|
||||
*/
|
||||
@Data
|
||||
public class ChannelPageRequest {
|
||||
/**
|
||||
* 区域编码(必填,查询该区域下的通道)
|
||||
*/
|
||||
private String regionIndexCode;
|
||||
/**
|
||||
* 是否包含子区域:0-否, 1-是
|
||||
*/
|
||||
private Integer cascadeFlag = 1;
|
||||
/** 页码,从1开始 */
|
||||
private Integer pageNo = 1;
|
||||
/** 每页数量,最大1000 */
|
||||
private Integer pageSize = 100;
|
||||
/**
|
||||
* 通道类型过滤(可选):
|
||||
* 1 - 枪机
|
||||
* 2 - 半球
|
||||
* 3 - 球机
|
||||
* 4 - 云台
|
||||
*/
|
||||
private String channelType;
|
||||
/**
|
||||
* 在线状态过滤(可选):0-离线, 1-在线
|
||||
*/
|
||||
private String onlineStatus;
|
||||
}
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
package com.hikvision.video.model;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 设备信息
|
||||
*/
|
||||
@Data
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public class DeviceInfo {
|
||||
/** 设备编码 */
|
||||
private String deviceIndexCode;
|
||||
/** 设备名称 */
|
||||
private String deviceName;
|
||||
/** 设备序列号 */
|
||||
private String deviceSerial;
|
||||
/** 设备类型 */
|
||||
private String deviceType;
|
||||
/** 设备类型名称 */
|
||||
private String deviceTypeName;
|
||||
/** 在线状态:0-离线, 1-在线 */
|
||||
private String onlineStatus;
|
||||
/** IP地址 */
|
||||
private String ip;
|
||||
/** 端口 */
|
||||
private Integer port;
|
||||
/** 所属区域编码 */
|
||||
private String regionIndexCode;
|
||||
/** 所属区域名称 */
|
||||
private String regionName;
|
||||
/** 固件版本 */
|
||||
private String firmware;
|
||||
/** 视频通道数 */
|
||||
private Integer channelNum;
|
||||
/** 创建时间 */
|
||||
private String createTime;
|
||||
/** 更新时间 */
|
||||
private String updateTime;
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
package com.hikvision.video.model;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 设备分页查询请求
|
||||
*/
|
||||
@Data
|
||||
public class DevicePageRequest {
|
||||
/** 区域编码(可选,按区域过滤) */
|
||||
private String regionIndexCode;
|
||||
/** 是否包含子区域:0-否, 1-是 */
|
||||
private Integer cascadeFlag = 1;
|
||||
/** 页码 */
|
||||
private Integer pageNo = 1;
|
||||
/** 每页数量 */
|
||||
private Integer pageSize = 100;
|
||||
/**
|
||||
* 设备在线状态(可选):0-离线, 1-在线
|
||||
*/
|
||||
private String onlineStatus;
|
||||
/**
|
||||
* 设备类型(可选):
|
||||
* IPC - 网络摄像机
|
||||
* DVR - 硬盘录像机
|
||||
* NVR - 网络录像机
|
||||
* IPD - IP对讲
|
||||
*/
|
||||
private String deviceType;
|
||||
}
|
||||
|
|
@ -0,0 +1,82 @@
|
|||
package com.hikvision.video.model;
|
||||
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
|
||||
/**
|
||||
* SSE 推送事件消息体
|
||||
* <p>
|
||||
* 每次向海康 API 抓取数据后,由 Service 构造并通过 SseEmitterManager 推送给前端。
|
||||
* </p>
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
public class FetchEvent {
|
||||
|
||||
/** 事件类型,对应具体的抓取动作 */
|
||||
public enum Type {
|
||||
REGION_PAGE, // 区域分页查询
|
||||
CHANNEL_PAGE, // 通道分页查询
|
||||
REGION_TREE, // 区域树构建(递归节点)
|
||||
CAMERA_PAGE, // 摄像头分页查询
|
||||
PREVIEW_URL, // 获取预览地址
|
||||
PLAYBACK_URL, // 获取回放地址
|
||||
DEVICE_PAGE, // 设备分页查询
|
||||
WARN, // 警告(如某区域摄像头获取失败)
|
||||
ERROR // 错误
|
||||
}
|
||||
|
||||
/** 日志级别 */
|
||||
public enum Level { INFO, WARN, ERROR }
|
||||
|
||||
/** 事件类型 */
|
||||
private Type type;
|
||||
|
||||
/** 日志级别 */
|
||||
private Level level;
|
||||
|
||||
/** 来源(类名简写) */
|
||||
private String source;
|
||||
|
||||
/** 人读消息 */
|
||||
private String message;
|
||||
|
||||
/** 事件时间(ISO 格式字符串,方便 JSON 序列化) */
|
||||
private String time;
|
||||
|
||||
/** 可选摘要数据(如查询结果条数、摄像头编码等) */
|
||||
private Object summary;
|
||||
|
||||
// -------- 快捷工厂方法 --------
|
||||
|
||||
private static final DateTimeFormatter FMT =
|
||||
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
|
||||
|
||||
public static FetchEvent info(Type type, String source, String message, Object summary) {
|
||||
return FetchEvent.builder()
|
||||
.type(type).level(Level.INFO)
|
||||
.source(source).message(message)
|
||||
.time(LocalDateTime.now().format(FMT))
|
||||
.summary(summary)
|
||||
.build();
|
||||
}
|
||||
|
||||
public static FetchEvent warn(String source, String message) {
|
||||
return FetchEvent.builder()
|
||||
.type(Type.WARN).level(Level.WARN)
|
||||
.source(source).message(message)
|
||||
.time(LocalDateTime.now().format(FMT))
|
||||
.build();
|
||||
}
|
||||
|
||||
public static FetchEvent error(String source, String message) {
|
||||
return FetchEvent.builder()
|
||||
.type(Type.ERROR).level(Level.ERROR)
|
||||
.source(source).message(message)
|
||||
.time(LocalDateTime.now().format(FMT))
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
package com.hikvision.video.model;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 海康API通用响应包装
|
||||
*/
|
||||
@Data
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
class HikBaseResponse<T> {
|
||||
/** 响应码,"0"表示成功 */
|
||||
private String code;
|
||||
/** 响应消息 */
|
||||
private String msg;
|
||||
/** 数据 */
|
||||
private T data;
|
||||
|
||||
public boolean isSuccess() {
|
||||
return "0".equals(code);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
package com.hikvision.video.model;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 分页数据
|
||||
*/
|
||||
@Data
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public class PageData<T> {
|
||||
private Integer total;
|
||||
private Integer pageNo;
|
||||
private Integer pageSize;
|
||||
private Integer totalPage;
|
||||
private List<T> list;
|
||||
}
|
||||
|
|
@ -0,0 +1,65 @@
|
|||
package com.hikvision.video.model;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 录像回放取流请求(v2)
|
||||
* <p>
|
||||
* 对应接口:POST /artemis/api/video/v2/cameras/playbackURLs
|
||||
* </p>
|
||||
*/
|
||||
@Data
|
||||
public class PlaybackUrlRequest {
|
||||
|
||||
/** 监控点编码(必填) */
|
||||
private String cameraIndexCode;
|
||||
|
||||
/**
|
||||
* 回放开始时间,ISO 8601 格式(必填)
|
||||
* 示例:2024-01-01T00:00:00.000+08:00
|
||||
*/
|
||||
private String beginTime;
|
||||
|
||||
/**
|
||||
* 回放结束时间,ISO 8601 格式(必填)
|
||||
*/
|
||||
private String endTime;
|
||||
|
||||
/**
|
||||
* 存储位置(必填)
|
||||
* "0" - 中心存储(默认)
|
||||
* "1" - 设备存储
|
||||
*/
|
||||
private String recordLocation = "0";
|
||||
|
||||
/**
|
||||
* 协议类型(v2 用字符串)
|
||||
* "rtsp"(默认)/ "rtmp" / "hls" / "flv"
|
||||
*/
|
||||
private String protocol = "rtsp";
|
||||
|
||||
/**
|
||||
* 传输协议:0-UDP(默认), 1-TCP
|
||||
*/
|
||||
private Integer transmode = 0;
|
||||
|
||||
/**
|
||||
* 扩展字段,例如 "streamform=rtp"(可选)
|
||||
*/
|
||||
private String expand;
|
||||
|
||||
/**
|
||||
* 流封装格式:"ps"(默认)/ "rtp"(可选)
|
||||
*/
|
||||
private String streamform = "ps";
|
||||
|
||||
/**
|
||||
* 锁定类型:0-不锁定(默认)(可选)
|
||||
*/
|
||||
private Integer lockType = 0;
|
||||
|
||||
/**
|
||||
* 唯一流标识(可选,不传由平台生成)
|
||||
*/
|
||||
private String uuid;
|
||||
}
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
package com.hikvision.video.model;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 预览取流请求(实时视频)
|
||||
*/
|
||||
@Data
|
||||
public class PreviewUrlRequest {
|
||||
/** 摄像头唯一编码(必填) */
|
||||
private String cameraIndexCode;
|
||||
/**
|
||||
* 流类型:0-主码流, 1-子码流, 2-第三码流
|
||||
* 默认主码流
|
||||
*/
|
||||
private Integer streamType = 0;
|
||||
/**
|
||||
* 协议类型:
|
||||
* 1 - RTSP
|
||||
* 2 - RTMP
|
||||
* 3 - HLS
|
||||
* 4 - FLV(HTTP-FLV)
|
||||
* 默认RTSP
|
||||
*/
|
||||
private Integer protocol = 1;
|
||||
/**
|
||||
* 传输协议:0-UDP, 1-TCP (仅RTSP有效)
|
||||
*/
|
||||
private Integer transmode = 1;
|
||||
/**
|
||||
* 扩展能力(可选):
|
||||
* 0 - 普通取流
|
||||
* 1 - 国标取流
|
||||
*/
|
||||
private Integer expand;
|
||||
}
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
package com.hikvision.video.model;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* v2 实时预览取流请求
|
||||
* <p>
|
||||
* 对应接口:POST /artemis/api/video/v2/cameras/prev
|
||||
* </p>
|
||||
*/
|
||||
@Data
|
||||
public class PreviewUrlV2Request {
|
||||
|
||||
/** 监控点编码(必填) */
|
||||
private String cameraIndexCode;
|
||||
|
||||
/**
|
||||
* 码流类型:0-主码流(默认), 1-子码流, 2-第三码流
|
||||
*/
|
||||
private Integer streamType = 0;
|
||||
|
||||
/**
|
||||
* 协议类型(必填):
|
||||
* "rtsp" / "rtmp" / "hls" / "flv" / "ws" / "wss"
|
||||
*/
|
||||
private String protocol = "rtsp";
|
||||
|
||||
/**
|
||||
* 传输协议:0-UDP, 1-TCP(默认)
|
||||
*/
|
||||
private Integer transmode = 1;
|
||||
|
||||
/**
|
||||
* 扩展参数,示例:"transcode=0"
|
||||
*/
|
||||
private String expand;
|
||||
|
||||
/**
|
||||
* 流媒体封装格式:"ps"(默认)/ "ts" / "es"
|
||||
*/
|
||||
private String streamform = "ps";
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
package com.hikvision.video.model;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 区域通道树节点(用于前端树形展示)
|
||||
*/
|
||||
@Data
|
||||
public class RegionChannelTree {
|
||||
/** 区域编码 */
|
||||
private String regionIndexCode;
|
||||
/** 区域名称 */
|
||||
private String regionName;
|
||||
/** 区域层级 */
|
||||
private Integer regionLevel;
|
||||
/** 该区域下的摄像头通道列表 */
|
||||
private List<ChannelInfo> channels;
|
||||
/** 子区域节点(递归) */
|
||||
private List<RegionChannelTree> children;
|
||||
|
||||
public RegionChannelTree() {}
|
||||
|
||||
public RegionChannelTree(RegionInfo region) {
|
||||
this.regionIndexCode = region.getRegionIndexCode();
|
||||
this.regionName = region.getRegionName();
|
||||
this.regionLevel = region.getRegionLevel();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
package com.hikvision.video.model;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonAlias;
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 区域信息
|
||||
* <p>
|
||||
* 同时兼容 v1 字段名(regionIndexCode / regionName)和
|
||||
* v2 字段名(indexCode / name),通过 {@code @JsonAlias} 实现。
|
||||
* </p>
|
||||
*/
|
||||
@Data
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public class RegionInfo {
|
||||
/** 区域唯一编码(v1: regionIndexCode, v2: indexCode) */
|
||||
@JsonAlias("indexCode")
|
||||
private String regionIndexCode;
|
||||
/** 区域名称(v1: regionName, v2: name) */
|
||||
@JsonAlias("name")
|
||||
private String regionName;
|
||||
/** 父级区域编码 */
|
||||
private String parentIndexCode;
|
||||
/** 区域层级(从0开始) */
|
||||
private Integer regionLevel;
|
||||
/** 区域类型:1-平台, 2-分组 */
|
||||
private String regionType;
|
||||
/** 排序号 */
|
||||
private Integer sort;
|
||||
/** 创建时间 */
|
||||
private String createTime;
|
||||
/** 更新时间 */
|
||||
private String updateTime;
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
package com.hikvision.video.model;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 区域树查询请求
|
||||
*/
|
||||
@Data
|
||||
public class RegionTreeRequest {
|
||||
/**
|
||||
* 父级区域编码,不传则查根节点
|
||||
*/
|
||||
private String regionIndexCode;
|
||||
/**
|
||||
* 是否递归查子节点:0-否(只查一级), 1-是(递归全部)
|
||||
* 默认只查一级,避免数据量过大
|
||||
*/
|
||||
private Integer cascadeFlag = 0;
|
||||
/** 页码 */
|
||||
private Integer pageNo = 1;
|
||||
/** 每页数量 */
|
||||
private Integer pageSize = 100;
|
||||
}
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
package com.hikvision.video.model;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* v2 区域通道聚合树节点
|
||||
* <p>
|
||||
* 基于 /v2/regions/subRegions + /v2/camera/search 构建的递归树结构。
|
||||
* </p>
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
public class RegionV2Tree {
|
||||
|
||||
/** 区域编码 */
|
||||
private String indexCode;
|
||||
|
||||
/** 区域名称 */
|
||||
private String name;
|
||||
|
||||
/** 父区域编码 */
|
||||
private String parentIndexCode;
|
||||
|
||||
/** 区域层级 */
|
||||
private Integer regionLevel;
|
||||
|
||||
/** 该区域下的摄像头列表(cascadeCamera=true 时填充) */
|
||||
private List<CameraInfoV2> cameras;
|
||||
|
||||
/** 子区域列表 */
|
||||
private List<RegionV2Tree> children;
|
||||
|
||||
public RegionV2Tree(SubRegionInfo info) {
|
||||
this.indexCode = info.getIndexCode();
|
||||
this.name = info.getName();
|
||||
this.parentIndexCode = info.getParentIndexCode();
|
||||
this.regionLevel = info.getRegionLevel();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
package com.hikvision.video.model;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 资源搜索请求
|
||||
*/
|
||||
@Data
|
||||
public class ResourceSearchRequest {
|
||||
/** 资源类型:camera-摄像头 */
|
||||
private String resourceType = "camera";
|
||||
/** 关键词搜索 */
|
||||
private String keywords;
|
||||
/** 组织编码(可选) */
|
||||
private String orgIndexCode;
|
||||
private Integer pageNo = 1;
|
||||
private Integer pageSize = 100;
|
||||
}
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
package com.hikvision.video.model;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* v2 子区域信息
|
||||
* <p>
|
||||
* 对应接口:POST /artemis/api/resource/v2/regions/subRegions
|
||||
* </p>
|
||||
*/
|
||||
@Data
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public class SubRegionInfo {
|
||||
|
||||
/** 区域唯一编码 */
|
||||
private String indexCode;
|
||||
|
||||
/** 区域名称 */
|
||||
private String name;
|
||||
|
||||
/** 父区域编码 */
|
||||
private String parentIndexCode;
|
||||
|
||||
/** 区域层级(从 0 开始) */
|
||||
private Integer regionLevel;
|
||||
|
||||
/** 状态:0-正常, 1-删除 */
|
||||
private String status;
|
||||
|
||||
/** 描述 */
|
||||
private String description;
|
||||
|
||||
/** 创建时间 */
|
||||
private String createTime;
|
||||
|
||||
/** 更新时间 */
|
||||
private String updateTime;
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
package com.hikvision.video.model;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 播放地址响应
|
||||
*/
|
||||
@Data
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public class VideoUrlResult {
|
||||
/** 播放地址 */
|
||||
private String url;
|
||||
/** 扩展信息 */
|
||||
private String expireTime;
|
||||
private String token;
|
||||
}
|
||||
|
|
@ -0,0 +1,252 @@
|
|||
package com.hikvision.video.service;
|
||||
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.hikvision.video.model.CaptureTask;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import jakarta.annotation.PreDestroy;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.*;
|
||||
|
||||
/**
|
||||
* 定时抓图任务服务
|
||||
* <p>
|
||||
* 使用 {@link ScheduledExecutorService} 为每个任务独立调度,
|
||||
* 任务配置持久化到 JSON 文件,重启后自动恢复并重新启动运行中的任务。
|
||||
* </p>
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class CaptureTaskService {
|
||||
|
||||
private final HikvisionVideoService hikvisionVideoService;
|
||||
|
||||
@Value("${app.capture.save-path:./captures}")
|
||||
private String capturesSavePath;
|
||||
|
||||
private static final DateTimeFormatter FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
|
||||
|
||||
/** 任务配置存储(内存) */
|
||||
private final Map<String, CaptureTask> taskMap = new ConcurrentHashMap<>();
|
||||
|
||||
/** 运行中的调度 Future */
|
||||
private final Map<String, ScheduledFuture<?>> futureMap = new ConcurrentHashMap<>();
|
||||
|
||||
/** 线程池:最多同时执行 10 个摄像头的抓图任务 */
|
||||
private final ScheduledExecutorService scheduler =
|
||||
Executors.newScheduledThreadPool(10, r -> {
|
||||
Thread t = new Thread(r, "capture-task");
|
||||
t.setDaemon(true);
|
||||
return t;
|
||||
});
|
||||
|
||||
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||
|
||||
/** 持久化文件路径 */
|
||||
private Path tasksFilePath;
|
||||
|
||||
// ==================== 生命周期 ====================
|
||||
|
||||
@PostConstruct
|
||||
public void init() {
|
||||
try {
|
||||
Path dir = Paths.get(capturesSavePath);
|
||||
Files.createDirectories(dir);
|
||||
tasksFilePath = dir.resolve("capture_tasks.json");
|
||||
loadFromDisk();
|
||||
// 自动恢复上次运行中的任务
|
||||
int resumed = 0;
|
||||
for (CaptureTask task : taskMap.values()) {
|
||||
if ("RUNNING".equals(task.getStatus()) || "ERROR".equals(task.getStatus())) {
|
||||
scheduleTask(task);
|
||||
resumed++;
|
||||
}
|
||||
}
|
||||
log.info("[CaptureTask] 初始化完成,共 {} 个任务,恢复 {} 个运行中", taskMap.size(), resumed);
|
||||
} catch (Exception e) {
|
||||
log.error("[CaptureTask] 初始化失败: {}", e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
@PreDestroy
|
||||
public void shutdown() {
|
||||
scheduler.shutdownNow();
|
||||
log.info("[CaptureTask] 调度器已关闭");
|
||||
}
|
||||
|
||||
// ==================== CRUD ====================
|
||||
|
||||
/**
|
||||
* 创建新任务(默认 STOPPED 状态,不自动启动)
|
||||
*/
|
||||
public CaptureTask createTask(String cameraIndexCode, String cameraName,
|
||||
int intervalSeconds, int maxCaptures) {
|
||||
if (cameraIndexCode == null || cameraIndexCode.isBlank()) {
|
||||
throw new IllegalArgumentException("cameraIndexCode 不能为空");
|
||||
}
|
||||
int interval = Math.max(10, intervalSeconds);
|
||||
|
||||
CaptureTask task = new CaptureTask();
|
||||
task.setId(UUID.randomUUID().toString().replace("-", "").substring(0, 12));
|
||||
task.setCameraIndexCode(cameraIndexCode);
|
||||
task.setCameraName(cameraName != null ? cameraName : cameraIndexCode);
|
||||
task.setIntervalSeconds(interval);
|
||||
task.setMaxCaptures(Math.max(0, maxCaptures));
|
||||
task.setStatus("STOPPED");
|
||||
task.setCreateTime(LocalDateTime.now().format(FMT));
|
||||
task.setCaptureCount(0);
|
||||
|
||||
taskMap.put(task.getId(), task);
|
||||
saveToDisk();
|
||||
log.info("[CaptureTask] 创建任务 id={} camera={} interval={}s", task.getId(), cameraIndexCode, interval);
|
||||
return task;
|
||||
}
|
||||
|
||||
/** 获取全部任务列表(按创建时间倒序) */
|
||||
public List<CaptureTask> listTasks() {
|
||||
List<CaptureTask> list = new ArrayList<>(taskMap.values());
|
||||
list.sort(Comparator.comparing(CaptureTask::getCreateTime, Comparator.reverseOrder()));
|
||||
return list;
|
||||
}
|
||||
|
||||
/** 获取单个任务 */
|
||||
public CaptureTask getTask(String id) {
|
||||
CaptureTask task = taskMap.get(id);
|
||||
if (task == null) throw new RuntimeException("任务不存在: " + id);
|
||||
return task;
|
||||
}
|
||||
|
||||
/** 删除任务(先停止) */
|
||||
public void deleteTask(String id) {
|
||||
stopTask(id);
|
||||
taskMap.remove(id);
|
||||
saveToDisk();
|
||||
log.info("[CaptureTask] 删除任务 id={}", id);
|
||||
}
|
||||
|
||||
// ==================== 启动 / 停止 ====================
|
||||
|
||||
/**
|
||||
* 启动任务
|
||||
*/
|
||||
public CaptureTask startTask(String id) {
|
||||
CaptureTask task = getTask(id);
|
||||
if ("RUNNING".equals(task.getStatus()) && futureMap.containsKey(id)) {
|
||||
return task; // 已在运行,幂等
|
||||
}
|
||||
task.setStatus("RUNNING");
|
||||
task.setLastError(null);
|
||||
scheduleTask(task);
|
||||
saveToDisk();
|
||||
log.info("[CaptureTask] 启动任务 id={} camera={} interval={}s",
|
||||
id, task.getCameraIndexCode(), task.getIntervalSeconds());
|
||||
return task;
|
||||
}
|
||||
|
||||
/**
|
||||
* 暂停/停止任务
|
||||
*/
|
||||
public CaptureTask stopTask(String id) {
|
||||
CaptureTask task = taskMap.get(id);
|
||||
if (task == null) return null;
|
||||
|
||||
ScheduledFuture<?> future = futureMap.remove(id);
|
||||
if (future != null) {
|
||||
future.cancel(false);
|
||||
}
|
||||
task.setStatus("STOPPED");
|
||||
saveToDisk();
|
||||
log.info("[CaptureTask] 停止任务 id={}", id);
|
||||
return task;
|
||||
}
|
||||
|
||||
// ==================== 调度核心 ====================
|
||||
|
||||
private void scheduleTask(CaptureTask task) {
|
||||
// 取消旧 future(防止重复调度)
|
||||
ScheduledFuture<?> old = futureMap.remove(task.getId());
|
||||
if (old != null) old.cancel(false);
|
||||
|
||||
long delayMs = task.getIntervalSeconds() * 1000L;
|
||||
|
||||
ScheduledFuture<?> future = scheduler.scheduleAtFixedRate(
|
||||
() -> executeCapture(task.getId()),
|
||||
0, delayMs, TimeUnit.MILLISECONDS);
|
||||
|
||||
futureMap.put(task.getId(), future);
|
||||
}
|
||||
|
||||
private void executeCapture(String taskId) {
|
||||
CaptureTask task = taskMap.get(taskId);
|
||||
if (task == null) return;
|
||||
|
||||
// 检查最大次数
|
||||
if (task.getMaxCaptures() > 0 && task.getCaptureCount() >= task.getMaxCaptures()) {
|
||||
log.info("[CaptureTask] 任务 {} 已达最大次数 {},自动停止", taskId, task.getMaxCaptures());
|
||||
stopTask(taskId);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
log.debug("[CaptureTask] 执行抓图 taskId={} camera={}", taskId, task.getCameraIndexCode());
|
||||
var result = hikvisionVideoService.captureImage(
|
||||
task.getCameraIndexCode(), task.getCameraName());
|
||||
|
||||
task.setLastCaptureTime(LocalDateTime.now().format(FMT));
|
||||
task.setCaptureCount(task.getCaptureCount() + 1);
|
||||
task.setLastCaptureUrl(result.getLocalUrl());
|
||||
task.setLastError(null);
|
||||
if (!"RUNNING".equals(task.getStatus())) {
|
||||
task.setStatus("RUNNING");
|
||||
}
|
||||
log.info("[CaptureTask] taskId={} 抓图成功 第{}次 file={}",
|
||||
taskId, task.getCaptureCount(), result.getFilename());
|
||||
} catch (Exception e) {
|
||||
task.setLastError(e.getMessage());
|
||||
task.setStatus("ERROR");
|
||||
log.warn("[CaptureTask] taskId={} 抓图失败: {}", taskId, e.getMessage());
|
||||
}
|
||||
|
||||
// 每次执行后持久化(异步,允许失败)
|
||||
try { saveToDisk(); } catch (Exception ignored) {}
|
||||
}
|
||||
|
||||
// ==================== 持久化 ====================
|
||||
|
||||
private synchronized void saveToDisk() {
|
||||
if (tasksFilePath == null) return;
|
||||
try {
|
||||
objectMapper.writerWithDefaultPrettyPrinter()
|
||||
.writeValue(tasksFilePath.toFile(), taskMap);
|
||||
} catch (IOException e) {
|
||||
log.warn("[CaptureTask] 持久化失败: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private synchronized void loadFromDisk() {
|
||||
if (tasksFilePath == null || !Files.exists(tasksFilePath)) return;
|
||||
try {
|
||||
Map<String, CaptureTask> loaded = objectMapper.readValue(
|
||||
tasksFilePath.toFile(),
|
||||
new TypeReference<Map<String, CaptureTask>>() {});
|
||||
if (loaded != null) {
|
||||
taskMap.putAll(loaded);
|
||||
}
|
||||
log.info("[CaptureTask] 从磁盘加载 {} 个任务", taskMap.size());
|
||||
} catch (IOException e) {
|
||||
log.warn("[CaptureTask] 读取任务文件失败: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,421 @@
|
|||
package com.hikvision.video.service;
|
||||
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.hikvision.video.config.HikvisionProperties;
|
||||
import com.hikvision.video.model.*;
|
||||
import com.hikvision.video.sse.SseEmitterManager;
|
||||
import com.hikvision.video.util.HikvisionSignUtil;
|
||||
import com.hikvision.video.util.HttpUtil;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 海康威视视频应用服务 API
|
||||
* <p>
|
||||
* 覆盖接口:
|
||||
* 1. 分页获取摄像头资源列表
|
||||
* 2. 获取摄像头详情
|
||||
* 3. 获取实时预览播放地址
|
||||
* 4. 获取录像回放播放地址
|
||||
* </p>
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class HikvisionVideoService {
|
||||
|
||||
// ==================== 接口路径常量 ====================
|
||||
/**
|
||||
* 摄像头资源分页列表(v2 搜索接口,替换不存在的 v1 /cameras/indexPage)
|
||||
*/
|
||||
private static final String API_CAMERA_PAGE = "/artemis/api/resource/v2/camera/search";
|
||||
/** 根区域查询(获取 rootIndexCode 用于无条件摄像头查询) */
|
||||
private static final String API_ROOT_REGION = "/artemis/api/resource/v1/regions/root";
|
||||
/** 摄像头详情 */
|
||||
private static final String API_CAMERA_DETAIL = "/artemis/api/resource/v1/cameras/previewInfos";
|
||||
/** 实时预览取流(v1) */
|
||||
private static final String API_PREVIEW_URL = "/artemis/api/video/v1/cameras/previewURLs";
|
||||
/** 录像回放取流(v2) */
|
||||
private static final String API_PLAYBACK_URL = "/artemis/api/video/v2/cameras/playbackURLs";
|
||||
/** 实时预览取流(v2) */
|
||||
private static final String API_PREVIEW_URL_V2 = "/artemis/api/video/v2/cameras/previewURLs";
|
||||
/** 手动抓图(返回 picUrl) */
|
||||
private static final String API_CAPTURE_URL = "/artemis/api/video/v1/manualCapture";
|
||||
|
||||
private static final String CONTENT_TYPE = "application/json";
|
||||
|
||||
private static final DateTimeFormatter TS_FMT = DateTimeFormatter.ofPattern("yyyyMMdd_HHmmss");
|
||||
|
||||
private final HikvisionProperties properties;
|
||||
private final SseEmitterManager sseEmitterManager;
|
||||
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||
|
||||
@Value("${app.capture.save-path:./captures}")
|
||||
private String capturesSavePath;
|
||||
|
||||
// ==================== 摄像头资源 ====================
|
||||
|
||||
/**
|
||||
* 分页获取摄像头资源列表
|
||||
*
|
||||
* @param request 分页及过滤参数
|
||||
* @return 分页摄像头列表
|
||||
*/
|
||||
public PageData<CameraInfo> getCameraPage(CameraPageRequest request) {
|
||||
String path = API_CAMERA_PAGE;
|
||||
Map<String, String> headers = buildHeaders("POST", path);
|
||||
String url = properties.getHost() + path;
|
||||
|
||||
// v2 camera/search 需要 regionIndexCodes 数组
|
||||
// orgIndexCode 为空时自动获取根区域并查全部摄像头
|
||||
String regionCode = request.getOrgIndexCode();
|
||||
if (regionCode == null || regionCode.isBlank()) {
|
||||
regionCode = fetchRootRegionCode();
|
||||
}
|
||||
Map<String, Object> body = new java.util.LinkedHashMap<>();
|
||||
body.put("pageNo", request.getPageNo());
|
||||
body.put("pageSize", request.getPageSize());
|
||||
body.put("regionIndexCodes", java.util.List.of(regionCode));
|
||||
body.put("isSubRegion", 1); // 含全部子区域
|
||||
if (request.getOnlineStatus() != null) {
|
||||
body.put("onlineStatus", request.getOnlineStatus());
|
||||
}
|
||||
|
||||
String responseStr = HttpUtil.postJson(url, headers, body);
|
||||
PageData<CameraInfo> result = extractData(responseStr, new TypeReference<PageData<CameraInfo>>() {});
|
||||
sseEmitterManager.push(FetchEvent.info(
|
||||
FetchEvent.Type.CAMERA_PAGE, "HikvisionVideoService",
|
||||
String.format("摄像头列表查询完成 page=%d/%d 返回%d条",
|
||||
request.getPageNo(), request.getPageSize(),
|
||||
result.getList() == null ? 0 : result.getList().size()),
|
||||
java.util.Map.of("total", result.getTotal() == null ? 0 : result.getTotal(),
|
||||
"count", result.getList() == null ? 0 : result.getList().size())
|
||||
));
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取摄像头详情(批量,最多1000个)
|
||||
*
|
||||
* @param cameraIndexCodes 摄像头编码列表(逗号分隔)
|
||||
* @return JSON节点(list数组)
|
||||
*/
|
||||
public JsonNode getCameraDetail(String... cameraIndexCodes) {
|
||||
String path = API_CAMERA_DETAIL;
|
||||
Map<String, String> headers = buildHeaders("POST", path);
|
||||
String url = properties.getHost() + path;
|
||||
|
||||
Map<String, Object> body = Map.of("cameraIndexCodes", cameraIndexCodes);
|
||||
String responseStr = HttpUtil.postJson(url, headers, body);
|
||||
return extractRawData(responseStr);
|
||||
}
|
||||
|
||||
// ==================== 预览播放地址 ====================
|
||||
|
||||
/**
|
||||
* 获取实时视频预览播放地址
|
||||
*
|
||||
* @param request 预览请求参数
|
||||
* @return 包含播放地址的结果对象
|
||||
*/
|
||||
public VideoUrlResult getPreviewUrl(PreviewUrlRequest request) {
|
||||
String path = API_PREVIEW_URL;
|
||||
Map<String, String> headers = buildHeaders("POST", path);
|
||||
String url = properties.getHost() + path;
|
||||
|
||||
log.info("[HIK] 获取预览地址, camera={}, protocol={}, stream={}",
|
||||
request.getCameraIndexCode(), request.getProtocol(), request.getStreamType());
|
||||
|
||||
String responseStr = HttpUtil.postJson(url, headers, request);
|
||||
VideoUrlResult result = extractData(responseStr, new TypeReference<VideoUrlResult>() {});
|
||||
sseEmitterManager.push(FetchEvent.info(
|
||||
FetchEvent.Type.PREVIEW_URL, "HikvisionVideoService",
|
||||
"预览地址获取成功 camera=" + request.getCameraIndexCode()
|
||||
+ " protocol=" + request.getProtocol()
|
||||
+ " stream=" + request.getStreamType(),
|
||||
java.util.Map.of("cameraIndexCode", request.getCameraIndexCode(),
|
||||
"protocol", request.getProtocol())
|
||||
));
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取录像回放播放地址
|
||||
*
|
||||
* @param request 回放请求参数
|
||||
* @return 包含播放地址的结果对象
|
||||
*/
|
||||
public VideoUrlResult getPlaybackUrl(PlaybackUrlRequest request) {
|
||||
String path = API_PLAYBACK_URL;
|
||||
Map<String, String> headers = buildHeaders("POST", path);
|
||||
String url = properties.getHost() + path;
|
||||
|
||||
log.info("[HIK-v2] 获取回放地址, camera={}, begin={}, end={}, location={}",
|
||||
request.getCameraIndexCode(), request.getBeginTime(),
|
||||
request.getEndTime(), request.getRecordLocation());
|
||||
|
||||
// v2 回放接口要求手动构建请求体,确保字段名和类型与 API 文档一致
|
||||
java.util.Map<String, Object> body = new java.util.LinkedHashMap<>();
|
||||
body.put("cameraIndexCode", request.getCameraIndexCode());
|
||||
body.put("beginTime", request.getBeginTime());
|
||||
body.put("endTime", request.getEndTime());
|
||||
body.put("recordLocation", request.getRecordLocation() != null ? request.getRecordLocation() : "0");
|
||||
body.put("protocol", request.getProtocol() != null ? request.getProtocol() : "rtsp");
|
||||
body.put("transmode", request.getTransmode() != null ? request.getTransmode() : 0);
|
||||
body.put("streamform", request.getStreamform() != null ? request.getStreamform() : "ps");
|
||||
body.put("lockType", request.getLockType() != null ? request.getLockType() : 0);
|
||||
if (request.getExpand() != null) body.put("expand", request.getExpand());
|
||||
if (request.getUuid() != null) body.put("uuid", request.getUuid());
|
||||
|
||||
String responseStr = HttpUtil.postJson(url, headers, body);
|
||||
log.debug("[HIK-v2] 回放地址原始响应: {}", responseStr.length() > 300 ? responseStr.substring(0, 300) : responseStr);
|
||||
VideoUrlResult result = extractData(responseStr, new TypeReference<VideoUrlResult>() {});
|
||||
sseEmitterManager.push(FetchEvent.info(
|
||||
FetchEvent.Type.PLAYBACK_URL, "HikvisionVideoService",
|
||||
"v2回放地址获取成功 camera=" + request.getCameraIndexCode()
|
||||
+ " begin=" + request.getBeginTime()
|
||||
+ " end=" + request.getEndTime(),
|
||||
java.util.Map.of("cameraIndexCode", request.getCameraIndexCode(),
|
||||
"beginTime", request.getBeginTime(),
|
||||
"endTime", request.getEndTime())
|
||||
));
|
||||
return result;
|
||||
}
|
||||
|
||||
// ==================== v2 预览地址 ====================
|
||||
|
||||
/**
|
||||
* 获取实时视频预览地址(v2,失败自动降级到 v1)
|
||||
* <p>
|
||||
* 优先调用 POST /artemis/api/video/v2/cameras/previewURLs(支持 ws/wss/hls 等协议)。
|
||||
* 若平台不支持 v2(返回 4xx/5xx 或业务错误),自动降级到
|
||||
* POST /artemis/api/video/v1/cameras/previewURLs,并将字符串协议映射为 v1 整数值:
|
||||
* rtsp/ws/wss → 1,rtmp → 2,hls → 3,flv → 4
|
||||
* </p>
|
||||
*
|
||||
* @param cameraIndexCode 监控点编码(必填)
|
||||
* @param streamType 码流类型:0-主码流(默认), 1-子码流
|
||||
* @param protocol 协议:"rtsp"(默认)/ "rtmp" / "hls" / "flv" / "ws" / "wss"
|
||||
* @param transmode 传输方式:0-UDP, 1-TCP(默认)
|
||||
* @return 包含播放地址的结果对象
|
||||
*/
|
||||
public VideoUrlResult getPreviewUrlV2(String cameraIndexCode, Integer streamType,
|
||||
String protocol, Integer transmode) {
|
||||
String effectiveProtocol = (protocol != null && !protocol.isBlank()) ? protocol : "rtsp";
|
||||
int effectiveStream = streamType != null ? streamType : 0;
|
||||
int effectiveTransmode = transmode != null ? transmode : 1;
|
||||
|
||||
// ---- 尝试 v2 ----
|
||||
try {
|
||||
String path = API_PREVIEW_URL_V2;
|
||||
Map<String, String> headers = buildHeaders("POST", path);
|
||||
String url = properties.getHost() + path;
|
||||
|
||||
PreviewUrlV2Request request = new PreviewUrlV2Request();
|
||||
request.setCameraIndexCode(cameraIndexCode);
|
||||
request.setStreamType(effectiveStream);
|
||||
request.setProtocol(effectiveProtocol);
|
||||
request.setTransmode(effectiveTransmode);
|
||||
request.setExpand("transcode=0");
|
||||
request.setStreamform("ps");
|
||||
|
||||
log.info("[HIK-v2] 获取预览地址 v2, camera={}, protocol={}, stream={}",
|
||||
cameraIndexCode, effectiveProtocol, effectiveStream);
|
||||
|
||||
String responseStr = HttpUtil.postJson(url, headers, request);
|
||||
VideoUrlResult result = extractData(responseStr, new TypeReference<VideoUrlResult>() {});
|
||||
sseEmitterManager.push(FetchEvent.info(
|
||||
FetchEvent.Type.PREVIEW_URL, "HikvisionVideoService",
|
||||
"v2预览地址获取成功 camera=" + cameraIndexCode
|
||||
+ " protocol=" + effectiveProtocol
|
||||
+ " stream=" + effectiveStream,
|
||||
java.util.Map.of("cameraIndexCode", cameraIndexCode,
|
||||
"protocol", effectiveProtocol)
|
||||
));
|
||||
return result;
|
||||
|
||||
} catch (RuntimeException v2Ex) {
|
||||
// ---- v2 失败,降级到 v1 ----
|
||||
log.warn("[HIK-v2] v2预览接口调用失败({}),自动降级到 v1", v2Ex.getMessage());
|
||||
|
||||
// 协议字符串 → v1 整数:rtsp/ws/wss=1, rtmp=2, hls=3, flv=4
|
||||
int v1Protocol;
|
||||
switch (effectiveProtocol.toLowerCase()) {
|
||||
case "rtmp": v1Protocol = 2; break;
|
||||
case "hls": v1Protocol = 3; break;
|
||||
case "flv": v1Protocol = 4; break;
|
||||
default: v1Protocol = 1; break; // rtsp / ws / wss → RTSP
|
||||
}
|
||||
|
||||
PreviewUrlRequest v1Req = new PreviewUrlRequest();
|
||||
v1Req.setCameraIndexCode(cameraIndexCode);
|
||||
v1Req.setStreamType(effectiveStream);
|
||||
v1Req.setProtocol(v1Protocol);
|
||||
v1Req.setTransmode(effectiveTransmode);
|
||||
|
||||
log.info("[HIK-v1] 降级预览地址 v1, camera={}, protocol(int)={}, stream={}",
|
||||
cameraIndexCode, v1Protocol, effectiveStream);
|
||||
|
||||
return getPreviewUrl(v1Req);
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 抓图 ====================
|
||||
|
||||
/**
|
||||
* 抓取指定摄像头的实时截图并保存到本地
|
||||
* <p>
|
||||
* 流程:
|
||||
* 1. 调用 POST /artemis/api/video/v1/cameras/captureURL 获取临时截图 URL
|
||||
* 2. 通过 HTTP GET 下载图片字节流
|
||||
* 3. 保存到本地 captures 目录
|
||||
* </p>
|
||||
*
|
||||
* @param cameraIndexCode 监控点编码(必填)
|
||||
* @param cameraName 监控点名称(用于展示,可为null)
|
||||
* @return 抓图结果(含本地访问 URL)
|
||||
*/
|
||||
public CaptureResult captureImage(String cameraIndexCode, String cameraName) {
|
||||
log.info("[HIK] 触发抓图 camera={} name={}", cameraIndexCode, cameraName);
|
||||
|
||||
// 1. 调用海康接口获取截图 URL
|
||||
Map<String, String> headers = buildHeaders("POST", API_CAPTURE_URL);
|
||||
String apiUrl = properties.getHost() + API_CAPTURE_URL;
|
||||
Map<String, Object> body = java.util.Map.of(
|
||||
"cameraIndexCode", cameraIndexCode,
|
||||
"lockType", 0
|
||||
);
|
||||
String responseStr = HttpUtil.postJson(apiUrl, headers, body);
|
||||
log.debug("[HIK] 抓图接口响应: {}", responseStr);
|
||||
|
||||
JsonNode data = extractRawData(responseStr);
|
||||
String sourceUrl = data.path("picUrl").asText(null);
|
||||
if (sourceUrl == null || sourceUrl.isBlank()) {
|
||||
throw new RuntimeException("抓图 URL 为空,请确认摄像头在线且平台支持截图功能");
|
||||
}
|
||||
log.info("[HIK] 获取截图 URL 成功: {}", sourceUrl);
|
||||
|
||||
// 2. 下载图片字节流
|
||||
byte[] imageBytes = HttpUtil.getBytes(sourceUrl);
|
||||
if (imageBytes == null || imageBytes.length == 0) {
|
||||
throw new RuntimeException("下载截图失败:图片内容为空");
|
||||
}
|
||||
|
||||
// 3. 保存到本地
|
||||
String timestamp = LocalDateTime.now().format(TS_FMT);
|
||||
String filename = cameraIndexCode + "_" + timestamp + ".jpg";
|
||||
try {
|
||||
Path dir = Paths.get(capturesSavePath);
|
||||
Files.createDirectories(dir);
|
||||
Path file = dir.resolve(filename);
|
||||
Files.write(file, imageBytes);
|
||||
log.info("[HIK] 截图已保存: {} ({} bytes)", file.toAbsolutePath(), imageBytes.length);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("保存截图失败: " + e.getMessage(), e);
|
||||
}
|
||||
|
||||
// 4. 构建返回结果
|
||||
CaptureResult result = new CaptureResult();
|
||||
result.setCameraIndexCode(cameraIndexCode);
|
||||
result.setCameraName(cameraName != null ? cameraName : cameraIndexCode);
|
||||
result.setFilename(filename);
|
||||
result.setLocalUrl("/api/captures/image/" + filename);
|
||||
result.setCaptureTime(LocalDateTime.now().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME));
|
||||
result.setFileSizeBytes(imageBytes.length);
|
||||
result.setSourceUrl(sourceUrl);
|
||||
|
||||
sseEmitterManager.push(FetchEvent.info(
|
||||
FetchEvent.Type.PREVIEW_URL, "HikvisionVideoService",
|
||||
"抓图成功 camera=" + cameraIndexCode + " file=" + filename + " size=" + imageBytes.length + "B",
|
||||
java.util.Map.of("cameraIndexCode", cameraIndexCode, "filename", filename)
|
||||
));
|
||||
return result;
|
||||
}
|
||||
|
||||
// ==================== 私有方法 ====================
|
||||
|
||||
/**
|
||||
* 获取平台根区域 indexCode(用于无条件摄像头查询)
|
||||
*/
|
||||
private String fetchRootRegionCode() {
|
||||
try {
|
||||
Map<String, String> headers = buildHeaders("POST", API_ROOT_REGION);
|
||||
String url = properties.getHost() + API_ROOT_REGION;
|
||||
String responseStr = HttpUtil.postJson(url, headers, java.util.Map.of("treeCode", "0"));
|
||||
JsonNode data = extractRawData(responseStr);
|
||||
String code = data.path("indexCode").asText(null);
|
||||
if (code == null || code.isBlank()) {
|
||||
throw new RuntimeException("根区域 indexCode 为空");
|
||||
}
|
||||
log.debug("[HIK] 自动获取根区域 indexCode={}", code);
|
||||
return code;
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("获取根区域失败: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 构造鉴权请求头
|
||||
*/
|
||||
private Map<String, String> buildHeaders(String method, String path) {
|
||||
return HikvisionSignUtil.buildAuthHeaders(
|
||||
properties.getAppKey(),
|
||||
properties.getAppSecret(),
|
||||
method,
|
||||
path,
|
||||
CONTENT_TYPE
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析响应,提取 data 字段并转换为指定类型
|
||||
*/
|
||||
private <T> T extractData(String responseStr, TypeReference<T> typeRef) {
|
||||
try {
|
||||
JsonNode root = objectMapper.readTree(responseStr);
|
||||
String code = root.path("code").asText();
|
||||
String msg = root.path("msg").asText();
|
||||
|
||||
if (!"0".equals(code)) {
|
||||
log.error("[HIK] API返回错误 code={}, msg={}", code, msg);
|
||||
throw new RuntimeException("海康API错误 [" + code + "]: " + msg);
|
||||
}
|
||||
|
||||
JsonNode dataNode = root.path("data");
|
||||
return objectMapper.convertValue(dataNode, typeRef);
|
||||
} catch (RuntimeException e) {
|
||||
throw e;
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("解析海康API响应失败: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析响应,直接返回 data 节点(用于结构不固定的响应)
|
||||
*/
|
||||
private JsonNode extractRawData(String responseStr) {
|
||||
try {
|
||||
JsonNode root = objectMapper.readTree(responseStr);
|
||||
String code = root.path("code").asText();
|
||||
if (!"0".equals(code)) {
|
||||
throw new RuntimeException("海康API错误 [" + code + "]: " + root.path("msg").asText());
|
||||
}
|
||||
return root.path("data");
|
||||
} catch (RuntimeException e) {
|
||||
throw e;
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("解析响应失败: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,599 @@
|
|||
package com.hikvision.video.service;
|
||||
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.hikvision.video.config.HikvisionProperties;
|
||||
import com.hikvision.video.model.*;
|
||||
import com.hikvision.video.sse.SseEmitterManager;
|
||||
import com.hikvision.video.util.HikvisionSignUtil;
|
||||
import com.hikvision.video.util.HttpUtil;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.Collections;
|
||||
|
||||
/**
|
||||
* 区域 & 视频通道 & 设备 Service
|
||||
* <p>
|
||||
* 覆盖接口:
|
||||
* 1. 获取区域列表(分页 / 树形)
|
||||
* 2. 获取指定区域下的视频通道(分页)
|
||||
* 3. 获取区域通道树(区域 + 摄像头聚合)
|
||||
* 4. 获取设备列表(分页)
|
||||
* 5. 获取设备详情
|
||||
* 6. 获取设备下的通道列表
|
||||
* </p>
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class RegionChannelService {
|
||||
|
||||
// ==================== 接口路径常量 ====================
|
||||
|
||||
/**
|
||||
* 区域子节点列表(v2,按父节点分页)
|
||||
* 替换旧 v1 路径 /resource/v1/regions/regionIndexCodes/page(404)
|
||||
*/
|
||||
private static final String API_REGION_PAGE = "/artemis/api/resource/v2/regions/subRegions";
|
||||
/** 区域树(按父节点查子节点) */
|
||||
private static final String API_REGION_TREE = "/artemis/api/resource/v2/regions/tree";
|
||||
/**
|
||||
* 按区域查摄像头(v2 搜索接口)
|
||||
* 替换旧 v1 路径 /resource/v1/cameras/indexPage(404)
|
||||
*/
|
||||
private static final String API_CHANNEL_BY_REGION = "/artemis/api/resource/v2/camera/search";
|
||||
/** 设备列表(分页) */
|
||||
private static final String API_DEVICE_PAGE = "/artemis/api/resource/v1/encodes/indexPage";
|
||||
/** 设备详情(批量) */
|
||||
private static final String API_DEVICE_DETAIL = "/artemis/api/resource/v1/encodes/indexCodes";
|
||||
/** 按设备查通道列表 */
|
||||
private static final String API_CHANNEL_BY_DEVICE = "/artemis/api/resource/v2/camera/search";
|
||||
|
||||
private static final String CONTENT_TYPE = "application/json";
|
||||
|
||||
/** 区域树最大递归深度,防止数据异常导致无限递归 */
|
||||
private static final int MAX_TREE_DEPTH = 10;
|
||||
|
||||
private final HikvisionProperties properties;
|
||||
private final SseEmitterManager sseEmitterManager;
|
||||
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||
|
||||
// ==================== 区域接口 ====================
|
||||
|
||||
/**
|
||||
* 分页获取区域列表
|
||||
*
|
||||
* @param request 查询参数(支持按父节点、层级过滤)
|
||||
*/
|
||||
public PageData<RegionInfo> getRegionPage(RegionTreeRequest request) {
|
||||
String path = API_REGION_PAGE;
|
||||
|
||||
// v2 subRegions 需要 parentIndexCode,null 时自动获取根节点
|
||||
String parentCode = request.getRegionIndexCode();
|
||||
if (parentCode == null || parentCode.isBlank()) {
|
||||
parentCode = getRootRegionIndexCode();
|
||||
}
|
||||
log.info("[HIK] 查询区域列表(v2) parent={} page={}/{}", parentCode, request.getPageNo(), request.getPageSize());
|
||||
|
||||
Map<String, Object> body = new LinkedHashMap<>();
|
||||
body.put("parentIndexCode", parentCode);
|
||||
body.put("resourceType", "region"); // v2 subRegions 必填
|
||||
body.put("pageNo", request.getPageNo());
|
||||
body.put("pageSize", request.getPageSize());
|
||||
|
||||
String responseStr = doPost(path, body);
|
||||
PageData<RegionInfo> result = extractData(responseStr, new TypeReference<PageData<RegionInfo>>() {});
|
||||
sseEmitterManager.push(FetchEvent.info(
|
||||
FetchEvent.Type.REGION_PAGE, "RegionChannelService",
|
||||
String.format("区域列表查询完成 parent=%s page=%d/%d 返回%d条",
|
||||
request.getRegionIndexCode(), request.getPageNo(), request.getPageSize(),
|
||||
result.getList() == null ? 0 : result.getList().size()),
|
||||
java.util.Map.of("total", result.getTotal() == null ? 0 : result.getTotal(),
|
||||
"count", result.getList() == null ? 0 : result.getList().size())
|
||||
));
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取区域树(从指定节点展开子树)
|
||||
*
|
||||
* @param parentRegionCode 父区域编码,null 则从根节点开始
|
||||
*/
|
||||
public List<RegionInfo> getRegionTree(String parentRegionCode) {
|
||||
String path = API_REGION_TREE;
|
||||
log.info("[HIK] 查询区域树 parent={}", parentRegionCode);
|
||||
|
||||
Map<String, Object> body = new LinkedHashMap<>();
|
||||
if (parentRegionCode != null && !parentRegionCode.isEmpty()) {
|
||||
body.put("regionIndexCode", parentRegionCode);
|
||||
}
|
||||
body.put("cascadeFlag", 1);
|
||||
|
||||
String responseStr = doPost(path, body);
|
||||
// 树形接口直接返回 list
|
||||
JsonNode dataNode = extractRawData(responseStr);
|
||||
if (dataNode.isArray()) {
|
||||
List<RegionInfo> list = new ArrayList<>();
|
||||
dataNode.forEach(node -> list.add(objectMapper.convertValue(node, RegionInfo.class)));
|
||||
return list;
|
||||
}
|
||||
return extractData(responseStr, new TypeReference<List<RegionInfo>>() {});
|
||||
}
|
||||
|
||||
// ==================== 视频通道接口 ====================
|
||||
|
||||
/**
|
||||
* 按区域分页查询视频通道
|
||||
*
|
||||
* @param request 查询参数(regionIndexCode 必填)
|
||||
*/
|
||||
public PageData<ChannelInfo> getChannelsByRegion(ChannelPageRequest request) {
|
||||
String path = API_CHANNEL_BY_REGION;
|
||||
log.info("[HIK] 查询区域通道(v2) region={} cascade={} page={}/{}",
|
||||
request.getRegionIndexCode(), request.getCascadeFlag(),
|
||||
request.getPageNo(), request.getPageSize());
|
||||
|
||||
// v2 camera/search: regionIndexCodes 为数组,isSubRegion 控制是否含子区域
|
||||
Map<String, Object> body = new LinkedHashMap<>();
|
||||
body.put("pageNo", request.getPageNo());
|
||||
body.put("pageSize", request.getPageSize());
|
||||
body.put("regionIndexCodes", Collections.singletonList(request.getRegionIndexCode()));
|
||||
// cascadeFlag 1 → isSubRegion 1(含子区域);0 → 仅本层
|
||||
body.put("isSubRegion", (request.getCascadeFlag() != null && request.getCascadeFlag() == 1) ? 1 : 0);
|
||||
if (request.getChannelType() != null) body.put("cameraType", request.getChannelType());
|
||||
if (request.getOnlineStatus() != null) body.put("onlineStatus", request.getOnlineStatus());
|
||||
|
||||
String responseStr = doPost(path, body);
|
||||
log.debug("[HIK] 通道查询原始响应(前500字符): {}", responseStr.length() > 500 ? responseStr.substring(0, 500) : responseStr);
|
||||
PageData<ChannelInfo> result = extractData(responseStr, new TypeReference<PageData<ChannelInfo>>() {});
|
||||
sseEmitterManager.push(FetchEvent.info(
|
||||
FetchEvent.Type.CHANNEL_PAGE, "RegionChannelService",
|
||||
String.format("通道查询完成 region=%s cascade=%d page=%d/%d 返回%d条",
|
||||
request.getRegionIndexCode(), request.getCascadeFlag(),
|
||||
request.getPageNo(), request.getPageSize(),
|
||||
result.getList() == null ? 0 : result.getList().size()),
|
||||
java.util.Map.of("total", result.getTotal() == null ? 0 : result.getTotal(),
|
||||
"count", result.getList() == null ? 0 : result.getList().size())
|
||||
));
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 按区域分页查询视频通道(POST请求体版本,支持全部参数)
|
||||
*/
|
||||
public PageData<ChannelInfo> getChannelsByRegionFull(ChannelPageRequest request) {
|
||||
return getChannelsByRegion(request);
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建区域通道聚合树(递归版,自动遍历所有层级)
|
||||
* <p>
|
||||
* 从 rootRegionCode 出发,递归展开每一级子区域并挂载摄像头,
|
||||
* 最终返回完整的多级树结构。递归深度由 {@code MAX_TREE_DEPTH} 限制。
|
||||
* </p>
|
||||
*
|
||||
* @param rootRegionCode 根区域编码,null 则从平台根节点开始
|
||||
* @param cascadeCamera 是否同时加载每个区域的摄像头列表
|
||||
*/
|
||||
public List<RegionChannelTree> buildRegionChannelTree(String rootRegionCode, boolean cascadeCamera) {
|
||||
return buildRegionChannelTree(rootRegionCode, cascadeCamera, MAX_TREE_DEPTH);
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建区域通道聚合树(递归版,支持自定义最大深度)
|
||||
*
|
||||
* @param rootRegionCode 根区域编码,null 则从平台根节点开始
|
||||
* @param cascadeCamera 是否同时加载每个区域的摄像头列表
|
||||
* @param maxDepth 最大递归深度(1 = 只展开一级,≤0 视为不限但受 MAX_TREE_DEPTH 兜底)
|
||||
*/
|
||||
public List<RegionChannelTree> buildRegionChannelTree(String rootRegionCode, boolean cascadeCamera, int maxDepth) {
|
||||
int effectiveMax = (maxDepth > 0 && maxDepth <= MAX_TREE_DEPTH) ? maxDepth : MAX_TREE_DEPTH;
|
||||
return buildRegionChannelTreeRecursive(rootRegionCode, cascadeCamera, 0, effectiveMax);
|
||||
}
|
||||
|
||||
/**
|
||||
* 内部递归实现:逐层构建区域通道树
|
||||
*
|
||||
* @param parentRegionCode 父区域编码
|
||||
* @param cascadeCamera 是否加载摄像头
|
||||
* @param depth 当前递归深度(从 0 开始)
|
||||
* @param maxDepth 最大允许深度
|
||||
*/
|
||||
private List<RegionChannelTree> buildRegionChannelTreeRecursive(
|
||||
String parentRegionCode, boolean cascadeCamera, int depth, int maxDepth) {
|
||||
|
||||
if (depth >= maxDepth) {
|
||||
log.warn("[HIK] 区域树递归已达最大深度 maxDepth={}, parent={}", maxDepth, parentRegionCode);
|
||||
return new ArrayList<>();
|
||||
}
|
||||
|
||||
// 1. 分页拉取当前层所有直属子区域
|
||||
List<RegionInfo> regions = fetchAllRegions(parentRegionCode);
|
||||
if (regions.isEmpty()) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
|
||||
log.info("[HIK] 区域树 depth={} parent={} 子区域数={}", depth, parentRegionCode, regions.size());
|
||||
sseEmitterManager.push(FetchEvent.info(
|
||||
FetchEvent.Type.REGION_TREE, "RegionChannelService",
|
||||
String.format("树形构建 depth=%d parent=%s 子区域%d个",
|
||||
depth, parentRegionCode == null ? "ROOT" : parentRegionCode, regions.size()),
|
||||
java.util.Map.of("depth", depth,
|
||||
"parent", parentRegionCode == null ? "ROOT" : parentRegionCode,
|
||||
"childCount", regions.size())
|
||||
));
|
||||
|
||||
// 2. 为每个子区域构建节点,并递归展开下一层
|
||||
return regions.stream().map(region -> {
|
||||
RegionChannelTree node = new RegionChannelTree(region);
|
||||
|
||||
// 2a. 可选:加载该区域的摄像头
|
||||
if (cascadeCamera) {
|
||||
node.setChannels(fetchChannelsSafe(region.getRegionIndexCode()));
|
||||
}
|
||||
|
||||
// 2b. 递归构建子区域(depth + 1)
|
||||
List<RegionChannelTree> children = buildRegionChannelTreeRecursive(
|
||||
region.getRegionIndexCode(), cascadeCamera, depth + 1, maxDepth);
|
||||
node.setChildren(children.isEmpty() ? null : children);
|
||||
|
||||
return node;
|
||||
}).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页拉取指定父节点下的所有直属子区域(自动翻页,聚合全量结果)
|
||||
*
|
||||
* @param parentRegionCode 父区域编码,null 表示查平台根节点
|
||||
*/
|
||||
private List<RegionInfo> fetchAllRegions(String parentRegionCode) {
|
||||
List<RegionInfo> all = new ArrayList<>();
|
||||
int pageNo = 1;
|
||||
int pageSize = 200;
|
||||
|
||||
while (true) {
|
||||
RegionTreeRequest req = new RegionTreeRequest();
|
||||
req.setRegionIndexCode(parentRegionCode);
|
||||
req.setCascadeFlag(0); // 只查直属一级,递归由本方法调用方控制
|
||||
req.setPageNo(pageNo);
|
||||
req.setPageSize(pageSize);
|
||||
|
||||
PageData<RegionInfo> page = getRegionPage(req);
|
||||
List<RegionInfo> list = page.getList();
|
||||
if (list == null || list.isEmpty()) break;
|
||||
|
||||
all.addAll(list);
|
||||
|
||||
// 已取完(总数已知 或 本页不足一页)
|
||||
if (page.getTotal() != null && all.size() >= page.getTotal()) break;
|
||||
if (list.size() < pageSize) break;
|
||||
|
||||
pageNo++;
|
||||
}
|
||||
return all;
|
||||
}
|
||||
|
||||
/**
|
||||
* 安全获取指定区域的摄像头列表(异常时返回空列表,不中断树构建)
|
||||
*
|
||||
* @param regionIndexCode 区域编码
|
||||
*/
|
||||
private List<ChannelInfo> fetchChannelsSafe(String regionIndexCode) {
|
||||
try {
|
||||
ChannelPageRequest req = new ChannelPageRequest();
|
||||
req.setRegionIndexCode(regionIndexCode);
|
||||
req.setCascadeFlag(0); // 只取本层摄像头,不含子区域
|
||||
req.setPageSize(100);
|
||||
PageData<ChannelInfo> page = getChannelsByRegion(req);
|
||||
return page.getList() != null ? page.getList() : new ArrayList<>();
|
||||
} catch (Exception e) {
|
||||
log.warn("[HIK] 获取区域 {} 的摄像头失败: {}", regionIndexCode, e.getMessage());
|
||||
sseEmitterManager.push(FetchEvent.warn(
|
||||
"RegionChannelService",
|
||||
"获取区域 " + regionIndexCode + " 摄像头失败: " + e.getMessage()
|
||||
));
|
||||
return new ArrayList<>();
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== v2 区域接口(新版 API) ====================
|
||||
|
||||
/** 根节点查询(v1,获取 indexCode) */
|
||||
private static final String API_ROOT_REGION = "/artemis/api/resource/v1/regions/root";
|
||||
/** 子区域列表查询(v2,按父节点分页) */
|
||||
private static final String API_SUB_REGIONS_V2 = "/artemis/api/resource/v2/regions/subRegions";
|
||||
/** 摄像头搜索(v2,按区域集合) */
|
||||
private static final String API_CAMERA_SEARCH_V2 = "/artemis/api/resource/v2/camera/search";
|
||||
|
||||
/**
|
||||
* 获取平台根区域 indexCode
|
||||
* <p>
|
||||
* 调用 POST /artemis/api/resource/v1/regions/root,body: {"treeCode":"0"}
|
||||
* </p>
|
||||
*
|
||||
* @return 根区域编码
|
||||
*/
|
||||
public String getRootRegionIndexCode() {
|
||||
log.info("[HIK-v2] 获取根区域 indexCode");
|
||||
Map<String, Object> body = Map.of("treeCode", "0");
|
||||
String responseStr = doPost(API_ROOT_REGION, body);
|
||||
JsonNode data = extractRawData(responseStr);
|
||||
String indexCode = data.path("indexCode").asText(null);
|
||||
if (indexCode == null || indexCode.isBlank()) {
|
||||
throw new RuntimeException("根区域 indexCode 为空,请检查平台配置");
|
||||
}
|
||||
log.info("[HIK-v2] 根区域 indexCode={}", indexCode);
|
||||
sseEmitterManager.push(FetchEvent.info(
|
||||
FetchEvent.Type.REGION_TREE, "RegionChannelService",
|
||||
"获取根区域成功 indexCode=" + indexCode,
|
||||
java.util.Map.of("indexCode", indexCode)
|
||||
));
|
||||
return indexCode;
|
||||
}
|
||||
|
||||
/**
|
||||
* v2 分页查询子区域列表
|
||||
* <p>
|
||||
* 调用 POST /artemis/api/resource/v2/regions/subRegions
|
||||
* </p>
|
||||
*
|
||||
* @param parentIndexCode 父区域编码(必填)
|
||||
* @param pageNo 页码(从 1 开始)
|
||||
* @param pageSize 每页数量(最大 1000)
|
||||
* @return 子区域分页数据
|
||||
*/
|
||||
public PageData<SubRegionInfo> getSubRegionsV2(String parentIndexCode, int pageNo, int pageSize) {
|
||||
log.info("[HIK-v2] 查询子区域 parent={} page={}/{}", parentIndexCode, pageNo, pageSize);
|
||||
Map<String, Object> body = new LinkedHashMap<>();
|
||||
body.put("parentIndexCode", parentIndexCode);
|
||||
body.put("resourceType", "region"); // v2 subRegions 必填
|
||||
body.put("pageNo", pageNo);
|
||||
body.put("pageSize", pageSize);
|
||||
|
||||
String responseStr = doPost(API_SUB_REGIONS_V2, body);
|
||||
PageData<SubRegionInfo> result = extractData(responseStr, new TypeReference<PageData<SubRegionInfo>>() {});
|
||||
sseEmitterManager.push(FetchEvent.info(
|
||||
FetchEvent.Type.REGION_PAGE, "RegionChannelService",
|
||||
String.format("v2子区域查询 parent=%s page=%d/%d 返回%d条",
|
||||
parentIndexCode, pageNo, pageSize,
|
||||
result.getList() == null ? 0 : result.getList().size()),
|
||||
java.util.Map.of("parent", parentIndexCode,
|
||||
"count", result.getList() == null ? 0 : result.getList().size())
|
||||
));
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* v2 按区域编码集合搜索摄像头(分页)
|
||||
* <p>
|
||||
* 调用 POST /artemis/api/resource/v2/camera/search
|
||||
* </p>
|
||||
*
|
||||
* @param regionIndexCodes 区域编码列表(必填)
|
||||
* @param isSubRegion 是否包含子区域:false-仅本层, true-含子区域
|
||||
* @param pageNo 页码
|
||||
* @param pageSize 每页数量
|
||||
* @return 摄像头分页数据
|
||||
*/
|
||||
public PageData<CameraInfoV2> searchCamerasV2(List<String> regionIndexCodes,
|
||||
boolean isSubRegion,
|
||||
int pageNo, int pageSize) {
|
||||
log.info("[HIK-v2] v2摄像头搜索 regions={} isSubRegion={} page={}/{}",
|
||||
regionIndexCodes, isSubRegion, pageNo, pageSize);
|
||||
Map<String, Object> body = new LinkedHashMap<>();
|
||||
body.put("regionIndexCodes", regionIndexCodes);
|
||||
body.put("isSubRegion", isSubRegion ? 1 : 0);
|
||||
body.put("pageNo", pageNo);
|
||||
body.put("pageSize", pageSize);
|
||||
|
||||
String responseStr = doPost(API_CAMERA_SEARCH_V2, body);
|
||||
PageData<CameraInfoV2> result = extractData(responseStr, new TypeReference<PageData<CameraInfoV2>>() {});
|
||||
sseEmitterManager.push(FetchEvent.info(
|
||||
FetchEvent.Type.CAMERA_PAGE, "RegionChannelService",
|
||||
String.format("v2摄像头搜索完成 region=%s 返回%d条",
|
||||
regionIndexCodes, result.getList() == null ? 0 : result.getList().size()),
|
||||
java.util.Map.of("total", result.getTotal() == null ? 0 : result.getTotal(),
|
||||
"count", result.getList() == null ? 0 : result.getList().size())
|
||||
));
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建 v2 区域通道聚合树(递归,从平台根节点展开)
|
||||
* <p>
|
||||
* 流程:根节点 → 逐级子区域 → 每层加载摄像头(可选)
|
||||
* </p>
|
||||
*
|
||||
* @param cascadeCamera 是否同步加载每区域的摄像头列表
|
||||
* @param maxDepth 最大递归深度(防止死循环,建议 ≤ 10)
|
||||
* @return 完整区域树列表
|
||||
*/
|
||||
public List<RegionV2Tree> buildV2RegionTree(boolean cascadeCamera, int maxDepth) {
|
||||
String rootCode = getRootRegionIndexCode();
|
||||
int effectiveMax = (maxDepth > 0 && maxDepth <= MAX_TREE_DEPTH) ? maxDepth : MAX_TREE_DEPTH;
|
||||
return buildV2TreeRecursive(rootCode, cascadeCamera, 0, effectiveMax);
|
||||
}
|
||||
|
||||
/**
|
||||
* v2 树递归内部实现
|
||||
*/
|
||||
private List<RegionV2Tree> buildV2TreeRecursive(
|
||||
String parentIndexCode, boolean cascadeCamera, int depth, int maxDepth) {
|
||||
|
||||
if (depth >= maxDepth) {
|
||||
log.warn("[HIK-v2] v2区域树递归已达最大深度 maxDepth={}, parent={}", maxDepth, parentIndexCode);
|
||||
return new ArrayList<>();
|
||||
}
|
||||
|
||||
// 1. 分页拉取所有直属子区域
|
||||
List<SubRegionInfo> subRegions = fetchAllSubRegionsV2(parentIndexCode);
|
||||
if (subRegions.isEmpty()) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
|
||||
log.info("[HIK-v2] v2树 depth={} parent={} 子区域数={}", depth, parentIndexCode, subRegions.size());
|
||||
sseEmitterManager.push(FetchEvent.info(
|
||||
FetchEvent.Type.REGION_TREE, "RegionChannelService",
|
||||
String.format("v2树构建 depth=%d parent=%s 子区域%d个",
|
||||
depth, parentIndexCode, subRegions.size()),
|
||||
java.util.Map.of("depth", depth, "parent", parentIndexCode, "childCount", subRegions.size())
|
||||
));
|
||||
|
||||
return subRegions.stream().map(region -> {
|
||||
RegionV2Tree node = new RegionV2Tree(region);
|
||||
|
||||
// 2. 可选:加载该区域的摄像头
|
||||
if (cascadeCamera) {
|
||||
node.setCameras(fetchCamerasForRegionSafe(region.getIndexCode()));
|
||||
}
|
||||
|
||||
// 3. 递归构建下一层
|
||||
List<RegionV2Tree> children = buildV2TreeRecursive(
|
||||
region.getIndexCode(), cascadeCamera, depth + 1, maxDepth);
|
||||
node.setChildren(children.isEmpty() ? null : children);
|
||||
|
||||
return node;
|
||||
}).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页拉取指定父节点下的所有直属子区域(自动翻页)
|
||||
*/
|
||||
private List<SubRegionInfo> fetchAllSubRegionsV2(String parentIndexCode) {
|
||||
List<SubRegionInfo> all = new ArrayList<>();
|
||||
int pageNo = 1;
|
||||
int pageSize = 200;
|
||||
while (true) {
|
||||
PageData<SubRegionInfo> page = getSubRegionsV2(parentIndexCode, pageNo, pageSize);
|
||||
List<SubRegionInfo> list = page.getList();
|
||||
if (list == null || list.isEmpty()) break;
|
||||
all.addAll(list);
|
||||
if (page.getTotal() != null && all.size() >= page.getTotal()) break;
|
||||
if (list.size() < pageSize) break;
|
||||
pageNo++;
|
||||
}
|
||||
return all;
|
||||
}
|
||||
|
||||
/**
|
||||
* 安全获取指定区域的 v2 摄像头列表(异常时返回空列表)
|
||||
*/
|
||||
private List<CameraInfoV2> fetchCamerasForRegionSafe(String regionIndexCode) {
|
||||
try {
|
||||
PageData<CameraInfoV2> page = searchCamerasV2(
|
||||
Collections.singletonList(regionIndexCode), false, 1, 100);
|
||||
return page.getList() != null ? page.getList() : new ArrayList<>();
|
||||
} catch (Exception e) {
|
||||
log.warn("[HIK-v2] 获取区域 {} 的摄像头失败: {}", regionIndexCode, e.getMessage());
|
||||
sseEmitterManager.push(FetchEvent.warn(
|
||||
"RegionChannelService",
|
||||
"v2获取区域 " + regionIndexCode + " 摄像头失败: " + e.getMessage()
|
||||
));
|
||||
return new ArrayList<>();
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 设备接口 ====================
|
||||
|
||||
/**
|
||||
* 分页获取编码设备列表
|
||||
*
|
||||
* @param request 查询参数
|
||||
*/
|
||||
public PageData<DeviceInfo> getDevicePage(DevicePageRequest request) {
|
||||
String path = API_DEVICE_PAGE;
|
||||
log.info("[HIK] 查询设备列表 region={} page={}/{}", request.getRegionIndexCode(), request.getPageNo(), request.getPageSize());
|
||||
|
||||
Map<String, Object> body = new LinkedHashMap<>();
|
||||
body.put("pageNo", request.getPageNo());
|
||||
body.put("pageSize", request.getPageSize());
|
||||
if (request.getRegionIndexCode() != null) body.put("regionIndexCode", request.getRegionIndexCode());
|
||||
if (request.getCascadeFlag() != null) body.put("cascadeFlag", request.getCascadeFlag());
|
||||
if (request.getOnlineStatus() != null) body.put("onlineStatus", request.getOnlineStatus());
|
||||
if (request.getDeviceType() != null) body.put("deviceType", request.getDeviceType());
|
||||
|
||||
String responseStr = doPost(path, body);
|
||||
return extractData(responseStr, new TypeReference<PageData<DeviceInfo>>() {});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取设备详情(批量,最多100个)
|
||||
*
|
||||
* @param deviceIndexCodes 设备编码数组
|
||||
*/
|
||||
public List<DeviceInfo> getDeviceDetail(String... deviceIndexCodes) {
|
||||
String path = API_DEVICE_DETAIL;
|
||||
log.info("[HIK] 查询设备详情 codes={}", Arrays.asList(deviceIndexCodes));
|
||||
|
||||
Map<String, Object> body = Map.of("indexCodes", deviceIndexCodes);
|
||||
String responseStr = doPost(path, body);
|
||||
return extractData(responseStr, new TypeReference<List<DeviceInfo>>() {});
|
||||
}
|
||||
|
||||
/**
|
||||
* 按设备查询下属视频通道列表
|
||||
*
|
||||
* @param deviceIndexCode 设备编码
|
||||
* @param pageNo 页码
|
||||
* @param pageSize 每页数量
|
||||
*/
|
||||
public PageData<ChannelInfo> getChannelsByDevice(String deviceIndexCode, int pageNo, int pageSize) {
|
||||
String path = API_CHANNEL_BY_DEVICE;
|
||||
log.info("[HIK] 按设备查通道 device={} page={}/{}", deviceIndexCode, pageNo, pageSize);
|
||||
|
||||
// 海康通过 deviceIndexCode 参数过滤(部分版本通过 pDeviceIndexCode)
|
||||
Map<String, Object> body = new LinkedHashMap<>();
|
||||
body.put("pageNo", pageNo);
|
||||
body.put("pageSize", pageSize);
|
||||
body.put("deviceIndexCode", deviceIndexCode);
|
||||
|
||||
String responseStr = doPost(path, body);
|
||||
return extractData(responseStr, new TypeReference<PageData<ChannelInfo>>() {});
|
||||
}
|
||||
|
||||
// ==================== 私有工具 ====================
|
||||
|
||||
private String doPost(String path, Object body) {
|
||||
Map<String, String> headers = HikvisionSignUtil.buildAuthHeaders(
|
||||
properties.getAppKey(), properties.getAppSecret(),
|
||||
"POST", path, CONTENT_TYPE);
|
||||
return HttpUtil.postJson(properties.getHost() + path, headers, body);
|
||||
}
|
||||
|
||||
private <T> T extractData(String responseStr, TypeReference<T> typeRef) {
|
||||
try {
|
||||
JsonNode root = objectMapper.readTree(responseStr);
|
||||
String code = root.path("code").asText();
|
||||
String msg = root.path("msg").asText();
|
||||
if (!"0".equals(code)) {
|
||||
log.error("[HIK] API错误 code={} msg={}", code, msg);
|
||||
throw new RuntimeException("海康API错误 [" + code + "]: " + msg);
|
||||
}
|
||||
return objectMapper.convertValue(root.path("data"), typeRef);
|
||||
} catch (RuntimeException e) {
|
||||
throw e;
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("解析响应失败: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
private JsonNode extractRawData(String responseStr) {
|
||||
try {
|
||||
JsonNode root = objectMapper.readTree(responseStr);
|
||||
String code = root.path("code").asText();
|
||||
if (!"0".equals(code)) {
|
||||
throw new RuntimeException("海康API错误 [" + code + "]: " + root.path("msg").asText());
|
||||
}
|
||||
return root.path("data");
|
||||
} catch (RuntimeException e) {
|
||||
throw e;
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("解析响应失败: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,94 @@
|
|||
package com.hikvision.video.sse;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.hikvision.video.model.FetchEvent;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
|
||||
/**
|
||||
* SSE 连接管理器
|
||||
* <p>
|
||||
* 维护所有活跃的前端订阅连接,并向其广播 {@link FetchEvent} 消息。
|
||||
* 使用 {@link CopyOnWriteArrayList} 保证多线程安全(递归树并发推送场景)。
|
||||
* </p>
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class SseEmitterManager {
|
||||
|
||||
/** 活跃的 SSE 订阅列表(线程安全) */
|
||||
private final CopyOnWriteArrayList<SseEmitter> emitters = new CopyOnWriteArrayList<>();
|
||||
|
||||
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||
|
||||
/**
|
||||
* 新建一个 SSE 连接并注册到管理列表
|
||||
*
|
||||
* @return 返回给 Controller 的 SseEmitter 实例
|
||||
*/
|
||||
public SseEmitter connect() {
|
||||
SseEmitter emitter = new SseEmitter(0L); // 0 = 不超时,由客户端重连机制维护
|
||||
emitters.add(emitter);
|
||||
|
||||
emitter.onCompletion(() -> {
|
||||
emitters.remove(emitter);
|
||||
log.debug("[SSE] 连接关闭,当前活跃连接数: {}", emitters.size());
|
||||
});
|
||||
emitter.onTimeout(() -> {
|
||||
emitters.remove(emitter);
|
||||
log.debug("[SSE] 连接超时,当前活跃连接数: {}", emitters.size());
|
||||
});
|
||||
emitter.onError(e -> {
|
||||
emitters.remove(emitter);
|
||||
log.debug("[SSE] 连接出错: {}", e.getMessage());
|
||||
});
|
||||
|
||||
log.info("[SSE] 新增订阅,当前活跃连接数: {}", emitters.size());
|
||||
return emitter;
|
||||
}
|
||||
|
||||
/**
|
||||
* 向所有活跃连接广播一条事件
|
||||
*
|
||||
* @param event 要推送的事件
|
||||
*/
|
||||
public void push(FetchEvent event) {
|
||||
if (emitters.isEmpty()) return;
|
||||
|
||||
String json;
|
||||
try {
|
||||
json = objectMapper.writeValueAsString(event);
|
||||
} catch (Exception e) {
|
||||
log.warn("[SSE] 事件序列化失败: {}", e.getMessage());
|
||||
return;
|
||||
}
|
||||
|
||||
List<SseEmitter> dead = new ArrayList<>();
|
||||
for (SseEmitter emitter : emitters) {
|
||||
try {
|
||||
emitter.send(
|
||||
SseEmitter.event()
|
||||
.name("fetch") // 前端监听的事件名
|
||||
.data(json)
|
||||
);
|
||||
} catch (Exception e) {
|
||||
dead.add(emitter);
|
||||
}
|
||||
}
|
||||
|
||||
if (!dead.isEmpty()) {
|
||||
emitters.removeAll(dead);
|
||||
log.debug("[SSE] 清理失效连接 {},剩余: {}", dead.size(), emitters.size());
|
||||
}
|
||||
}
|
||||
|
||||
/** 当前活跃连接数(监控用) */
|
||||
public int activeCount() {
|
||||
return emitters.size();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,84 @@
|
|||
package com.hikvision.video.util;
|
||||
|
||||
import javax.crypto.Mac;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* 海康威视 API 签名工具类
|
||||
* <p>
|
||||
* 签名算法参考:https://open.hikvision.com/docs(ISAPI / Open API 鉴权)
|
||||
* 签名步骤:
|
||||
* 1. 构造签名字符串(method\naccept\ncontent-type\nx-ca-timestamp\nx-ca-nonce\npath)
|
||||
* 2. 使用 HmacSHA256 + AppSecret 计算签名
|
||||
* 3. Base64 编码后放入请求头 X-Ca-Signature
|
||||
* </p>
|
||||
*/
|
||||
public class HikvisionSignUtil {
|
||||
|
||||
private static final String HMAC_SHA256 = "HmacSHA256";
|
||||
|
||||
/**
|
||||
* 生成请求所需的鉴权请求头
|
||||
*
|
||||
* @param appKey AppKey
|
||||
* @param appSecret AppSecret
|
||||
* @param method HTTP方法(GET/POST)
|
||||
* @param path 请求路径,含 query string,例如 /artemis/api/video/v1/cameras/indexPage
|
||||
* @param contentType Content-Type,一般为 application/json
|
||||
* @return 包含鉴权信息的请求头 Map
|
||||
*/
|
||||
public static Map<String, String> buildAuthHeaders(
|
||||
String appKey,
|
||||
String appSecret,
|
||||
String method,
|
||||
String path,
|
||||
String contentType) {
|
||||
|
||||
String accept = "application/json";
|
||||
String timestamp = String.valueOf(System.currentTimeMillis());
|
||||
String nonce = UUID.randomUUID().toString().replace("-", "");
|
||||
|
||||
// 构造待签名字符串
|
||||
String stringToSign = method.toUpperCase() + "\n"
|
||||
+ accept + "\n"
|
||||
+ contentType + "\n"
|
||||
+ "x-ca-key:" + appKey + "\n"
|
||||
+ "x-ca-nonce:" + nonce + "\n"
|
||||
+ "x-ca-timestamp:" + timestamp + "\n"
|
||||
+ path;
|
||||
|
||||
String signature = hmacSha256(appSecret, stringToSign);
|
||||
|
||||
Map<String, String> headers = new LinkedHashMap<>();
|
||||
headers.put("Accept", accept);
|
||||
headers.put("Content-Type", contentType);
|
||||
headers.put("X-Ca-Key", appKey);
|
||||
headers.put("X-Ca-Timestamp", timestamp);
|
||||
headers.put("X-Ca-Nonce", nonce);
|
||||
headers.put("X-Ca-Signature", signature);
|
||||
headers.put("X-Ca-Signature-Headers", "x-ca-key,x-ca-nonce,x-ca-timestamp");
|
||||
return headers;
|
||||
}
|
||||
|
||||
/**
|
||||
* HmacSHA256 签名,结果 Base64 编码
|
||||
*/
|
||||
private static String hmacSha256(String secret, String data) {
|
||||
try {
|
||||
Mac mac = Mac.getInstance(HMAC_SHA256);
|
||||
SecretKeySpec keySpec = new SecretKeySpec(
|
||||
secret.getBytes(StandardCharsets.UTF_8), HMAC_SHA256);
|
||||
mac.init(keySpec);
|
||||
byte[] rawHmac = mac.doFinal(data.getBytes(StandardCharsets.UTF_8));
|
||||
return Base64.getEncoder().encodeToString(rawHmac);
|
||||
} catch (NoSuchAlgorithmException | InvalidKeyException e) {
|
||||
throw new RuntimeException("HMAC-SHA256 签名失败", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,122 @@
|
|||
package com.hikvision.video.util;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.hc.client5.http.classic.methods.HttpGet;
|
||||
import org.apache.hc.client5.http.classic.methods.HttpPost;
|
||||
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
|
||||
import org.apache.hc.client5.http.impl.classic.HttpClients;
|
||||
import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManagerBuilder;
|
||||
import org.apache.hc.client5.http.ssl.NoopHostnameVerifier;
|
||||
import org.apache.hc.client5.http.ssl.SSLConnectionSocketFactory;
|
||||
import org.apache.hc.core5.http.io.entity.EntityUtils;
|
||||
import org.apache.hc.core5.http.io.entity.StringEntity;
|
||||
import org.apache.hc.core5.ssl.SSLContextBuilder;
|
||||
|
||||
import javax.net.ssl.SSLContext;
|
||||
import java.io.InputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* HTTP 请求工具(支持 HTTPS 忽略证书,适合内网海康平台)
|
||||
*/
|
||||
@Slf4j
|
||||
public class HttpUtil {
|
||||
|
||||
private static final ObjectMapper MAPPER = new ObjectMapper();
|
||||
|
||||
/**
|
||||
* 发送 POST JSON 请求
|
||||
*
|
||||
* @param url 完整URL
|
||||
* @param headers 请求头
|
||||
* @param body 请求体对象(自动序列化为JSON)
|
||||
* @return 响应字符串
|
||||
*/
|
||||
public static String postJson(String url, Map<String, String> headers, Object body) {
|
||||
try {
|
||||
SSLContext sslContext = SSLContextBuilder.create()
|
||||
.loadTrustMaterial(null, (chain, authType) -> true)
|
||||
.build();
|
||||
SSLConnectionSocketFactory sslSocketFactory =
|
||||
new SSLConnectionSocketFactory(sslContext, NoopHostnameVerifier.INSTANCE);
|
||||
|
||||
try (CloseableHttpClient client = HttpClients.custom()
|
||||
.setConnectionManager(
|
||||
PoolingHttpClientConnectionManagerBuilder.create()
|
||||
.setSSLSocketFactory(sslSocketFactory)
|
||||
.build())
|
||||
.build()) {
|
||||
|
||||
HttpPost post = new HttpPost(url);
|
||||
|
||||
if (headers != null) {
|
||||
headers.forEach(post::addHeader);
|
||||
}
|
||||
|
||||
if (body != null) {
|
||||
String json = MAPPER.writeValueAsString(body);
|
||||
log.debug("[HIK] POST {} body: {}", url, json);
|
||||
post.setEntity(new StringEntity(json, StandardCharsets.UTF_8));
|
||||
}
|
||||
|
||||
return client.execute(post, response -> {
|
||||
int statusCode = response.getCode();
|
||||
String responseBody = EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8);
|
||||
log.debug("[HIK] Response [{}]: {}", statusCode, responseBody);
|
||||
if (statusCode != 200) {
|
||||
throw new RuntimeException("HTTP Error " + statusCode + ": " + responseBody);
|
||||
}
|
||||
return responseBody;
|
||||
});
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("[HIK] HTTP请求失败: {}", e.getMessage(), e);
|
||||
throw new RuntimeException("海康API请求失败: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送 GET 请求,返回字节数组(用于下载图片等二进制数据)
|
||||
* <p>
|
||||
* 同样忽略 SSL 证书,适用于海康内网自签名环境。
|
||||
* </p>
|
||||
*
|
||||
* @param url 完整URL
|
||||
* @return 响应字节数组
|
||||
*/
|
||||
public static byte[] getBytes(String url) {
|
||||
try {
|
||||
SSLContext sslContext = SSLContextBuilder.create()
|
||||
.loadTrustMaterial(null, (chain, authType) -> true)
|
||||
.build();
|
||||
SSLConnectionSocketFactory sslSocketFactory =
|
||||
new SSLConnectionSocketFactory(sslContext, NoopHostnameVerifier.INSTANCE);
|
||||
|
||||
try (CloseableHttpClient client = HttpClients.custom()
|
||||
.setConnectionManager(
|
||||
PoolingHttpClientConnectionManagerBuilder.create()
|
||||
.setSSLSocketFactory(sslSocketFactory)
|
||||
.build())
|
||||
.build()) {
|
||||
|
||||
HttpGet get = new HttpGet(url);
|
||||
log.debug("[HIK] GET (bytes) {}", url);
|
||||
|
||||
return client.execute(get, response -> {
|
||||
int statusCode = response.getCode();
|
||||
if (statusCode != 200) {
|
||||
throw new RuntimeException("HTTP Error " + statusCode + " 下载图片失败: " + url);
|
||||
}
|
||||
try (InputStream is = response.getEntity().getContent()) {
|
||||
return is.readAllBytes();
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("[HIK] 下载图片失败 url={}: {}", url, e.getMessage(), e);
|
||||
throw new RuntimeException("下载图片失败: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
server:
|
||||
port: 8099
|
||||
|
||||
# 海康威视开放平台配置
|
||||
hikvision:
|
||||
api:
|
||||
# 平台地址(综合安防管理平台 iSecure Center 或 ISUP服务地址)
|
||||
host: https://10.60.49.82:443
|
||||
# AppKey(在开放平台创建应用后获取)
|
||||
app-key: 23046693
|
||||
# AppSecret
|
||||
app-secret: sxsSY3lfL001LB6lLhpO
|
||||
|
||||
spring:
|
||||
application:
|
||||
name: hikvision-video-api
|
||||
jackson:
|
||||
default-property-inclusion: non_null
|
||||
time-zone: GMT+8
|
||||
|
||||
# 抓图本地保存目录(相对于启动目录,或使用绝对路径)
|
||||
app:
|
||||
capture:
|
||||
save-path: ./captures
|
||||
|
||||
logging:
|
||||
level:
|
||||
com.hikvision: DEBUG
|
||||
|
|
@ -0,0 +1,446 @@
|
|||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<title>预览Demo</title>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
||||
<meta http-equiv="Pragma" content="no-cache" />
|
||||
<meta http-equiv="Cache-Control" content="no-cache, must-revalidate" />
|
||||
<meta http-equiv="Expires" content="0" />
|
||||
</head>
|
||||
<style>
|
||||
html, body {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
.playWnd {
|
||||
margin: 30px 0 0 50px;
|
||||
width: 800px;
|
||||
height: 400px;
|
||||
border: 1px solid red;
|
||||
}
|
||||
.cbInfoDiv {
|
||||
float: left;
|
||||
width: 360px;
|
||||
margin-left: 16px;
|
||||
border:1px solid #7F9DB9;
|
||||
}
|
||||
.cbInfo {
|
||||
height: 200px;
|
||||
padding: 5px;
|
||||
border: 1px solid #7F9DB9;
|
||||
word-break: break-all;
|
||||
overflow: scroll/auto;
|
||||
}
|
||||
.operate {
|
||||
margin-top: 24px;
|
||||
}
|
||||
.operate::after {
|
||||
content: '';
|
||||
display: block;
|
||||
clear: both;
|
||||
}
|
||||
.operate .btns {
|
||||
height: 32px;
|
||||
}
|
||||
.module {
|
||||
float: left;
|
||||
width: 120px;
|
||||
min-height: 290px;
|
||||
margin-left: 10px;
|
||||
padding: 16px 8px;
|
||||
box-sizing: border-box;
|
||||
border: 1px solid #e5e5e5;
|
||||
}
|
||||
.module .item {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.module .label {
|
||||
width: 150px;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
margin-right: 8px;
|
||||
text-align: right;
|
||||
}
|
||||
.module input[type="text"],
|
||||
.module select {
|
||||
box-sizing: border-box;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
margin-left: 0;
|
||||
width: 150px;
|
||||
min-height: 20px;
|
||||
}
|
||||
.module .btn {
|
||||
min-width: 80px;
|
||||
min-height: 24px;
|
||||
margin-top: 16px;
|
||||
margin-left: 158px;
|
||||
}
|
||||
</style>
|
||||
<body>
|
||||
<div id="playWnd" class="playWnd" style="left: 109px; top: 133px;"></div>
|
||||
<div id="operate" class="operate">
|
||||
<!--初始化、反初始化、设置认证信息接口调用入口。
|
||||
1.插件所有接口都需要在调用初始化并返回成功后才能调用
|
||||
2.设置认证信息仅适用于对接多平台时的情况,具体参照开发指南
|
||||
3.反初始化后,插件资源销毁-->
|
||||
<div class="module" style="left:30px;height:30px;width:280px;padding:10;margin:10;">
|
||||
<div class="item">
|
||||
<label >初始化相关参数:</label>
|
||||
<textarea id="initParam" type="text" style="width:260px;height:200px;">
|
||||
{
|
||||
"argument": {
|
||||
"appkey": "",
|
||||
"ip": "",
|
||||
"port": 443,
|
||||
"secret": "",
|
||||
"enableHTTPS": 1,
|
||||
"layout": "2x2",
|
||||
"playMode": 0
|
||||
},
|
||||
"funcName": "init"
|
||||
}
|
||||
</textarea>
|
||||
</div>
|
||||
<div class="item">
|
||||
|
||||
<button style="width:10px;padding:0;margin:0;" id="initBtn" class="btn">执行</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!--单个点位播放、批量点位播放、批量停止播放、全部停止播放接口调用入口。
|
||||
1.authUuid为对接多平台时必须的播放字段,对接单平台时,可不指定-->
|
||||
<div class="module" style="height:30;width:280px;padding:10;margin:10;">
|
||||
<div class="item">
|
||||
<label >播放相关参数:</label>
|
||||
<textarea id="playParam" type="text" style="width:260px;height:200px;">
|
||||
{
|
||||
"argument": {
|
||||
"cameraIndexCode": "",
|
||||
"ezvizDirect": 0,
|
||||
"gpuMode": 0,
|
||||
"streamMode": 0,
|
||||
"transMode": 1,
|
||||
"wndId": -1
|
||||
},
|
||||
"funcName": "startPreview"
|
||||
}
|
||||
</textarea>
|
||||
</div>
|
||||
<div class="item">
|
||||
|
||||
<button style="width:10px;padding:0;margin:0;" id="playBtn" class="btn">执行</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="module" style="height:50;width:300px;padding:10;margin:10;">
|
||||
<legend>返回值信息</legend>
|
||||
<div id="cbInfo" class="cbInfo"></div>
|
||||
<button style="width:80px;height:24px;padding:30;margin:0;" id="clear">清空</button>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
<script src="jquery-1.12.4.min.js"></script>
|
||||
<script src="jsencrypt.min.js"></script>
|
||||
<script src="web-control_1.2.7.min.js"></script>
|
||||
|
||||
<script type="text/javascript">
|
||||
// 插件对象实例,初始化为null,需要创建多个插件窗口时,需要定义多个插件对象实例变量,各个变量唯一标志对应的插件实例
|
||||
var oWebControl = null;
|
||||
var bIE = (!!window.ActiveXObject || 'ActiveXObject' in window);// 是否为IE浏览器
|
||||
var pubKey = ''; // demo中未使用加密,可根据需求参照开发指南自行使用加密功能
|
||||
var initCount = 0; // 异常重启计数
|
||||
var iframePos = {}; // iframe相对文档的位置
|
||||
var parentTitle = ''; // 父页面标题
|
||||
var iframeClientPos = null; // iframe相对视窗的位置
|
||||
var iframeParentShowSize = null; // 视窗大小 width height
|
||||
|
||||
// 标签关闭
|
||||
$(window).unload(function () {
|
||||
if (oWebControl != null){
|
||||
oWebControl.JS_HideWnd(); // 先让窗口隐藏,规避可能的插件窗口滞后于浏览器消失问题
|
||||
oWebControl.JS_Disconnect().then(function(){}, function() {});
|
||||
}
|
||||
});
|
||||
|
||||
//监听父页面的消息
|
||||
window.addEventListener('message', function(e){
|
||||
if(e && e.data){
|
||||
switch (e.data.action){
|
||||
case 'sendTitle': // 父页面将其标题发送过来,子页面保存该标题,以便创建插件窗口成功后将标题设置回给父页面
|
||||
parentTitle = e.data.info;
|
||||
break;
|
||||
case 'updatePos': // 更新插件位置
|
||||
var scrollValue = e.data.scrollValue; // 滚动条滚动偏移量
|
||||
oWebControl.JS_SetDocOffset({
|
||||
left: iframePos.left + scrollValue.left,
|
||||
top: iframePos.top + scrollValue.top
|
||||
}); // 更新插件窗口位置
|
||||
|
||||
oWebControl.JS_Resize(800, 400);
|
||||
setWndCover();
|
||||
break;
|
||||
case 'scroll':
|
||||
iframeParentShowSize = e.data.showSize; // 视窗大小
|
||||
iframePos = e.data.iframeOffset; // iframe与文档的偏移量
|
||||
iframeClientPos = e.data.iframeClientPos; // iframe相对视窗的位置
|
||||
var scrollValue = e.data.scrollValue; // 滚动条滚动偏移量
|
||||
if(oWebControl){
|
||||
oWebControl.JS_SetDocOffset({
|
||||
left: iframePos.left + scrollValue.left,
|
||||
top: iframePos.top + scrollValue.top
|
||||
}); // 更新插件窗口位置
|
||||
oWebControl.JS_Resize(800, 400);
|
||||
setWndCover();
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 顶部:iframe.getBoundingClientRect().top小于0并且其绝对值超过DIV.get(0).getBoundingClientRect().top部分需要剪切
|
||||
// 底部:(iframe.getBoundingClientRect().bottom - iframe父窗口可视域高度,为H1)为不可见部分
|
||||
// ($(window).height() - DIV.get(0).getBoundingClientRect().bottom)
|
||||
// 为DIV底部与其所在iframe底部之间的距离H2,H1-H2的值大于0则表示DIV有部分在不可见区域
|
||||
// 左边:iframe.getBoundingClientRect().left小于0并且其绝对值超过DIV.get(0).getBoundingClientRect().left部分需要剪切
|
||||
// 右边:(iframe宽度 - DIV.get(0).getBoundingClientRect().right表示DIV右边与其父iframe右边之间的距离,为W1)
|
||||
// (iframe父窗口可视域宽度-iframe.getBoundingClientRect().left表示iframe左边与iframe父窗口可视域右边之间的距离,为W2)
|
||||
// (iframe宽度 - W2 - W1)如果大于0,则表示DIV右边超出了iframe父窗口可视域,需要剪切超过的部分
|
||||
function setWndCover() {
|
||||
if (oWebControl){
|
||||
// 准备要用到的一些数据
|
||||
var iframeWndHeight = $(window).height(); // iframe窗口高度
|
||||
var iframeWndWidth = $(window).width(); // iframe窗口宽度
|
||||
var divLeft = $("#playWnd").get(0).getBoundingClientRect().left;
|
||||
var divTop = $("#playWnd").get(0).getBoundingClientRect().top;
|
||||
var divRight = $("#playWnd").get(0).getBoundingClientRect().right;
|
||||
var divBottom = $("#playWnd").get(0).getBoundingClientRect().bottom;
|
||||
var divWidth = $("#playWnd").width();
|
||||
var divHeight = $("#playWnd").height();
|
||||
|
||||
oWebControl.JS_RepairPartWindow(0, 0, 801, 401); // 多1个像素点防止还原后边界缺失一个像素条
|
||||
|
||||
// 判断剪切矩形的上边距
|
||||
if (iframeClientPos.top < 0 && Math.abs(iframeClientPos.top) > divTop){
|
||||
var deltaTop = Math.abs(iframeClientPos.top) - divTop;
|
||||
oWebControl.JS_CuttingPartWindow(0, 0, 801, deltaTop + 1);
|
||||
//console.log({deltaTop: deltaTop});
|
||||
}
|
||||
|
||||
// 判断剪切矩形的左边距
|
||||
if (iframeClientPos.left < 0 && Math.abs(iframeClientPos.left) > divLeft){
|
||||
var deltaLeft = Math.abs(iframeClientPos.left) - divLeft;
|
||||
//console.log({deltaLeft: deltaLeft});
|
||||
oWebControl.JS_CuttingPartWindow(0, 0, deltaLeft, 401); // 多剪掉一个像素条,防止出现剪掉一部分窗口后出现一个像素条
|
||||
}
|
||||
|
||||
// 判断剪切矩形的右边距
|
||||
var W1 = iframeWndWidth - divRight;
|
||||
var W2 = iframeParentShowSize.width - iframeClientPos.left;
|
||||
if (W2 < divWidth){
|
||||
var deltaRight = iframeWndWidth - W2 - W1;
|
||||
if (deltaRight > 0) {
|
||||
oWebControl.JS_CuttingPartWindow(800 - deltaRight, 0, deltaRight + 1, 401);
|
||||
}
|
||||
}
|
||||
|
||||
// 判断剪切矩形的下边距
|
||||
var H1 = iframeClientPos.bottom - iframeParentShowSize.height;
|
||||
var H2 = iframeWndHeight - divBottom;
|
||||
var deltaBottom = H1 - H2;
|
||||
//console.log({deltaBottom: deltaBottom});
|
||||
if (deltaBottom > 0) {
|
||||
oWebControl.JS_CuttingPartWindow(0, 400 - deltaBottom, 801, deltaBottom + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 创建插件实例,并启动本地服务建立websocket连接,创建插件窗口
|
||||
function initPlugin () {
|
||||
oWebControl = new WebControl({
|
||||
szPluginContainer: "playWnd",
|
||||
iServicePortStart: 15900,
|
||||
iServicePortEnd: 15900,
|
||||
szClassId:"23BF3B0A-2C56-4D97-9C03-0CB103AA8F11", // 用于IE10使用ActiveX的clsid
|
||||
cbConnectSuccess: function () {
|
||||
initCount = 0;
|
||||
setCallbacks();
|
||||
oWebControl.JS_StartService("window", {
|
||||
dllPath: "./VideoPluginConnect.dll"
|
||||
}).then(function () {
|
||||
// 步骤2:JS_CreateWnd时指定cbSetDocTitle回调,并在回调中向父页面发送更新标题消息,标题为回调出来的uuid
|
||||
oWebControl.JS_CreateWnd("playWnd", 800, 400, {
|
||||
bEmbed: true,
|
||||
cbSetDocTitle: function (uuid) {
|
||||
oWebControl._pendBg = false;
|
||||
window.parent.postMessage({
|
||||
action:'updateTitle',
|
||||
msg:'子页面通知父页面修改title',
|
||||
info:uuid
|
||||
}, '\*'); // '\*'表示跨域参数,请结合自身业务合理设置
|
||||
}
|
||||
}).then(function () {
|
||||
// 步骤3:JS_CreateWnd成功后通知父页面将其标题修改回去
|
||||
console.log("JS_CreateWnd success");
|
||||
window.parent.postMessage({
|
||||
action:'updateTitle',
|
||||
msg:'子页面通知父页面更新title',
|
||||
info: parentTitle
|
||||
}, '\*');
|
||||
|
||||
// 步骤4:发消息更新插件窗口位置:这里不直接更新的原因是,父页面默认可能就存在滚动条,此时有滚动量
|
||||
window.parent.postMessage({
|
||||
action:'updatePos',
|
||||
msg:'更新Pos'
|
||||
}, '\*');
|
||||
|
||||
initBtnClicked();
|
||||
});
|
||||
}, function () {
|
||||
console.log("JS_CreateWnd fail");
|
||||
});
|
||||
},
|
||||
cbConnectError: function () {
|
||||
console.log("cbConnectError");
|
||||
oWebControl = null;
|
||||
$("#playWnd").html("插件未启动,正在尝试启动,请稍候...");
|
||||
WebControl.JS_WakeUp("VideoWebPlugin://");
|
||||
initCount ++;
|
||||
if (initCount < 3) {
|
||||
setTimeout(function () {
|
||||
initPlugin();
|
||||
}, 3000)
|
||||
} else {
|
||||
$("#playWnd").html("插件启动失败,请检查插件是否安装!");
|
||||
}
|
||||
},
|
||||
cbConnectClose: function (bNormalClose) {
|
||||
// 异常断开:bNormalClose = false
|
||||
// JS_Disconnect正常断开:bNormalClose = true
|
||||
if (true == bNormalClose){
|
||||
console.log("cbConnectClose normal");
|
||||
}else{
|
||||
console.log("cbConnectClose exception");
|
||||
}
|
||||
|
||||
oWebControl = null;
|
||||
$("#playWnd").html("插件未启动,正在尝试启动,请稍候...");
|
||||
WebControl.JS_WakeUp("VideoWebPlugin://");
|
||||
initCount ++;
|
||||
if (initCount < 3) {
|
||||
setTimeout(function () {
|
||||
initPlugin();
|
||||
}, 3000)
|
||||
} else {
|
||||
$("#playWnd").html("插件启动失败,请检查插件是否安装!");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
initPlugin();
|
||||
|
||||
// 获取公钥
|
||||
function getPubKey (callback) {
|
||||
oWebControl.JS_RequestInterface({
|
||||
funcName: "getRSAPubKey",
|
||||
argument: JSON.stringify({
|
||||
keyLength: 1024
|
||||
})
|
||||
}).then(function (oData) {
|
||||
console.log(oData)
|
||||
if (oData.responseMsg.data) {
|
||||
pubKey = oData.responseMsg.data
|
||||
callback()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 设置窗口控制回调
|
||||
function setCallbacks() {
|
||||
oWebControl.JS_SetWindowControlCallback({
|
||||
cbIntegrationCallBack: cbIntegrationCallBack
|
||||
});
|
||||
}
|
||||
|
||||
// 推送消息
|
||||
function cbIntegrationCallBack(oData) {
|
||||
showCBInfo(JSON.stringify(oData.responseMsg));
|
||||
}
|
||||
|
||||
// RSA加密
|
||||
function setEncrypt (value) {
|
||||
var encrypt = new JSEncrypt();
|
||||
encrypt.setPublicKey(pubKey);
|
||||
return encrypt.encrypt(value);
|
||||
}
|
||||
|
||||
function requestInterface(value)
|
||||
{
|
||||
oWebControl.JS_RequestInterface(JSON.parse(value)).then(function (oData) {
|
||||
console.log(oData)
|
||||
showCBInfo(JSON.stringify(oData ? oData.responseMsg : ''));
|
||||
});
|
||||
}
|
||||
|
||||
// 显示接口返回的消息及插件回调信息
|
||||
function showCBInfo(szInfo, type) {
|
||||
if (type === 'error') {
|
||||
szInfo = "<div style='color: red;'>" + dateFormat(new Date(), "yyyy-MM-dd hh:mm:ss") + " " + szInfo + "</div>";
|
||||
} else {
|
||||
szInfo = "<div>" + dateFormat(new Date(), "yyyy-MM-dd hh:mm:ss") + " " + szInfo + "</div>";
|
||||
}
|
||||
$("#cbInfo").html(szInfo + $("#cbInfo").html());
|
||||
}
|
||||
|
||||
function initBtnClicked(){
|
||||
var param = $("#initParam").val();
|
||||
//删除字符串中的回车换行
|
||||
param = param.replace(/(\s*)/g, "");
|
||||
|
||||
// 执行初始化
|
||||
requestInterface(param);
|
||||
}
|
||||
|
||||
$("#initBtn").click(function() {
|
||||
initBtnClicked();
|
||||
})
|
||||
|
||||
$("#playBtn").click(function() {
|
||||
var param = $("#playParam").val();
|
||||
//删除字符串中的回车换行
|
||||
param = param.replace(/(\s*)/g, "");
|
||||
|
||||
// 执行预览
|
||||
requestInterface(param);
|
||||
})
|
||||
|
||||
// 清空
|
||||
$("#clear").click(function() {
|
||||
$("#cbInfo").html('');
|
||||
})
|
||||
|
||||
// 格式化时间
|
||||
function dateFormat(oDate, fmt) {
|
||||
var o = {
|
||||
"M+": oDate.getMonth() + 1, //月份
|
||||
"d+": oDate.getDate(), //日
|
||||
"h+": oDate.getHours(), //小时
|
||||
"m+": oDate.getMinutes(), //分
|
||||
"s+": oDate.getSeconds(), //秒
|
||||
"q+": Math.floor((oDate.getMonth() + 3) / 3), //季度
|
||||
"S": oDate.getMilliseconds()//毫秒
|
||||
};
|
||||
if (/(y+)/.test(fmt)) {
|
||||
fmt = fmt.replace(RegExp.$1, (oDate.getFullYear() + "").substr(4 - RegExp.$1.length));
|
||||
}
|
||||
for (var k in o) {
|
||||
if (new RegExp("(" + k + ")").test(fmt)) {
|
||||
fmt = fmt.replace(RegExp.$1, (RegExp.$1.length == 1) ? (o[k]) : (("00" + o[k]).substr(("" + o[k]).length)));
|
||||
}
|
||||
}
|
||||
return fmt;
|
||||
}
|
||||
</script>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,178 @@
|
|||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Window Demo</title>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
||||
<meta http-equiv="Pragma" content="no-cache" />
|
||||
<meta http-equiv="Cache-Control" content="no-cache, must-revalidate" />
|
||||
<meta http-equiv="Expires" content="0" />
|
||||
</head>
|
||||
<style>
|
||||
html, body {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
.iframe {
|
||||
margin: 100px;
|
||||
border: 1px solid blue;
|
||||
}
|
||||
</style>
|
||||
<body>
|
||||
<!-- 步骤1:src指定待嵌入的子页面,scrolling指定no禁用滚动条 -->
|
||||
<iframe id="iframe1" class="iframe" src="http://127.0.0.1/demo_embedded_for_iframe.html" scrolling="no" frameborder="0" width="900" height="750"></iframe>
|
||||
</body>
|
||||
<script src="jquery-1.12.4.min.js"></script>
|
||||
<script src="jsencrypt.min.js"></script>
|
||||
<script src="web-control_1.2.7.min.js"></script>
|
||||
<!-- <script src="JsonToXml.js"></script>
|
||||
<script src="jsWebControl-Bridge.js"></script> -->
|
||||
<script type="text/javascript">
|
||||
// 步骤2:嵌入子页面的页面中在iframe的onload事件中向子页面抛以下消息
|
||||
var iframeWin = document.getElementById("iframe1");
|
||||
{
|
||||
iframeWin.onload = function(){
|
||||
iframeWin.contentWindow.postMessage({
|
||||
action:'sendTitle', // 告诉子页面本页面的标题(action自行指定,但需要与子页面中监听的action保持一致
|
||||
msg: '将标题发给子页面',
|
||||
info: document.title
|
||||
}, '\*');
|
||||
iframeWin.contentWindow.postMessage({
|
||||
action:'updateInitParam', // 告诉子页面一些初始值,包括浏览器视窗高度与宽度、iframe偏离文档的位置、iframe相对视窗的位置
|
||||
msg: '更新子页面一些初始值',
|
||||
showSize: { // 浏览器视窗高度与宽度
|
||||
width: $(window).width(),
|
||||
height: $(window).height()
|
||||
},
|
||||
iframeOffset: { // iframe偏离文档的位置
|
||||
left: iframeWin.offsetLeft,
|
||||
top: iframeWin.offsetTop
|
||||
},
|
||||
iframeClientPos: { // iframe相对视窗的位置
|
||||
left: iframeWin.getBoundingClientRect().left,
|
||||
right: iframeWin.getBoundingClientRect().right,
|
||||
top: iframeWin.getBoundingClientRect().top,
|
||||
bottom: iframeWin.getBoundingClientRect().bottom
|
||||
}
|
||||
}, '\*'); // '\*'表示跨域参数,请结合自身业务合理设置
|
||||
}
|
||||
}
|
||||
|
||||
// 步骤3:监听嵌入子页面的事件
|
||||
window.addEventListener('message', function(e){
|
||||
console.log(e.data.msg);
|
||||
if(e && e.data){
|
||||
switch (e.data.action){
|
||||
case 'updateTitle': // 本页面收到子页面通知更新标题通知,更新本页面标题
|
||||
document.title = e.data.info;
|
||||
break;
|
||||
case 'updatePos':
|
||||
var scrollLeftValue = document.documentElement.scrollLeft;
|
||||
var scrollTopValue = document.documentElement.scrollTop;
|
||||
iframeWin.contentWindow.postMessage({
|
||||
action:'updatePos',
|
||||
msg: '更新Pos',
|
||||
scrollValue: { // 滚动条滚动的偏移量
|
||||
left: -1 * scrollLeftValue,
|
||||
top: -1 * scrollTopValue,
|
||||
}
|
||||
}, '\*'); // '\*'表示跨域参数,请结合自身业务合理设置
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 步骤4:兼听本页面的resize事件,并将一些状态值发送给嵌入的子页面
|
||||
var resizeTimer = null;
|
||||
var resizeDate;
|
||||
$(window).resize(function () {
|
||||
resizeDate = new Date();
|
||||
if (resizeTimer === null){
|
||||
resizeTimer = setTimeout(checkResizeEndTimer, 100);
|
||||
}
|
||||
});
|
||||
|
||||
function checkResizeEndTimer(){
|
||||
if (new Date() - resizeDate > 100){ // resize结束后再发消息,避免残影问题
|
||||
clearTimeout(resizeTimer);
|
||||
resizeTimer = null;
|
||||
postResizeEvent();
|
||||
} else{
|
||||
setTimeout(checkResizeEndTimer, 100);
|
||||
}
|
||||
}
|
||||
|
||||
function postResizeEvent(){
|
||||
iframeWin.contentWindow.postMessage({
|
||||
action: 'resize',
|
||||
msg: 'resize事件',
|
||||
showSize: { // 告诉嵌入的子页面视窗高度与宽度
|
||||
width: $(window).width(),
|
||||
height: $(window).height()
|
||||
},
|
||||
iframeClientPos: { // iframe相对视窗的位置
|
||||
left: iframeWin.getBoundingClientRect().left,
|
||||
right: iframeWin.getBoundingClientRect().right,
|
||||
top: iframeWin.getBoundingClientRect().top,
|
||||
bottom: iframeWin.getBoundingClientRect().bottom
|
||||
},
|
||||
iframeOffset: { // iframe偏离文档的位置
|
||||
left: iframeWin.offsetLeft,
|
||||
top: iframeWin.offsetTop
|
||||
}
|
||||
}, '\*'); // '\*'表示跨域参数,请结合自身业务合理设置
|
||||
}
|
||||
|
||||
// 步骤5:兼听本页面的scroll事件,并将一些状态值发送给嵌入的子页面
|
||||
// 为性能考虑,可以在定时器中处理
|
||||
var scrollTimer = null;
|
||||
var scrollDate;
|
||||
$(window).scroll(function (event) {
|
||||
postScrollEvent();
|
||||
scrollDate = new Date();
|
||||
if (scrollTimer === null){
|
||||
scrollTimer = setTimeout(checkScrollEndTimer, 100);
|
||||
}
|
||||
});
|
||||
|
||||
function checkScrollEndTimer(){
|
||||
if (new Date() - scrollDate > 100){ // resize结束后再发消息,避免残影问题
|
||||
clearTimeout(scrollTimer);
|
||||
scrollTimer = null;
|
||||
} else{
|
||||
postScrollEvent();
|
||||
setTimeout(checkScrollEndTimer, 100);
|
||||
}
|
||||
}
|
||||
|
||||
function postScrollEvent(){
|
||||
// 计算滚动条偏移量
|
||||
var scrollLeftValue = document.documentElement.scrollLeft;
|
||||
var scrollTopValue = document.documentElement.scrollTop;
|
||||
iframeWin.contentWindow.postMessage({
|
||||
action:'scroll',
|
||||
msg: 'scroll事件',
|
||||
scrollValue: { // 滚动条滚动的偏移量
|
||||
left: -1 * scrollLeftValue,
|
||||
top: -1 * scrollTopValue,
|
||||
},
|
||||
iframeClientPos: { // iframe相对视窗的位置
|
||||
left: iframeWin.getBoundingClientRect().left,
|
||||
right: iframeWin.getBoundingClientRect().right,
|
||||
top: iframeWin.getBoundingClientRect().top,
|
||||
bottom: iframeWin.getBoundingClientRect().bottom
|
||||
},
|
||||
showSize: { // 告诉嵌入的子页面视窗高度与宽度
|
||||
width: $(window).width(), // 视窗宽度
|
||||
height: $(window).height() // 视窗高度
|
||||
},
|
||||
iframeOffset: { // iframe偏离文档的位置
|
||||
left: iframeWin.offsetLeft,
|
||||
top: iframeWin.offsetTop
|
||||
}
|
||||
}, '\*'); // '\*'表示跨域参数,请结合自身业务合理设置
|
||||
}
|
||||
|
||||
</script>
|
||||
</html>
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,341 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>playback</title>
|
||||
</head>
|
||||
<style>
|
||||
html, body {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
.playWnd {
|
||||
margin: 30px 0 0 400px;
|
||||
width: 1000px; /*播放容器的宽和高设定*/
|
||||
height: 600px;
|
||||
border: 1px solid red;
|
||||
}
|
||||
.operate {
|
||||
margin-top: 24px;
|
||||
}
|
||||
.operate::after {
|
||||
content: '';
|
||||
display: block;
|
||||
clear: both;
|
||||
}
|
||||
.module {
|
||||
float: left;
|
||||
width: 340px;
|
||||
/*min-height: 320px;*/
|
||||
margin-left: 16px;
|
||||
padding: 16px 8px;
|
||||
box-sizing: border-box;
|
||||
border: 1px solid #e5e5e5;
|
||||
}
|
||||
.module .item {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.module input[type="text"] {
|
||||
box-sizing: border-box;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
margin-left: 0;
|
||||
width: 150px;
|
||||
min-height: 20px;
|
||||
}
|
||||
.module .btn {
|
||||
min-width: 80px;
|
||||
min-height: 24px;
|
||||
margin-top: 100px;
|
||||
margin-left: 80px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<body>
|
||||
<!--回放界面-->
|
||||
<div id="operate" class="operate">
|
||||
<div class="module">
|
||||
<div class="item"><span class="label">监控点编号:</span><input id="cameraIndexCode" type="text" value=""></div>
|
||||
<div class="item"><span class="label">回放开始时间:</span><input id="startTimeStamp" type="text" placeholder="yyyy-MM-dd hh:mm:ss格式"></div>
|
||||
<div class="item"><span class="label">回放结束时间:</span><input id="endTimeStamp" type="text" placeholder="yyyy-MM-dd hh:mm:ss格式"></div>
|
||||
<div class="item" style="margin-top: 20px;margin-left: -20px;">
|
||||
|
||||
<button style="width:90px;padding:0;margin:0;" id="startPlayback" class="btn">回放</button>
|
||||
|
||||
<button style="width:90px;padding:0;margin:0;" id="stopAllPlayback" class="btn">停止全部回放</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="margin-left: 15px;">注意: 开始回放前请打开该文件的源代码,将appkey、录像存储位置等信息修改为实际项目中的信息</div>
|
||||
|
||||
<!--视频窗口展示-->
|
||||
<div id="playWnd" class="playWnd" style="left: 109px; top: 133px;"></div>
|
||||
</body>
|
||||
|
||||
<!--三个必要的js文件引入-->
|
||||
<script src="jquery-1.12.4.min.js"></script>
|
||||
<script src="jsencrypt.min.js"></script>
|
||||
<script src="web-control_1.2.7.min.js"></script>
|
||||
|
||||
<script type="text/javascript">
|
||||
|
||||
//页面加载时创建播放实例初始化
|
||||
$(window).load(function () {
|
||||
initPlugin();
|
||||
});
|
||||
|
||||
//声明公用变量
|
||||
var initCount = 0;
|
||||
var pubKey = '';
|
||||
|
||||
// 创建WebControl实例与启动插件
|
||||
function initPlugin () {
|
||||
oWebControl = new WebControl({
|
||||
szPluginContainer: "playWnd", //指定容器id
|
||||
iServicePortStart: 15900, //指定起止端口号,建议使用该值
|
||||
iServicePortEnd: 15900,
|
||||
cbConnectSuccess: function () {
|
||||
// setCallbacks();
|
||||
//实例创建成功后需要启动服务
|
||||
oWebControl.JS_StartService("window", {
|
||||
dllPath: "./VideoPluginConnect.dll"
|
||||
}).then(function () {
|
||||
oWebControl.JS_SetWindowControlCallback({ // 设置消息回调
|
||||
cbIntegrationCallBack: cbIntegrationCallBack
|
||||
});
|
||||
|
||||
oWebControl.JS_CreateWnd("playWnd", 1000, 600).then(function () { //JS_CreateWnd创建视频播放窗口,宽高可设定
|
||||
console.log("JS_CreateWnd success");
|
||||
init(); //创建播放实例成功后初始化
|
||||
});
|
||||
}, function () {
|
||||
|
||||
});
|
||||
},
|
||||
cbConnectError: function () {
|
||||
console.log("cbConnectError");
|
||||
oWebControl = null;
|
||||
$("#playWnd").html("插件未启动,正在尝试启动,请稍候...");
|
||||
WebControl.JS_WakeUp("VideoWebPlugin://"); //程序未启动时执行error函数,采用wakeup来启动程序
|
||||
initCount ++;
|
||||
if (initCount < 3) {
|
||||
setTimeout(function () {
|
||||
initPlugin();
|
||||
}, 3000)
|
||||
} else {
|
||||
$("#playWnd").html("插件启动失败,请检查插件是否安装!");
|
||||
}
|
||||
},
|
||||
cbConnectClose: function () {
|
||||
console.log("cbConnectClose");
|
||||
oWebControl = null;
|
||||
$("#playWnd").html("插件未启动,正在尝试启动,请稍候...");
|
||||
WebControl.JS_WakeUp("VideoWebPlugin://");
|
||||
initCount ++;
|
||||
if (initCount < 3) {
|
||||
setTimeout(function () {
|
||||
initPlugin();
|
||||
}, 3000)
|
||||
} else {
|
||||
$("#playWnd").html("插件启动失败,请检查插件是否安装!");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 推送消息
|
||||
function cbIntegrationCallBack(oData) {
|
||||
console.log(JSON.stringify(oData.responseMsg));
|
||||
}
|
||||
|
||||
//初始化
|
||||
function init()
|
||||
{
|
||||
getPubKey(function () {
|
||||
|
||||
////////////////////////////////// 请自行修改以下变量值 ////////////////////////////////////
|
||||
var appkey = "28730366"; //综合安防管理平台提供的appkey,必填
|
||||
var secret = setEncrypt("HSZkCJpSJ7gSUYrO6wVi"); //综合安防管理平台提供的secret,必填
|
||||
var ip = "10.19.132.75"; //综合安防管理平台IP地址,必填
|
||||
var playMode = 1; //初始播放模式:0-预览,1-回放
|
||||
var port = 443; //综合安防管理平台端口,若启用HTTPS协议,默认443
|
||||
var snapDir = "D:\\SnapDir"; //抓图存储路径
|
||||
var videoDir = "D:\\VideoDir"; //紧急录像或录像剪辑存储路径
|
||||
var layout = "1x1"; //playMode指定模式的布局
|
||||
var enableHTTPS = 1; //是否启用HTTPS协议与综合安防管理平台交互,这里总是填1
|
||||
var encryptedFields = 'secret'; //加密字段,默认加密领域为secret
|
||||
var showToolbar = 1; //是否显示工具栏,0-不显示,非0-显示
|
||||
var showSmart = 1; //是否显示智能信息(如配置移动侦测后画面上的线框),0-不显示,非0-显示
|
||||
var buttonIDs = "0,16,256,257,258,259,260,512,513,514,515,516,517,768,769"; //自定义工具条按钮
|
||||
//var reconnectTimes = 2; // 重连次数,回放异常情况下有效
|
||||
//var reconnectTime = 4; // 每次重连的重连间隔 >= reconnectTime
|
||||
////////////////////////////////// 请自行修改以上变量值 ////////////////////////////////////
|
||||
|
||||
oWebControl.JS_RequestInterface({
|
||||
funcName: "init",
|
||||
argument: JSON.stringify({
|
||||
appkey: appkey, //API网关提供的appkey
|
||||
secret: secret, //API网关提供的secret
|
||||
ip: ip, //API网关IP地址
|
||||
playMode: playMode, //播放模式(决定显示预览还是回放界面)
|
||||
port: port, //端口
|
||||
snapDir: snapDir, //抓图存储路径
|
||||
videoDir: videoDir, //紧急录像或录像剪辑存储路径
|
||||
layout: layout, //布局
|
||||
enableHTTPS: enableHTTPS, //是否启用HTTPS协议
|
||||
encryptedFields: encryptedFields, //加密字段
|
||||
showToolbar: showToolbar, //是否显示工具栏
|
||||
showSmart: showSmart, //是否显示智能信息
|
||||
buttonIDs: buttonIDs //自定义工具条按钮
|
||||
//reconnectTimes:reconnectTimes, //重连次数
|
||||
//reconnectDuration:reconnectTime //重连间隔
|
||||
})
|
||||
}).then(function (oData) {
|
||||
oWebControl.JS_Resize(1000, 600); // 初始化后resize一次,规避firefox下首次显示窗口后插件窗口未与DIV窗口重合问题
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 获取公钥
|
||||
function getPubKey (callback) {
|
||||
oWebControl.JS_RequestInterface({
|
||||
funcName: "getRSAPubKey",
|
||||
argument: JSON.stringify({
|
||||
keyLength: 1024
|
||||
})
|
||||
}).then(function (oData) {
|
||||
console.log(oData);
|
||||
if (oData.responseMsg.data) {
|
||||
pubKey = oData.responseMsg.data;
|
||||
callback()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// RSA加密
|
||||
function setEncrypt (value) {
|
||||
var encrypt = new JSEncrypt();
|
||||
encrypt.setPublicKey(pubKey);
|
||||
return encrypt.encrypt(value);
|
||||
}
|
||||
|
||||
// 监听resize事件,使插件窗口尺寸跟随DIV窗口变化
|
||||
$(window).resize(function () {
|
||||
if (oWebControl != null) {
|
||||
oWebControl.JS_Resize(1000, 600);
|
||||
setWndCover();
|
||||
}
|
||||
});
|
||||
|
||||
// 监听滚动条scroll事件,使插件窗口跟随浏览器滚动而移动
|
||||
$(window).scroll(function () {
|
||||
if (oWebControl != null) {
|
||||
oWebControl.JS_Resize(1000, 600);
|
||||
setWndCover();
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// 设置窗口裁剪,当因滚动条滚动导致窗口需要被遮住的情况下需要JS_CuttingPartWindow部分窗口
|
||||
function setWndCover() {
|
||||
var iWidth = $(window).width();
|
||||
var iHeight = $(window).height();
|
||||
var oDivRect = $("#playWnd").get(0).getBoundingClientRect();
|
||||
|
||||
var iCoverLeft = (oDivRect.left < 0) ? Math.abs(oDivRect.left): 0;
|
||||
var iCoverTop = (oDivRect.top < 0) ? Math.abs(oDivRect.top): 0;
|
||||
var iCoverRight = (oDivRect.right - iWidth > 0) ? Math.round(oDivRect.right - iWidth) : 0;
|
||||
var iCoverBottom = (oDivRect.bottom - iHeight > 0) ? Math.round(oDivRect.bottom - iHeight) : 0;
|
||||
|
||||
iCoverLeft = (iCoverLeft > 1000) ? 1000 : iCoverLeft;
|
||||
iCoverTop = (iCoverTop > 600) ? 600 : iCoverTop;
|
||||
iCoverRight = (iCoverRight > 1000) ? 1000 : iCoverRight;
|
||||
iCoverBottom = (iCoverBottom > 600) ? 600 : iCoverBottom;
|
||||
|
||||
oWebControl.JS_RepairPartWindow(0, 0, 1001, 600); // 多1个像素点防止还原后边界缺失一个像素条
|
||||
if (iCoverLeft != 0) {
|
||||
oWebControl.JS_CuttingPartWindow(0, 0, iCoverLeft, 600);
|
||||
}
|
||||
if (iCoverTop != 0) {
|
||||
oWebControl.JS_CuttingPartWindow(0, 0, 1001, iCoverTop); // 多剪掉一个像素条,防止出现剪掉一部分窗口后出现一个像素条
|
||||
}
|
||||
if (iCoverRight != 0) {
|
||||
oWebControl.JS_CuttingPartWindow(1000 - iCoverRight, 0, iCoverRight, 600);
|
||||
}
|
||||
if (iCoverBottom != 0) {
|
||||
oWebControl.JS_CuttingPartWindow(0, 600 - iCoverBottom, 1000, iCoverBottom);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//录像回放功能
|
||||
$("#startPlayback").click(function () {
|
||||
|
||||
var cameraIndexCode = $("#cameraIndexCode").val(); //获取输入的监控点编号值,必填
|
||||
var startTimeStamp = new Date($("#startTimeStamp").val().replace('-', '/').replace('-', '/')).getTime(); //回放开始时间戳,必填
|
||||
var endTimeStamp = new Date($("#endTimeStamp").val().replace('-', '/').replace('-', '/')).getTime(); //回放结束时间戳,必填
|
||||
var recordLocation = 0; //录像存储位置:0-中心存储,1-设备存储
|
||||
var transMode = 1; //传输协议:0-UDP,1-TCP
|
||||
var gpuMode = 0; //是否启用GPU硬解,0-不启用,1-启用
|
||||
var wndId = -1; //播放窗口序号(在2x2以上布局下可指定播放窗口)
|
||||
|
||||
oWebControl.JS_RequestInterface({
|
||||
funcName: "startPlayback",
|
||||
argument: JSON.stringify({
|
||||
cameraIndexCode: cameraIndexCode, //监控点编号
|
||||
startTimeStamp: Math.floor(startTimeStamp / 1000).toString(), //录像查询开始时间戳,单位:秒
|
||||
endTimeStamp: Math.floor(endTimeStamp / 1000).toString(), //录像结束开始时间戳,单位:秒
|
||||
recordLocation: recordLocation, //录像存储类型:0-中心存储,1-设备存储
|
||||
transMode: transMode, //传输协议:0-UDP,1-TCP
|
||||
gpuMode: gpuMode, //是否启用GPU硬解,0-不启用,1-启用
|
||||
wndId:wndId //可指定播放窗口
|
||||
})
|
||||
})
|
||||
});
|
||||
|
||||
// 停止回放
|
||||
$("#stopAllPlayback").click(function () {
|
||||
oWebControl.JS_RequestInterface({
|
||||
funcName: "stopAllPlayback"
|
||||
})
|
||||
});
|
||||
|
||||
//设置录像回放时间的默认值
|
||||
var endTime = dateFormat(new Date(), "yyyy-MM-dd 23:59:59");
|
||||
var startTime = dateFormat(new Date(), "yyyy-MM-dd 00:00:00");
|
||||
$("#startTimeStamp").val(startTime);
|
||||
$("#endTimeStamp").val(endTime);
|
||||
|
||||
// 格式化时间
|
||||
function dateFormat(oDate, fmt) {
|
||||
var o = {
|
||||
"M+": oDate.getMonth() + 1, //月份
|
||||
"d+": oDate.getDate(), //日
|
||||
"h+": oDate.getHours(), //小时
|
||||
"m+": oDate.getMinutes(), //分
|
||||
"s+": oDate.getSeconds(), //秒
|
||||
"q+": Math.floor((oDate.getMonth() + 3) / 3), //季度
|
||||
"S": oDate.getMilliseconds()//毫秒
|
||||
};
|
||||
if (/(y+)/.test(fmt)) {
|
||||
fmt = fmt.replace(RegExp.$1, (oDate.getFullYear() + "").substr(4 - RegExp.$1.length));
|
||||
}
|
||||
for (var k in o) {
|
||||
if (new RegExp("(" + k + ")").test(fmt)) {
|
||||
fmt = fmt.replace(RegExp.$1, (RegExp.$1.length == 1) ? (o[k]) : (("00" + o[k]).substr(("" + o[k]).length)));
|
||||
}
|
||||
}
|
||||
return fmt;
|
||||
}
|
||||
|
||||
// 标签关闭
|
||||
$(window).unload(function () {
|
||||
if (oWebControl != null){
|
||||
oWebControl.JS_HideWnd(); // 先让窗口隐藏,规避插件窗口滞后于浏览器消失问题
|
||||
oWebControl.JS_Disconnect().then(function(){}, function() {});
|
||||
}
|
||||
});
|
||||
|
||||
</script>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,309 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>preview_demo</title>
|
||||
</head>
|
||||
<style>
|
||||
html, body {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
.playWnd {
|
||||
margin: 30px 0 0 400px;
|
||||
width: 1000px; /*播放容器的宽和高设定*/
|
||||
height: 600px;
|
||||
border: 1px solid red;
|
||||
}
|
||||
.operate {
|
||||
margin-top: 24px;
|
||||
}
|
||||
.operate::after {
|
||||
content: '';
|
||||
display: block;
|
||||
clear: both;
|
||||
}
|
||||
.module {
|
||||
float: left;
|
||||
width: 340px;
|
||||
/*min-height: 320px;*/
|
||||
margin-left: 16px;
|
||||
padding: 16px 8px;
|
||||
box-sizing: border-box;
|
||||
border: 1px solid #e5e5e5;
|
||||
}
|
||||
.module .item {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.module input[type="text"] {
|
||||
box-sizing: border-box;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
margin-left: 0;
|
||||
width: 150px;
|
||||
min-height: 20px;
|
||||
}
|
||||
.module .btn {
|
||||
min-width: 80px;
|
||||
min-height: 24px;
|
||||
margin-top: 100px;
|
||||
margin-left: 80px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<body>
|
||||
<!--预览界面-->
|
||||
<div id="operate" class="operate">
|
||||
<div class="module">
|
||||
<div class="item"><span class="label">监控点编号:</span><input id="cameraIndexCode" type="text" value=""></div>
|
||||
<div class="item" style="margin-top: 20px;margin-left: -20px;">
|
||||
|
||||
<button style="width:20px;padding:0;margin:0;" id="startPreview" class="btn">预览</button>
|
||||
|
||||
<button style="width:90px;padding:0;margin:0;" id="stopAllPreview" class="btn">停止全部预览</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!--视频窗口展示-->
|
||||
<div id="playWnd" class="playWnd" style="left: 109px; top: 133px;"></div>
|
||||
</body>
|
||||
|
||||
<!--三个必要的js文件引入-->
|
||||
<script src="jquery-1.12.4.min.js"></script>
|
||||
<script src="jsencrypt.min.js"></script> <!-- 用于RSA加密 -->
|
||||
<script src="web-control_1.2.7.min.js"></script> <!-- 用于前端与插件交互 -->
|
||||
|
||||
<script type="text/javascript">
|
||||
|
||||
//页面加载时创建播放实例初始化
|
||||
$(window).load(function () {
|
||||
initPlugin();
|
||||
});
|
||||
|
||||
//声明公用变量
|
||||
var initCount = 0;
|
||||
var pubKey = '';
|
||||
|
||||
// 创建播放实例
|
||||
function initPlugin () {
|
||||
oWebControl = new WebControl({
|
||||
szPluginContainer: "playWnd", // 指定容器id
|
||||
iServicePortStart: 15900, // 指定起止端口号,建议使用该值
|
||||
iServicePortEnd: 15900,
|
||||
szClassId:"23BF3B0A-2C56-4D97-9C03-0CB103AA8F11", // 用于IE10使用ActiveX的clsid
|
||||
cbConnectSuccess: function () { // 创建WebControl实例成功
|
||||
oWebControl.JS_StartService("window", { // WebControl实例创建成功后需要启动服务
|
||||
dllPath: "./VideoPluginConnect.dll" // 值"./VideoPluginConnect.dll"写死
|
||||
}).then(function () { // 启动插件服务成功
|
||||
oWebControl.JS_SetWindowControlCallback({ // 设置消息回调
|
||||
cbIntegrationCallBack: cbIntegrationCallBack
|
||||
});
|
||||
|
||||
oWebControl.JS_CreateWnd("playWnd", 1000, 600).then(function () { //JS_CreateWnd创建视频播放窗口,宽高可设定
|
||||
init(); // 创建播放实例成功后初始化
|
||||
});
|
||||
}, function () { // 启动插件服务失败
|
||||
});
|
||||
},
|
||||
cbConnectError: function () { // 创建WebControl实例失败
|
||||
oWebControl = null;
|
||||
$("#playWnd").html("插件未启动,正在尝试启动,请稍候...");
|
||||
WebControl.JS_WakeUp("VideoWebPlugin://"); // 程序未启动时执行error函数,采用wakeup来启动程序
|
||||
initCount ++;
|
||||
if (initCount < 3) {
|
||||
setTimeout(function () {
|
||||
initPlugin();
|
||||
}, 3000)
|
||||
} else {
|
||||
$("#playWnd").html("插件启动失败,请检查插件是否安装!");
|
||||
}
|
||||
},
|
||||
cbConnectClose: function (bNormalClose) {
|
||||
// 异常断开:bNormalClose = false
|
||||
// JS_Disconnect正常断开:bNormalClose = true
|
||||
console.log("cbConnectClose");
|
||||
oWebControl = null;
|
||||
$("#playWnd").html("插件未启动,正在尝试启动,请稍候...");
|
||||
WebControl.JS_WakeUp("VideoWebPlugin://");
|
||||
initCount ++;
|
||||
if (initCount < 3) {
|
||||
setTimeout(function () {
|
||||
initPlugin();
|
||||
}, 3000)
|
||||
} else {
|
||||
$("#playWnd").html("插件启动失败,请检查插件是否安装!");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 设置窗口控制回调
|
||||
function setCallbacks() {
|
||||
oWebControl.JS_SetWindowControlCallback({
|
||||
cbIntegrationCallBack: cbIntegrationCallBack
|
||||
});
|
||||
}
|
||||
|
||||
// 推送消息
|
||||
function cbIntegrationCallBack(oData) {
|
||||
showCBInfo(JSON.stringify(oData.responseMsg));
|
||||
}
|
||||
|
||||
//初始化
|
||||
function init()
|
||||
{
|
||||
getPubKey(function () {
|
||||
|
||||
////////////////////////////////// 请自行修改以下变量值 ////////////////////////////////////
|
||||
var appkey = "28730366"; //综合安防管理平台提供的appkey,必填
|
||||
var secret = setEncrypt("HSZkCJpSJ7gSUYrO6wVi"); //综合安防管理平台提供的secret,必填
|
||||
var ip = "10.19.132.75"; //综合安防管理平台IP地址,必填
|
||||
var playMode = 0; //初始播放模式:0-预览,1-回放
|
||||
var port = 443; //综合安防管理平台端口,若启用HTTPS协议,默认443
|
||||
var snapDir = "D:\\SnapDir"; //抓图存储路径
|
||||
var videoDir = "D:\\VideoDir"; //紧急录像或录像剪辑存储路径
|
||||
var layout = "1x1"; //playMode指定模式的布局
|
||||
var enableHTTPS = 1; //是否启用HTTPS协议与综合安防管理平台交互,这里总是填1
|
||||
var encryptedFields = 'secret'; //加密字段,默认加密领域为secret
|
||||
var showToolbar = 1; //是否显示工具栏,0-不显示,非0-显示
|
||||
var showSmart = 1; //是否显示智能信息(如配置移动侦测后画面上的线框),0-不显示,非0-显示
|
||||
var buttonIDs = "0,16,256,257,258,259,260,512,513,514,515,516,517,768,769"; //自定义工具条按钮
|
||||
////////////////////////////////// 请自行修改以上变量值 ////////////////////////////////////
|
||||
|
||||
oWebControl.JS_RequestInterface({
|
||||
funcName: "init",
|
||||
argument: JSON.stringify({
|
||||
appkey: appkey, //API网关提供的appkey
|
||||
secret: secret, //API网关提供的secret
|
||||
ip: ip, //API网关IP地址
|
||||
playMode: playMode, //播放模式(决定显示预览还是回放界面)
|
||||
port: port, //端口
|
||||
snapDir: snapDir, //抓图存储路径
|
||||
videoDir: videoDir, //紧急录像或录像剪辑存储路径
|
||||
layout: layout, //布局
|
||||
enableHTTPS: enableHTTPS, //是否启用HTTPS协议
|
||||
encryptedFields: encryptedFields, //加密字段
|
||||
showToolbar: showToolbar, //是否显示工具栏
|
||||
showSmart: showSmart, //是否显示智能信息
|
||||
buttonIDs: buttonIDs //自定义工具条按钮
|
||||
})
|
||||
}).then(function (oData) {
|
||||
oWebControl.JS_Resize(1000, 600); // 初始化后resize一次,规避firefox下首次显示窗口后插件窗口未与DIV窗口重合问题
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
//获取公钥
|
||||
function getPubKey (callback) {
|
||||
oWebControl.JS_RequestInterface({
|
||||
funcName: "getRSAPubKey",
|
||||
argument: JSON.stringify({
|
||||
keyLength: 1024
|
||||
})
|
||||
}).then(function (oData) {
|
||||
console.log(oData);
|
||||
if (oData.responseMsg.data) {
|
||||
pubKey = oData.responseMsg.data;
|
||||
callback()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
//RSA加密
|
||||
function setEncrypt (value) {
|
||||
var encrypt = new JSEncrypt();
|
||||
encrypt.setPublicKey(pubKey);
|
||||
return encrypt.encrypt(value);
|
||||
}
|
||||
|
||||
// 监听resize事件,使插件窗口尺寸跟随DIV窗口变化
|
||||
$(window).resize(function () {
|
||||
if (oWebControl != null) {
|
||||
oWebControl.JS_Resize(1000, 600);
|
||||
setWndCover();
|
||||
}
|
||||
});
|
||||
|
||||
// 监听滚动条scroll事件,使插件窗口跟随浏览器滚动而移动
|
||||
$(window).scroll(function () {
|
||||
if (oWebControl != null) {
|
||||
oWebControl.JS_Resize(1000, 600);
|
||||
setWndCover();
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// 设置窗口裁剪,当因滚动条滚动导致窗口需要被遮住的情况下需要JS_CuttingPartWindow部分窗口
|
||||
function setWndCover() {
|
||||
var iWidth = $(window).width();
|
||||
var iHeight = $(window).height();
|
||||
var oDivRect = $("#playWnd").get(0).getBoundingClientRect();
|
||||
|
||||
var iCoverLeft = (oDivRect.left < 0) ? Math.abs(oDivRect.left): 0;
|
||||
var iCoverTop = (oDivRect.top < 0) ? Math.abs(oDivRect.top): 0;
|
||||
var iCoverRight = (oDivRect.right - iWidth > 0) ? Math.round(oDivRect.right - iWidth) : 0;
|
||||
var iCoverBottom = (oDivRect.bottom - iHeight > 0) ? Math.round(oDivRect.bottom - iHeight) : 0;
|
||||
|
||||
iCoverLeft = (iCoverLeft > 1000) ? 1000 : iCoverLeft;
|
||||
iCoverTop = (iCoverTop > 600) ? 600 : iCoverTop;
|
||||
iCoverRight = (iCoverRight > 1000) ? 1000 : iCoverRight;
|
||||
iCoverBottom = (iCoverBottom > 600) ? 600 : iCoverBottom;
|
||||
|
||||
oWebControl.JS_RepairPartWindow(0, 0, 1001, 600); // 多1个像素点防止还原后边界缺失一个像素条
|
||||
if (iCoverLeft != 0) {
|
||||
oWebControl.JS_CuttingPartWindow(0, 0, iCoverLeft, 600);
|
||||
}
|
||||
if (iCoverTop != 0) {
|
||||
oWebControl.JS_CuttingPartWindow(0, 0, 1001, iCoverTop); // 多剪掉一个像素条,防止出现剪掉一部分窗口后出现一个像素条
|
||||
}
|
||||
if (iCoverRight != 0) {
|
||||
oWebControl.JS_CuttingPartWindow(1000 - iCoverRight, 0, iCoverRight, 600);
|
||||
}
|
||||
if (iCoverBottom != 0) {
|
||||
oWebControl.JS_CuttingPartWindow(0, 600 - iCoverBottom, 1000, iCoverBottom);
|
||||
}
|
||||
}
|
||||
|
||||
//视频预览功能
|
||||
$("#startPreview").click(function () {
|
||||
var cameraIndexCode = $("#cameraIndexCode").val(); //获取输入的监控点编号值,必填
|
||||
var streamMode = 0; //主子码流标识:0-主码流,1-子码流
|
||||
var transMode = 1; //传输协议:0-UDP,1-TCP
|
||||
var gpuMode = 0; //是否启用GPU硬解,0-不启用,1-启用
|
||||
var wndId = -1; //播放窗口序号(在2x2以上布局下可指定播放窗口)
|
||||
|
||||
cameraIndexCode = cameraIndexCode.replace(/(^\s*)/g, "");
|
||||
cameraIndexCode = cameraIndexCode.replace(/(\s*$)/g, "");
|
||||
|
||||
oWebControl.JS_RequestInterface({
|
||||
funcName: "startPreview",
|
||||
argument: JSON.stringify({
|
||||
cameraIndexCode:cameraIndexCode, //监控点编号
|
||||
streamMode: streamMode, //主子码流标识
|
||||
transMode: transMode, //传输协议
|
||||
gpuMode: gpuMode, //是否开启GPU硬解
|
||||
wndId:wndId //可指定播放窗口
|
||||
})
|
||||
})
|
||||
});
|
||||
|
||||
//停止全部预览
|
||||
$("#stopAllPreview").click(function () {
|
||||
oWebControl.JS_RequestInterface({
|
||||
funcName: "stopAllPreview"
|
||||
});
|
||||
});
|
||||
|
||||
// 标签关闭
|
||||
$(window).unload(function () {
|
||||
if (oWebControl != null){
|
||||
oWebControl.JS_HideWnd(); // 先让窗口隐藏,规避可能的插件窗口滞后于浏览器消失问题
|
||||
oWebControl.JS_Disconnect().then(function(){ // 断开与插件服务连接成功
|
||||
},
|
||||
function() { // 断开与插件服务连接失败
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
</script>
|
||||
</html>
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,24 @@
|
|||
# 海康 VideoWebPlugin JS 文件放置说明
|
||||
|
||||
请将从海康官方下载的 **视频WEB插件** 包中以下文件复制到此目录:
|
||||
|
||||
| 文件名 | 说明 |
|
||||
|--------|------|
|
||||
| `jsWebControl-1.0.0.min.js` | 海康 VideoWebPlugin 核心 JS(必须) |
|
||||
| `jsencrypt.min.js` | RSA加密库,用于加密 AppSecret(必须) |
|
||||
| `jquery-3.6.0.min.js` | jQuery(可从 CDN 下载或使用插件包内版本) |
|
||||
|
||||
## 下载地址
|
||||
|
||||
海康开放平台工具下载:
|
||||
https://open.hikvision.com/download/5c67f1e2f05948198c909700?type=10
|
||||
|
||||
下载「视频WEB插件」后解压,将 `demo/` 目录下的对应 JS 文件复制到此目录。
|
||||
|
||||
## 客户端安装
|
||||
|
||||
还需在运行浏览器的 Windows 电脑上安装 `bin/VideoWebPlugin.exe`,
|
||||
安装后插件会在后台以本地服务方式运行(端口 15900-15909),
|
||||
浏览器通过 WebSocket 与其通信实现视频播放。
|
||||
|
||||
> 注意:VideoWebPlugin 仅支持 Windows 系统。
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Binary file not shown.
|
|
@ -0,0 +1,705 @@
|
|||
/**
|
||||
* Created by wangweijie5 on 2016/12/5.
|
||||
*/
|
||||
(function (event) {
|
||||
const AUDIO_TYPE = 0; // 音频
|
||||
const VIDEO_TYPE = 1; // 视频
|
||||
const PRIVT_TYPE = 2; // 私有帧
|
||||
|
||||
const PLAYM4_AUDIO_FRAME = 100; // 音频帧
|
||||
const PLAYM4_VIDEO_FRAME = 101; // 视频帧
|
||||
|
||||
const PLAYM4_OK = 1;
|
||||
const PLAYM4_ORDER_ERROR = 2;
|
||||
const PLAYM4_DECODE_ERROR = 44 // 解码失败
|
||||
const PLAYM4_NOT_KEYFRAME = 48; // 非关键帧
|
||||
const PLAYM4_NEED_MORE_DATA = 31; // 需要更多数据才能解析
|
||||
const PLAYM4_NEED_NEET_LOOP = 35; //丢帧需要下个循环
|
||||
const PLAYM4_SYS_NOT_SUPPORT = 16; // 不支持
|
||||
|
||||
// importScripts('Decoder.js');
|
||||
// Module.addOnPostRun(function () {
|
||||
// postMessage({ 'function': "loaded" });
|
||||
// });
|
||||
|
||||
var iStreamMode = 0; // 流模式
|
||||
|
||||
var bOpenMode = false;
|
||||
var bOpenStream = false;
|
||||
|
||||
var funGetFrameData = null;
|
||||
var funGetAudFrameData = null;
|
||||
|
||||
var bWorkerPrintLog = false;//worker层log开关
|
||||
|
||||
var g_nPort = -1;
|
||||
var pInputData = null;
|
||||
var inputBufferSize = 40960;
|
||||
|
||||
self.JSPlayM4_RunTimeInfoCallBack = function (nPort, pstRunTimeInfo, pUser) {
|
||||
let port = nPort;
|
||||
let user = pUser;
|
||||
let nRunTimeModule = Module.HEAP32[pstRunTimeInfo >> 2];
|
||||
let nStrVersion = Module.HEAP32[pstRunTimeInfo + 4 >> 2];
|
||||
let nFrameTimeStamp = Module.HEAP32[pstRunTimeInfo + 8 >> 2];
|
||||
let nFrameNum = Module.HEAP32[pstRunTimeInfo + 12 >> 2];
|
||||
let nErrorCode = Module.HEAP32[pstRunTimeInfo + 16 >> 2];
|
||||
// console.log("nRunTimeModule:"+nRunTimeModule+",nFrameNum:"+nFrameNum+",nErrorCode:"+nErrorCode);
|
||||
postMessage({ 'function': "RunTimeInfoCallBack", 'nRunTimeModule': nRunTimeModule, 'nStrVersion': nStrVersion, 'nFrameTimeStamp': nFrameTimeStamp, 'nFrameNum': nFrameNum, 'nErrorCode': nErrorCode });
|
||||
}
|
||||
|
||||
self.JSPlayM4_StreamInfoCallBack = function (nPort, pstStreamInfo, pUser)
|
||||
{
|
||||
let port = nPort;
|
||||
let user = pUser;
|
||||
let nSystemformat = Module.HEAP16[pstStreamInfo >> 1]; //封装类型
|
||||
let nVideoformat = Module.HEAP16[pstStreamInfo + 2 >> 1];//视频编码类型
|
||||
let nAudioformat = Module.HEAP16[pstStreamInfo + 4 >> 1];//音频编码类型
|
||||
let nAudiochannels = Module.HEAP16[pstStreamInfo + 6 >> 1]; //音频通道数
|
||||
let nAudiobitspersample = Module.HEAP32[pstStreamInfo + 8 >> 2];//音频样位率
|
||||
let nAudiosamplesrate = Module.HEAP32[pstStreamInfo + 12 >> 2];//音频采样率
|
||||
let nAudiobitrate = Module.HEAP32[pstStreamInfo + 16 >> 2];//音频比特率,单位:bit
|
||||
//console.log("nSystemformat:" + nSystemformat + ",nVideoformat:" + nVideoformat + ",nAudioformat:" + nAudioformat + ",nAudiochannels:" + nAudiochannels + ",nAudiobitspersample:" + nAudiobitspersample + ",nAudiosamplesrate:" + nAudiosamplesrate + ",nAudiobitrate:" + nAudiobitrate);
|
||||
postMessage({ 'function': "StreamInfoCallBack", 'nSystemformat': nSystemformat, 'nVideoformat': nVideoformat, 'nAudioformat': nAudioformat, 'nAudiochannels': nAudiochannels, 'nAudiobitspersample': nAudiobitspersample, 'nAudiosamplesrate': nAudiosamplesrate, 'nAudiobitrate': nAudiobitrate});
|
||||
}
|
||||
|
||||
onmessage = function (event) {
|
||||
var eventData = event.data;
|
||||
var res = 0;
|
||||
switch (eventData.command) {
|
||||
case "importScripts":
|
||||
const decodebase = eventData.data + "Decoder.js"
|
||||
importScripts(decodebase);
|
||||
Module.addOnPostRun(function () {
|
||||
postMessage({ 'function': "loaded" });
|
||||
});
|
||||
break;
|
||||
case "printLog":
|
||||
let downloadFlag = eventData.data;
|
||||
if (downloadFlag === true) {
|
||||
bWorkerPrintLog = true;
|
||||
res = Module._SetPrintLogFlag(g_nPort, downloadFlag);
|
||||
}
|
||||
else {
|
||||
bWorkerPrintLog = false;
|
||||
res = Module._SetPrintLogFlag(g_nPort, downloadFlag);
|
||||
}
|
||||
|
||||
if (res !== PLAYM4_OK) {
|
||||
console.log("DecodeWorker.js: PlayerSDK print log failed,res" + res);
|
||||
postMessage({ 'function': "printLog", 'errorCode': res });
|
||||
}
|
||||
break;
|
||||
case "SetPlayPosition":
|
||||
let nFrameNumOrTime = eventData.data;
|
||||
let enPosType = eventData.type;
|
||||
// res = Module._SetPlayPosition(nFrameNumOrTime,enPosType);
|
||||
// if (res !== PLAYM4_OK)
|
||||
// {
|
||||
// postMessage({'function': "SetPlayPosition", 'errorCode': res});
|
||||
// return;
|
||||
// }
|
||||
// //有没有buffer需要清除
|
||||
|
||||
break;
|
||||
case "SetStreamOpenMode":
|
||||
//获取端口号
|
||||
g_nPort = Module._GetPort();
|
||||
//设置流打开模式
|
||||
iStreamMode = eventData.data;
|
||||
res = Module._SetStreamOpenMode(g_nPort, iStreamMode);
|
||||
if (res !== PLAYM4_OK) {
|
||||
postMessage({ 'function': "SetStreamOpenMode", 'errorCode': res });
|
||||
return;
|
||||
}
|
||||
bOpenMode = true;
|
||||
break;
|
||||
|
||||
case "OpenStream":
|
||||
// 接收到的数据
|
||||
var iHeadLen = eventData.dataSize;
|
||||
var pHead = Module._malloc(iHeadLen + 4);
|
||||
if (pHead === null) {
|
||||
return;
|
||||
}
|
||||
var aHead = Module.HEAPU8.subarray(pHead, pHead + iHeadLen);
|
||||
aHead.set(new Uint8Array(eventData.data));
|
||||
res = Module._OpenStream(g_nPort, pHead, iHeadLen, eventData.bufPoolSize);
|
||||
postMessage({ 'function': "OpenStream", 'errorCode': res });
|
||||
if (res !== PLAYM4_OK) {
|
||||
//释放内存
|
||||
Module._free(pHead);
|
||||
pHead = null;
|
||||
return;
|
||||
}
|
||||
bOpenStream = true;
|
||||
break;
|
||||
case "Play":
|
||||
let resP = Module._Play(g_nPort);
|
||||
if (resP !== PLAYM4_OK) {
|
||||
return;
|
||||
}
|
||||
break;
|
||||
case "InputData":
|
||||
// 接收到的数据
|
||||
var iLen = eventData.dataSize;
|
||||
if (iLen > 0) {
|
||||
if (pInputData == null || iLen > inputBufferSize) {
|
||||
if (pInputData != null) {
|
||||
Module._free(pInputData);
|
||||
pInputData = null;
|
||||
}
|
||||
if (iLen > inputBufferSize) {
|
||||
inputBufferSize = iLen;
|
||||
}
|
||||
|
||||
pInputData = Module._malloc(inputBufferSize);
|
||||
if (pInputData === null) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
var inputData = new Uint8Array(eventData.data);
|
||||
// var aInputData = Module.HEAPU8.subarray(pInputData, pInputData + iLen);
|
||||
// aInputData.set(inputData);
|
||||
Module.writeArrayToMemory(inputData, pInputData);
|
||||
inputData = null;
|
||||
res = Module._InputData(g_nPort, pInputData, iLen);
|
||||
if (res !== PLAYM4_OK) {
|
||||
let errorCode = Module._GetLastError(g_nPort);
|
||||
let sourceRemain = Module._GetSourceBufferRemain(g_nPort);
|
||||
postMessage({ 'function': "InputData", 'errorCode': errorCode, "sourceRemain": sourceRemain });
|
||||
}
|
||||
//Module._free(pInputData);
|
||||
//pInputData = null;
|
||||
} else {
|
||||
let sourceRemain = Module._GetSourceBufferRemain(g_nPort);
|
||||
if (sourceRemain == 0) {
|
||||
postMessage({ 'function': "InputData", 'errorCode': PLAYM4_NEED_MORE_DATA });
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/////////////////////
|
||||
// if (funGetFrameData === null) {
|
||||
// funGetFrameData = Module.cwrap('GetFrameData', 'number');
|
||||
// }
|
||||
|
||||
while (bOpenMode && bOpenStream) {
|
||||
|
||||
var ret = getFrameData();
|
||||
// 直到获取视频帧或数据不足为止
|
||||
if (PLAYM4_VIDEO_FRAME === ret || PLAYM4_NEED_MORE_DATA === ret || PLAYM4_ORDER_ERROR === ret)//PLAYM4_VIDEO_FRAME === ret || || PLAYM4_NEED_NEET_LOOP === ret
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case "SetSecretKey":
|
||||
var keyLen = eventData.nKeyLen;
|
||||
var pKeyData = Module._malloc(keyLen);
|
||||
if (pKeyData === null) {
|
||||
return;
|
||||
}
|
||||
var nKeySize = eventData.data.length
|
||||
var bufData = stringToBytes(eventData.data);
|
||||
var aKeyData = Module.HEAPU8.subarray(pKeyData, pKeyData + keyLen);
|
||||
let u8array = new Uint8Array(keyLen);
|
||||
aKeyData.set(u8array, 0);
|
||||
aKeyData.set(new Uint8Array(bufData));
|
||||
aKeyData = null;
|
||||
u8array = null;
|
||||
|
||||
res = Module._SetSecretKey(g_nPort, eventData.nKeyType, pKeyData, keyLen);//, nKeySize
|
||||
if (res !== PLAYM4_OK) {
|
||||
postMessage({ 'function': "SetSecretKey", 'errorCode': res });
|
||||
Module._free(pKeyData);
|
||||
pKeyData = null;
|
||||
return;
|
||||
}
|
||||
|
||||
Module._free(pKeyData);
|
||||
pKeyData = null;
|
||||
break;
|
||||
|
||||
case "GetBMP":
|
||||
var nBMPWidth = eventData.width;
|
||||
var nBMPHeight = eventData.height;
|
||||
var pYUVData = eventData.data;
|
||||
var nYUVSize = nBMPWidth * nBMPHeight * 3 / 2;
|
||||
var oBMPCropRect = {
|
||||
left: eventData.left,
|
||||
top: eventData.top,
|
||||
right: eventData.right,
|
||||
bottom: eventData.bottom
|
||||
};
|
||||
|
||||
var pDataYUV = Module._malloc(nYUVSize);
|
||||
if (pDataYUV === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
Module.writeArrayToMemory(new Uint8Array(pYUVData, 0, nYUVSize), pDataYUV);
|
||||
|
||||
// 分配BMP空间
|
||||
var nBmpSize = nBMPWidth * nBMPHeight * 4 + 60;
|
||||
var pBmpData = Module._malloc(nBmpSize);
|
||||
var pBmpSize = Module._malloc(4);
|
||||
if (pBmpData === null || pBmpSize === null) {
|
||||
Module._free(pDataYUV);
|
||||
pDataYUV = null;
|
||||
|
||||
if (pBmpData != null) {
|
||||
Module._free(pBmpData);
|
||||
pBmpData = null;
|
||||
}
|
||||
|
||||
if (pBmpSize != null) {
|
||||
Module._free(pBmpSize);
|
||||
pBmpSize = null;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
//Module._memset(pBmpSize, nBmpSize, 4); // 防止bmp截图出现输入数据过大的错误码
|
||||
Module.setValue(pBmpSize, nBmpSize, "i32");
|
||||
res = Module._GetBMP(g_nPort, pDataYUV, nYUVSize, pBmpData, pBmpSize,
|
||||
oBMPCropRect.left, oBMPCropRect.top, oBMPCropRect.right, oBMPCropRect.bottom);
|
||||
if (res !== PLAYM4_OK) {
|
||||
postMessage({ 'function': "GetBMP", 'errorCode': res });
|
||||
Module._free(pDataYUV);
|
||||
pDataYUV = null;
|
||||
Module._free(pBmpData);
|
||||
pBmpData = null;
|
||||
Module._free(pBmpSize);
|
||||
pBmpSize = null;
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取BMP图片大小
|
||||
var nBmpDataSize = Module.getValue(pBmpSize, "i32");
|
||||
|
||||
// 获取BMP图片数据
|
||||
var aBmpData = new Uint8Array(nBmpDataSize);
|
||||
aBmpData.set(Module.HEAPU8.subarray(pBmpData, pBmpData + nBmpDataSize));
|
||||
|
||||
postMessage({ 'function': "GetBMP", 'data': aBmpData, 'errorCode': res }, [aBmpData.buffer]);
|
||||
aBmpData = null;
|
||||
if (pDataYUV != null) {
|
||||
Module._free(pDataYUV);
|
||||
pDataYUV = null;
|
||||
}
|
||||
if (pBmpData != null) {
|
||||
Module._free(pBmpData);
|
||||
pBmpData = null;
|
||||
}
|
||||
if (pBmpSize != null) {
|
||||
Module._free(pBmpSize);
|
||||
pBmpSize = null;
|
||||
}
|
||||
break;
|
||||
|
||||
case "GetJPEG":
|
||||
var nJpegWidth = eventData.width;
|
||||
var nJpegHeight = eventData.height;
|
||||
var pYUVData1 = eventData.data;
|
||||
var nYUVSize1 = nJpegWidth * nJpegHeight * 3 / 2;
|
||||
var oJpegCropRect = {
|
||||
left: eventData.left,
|
||||
top: eventData.top,
|
||||
right: eventData.right,
|
||||
bottom: eventData.bottom
|
||||
};
|
||||
|
||||
var pDataYUV1 = Module._malloc(nYUVSize1);
|
||||
if (pDataYUV1 === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
Module.writeArrayToMemory(new Uint8Array(pYUVData1, 0, nYUVSize1), pDataYUV1);
|
||||
|
||||
// 分配JPEG空间
|
||||
var pJpegData = Module._malloc(nYUVSize1);
|
||||
var pJpegSize = Module._malloc(4);
|
||||
if (pJpegData === null || pJpegSize === null) {
|
||||
if (pJpegData != null) {
|
||||
Module._free(pJpegData);
|
||||
pJpegData = null;
|
||||
}
|
||||
|
||||
if (pJpegSize != null) {
|
||||
Module._free(pJpegSize);
|
||||
pJpegSize = null;
|
||||
}
|
||||
|
||||
if (pDataYUV1 != null) {
|
||||
Module._free(pDataYUV1);
|
||||
pDataYUV1 = null;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
Module.setValue(pJpegSize, nJpegWidth * nJpegHeight * 2, "i32"); // JPEG抓图,输入缓冲长度不小于当前帧YUV大小
|
||||
|
||||
res = Module._GetJPEG(g_nPort, pDataYUV1, nYUVSize1, pJpegData, pJpegSize,
|
||||
oJpegCropRect.left, oJpegCropRect.top, oJpegCropRect.right, oJpegCropRect.bottom);
|
||||
if (res !== PLAYM4_OK) {
|
||||
postMessage({ 'function': "GetJPEG", 'errorCode': res });
|
||||
if (pJpegData != null) {
|
||||
Module._free(pJpegData);
|
||||
pJpegData = null;
|
||||
}
|
||||
|
||||
if (pJpegSize != null) {
|
||||
Module._free(pJpegSize);
|
||||
pJpegSize = null;
|
||||
}
|
||||
|
||||
if (pDataYUV1 != null) {
|
||||
Module._free(pDataYUV1);
|
||||
pDataYUV1 = null;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取JPEG图片大小
|
||||
var nJpegSize = Module.getValue(pJpegSize, "i32");
|
||||
|
||||
// 获取JPEG图片数据
|
||||
var aJpegData = new Uint8Array(nJpegSize);
|
||||
aJpegData.set(Module.HEAPU8.subarray(pJpegData, pJpegData + nJpegSize));
|
||||
|
||||
postMessage({ 'function': "GetJPEG", 'data': aJpegData, 'errorCode': res }, [aJpegData.buffer]);
|
||||
|
||||
nJpegSize = null;
|
||||
aJpegData = null;
|
||||
|
||||
if (pDataYUV1 != null) {
|
||||
Module._free(pDataYUV1);
|
||||
pDataYUV1 = null;
|
||||
}
|
||||
if (pJpegData != null) {
|
||||
Module._free(pJpegData);
|
||||
pJpegData = null;
|
||||
}
|
||||
if (pJpegSize != null) {
|
||||
Module._free(pJpegSize);
|
||||
pJpegSize = null;
|
||||
}
|
||||
break;
|
||||
|
||||
case "SetDecodeFrameType":
|
||||
var nFrameType = eventData.data;
|
||||
res = Module._SetDecodeFrameType(g_nPort, nFrameType);
|
||||
if (res !== PLAYM4_OK) {
|
||||
postMessage({ 'function': "SetDecodeFrameType", 'errorCode': res });
|
||||
return;
|
||||
}
|
||||
break;
|
||||
case "CloseStream":
|
||||
//stop
|
||||
let resS = Module._Stop(g_nPort);
|
||||
if (resS !== PLAYM4_OK) {
|
||||
postMessage({ 'function': "Stop", 'errorCode': res });
|
||||
return;
|
||||
}
|
||||
//closeStream
|
||||
res = Module._CloseStream(g_nPort);
|
||||
if (res !== PLAYM4_OK) {
|
||||
postMessage({ 'function': "CloseStream", 'errorCode': res });
|
||||
return;
|
||||
}
|
||||
//freePort
|
||||
let resF = Module._FreePort(g_nPort);
|
||||
if (resF !== PLAYM4_OK) {
|
||||
postMessage({ 'function': "FreePort", 'errorCode': res });
|
||||
return;
|
||||
}
|
||||
if (pInputData != null) {
|
||||
Module._free(pInputData);
|
||||
pInputData = null;
|
||||
}
|
||||
break;
|
||||
case "PlaySound":
|
||||
let resPS = Module._PlaySound(g_nPort);
|
||||
if (resPS !== PLAYM4_OK) {
|
||||
console.log("PlaySound failed");
|
||||
return;
|
||||
}
|
||||
break;
|
||||
case "StopSound":
|
||||
let resSS = Module._StopSound();
|
||||
if (resSS !== PLAYM4_OK) {
|
||||
console.log("StopSound failed");
|
||||
return;
|
||||
}
|
||||
break;
|
||||
case "SetVolume":
|
||||
let resSV = Module._SetVolume(g_nPort, eventData.volume);
|
||||
if (resSV !== PLAYM4_OK) {
|
||||
console.log("Audio SetVolume failed");
|
||||
return;
|
||||
}
|
||||
break;
|
||||
case "GetVolume":
|
||||
let volume = Module._GetVolume();
|
||||
if (volume > 0) {
|
||||
postMessage({ 'function': "GetVolume", 'volume': volume });
|
||||
}
|
||||
else {
|
||||
console.log("Audio GetVolume failed");
|
||||
return;
|
||||
}
|
||||
break;
|
||||
case "OnlyPlaySound":
|
||||
let resOPS = Module._OnlyPlaySound(g_nPort);
|
||||
if (resOPS !== PLAYM4_OK) {
|
||||
console.log("OnlyPlaySound failed");
|
||||
return;
|
||||
}
|
||||
break;
|
||||
case "Pause":
|
||||
let resPa = Module._Pause(g_nPort, eventData.bPlay);
|
||||
if (resPa !== PLAYM4_OK) {
|
||||
console.log("Pause failed");
|
||||
return;
|
||||
}
|
||||
case "PlayRate":
|
||||
Module._SetPlayRate(g_nPort, eventData.playRate);
|
||||
break;
|
||||
case "SetIFrameDecInterval":
|
||||
Module._SetIFrameDecInterval(g_nPort, eventData.data);
|
||||
break;
|
||||
case "SetLostFrameMode":
|
||||
Module._SetLostFrameMode(g_nPort, eventData.data, 0);
|
||||
break;
|
||||
case "SetDemuxModel":
|
||||
let resSDM = Module._SetDemuxModel(g_nPort, eventData.nIdemuxType, eventData.bTrue);
|
||||
break;
|
||||
case "SkipErrorData":
|
||||
Module._SkipErrorData(g_nPort, eventData.bSkip);
|
||||
break;
|
||||
case "SetDecodeERC":
|
||||
Module._SetDecodeERC(g_nPort, eventData.nLevel);
|
||||
break;
|
||||
case "SetANRParam":
|
||||
Module._SetANRParam(g_nPort, eventData.nEnable, eventData.nANRLevel);
|
||||
break;
|
||||
case "SetResampleValue":
|
||||
Module._SetResampleValue(g_nPort, eventData.nEnable, eventData.resampleValue);
|
||||
break;
|
||||
case "GetLastError":
|
||||
let errorCode = Module._GetLastError(g_nPort);
|
||||
postMessage({ 'function': "GetLastError", 'errorCode': errorCode });
|
||||
break;
|
||||
case "SetGlobalBaseTime":
|
||||
Module._SetGlobalBaseTime(g_nPort, eventData.year, eventData.month, eventData.day, eventData.hour, eventData.min, eventData.sec, eventData.ms);
|
||||
break;
|
||||
case "SetRunTimeInfoCB":
|
||||
Module._SetRunTimeInfoCallBackEx(g_nPort, eventData.nModuleType, 0);
|
||||
break;
|
||||
case "SetStreamInfoCB":
|
||||
Module._SetStreamInfoCallBack(g_nPort, eventData.nType, 0);
|
||||
break;
|
||||
case "ResetBuffer":
|
||||
Module._JSPlayM4_ResetBuffer(g_nPort, eventData.type);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
function getOSDTime(oFrameInfo) {
|
||||
var iYear = oFrameInfo.year;
|
||||
var iMonth = oFrameInfo.month;
|
||||
var iDay = oFrameInfo.day;
|
||||
var iHour = oFrameInfo.hour;
|
||||
var iMinute = oFrameInfo.minute;
|
||||
var iSecond = oFrameInfo.second;
|
||||
var iMiSecond = oFrameInfo.misecond
|
||||
|
||||
if (iMonth < 10) {
|
||||
iMonth = "0" + iMonth;
|
||||
}
|
||||
if (iDay < 10) {
|
||||
iDay = "0" + iDay;
|
||||
}
|
||||
if (iHour < 10) {
|
||||
iHour = "0" + iHour;
|
||||
}
|
||||
if (iMinute < 10) {
|
||||
iMinute = "0" + iMinute;
|
||||
}
|
||||
if (iSecond < 10) {
|
||||
iSecond = "0" + iSecond;
|
||||
}
|
||||
let osdTime = {};
|
||||
|
||||
osdTime.year = iYear;
|
||||
osdTime.month = iMonth;
|
||||
osdTime.week = 0;
|
||||
osdTime.day = iDay;
|
||||
osdTime.hour = iHour;
|
||||
osdTime.minute = iMinute;
|
||||
osdTime.second = iSecond;
|
||||
osdTime.milliseconds = iMiSecond;
|
||||
return osdTime;
|
||||
//return iYear + "-" + iMonth + "-" + iDay + " " + iHour + ":" + iMinute + ":" + iSecond;
|
||||
}
|
||||
// 获取帧数据
|
||||
function getFrameData() {
|
||||
// function getFrameData() {
|
||||
// 获取帧数据
|
||||
var res = Module._GetFrameData();
|
||||
//var res = fun();
|
||||
if (res === PLAYM4_OK) {
|
||||
var iFrameInfo = Module._GetFrameInfo();
|
||||
let oFrameInfo = {};
|
||||
oFrameInfo.frameType = Module.HEAP32[iFrameInfo >> 2];
|
||||
oFrameInfo.frameSize = Module.HEAP32[iFrameInfo + 4 >> 2];
|
||||
oFrameInfo.width = Module.HEAP32[iFrameInfo + 8 >> 2];
|
||||
oFrameInfo.height = Module.HEAP32[iFrameInfo + 12 >> 2];
|
||||
oFrameInfo.timeStamp = Module.HEAP32[iFrameInfo + 16 >> 2];
|
||||
oFrameInfo.frameRate = Module.HEAP32[iFrameInfo + 20 >> 2];
|
||||
oFrameInfo.bitsPerSample = Module.HEAP32[iFrameInfo + 24 >> 2];
|
||||
oFrameInfo.samplesPerSec = Module.HEAP32[iFrameInfo + 28 >> 2];
|
||||
oFrameInfo.channels = Module.HEAP32[iFrameInfo + 32 >> 2];
|
||||
oFrameInfo.frameNum = Module.HEAP32[iFrameInfo + 36 >> 2];
|
||||
|
||||
oFrameInfo.cropLeft = Module.HEAP32[iFrameInfo + 40 >> 2];
|
||||
oFrameInfo.cropRight = Module.HEAP32[iFrameInfo + 44 >> 2];
|
||||
oFrameInfo.cropTop = Module.HEAP32[iFrameInfo + 48 >> 2];
|
||||
oFrameInfo.cropBottom = Module.HEAP32[iFrameInfo + 52 >> 2];
|
||||
|
||||
oFrameInfo.year = Module.HEAP16[iFrameInfo + 64 >> 1];
|
||||
oFrameInfo.month = Module.HEAP16[iFrameInfo + 66 >> 1];
|
||||
oFrameInfo.day = Module.HEAP16[iFrameInfo + 68 >> 1];
|
||||
oFrameInfo.hour = Module.HEAP16[iFrameInfo + 70 >> 1];
|
||||
oFrameInfo.minute = Module.HEAP16[iFrameInfo + 72 >> 1];
|
||||
oFrameInfo.second = Module.HEAP16[iFrameInfo + 74 >> 1];
|
||||
oFrameInfo.misecond = Module.HEAP16[iFrameInfo + 76 >> 1];
|
||||
switch (oFrameInfo.frameType) {
|
||||
case AUDIO_TYPE:
|
||||
var iSize = oFrameInfo.frameSize;
|
||||
if (0 === iSize) {
|
||||
return -1;
|
||||
}
|
||||
var pPCM = Module._GetFrameBuffer();
|
||||
// var audioBuf = new ArrayBuffer(iSize);
|
||||
var aPCMData = new Uint8Array(iSize);
|
||||
aPCMData.set(Module.HEAPU8.subarray(pPCM, pPCM + iSize));
|
||||
if (bWorkerPrintLog) {
|
||||
console.log("<<<Worker: audio media Info: nSise:" + oFrameInfo.frameSize + ",nSampleRate:" + oFrameInfo.samplesPerSec + ',channel:' + oFrameInfo.channels + ',bitsPerSample:' + oFrameInfo.bitsPerSample);
|
||||
}
|
||||
postMessage({
|
||||
'function': "GetFrameData", 'type': "audioType", 'data': aPCMData.buffer,
|
||||
'frameInfo': oFrameInfo, 'errorCode': res
|
||||
}, [aPCMData.buffer]);
|
||||
|
||||
oFrameInfo = null;
|
||||
pPCM = null;
|
||||
aPCMData = null;
|
||||
return PLAYM4_AUDIO_FRAME;
|
||||
|
||||
case VIDEO_TYPE:
|
||||
var szOSDTime = getOSDTime(oFrameInfo);
|
||||
|
||||
var iWidth = oFrameInfo.width;
|
||||
var iHeight = oFrameInfo.height;
|
||||
|
||||
var iYUVSize = iWidth * iHeight * 3 / 2;
|
||||
if (0 === iYUVSize) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
var pYUV = Module._GetFrameBuffer();
|
||||
|
||||
// 图像数据渲染后压回,若从主码流切到子码流,存在数组大小与图像大小不匹配现象
|
||||
var aYUVData = new Uint8Array(iYUVSize);
|
||||
aYUVData.set(Module.HEAPU8.subarray(pYUV, pYUV + iYUVSize));
|
||||
if (bWorkerPrintLog) {
|
||||
console.log("<<<Worker: video media Info: Width:" + oFrameInfo.width + ",Height:" + oFrameInfo.height + ",timeStamp:" + oFrameInfo.timeStamp);
|
||||
}
|
||||
|
||||
postMessage({
|
||||
'function': "GetFrameData", 'type': "videoType", 'data': aYUVData.buffer,
|
||||
'dataLen': aYUVData.length, 'osd': szOSDTime, 'frameInfo': oFrameInfo, 'errorCode': res
|
||||
}, [aYUVData.buffer]);
|
||||
|
||||
oFrameInfo = null;
|
||||
pYUV = null;
|
||||
aYUVData = null;
|
||||
return PLAYM4_VIDEO_FRAME;
|
||||
|
||||
case PRIVT_TYPE:
|
||||
postMessage({
|
||||
'function': "GetFrameData", 'type': "", 'data': null,
|
||||
'dataLen': -1, 'osd': 0, 'frameInfo': null, 'errorCode': PLAYM4_SYS_NOT_SUPPORT
|
||||
});
|
||||
return PLAYM4_SYS_NOT_SUPPORT;
|
||||
|
||||
default:
|
||||
postMessage({
|
||||
'function': "GetFrameData", 'type': "", 'data': null,
|
||||
'dataLen': -1, 'osd': 0, 'frameInfo': null, 'errorCode': PLAYM4_SYS_NOT_SUPPORT
|
||||
});
|
||||
return PLAYM4_SYS_NOT_SUPPORT;
|
||||
}
|
||||
}
|
||||
else {
|
||||
let errorCode = Module._GetLastError(g_nPort);
|
||||
//解码失败返回裸数据
|
||||
if (PLAYM4_DECODE_ERROR === errorCode) {
|
||||
var rawInfo = Module._GetRawDataInfo();
|
||||
var pRawData = Module._GetRawDataBuffer();
|
||||
var aRawData = new Uint8Array(rawInfo.isize);
|
||||
aRawData.set(Module.HEAPU8.subarray(pRawData, pRawData + rawInfo.isize));
|
||||
postMessage({
|
||||
'function': "GetRawData", 'type': "", 'data': aRawData.buffer,
|
||||
'rawDataLen': rawInfo.isize, 'osd': 0, 'frameInfo': null, 'errorCode': errorCode
|
||||
});
|
||||
rawInfo = null;
|
||||
pRawData = null;
|
||||
aRawData = null;
|
||||
}
|
||||
//需要更多数据
|
||||
if (PLAYM4_NEED_MORE_DATA === errorCode || PLAYM4_SYS_NOT_SUPPORT === errorCode || PLAYM4_NEED_NEET_LOOP === errorCode) {
|
||||
postMessage({
|
||||
'function': "GetFrameData", 'type': "", 'data': null,
|
||||
'dataLen': -1, 'osd': 0, 'frameInfo': null, 'errorCode': errorCode
|
||||
});
|
||||
}
|
||||
return errorCode;
|
||||
}
|
||||
}
|
||||
|
||||
// 开始计算时间
|
||||
function startTime() {
|
||||
return new Date().getTime();
|
||||
}
|
||||
|
||||
// 结束计算时间
|
||||
function endTime() {
|
||||
return new Date().getTime();
|
||||
}
|
||||
|
||||
// 字母字符串转byte数组
|
||||
function stringToBytes(str) {
|
||||
var ch, st, re = [];
|
||||
for (var i = 0; i < str.length; i++) {
|
||||
ch = str.charCodeAt(i); // get char
|
||||
st = []; // set up "stack"
|
||||
do {
|
||||
st.push(ch & 0xFF); // push byte to stack
|
||||
ch = ch >> 8; // shift value down by 1 byte
|
||||
}
|
||||
while (ch);
|
||||
// add stack contents to result
|
||||
// done because chars have "wrong" endianness
|
||||
re = re.concat(st.reverse());
|
||||
}
|
||||
// return an array of bytes
|
||||
return re;
|
||||
}
|
||||
})();
|
||||
Binary file not shown.
|
|
@ -0,0 +1,705 @@
|
|||
/**
|
||||
* Created by wangweijie5 on 2016/12/5.
|
||||
*/
|
||||
(function (event) {
|
||||
const AUDIO_TYPE = 0; // 音频
|
||||
const VIDEO_TYPE = 1; // 视频
|
||||
const PRIVT_TYPE = 2; // 私有帧
|
||||
|
||||
const PLAYM4_AUDIO_FRAME = 100; // 音频帧
|
||||
const PLAYM4_VIDEO_FRAME = 101; // 视频帧
|
||||
|
||||
const PLAYM4_OK = 1;
|
||||
const PLAYM4_ORDER_ERROR = 2;
|
||||
const PLAYM4_DECODE_ERROR = 44 // 解码失败
|
||||
const PLAYM4_NOT_KEYFRAME = 48; // 非关键帧
|
||||
const PLAYM4_NEED_MORE_DATA = 31; // 需要更多数据才能解析
|
||||
const PLAYM4_NEED_NEET_LOOP = 35; //丢帧需要下个循环
|
||||
const PLAYM4_SYS_NOT_SUPPORT = 16; // 不支持
|
||||
|
||||
// importScripts('Decoder.js');
|
||||
// Module.addOnPostRun(function () {
|
||||
// postMessage({ 'function': "loaded" });
|
||||
// });
|
||||
|
||||
var iStreamMode = 0; // 流模式
|
||||
|
||||
var bOpenMode = false;
|
||||
var bOpenStream = false;
|
||||
|
||||
var funGetFrameData = null;
|
||||
var funGetAudFrameData = null;
|
||||
|
||||
var bWorkerPrintLog = false;//worker层log开关
|
||||
|
||||
var g_nPort = -1;
|
||||
var pInputData = null;
|
||||
var inputBufferSize = 40960;
|
||||
|
||||
self.JSPlayM4_RunTimeInfoCallBack = function (nPort, pstRunTimeInfo, pUser) {
|
||||
let port = nPort;
|
||||
let user = pUser;
|
||||
let nRunTimeModule = Module.HEAP32[pstRunTimeInfo >> 2];
|
||||
let nStrVersion = Module.HEAP32[pstRunTimeInfo + 4 >> 2];
|
||||
let nFrameTimeStamp = Module.HEAP32[pstRunTimeInfo + 8 >> 2];
|
||||
let nFrameNum = Module.HEAP32[pstRunTimeInfo + 12 >> 2];
|
||||
let nErrorCode = Module.HEAP32[pstRunTimeInfo + 16 >> 2];
|
||||
// console.log("nRunTimeModule:"+nRunTimeModule+",nFrameNum:"+nFrameNum+",nErrorCode:"+nErrorCode);
|
||||
postMessage({ 'function': "RunTimeInfoCallBack", 'nRunTimeModule': nRunTimeModule, 'nStrVersion': nStrVersion, 'nFrameTimeStamp': nFrameTimeStamp, 'nFrameNum': nFrameNum, 'nErrorCode': nErrorCode });
|
||||
}
|
||||
|
||||
self.JSPlayM4_StreamInfoCallBack = function (nPort, pstStreamInfo, pUser)
|
||||
{
|
||||
let port = nPort;
|
||||
let user = pUser;
|
||||
let nSystemformat = Module.HEAP16[pstStreamInfo >> 1]; //封装类型
|
||||
let nVideoformat = Module.HEAP16[pstStreamInfo + 2 >> 1];//视频编码类型
|
||||
let nAudioformat = Module.HEAP16[pstStreamInfo + 4 >> 1];//音频编码类型
|
||||
let nAudiochannels = Module.HEAP16[pstStreamInfo + 6 >> 1]; //音频通道数
|
||||
let nAudiobitspersample = Module.HEAP32[pstStreamInfo + 8 >> 2];//音频样位率
|
||||
let nAudiosamplesrate = Module.HEAP32[pstStreamInfo + 12 >> 2];//音频采样率
|
||||
let nAudiobitrate = Module.HEAP32[pstStreamInfo + 16 >> 2];//音频比特率,单位:bit
|
||||
//console.log("nSystemformat:" + nSystemformat + ",nVideoformat:" + nVideoformat + ",nAudioformat:" + nAudioformat + ",nAudiochannels:" + nAudiochannels + ",nAudiobitspersample:" + nAudiobitspersample + ",nAudiosamplesrate:" + nAudiosamplesrate + ",nAudiobitrate:" + nAudiobitrate);
|
||||
postMessage({ 'function': "StreamInfoCallBack", 'nSystemformat': nSystemformat, 'nVideoformat': nVideoformat, 'nAudioformat': nAudioformat, 'nAudiochannels': nAudiochannels, 'nAudiobitspersample': nAudiobitspersample, 'nAudiosamplesrate': nAudiosamplesrate, 'nAudiobitrate': nAudiobitrate});
|
||||
}
|
||||
|
||||
onmessage = function (event) {
|
||||
var eventData = event.data;
|
||||
var res = 0;
|
||||
switch (eventData.command) {
|
||||
case "importScripts":
|
||||
const decodebase = eventData.data + "Decoder.js"
|
||||
importScripts(decodebase);
|
||||
Module.addOnPostRun(function () {
|
||||
postMessage({ 'function': "loaded" });
|
||||
});
|
||||
break;
|
||||
case "printLog":
|
||||
let downloadFlag = eventData.data;
|
||||
if (downloadFlag === true) {
|
||||
bWorkerPrintLog = true;
|
||||
res = Module._SetPrintLogFlag(g_nPort, downloadFlag);
|
||||
}
|
||||
else {
|
||||
bWorkerPrintLog = false;
|
||||
res = Module._SetPrintLogFlag(g_nPort, downloadFlag);
|
||||
}
|
||||
|
||||
if (res !== PLAYM4_OK) {
|
||||
console.log("DecodeWorker.js: PlayerSDK print log failed,res" + res);
|
||||
postMessage({ 'function': "printLog", 'errorCode': res });
|
||||
}
|
||||
break;
|
||||
case "SetPlayPosition":
|
||||
let nFrameNumOrTime = eventData.data;
|
||||
let enPosType = eventData.type;
|
||||
// res = Module._SetPlayPosition(nFrameNumOrTime,enPosType);
|
||||
// if (res !== PLAYM4_OK)
|
||||
// {
|
||||
// postMessage({'function': "SetPlayPosition", 'errorCode': res});
|
||||
// return;
|
||||
// }
|
||||
// //有没有buffer需要清除
|
||||
|
||||
break;
|
||||
case "SetStreamOpenMode":
|
||||
//获取端口号
|
||||
g_nPort = Module._GetPort();
|
||||
//设置流打开模式
|
||||
iStreamMode = eventData.data;
|
||||
res = Module._SetStreamOpenMode(g_nPort, iStreamMode);
|
||||
if (res !== PLAYM4_OK) {
|
||||
postMessage({ 'function': "SetStreamOpenMode", 'errorCode': res });
|
||||
return;
|
||||
}
|
||||
bOpenMode = true;
|
||||
break;
|
||||
|
||||
case "OpenStream":
|
||||
// 接收到的数据
|
||||
var iHeadLen = eventData.dataSize;
|
||||
var pHead = Module._malloc(iHeadLen + 4);
|
||||
if (pHead === null) {
|
||||
return;
|
||||
}
|
||||
var aHead = Module.HEAPU8.subarray(pHead, pHead + iHeadLen);
|
||||
aHead.set(new Uint8Array(eventData.data));
|
||||
res = Module._OpenStream(g_nPort, pHead, iHeadLen, eventData.bufPoolSize);
|
||||
postMessage({ 'function': "OpenStream", 'errorCode': res });
|
||||
if (res !== PLAYM4_OK) {
|
||||
//释放内存
|
||||
Module._free(pHead);
|
||||
pHead = null;
|
||||
return;
|
||||
}
|
||||
bOpenStream = true;
|
||||
break;
|
||||
case "Play":
|
||||
let resP = Module._Play(g_nPort);
|
||||
if (resP !== PLAYM4_OK) {
|
||||
return;
|
||||
}
|
||||
break;
|
||||
case "InputData":
|
||||
// 接收到的数据
|
||||
var iLen = eventData.dataSize;
|
||||
if (iLen > 0) {
|
||||
if (pInputData == null || iLen > inputBufferSize) {
|
||||
if (pInputData != null) {
|
||||
Module._free(pInputData);
|
||||
pInputData = null;
|
||||
}
|
||||
if (iLen > inputBufferSize) {
|
||||
inputBufferSize = iLen;
|
||||
}
|
||||
|
||||
pInputData = Module._malloc(inputBufferSize);
|
||||
if (pInputData === null) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
var inputData = new Uint8Array(eventData.data);
|
||||
// var aInputData = Module.HEAPU8.subarray(pInputData, pInputData + iLen);
|
||||
// aInputData.set(inputData);
|
||||
Module.writeArrayToMemory(inputData, pInputData);
|
||||
inputData = null;
|
||||
res = Module._InputData(g_nPort, pInputData, iLen);
|
||||
if (res !== PLAYM4_OK) {
|
||||
let errorCode = Module._GetLastError(g_nPort);
|
||||
let sourceRemain = Module._GetSourceBufferRemain(g_nPort);
|
||||
postMessage({ 'function': "InputData", 'errorCode': errorCode, "sourceRemain": sourceRemain });
|
||||
}
|
||||
//Module._free(pInputData);
|
||||
//pInputData = null;
|
||||
} else {
|
||||
let sourceRemain = Module._GetSourceBufferRemain(g_nPort);
|
||||
if (sourceRemain == 0) {
|
||||
postMessage({ 'function': "InputData", 'errorCode': PLAYM4_NEED_MORE_DATA });
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/////////////////////
|
||||
// if (funGetFrameData === null) {
|
||||
// funGetFrameData = Module.cwrap('GetFrameData', 'number');
|
||||
// }
|
||||
|
||||
while (bOpenMode && bOpenStream) {
|
||||
|
||||
var ret = getFrameData();
|
||||
// 直到获取视频帧或数据不足为止
|
||||
if (PLAYM4_VIDEO_FRAME === ret || PLAYM4_NEED_MORE_DATA === ret || PLAYM4_ORDER_ERROR === ret)//PLAYM4_VIDEO_FRAME === ret || || PLAYM4_NEED_NEET_LOOP === ret
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case "SetSecretKey":
|
||||
var keyLen = eventData.nKeyLen;
|
||||
var pKeyData = Module._malloc(keyLen);
|
||||
if (pKeyData === null) {
|
||||
return;
|
||||
}
|
||||
var nKeySize = eventData.data.length
|
||||
var bufData = stringToBytes(eventData.data);
|
||||
var aKeyData = Module.HEAPU8.subarray(pKeyData, pKeyData + keyLen);
|
||||
let u8array = new Uint8Array(keyLen);
|
||||
aKeyData.set(u8array, 0);
|
||||
aKeyData.set(new Uint8Array(bufData));
|
||||
aKeyData = null;
|
||||
u8array = null;
|
||||
|
||||
res = Module._SetSecretKey(g_nPort, eventData.nKeyType, pKeyData, keyLen);//, nKeySize
|
||||
if (res !== PLAYM4_OK) {
|
||||
postMessage({ 'function': "SetSecretKey", 'errorCode': res });
|
||||
Module._free(pKeyData);
|
||||
pKeyData = null;
|
||||
return;
|
||||
}
|
||||
|
||||
Module._free(pKeyData);
|
||||
pKeyData = null;
|
||||
break;
|
||||
|
||||
case "GetBMP":
|
||||
var nBMPWidth = eventData.width;
|
||||
var nBMPHeight = eventData.height;
|
||||
var pYUVData = eventData.data;
|
||||
var nYUVSize = nBMPWidth * nBMPHeight * 3 / 2;
|
||||
var oBMPCropRect = {
|
||||
left: eventData.left,
|
||||
top: eventData.top,
|
||||
right: eventData.right,
|
||||
bottom: eventData.bottom
|
||||
};
|
||||
|
||||
var pDataYUV = Module._malloc(nYUVSize);
|
||||
if (pDataYUV === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
Module.writeArrayToMemory(new Uint8Array(pYUVData, 0, nYUVSize), pDataYUV);
|
||||
|
||||
// 分配BMP空间
|
||||
var nBmpSize = nBMPWidth * nBMPHeight * 4 + 60;
|
||||
var pBmpData = Module._malloc(nBmpSize);
|
||||
var pBmpSize = Module._malloc(4);
|
||||
if (pBmpData === null || pBmpSize === null) {
|
||||
Module._free(pDataYUV);
|
||||
pDataYUV = null;
|
||||
|
||||
if (pBmpData != null) {
|
||||
Module._free(pBmpData);
|
||||
pBmpData = null;
|
||||
}
|
||||
|
||||
if (pBmpSize != null) {
|
||||
Module._free(pBmpSize);
|
||||
pBmpSize = null;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
//Module._memset(pBmpSize, nBmpSize, 4); // 防止bmp截图出现输入数据过大的错误码
|
||||
Module.setValue(pBmpSize, nBmpSize, "i32");
|
||||
res = Module._GetBMP(g_nPort, pDataYUV, nYUVSize, pBmpData, pBmpSize,
|
||||
oBMPCropRect.left, oBMPCropRect.top, oBMPCropRect.right, oBMPCropRect.bottom);
|
||||
if (res !== PLAYM4_OK) {
|
||||
postMessage({ 'function': "GetBMP", 'errorCode': res });
|
||||
Module._free(pDataYUV);
|
||||
pDataYUV = null;
|
||||
Module._free(pBmpData);
|
||||
pBmpData = null;
|
||||
Module._free(pBmpSize);
|
||||
pBmpSize = null;
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取BMP图片大小
|
||||
var nBmpDataSize = Module.getValue(pBmpSize, "i32");
|
||||
|
||||
// 获取BMP图片数据
|
||||
var aBmpData = new Uint8Array(nBmpDataSize);
|
||||
aBmpData.set(Module.HEAPU8.subarray(pBmpData, pBmpData + nBmpDataSize));
|
||||
|
||||
postMessage({ 'function': "GetBMP", 'data': aBmpData, 'errorCode': res }, [aBmpData.buffer]);
|
||||
aBmpData = null;
|
||||
if (pDataYUV != null) {
|
||||
Module._free(pDataYUV);
|
||||
pDataYUV = null;
|
||||
}
|
||||
if (pBmpData != null) {
|
||||
Module._free(pBmpData);
|
||||
pBmpData = null;
|
||||
}
|
||||
if (pBmpSize != null) {
|
||||
Module._free(pBmpSize);
|
||||
pBmpSize = null;
|
||||
}
|
||||
break;
|
||||
|
||||
case "GetJPEG":
|
||||
var nJpegWidth = eventData.width;
|
||||
var nJpegHeight = eventData.height;
|
||||
var pYUVData1 = eventData.data;
|
||||
var nYUVSize1 = nJpegWidth * nJpegHeight * 3 / 2;
|
||||
var oJpegCropRect = {
|
||||
left: eventData.left,
|
||||
top: eventData.top,
|
||||
right: eventData.right,
|
||||
bottom: eventData.bottom
|
||||
};
|
||||
|
||||
var pDataYUV1 = Module._malloc(nYUVSize1);
|
||||
if (pDataYUV1 === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
Module.writeArrayToMemory(new Uint8Array(pYUVData1, 0, nYUVSize1), pDataYUV1);
|
||||
|
||||
// 分配JPEG空间
|
||||
var pJpegData = Module._malloc(nYUVSize1);
|
||||
var pJpegSize = Module._malloc(4);
|
||||
if (pJpegData === null || pJpegSize === null) {
|
||||
if (pJpegData != null) {
|
||||
Module._free(pJpegData);
|
||||
pJpegData = null;
|
||||
}
|
||||
|
||||
if (pJpegSize != null) {
|
||||
Module._free(pJpegSize);
|
||||
pJpegSize = null;
|
||||
}
|
||||
|
||||
if (pDataYUV1 != null) {
|
||||
Module._free(pDataYUV1);
|
||||
pDataYUV1 = null;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
Module.setValue(pJpegSize, nJpegWidth * nJpegHeight * 2, "i32"); // JPEG抓图,输入缓冲长度不小于当前帧YUV大小
|
||||
|
||||
res = Module._GetJPEG(g_nPort, pDataYUV1, nYUVSize1, pJpegData, pJpegSize,
|
||||
oJpegCropRect.left, oJpegCropRect.top, oJpegCropRect.right, oJpegCropRect.bottom);
|
||||
if (res !== PLAYM4_OK) {
|
||||
postMessage({ 'function': "GetJPEG", 'errorCode': res });
|
||||
if (pJpegData != null) {
|
||||
Module._free(pJpegData);
|
||||
pJpegData = null;
|
||||
}
|
||||
|
||||
if (pJpegSize != null) {
|
||||
Module._free(pJpegSize);
|
||||
pJpegSize = null;
|
||||
}
|
||||
|
||||
if (pDataYUV1 != null) {
|
||||
Module._free(pDataYUV1);
|
||||
pDataYUV1 = null;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取JPEG图片大小
|
||||
var nJpegSize = Module.getValue(pJpegSize, "i32");
|
||||
|
||||
// 获取JPEG图片数据
|
||||
var aJpegData = new Uint8Array(nJpegSize);
|
||||
aJpegData.set(Module.HEAPU8.subarray(pJpegData, pJpegData + nJpegSize));
|
||||
|
||||
postMessage({ 'function': "GetJPEG", 'data': aJpegData, 'errorCode': res }, [aJpegData.buffer]);
|
||||
|
||||
nJpegSize = null;
|
||||
aJpegData = null;
|
||||
|
||||
if (pDataYUV1 != null) {
|
||||
Module._free(pDataYUV1);
|
||||
pDataYUV1 = null;
|
||||
}
|
||||
if (pJpegData != null) {
|
||||
Module._free(pJpegData);
|
||||
pJpegData = null;
|
||||
}
|
||||
if (pJpegSize != null) {
|
||||
Module._free(pJpegSize);
|
||||
pJpegSize = null;
|
||||
}
|
||||
break;
|
||||
|
||||
case "SetDecodeFrameType":
|
||||
var nFrameType = eventData.data;
|
||||
res = Module._SetDecodeFrameType(g_nPort, nFrameType);
|
||||
if (res !== PLAYM4_OK) {
|
||||
postMessage({ 'function': "SetDecodeFrameType", 'errorCode': res });
|
||||
return;
|
||||
}
|
||||
break;
|
||||
case "CloseStream":
|
||||
//stop
|
||||
let resS = Module._Stop(g_nPort);
|
||||
if (resS !== PLAYM4_OK) {
|
||||
postMessage({ 'function': "Stop", 'errorCode': res });
|
||||
return;
|
||||
}
|
||||
//closeStream
|
||||
res = Module._CloseStream(g_nPort);
|
||||
if (res !== PLAYM4_OK) {
|
||||
postMessage({ 'function': "CloseStream", 'errorCode': res });
|
||||
return;
|
||||
}
|
||||
//freePort
|
||||
let resF = Module._FreePort(g_nPort);
|
||||
if (resF !== PLAYM4_OK) {
|
||||
postMessage({ 'function': "FreePort", 'errorCode': res });
|
||||
return;
|
||||
}
|
||||
if (pInputData != null) {
|
||||
Module._free(pInputData);
|
||||
pInputData = null;
|
||||
}
|
||||
break;
|
||||
case "PlaySound":
|
||||
let resPS = Module._PlaySound(g_nPort);
|
||||
if (resPS !== PLAYM4_OK) {
|
||||
console.log("PlaySound failed");
|
||||
return;
|
||||
}
|
||||
break;
|
||||
case "StopSound":
|
||||
let resSS = Module._StopSound();
|
||||
if (resSS !== PLAYM4_OK) {
|
||||
console.log("StopSound failed");
|
||||
return;
|
||||
}
|
||||
break;
|
||||
case "SetVolume":
|
||||
let resSV = Module._SetVolume(g_nPort, eventData.volume);
|
||||
if (resSV !== PLAYM4_OK) {
|
||||
console.log("Audio SetVolume failed");
|
||||
return;
|
||||
}
|
||||
break;
|
||||
case "GetVolume":
|
||||
let volume = Module._GetVolume();
|
||||
if (volume > 0) {
|
||||
postMessage({ 'function': "GetVolume", 'volume': volume });
|
||||
}
|
||||
else {
|
||||
console.log("Audio GetVolume failed");
|
||||
return;
|
||||
}
|
||||
break;
|
||||
case "OnlyPlaySound":
|
||||
let resOPS = Module._OnlyPlaySound(g_nPort);
|
||||
if (resOPS !== PLAYM4_OK) {
|
||||
console.log("OnlyPlaySound failed");
|
||||
return;
|
||||
}
|
||||
break;
|
||||
case "Pause":
|
||||
let resPa = Module._Pause(g_nPort, eventData.bPlay);
|
||||
if (resPa !== PLAYM4_OK) {
|
||||
console.log("Pause failed");
|
||||
return;
|
||||
}
|
||||
case "PlayRate":
|
||||
Module._SetPlayRate(g_nPort, eventData.playRate);
|
||||
break;
|
||||
case "SetIFrameDecInterval":
|
||||
Module._SetIFrameDecInterval(g_nPort, eventData.data);
|
||||
break;
|
||||
case "SetLostFrameMode":
|
||||
Module._SetLostFrameMode(g_nPort, eventData.data, 0);
|
||||
break;
|
||||
case "SetDemuxModel":
|
||||
let resSDM = Module._SetDemuxModel(g_nPort, eventData.nIdemuxType, eventData.bTrue);
|
||||
break;
|
||||
case "SkipErrorData":
|
||||
Module._SkipErrorData(g_nPort, eventData.bSkip);
|
||||
break;
|
||||
case "SetDecodeERC":
|
||||
Module._SetDecodeERC(g_nPort, eventData.nLevel);
|
||||
break;
|
||||
case "SetANRParam":
|
||||
Module._SetANRParam(g_nPort, eventData.nEnable, eventData.nANRLevel);
|
||||
break;
|
||||
case "SetResampleValue":
|
||||
Module._SetResampleValue(g_nPort, eventData.nEnable, eventData.resampleValue);
|
||||
break;
|
||||
case "GetLastError":
|
||||
let errorCode = Module._GetLastError(g_nPort);
|
||||
postMessage({ 'function': "GetLastError", 'errorCode': errorCode });
|
||||
break;
|
||||
case "SetGlobalBaseTime":
|
||||
Module._SetGlobalBaseTime(g_nPort, eventData.year, eventData.month, eventData.day, eventData.hour, eventData.min, eventData.sec, eventData.ms);
|
||||
break;
|
||||
case "SetRunTimeInfoCB":
|
||||
Module._SetRunTimeInfoCallBackEx(g_nPort, eventData.nModuleType, 0);
|
||||
break;
|
||||
case "SetStreamInfoCB":
|
||||
Module._SetStreamInfoCallBack(g_nPort, eventData.nType, 0);
|
||||
break;
|
||||
case "ResetBuffer":
|
||||
Module._JSPlayM4_ResetBuffer(g_nPort, eventData.type);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
function getOSDTime(oFrameInfo) {
|
||||
var iYear = oFrameInfo.year;
|
||||
var iMonth = oFrameInfo.month;
|
||||
var iDay = oFrameInfo.day;
|
||||
var iHour = oFrameInfo.hour;
|
||||
var iMinute = oFrameInfo.minute;
|
||||
var iSecond = oFrameInfo.second;
|
||||
var iMiSecond = oFrameInfo.misecond
|
||||
|
||||
if (iMonth < 10) {
|
||||
iMonth = "0" + iMonth;
|
||||
}
|
||||
if (iDay < 10) {
|
||||
iDay = "0" + iDay;
|
||||
}
|
||||
if (iHour < 10) {
|
||||
iHour = "0" + iHour;
|
||||
}
|
||||
if (iMinute < 10) {
|
||||
iMinute = "0" + iMinute;
|
||||
}
|
||||
if (iSecond < 10) {
|
||||
iSecond = "0" + iSecond;
|
||||
}
|
||||
let osdTime = {};
|
||||
|
||||
osdTime.year = iYear;
|
||||
osdTime.month = iMonth;
|
||||
osdTime.week = 0;
|
||||
osdTime.day = iDay;
|
||||
osdTime.hour = iHour;
|
||||
osdTime.minute = iMinute;
|
||||
osdTime.second = iSecond;
|
||||
osdTime.milliseconds = iMiSecond;
|
||||
return osdTime;
|
||||
//return iYear + "-" + iMonth + "-" + iDay + " " + iHour + ":" + iMinute + ":" + iSecond;
|
||||
}
|
||||
// 获取帧数据
|
||||
function getFrameData() {
|
||||
// function getFrameData() {
|
||||
// 获取帧数据
|
||||
var res = Module._GetFrameData();
|
||||
//var res = fun();
|
||||
if (res === PLAYM4_OK) {
|
||||
var iFrameInfo = Module._GetFrameInfo();
|
||||
let oFrameInfo = {};
|
||||
oFrameInfo.frameType = Module.HEAP32[iFrameInfo >> 2];
|
||||
oFrameInfo.frameSize = Module.HEAP32[iFrameInfo + 4 >> 2];
|
||||
oFrameInfo.width = Module.HEAP32[iFrameInfo + 8 >> 2];
|
||||
oFrameInfo.height = Module.HEAP32[iFrameInfo + 12 >> 2];
|
||||
oFrameInfo.timeStamp = Module.HEAP32[iFrameInfo + 16 >> 2];
|
||||
oFrameInfo.frameRate = Module.HEAP32[iFrameInfo + 20 >> 2];
|
||||
oFrameInfo.bitsPerSample = Module.HEAP32[iFrameInfo + 24 >> 2];
|
||||
oFrameInfo.samplesPerSec = Module.HEAP32[iFrameInfo + 28 >> 2];
|
||||
oFrameInfo.channels = Module.HEAP32[iFrameInfo + 32 >> 2];
|
||||
oFrameInfo.frameNum = Module.HEAP32[iFrameInfo + 36 >> 2];
|
||||
|
||||
oFrameInfo.cropLeft = Module.HEAP32[iFrameInfo + 40 >> 2];
|
||||
oFrameInfo.cropRight = Module.HEAP32[iFrameInfo + 44 >> 2];
|
||||
oFrameInfo.cropTop = Module.HEAP32[iFrameInfo + 48 >> 2];
|
||||
oFrameInfo.cropBottom = Module.HEAP32[iFrameInfo + 52 >> 2];
|
||||
|
||||
oFrameInfo.year = Module.HEAP16[iFrameInfo + 64 >> 1];
|
||||
oFrameInfo.month = Module.HEAP16[iFrameInfo + 66 >> 1];
|
||||
oFrameInfo.day = Module.HEAP16[iFrameInfo + 68 >> 1];
|
||||
oFrameInfo.hour = Module.HEAP16[iFrameInfo + 70 >> 1];
|
||||
oFrameInfo.minute = Module.HEAP16[iFrameInfo + 72 >> 1];
|
||||
oFrameInfo.second = Module.HEAP16[iFrameInfo + 74 >> 1];
|
||||
oFrameInfo.misecond = Module.HEAP16[iFrameInfo + 76 >> 1];
|
||||
switch (oFrameInfo.frameType) {
|
||||
case AUDIO_TYPE:
|
||||
var iSize = oFrameInfo.frameSize;
|
||||
if (0 === iSize) {
|
||||
return -1;
|
||||
}
|
||||
var pPCM = Module._GetFrameBuffer();
|
||||
// var audioBuf = new ArrayBuffer(iSize);
|
||||
var aPCMData = new Uint8Array(iSize);
|
||||
aPCMData.set(Module.HEAPU8.subarray(pPCM, pPCM + iSize));
|
||||
if (bWorkerPrintLog) {
|
||||
console.log("<<<Worker: audio media Info: nSise:" + oFrameInfo.frameSize + ",nSampleRate:" + oFrameInfo.samplesPerSec + ',channel:' + oFrameInfo.channels + ',bitsPerSample:' + oFrameInfo.bitsPerSample);
|
||||
}
|
||||
postMessage({
|
||||
'function': "GetFrameData", 'type': "audioType", 'data': aPCMData.buffer,
|
||||
'frameInfo': oFrameInfo, 'errorCode': res
|
||||
}, [aPCMData.buffer]);
|
||||
|
||||
oFrameInfo = null;
|
||||
pPCM = null;
|
||||
aPCMData = null;
|
||||
return PLAYM4_AUDIO_FRAME;
|
||||
|
||||
case VIDEO_TYPE:
|
||||
var szOSDTime = getOSDTime(oFrameInfo);
|
||||
|
||||
var iWidth = oFrameInfo.width;
|
||||
var iHeight = oFrameInfo.height;
|
||||
|
||||
var iYUVSize = iWidth * iHeight * 3 / 2;
|
||||
if (0 === iYUVSize) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
var pYUV = Module._GetFrameBuffer();
|
||||
|
||||
// 图像数据渲染后压回,若从主码流切到子码流,存在数组大小与图像大小不匹配现象
|
||||
var aYUVData = new Uint8Array(iYUVSize);
|
||||
aYUVData.set(Module.HEAPU8.subarray(pYUV, pYUV + iYUVSize));
|
||||
if (bWorkerPrintLog) {
|
||||
console.log("<<<Worker: video media Info: Width:" + oFrameInfo.width + ",Height:" + oFrameInfo.height + ",timeStamp:" + oFrameInfo.timeStamp);
|
||||
}
|
||||
|
||||
postMessage({
|
||||
'function': "GetFrameData", 'type': "videoType", 'data': aYUVData.buffer,
|
||||
'dataLen': aYUVData.length, 'osd': szOSDTime, 'frameInfo': oFrameInfo, 'errorCode': res
|
||||
}, [aYUVData.buffer]);
|
||||
|
||||
oFrameInfo = null;
|
||||
pYUV = null;
|
||||
aYUVData = null;
|
||||
return PLAYM4_VIDEO_FRAME;
|
||||
|
||||
case PRIVT_TYPE:
|
||||
postMessage({
|
||||
'function': "GetFrameData", 'type': "", 'data': null,
|
||||
'dataLen': -1, 'osd': 0, 'frameInfo': null, 'errorCode': PLAYM4_SYS_NOT_SUPPORT
|
||||
});
|
||||
return PLAYM4_SYS_NOT_SUPPORT;
|
||||
|
||||
default:
|
||||
postMessage({
|
||||
'function': "GetFrameData", 'type': "", 'data': null,
|
||||
'dataLen': -1, 'osd': 0, 'frameInfo': null, 'errorCode': PLAYM4_SYS_NOT_SUPPORT
|
||||
});
|
||||
return PLAYM4_SYS_NOT_SUPPORT;
|
||||
}
|
||||
}
|
||||
else {
|
||||
let errorCode = Module._GetLastError(g_nPort);
|
||||
//解码失败返回裸数据
|
||||
if (PLAYM4_DECODE_ERROR === errorCode) {
|
||||
var rawInfo = Module._GetRawDataInfo();
|
||||
var pRawData = Module._GetRawDataBuffer();
|
||||
var aRawData = new Uint8Array(rawInfo.isize);
|
||||
aRawData.set(Module.HEAPU8.subarray(pRawData, pRawData + rawInfo.isize));
|
||||
postMessage({
|
||||
'function': "GetRawData", 'type': "", 'data': aRawData.buffer,
|
||||
'rawDataLen': rawInfo.isize, 'osd': 0, 'frameInfo': null, 'errorCode': errorCode
|
||||
});
|
||||
rawInfo = null;
|
||||
pRawData = null;
|
||||
aRawData = null;
|
||||
}
|
||||
//需要更多数据
|
||||
if (PLAYM4_NEED_MORE_DATA === errorCode || PLAYM4_SYS_NOT_SUPPORT === errorCode || PLAYM4_NEED_NEET_LOOP === errorCode) {
|
||||
postMessage({
|
||||
'function': "GetFrameData", 'type': "", 'data': null,
|
||||
'dataLen': -1, 'osd': 0, 'frameInfo': null, 'errorCode': errorCode
|
||||
});
|
||||
}
|
||||
return errorCode;
|
||||
}
|
||||
}
|
||||
|
||||
// 开始计算时间
|
||||
function startTime() {
|
||||
return new Date().getTime();
|
||||
}
|
||||
|
||||
// 结束计算时间
|
||||
function endTime() {
|
||||
return new Date().getTime();
|
||||
}
|
||||
|
||||
// 字母字符串转byte数组
|
||||
function stringToBytes(str) {
|
||||
var ch, st, re = [];
|
||||
for (var i = 0; i < str.length; i++) {
|
||||
ch = str.charCodeAt(i); // get char
|
||||
st = []; // set up "stack"
|
||||
do {
|
||||
st.push(ch & 0xFF); // push byte to stack
|
||||
ch = ch >> 8; // shift value down by 1 byte
|
||||
}
|
||||
while (ch);
|
||||
// add stack contents to result
|
||||
// done because chars have "wrong" endianness
|
||||
re = re.concat(st.reverse());
|
||||
}
|
||||
// return an array of bytes
|
||||
return re;
|
||||
}
|
||||
})();
|
||||
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
|
|
@ -0,0 +1 @@
|
|||
"use strict";var Module={};var initializedJS=false;function threadPrintErr(){var text=Array.prototype.slice.call(arguments).join(" ");console.error(text)}function threadAlert(){var text=Array.prototype.slice.call(arguments).join(" ");postMessage({cmd:"alert",text:text,threadId:Module["_pthread_self"]()})}var err=threadPrintErr;self.alert=threadAlert;Module["instantiateWasm"]=(info,receiveInstance)=>{var module=Module["wasmModule"];Module["wasmModule"]=null;var instance=new WebAssembly.Instance(module,info);return receiveInstance(instance)};self.onunhandledrejection=e=>{throw e.reason??e};function handleMessage(e){try{if(e.data.cmd==="load"){let messageQueue=[];self.onmessage=e=>messageQueue.push(e);self.startWorker=instance=>{Module=instance;postMessage({"cmd":"loaded"});for(let msg of messageQueue){handleMessage(msg)}self.onmessage=handleMessage};Module["wasmModule"]=e.data.wasmModule;for(const handler of e.data.handlers){Module[handler]=(...args)=>{postMessage({cmd:"callHandler",handler:handler,args:args})}}Module["wasmMemory"]=e.data.wasmMemory;Module["buffer"]=Module["wasmMemory"].buffer;Module["ENVIRONMENT_IS_PTHREAD"]=true;if(typeof e.data.urlOrBlob=="string"){importScripts(e.data.urlOrBlob)}else{var objectUrl=URL.createObjectURL(e.data.urlOrBlob);importScripts(objectUrl);URL.revokeObjectURL(objectUrl)}JSPlayerModule(Module)}else if(e.data.cmd==="run"){Module["__emscripten_thread_init"](e.data.pthread_ptr,0,0,1);Module["__emscripten_thread_mailbox_await"](e.data.pthread_ptr);Module["establishStackSpace"]();Module["PThread"].receiveObjectTransfer(e.data);Module["PThread"].threadInitTLS();if(!initializedJS){Module["__embind_initialize_bindings"]();initializedJS=true}try{Module["invokeEntryPoint"](e.data.start_routine,e.data.arg)}catch(ex){if(ex!="unwind"){throw ex}}}else if(e.data.cmd==="cancel"){if(Module["_pthread_self"]()){Module["__emscripten_thread_exit"](-1)}}else if(e.data.target==="setimmediate"){}else if(e.data.cmd==="checkMailbox"){if(initializedJS){Module["checkMailbox"]()}}else if(e.data.cmd){err("worker.js received unknown command "+e.data.cmd);err(e.data)}}catch(ex){if(Module["__emscripten_thread_crashed"]){Module["__emscripten_thread_crashed"]()}throw ex}}self.onmessage=handleMessage;
|
||||
File diff suppressed because one or more lines are too long
Binary file not shown.
|
|
@ -0,0 +1 @@
|
|||
"use strict";var Module={};var initializedJS=false;function threadPrintErr(){var text=Array.prototype.slice.call(arguments).join(" ");console.error(text)}function threadAlert(){var text=Array.prototype.slice.call(arguments).join(" ");postMessage({cmd:"alert",text:text,threadId:Module["_pthread_self"]()})}var err=threadPrintErr;self.alert=threadAlert;Module["instantiateWasm"]=(info,receiveInstance)=>{var module=Module["wasmModule"];Module["wasmModule"]=null;var instance=new WebAssembly.Instance(module,info);return receiveInstance(instance)};self.onunhandledrejection=e=>{throw e.reason??e};function handleMessage(e){try{if(e.data.cmd==="load"){let messageQueue=[];self.onmessage=e=>messageQueue.push(e);self.startWorker=instance=>{Module=instance;postMessage({"cmd":"loaded"});for(let msg of messageQueue){handleMessage(msg)}self.onmessage=handleMessage};Module["wasmModule"]=e.data.wasmModule;for(const handler of e.data.handlers){Module[handler]=(...args)=>{postMessage({cmd:"callHandler",handler:handler,args:args})}}Module["wasmMemory"]=e.data.wasmMemory;Module["buffer"]=Module["wasmMemory"].buffer;Module["ENVIRONMENT_IS_PTHREAD"]=true;if(typeof e.data.urlOrBlob=="string"){importScripts(e.data.urlOrBlob)}else{var objectUrl=URL.createObjectURL(e.data.urlOrBlob);importScripts(objectUrl);URL.revokeObjectURL(objectUrl)}JSPlayerModule(Module)}else if(e.data.cmd==="run"){Module["__emscripten_thread_init"](e.data.pthread_ptr,0,0,1);Module["__emscripten_thread_mailbox_await"](e.data.pthread_ptr);Module["establishStackSpace"]();Module["PThread"].receiveObjectTransfer(e.data);Module["PThread"].threadInitTLS();if(!initializedJS){Module["__embind_initialize_bindings"]();initializedJS=true}try{Module["invokeEntryPoint"](e.data.start_routine,e.data.arg)}catch(ex){if(ex!="unwind"){throw ex}}}else if(e.data.cmd==="cancel"){if(Module["_pthread_self"]()){Module["__emscripten_thread_exit"](-1)}}else if(e.data.target==="setimmediate"){}else if(e.data.cmd==="checkMailbox"){if(initializedJS){Module["checkMailbox"]()}}else if(e.data.cmd){err("worker.js received unknown command "+e.data.cmd);err(e.data)}}catch(ex){if(Module["__emscripten_thread_crashed"]){Module["__emscripten_thread_crashed"]()}throw ex}}self.onmessage=handleMessage;
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
server:
|
||||
port: 8099
|
||||
|
||||
# 海康威视开放平台配置
|
||||
hikvision:
|
||||
api:
|
||||
# 平台地址(综合安防管理平台 iSecure Center 或 ISUP服务地址)
|
||||
host: https://10.60.49.82:443
|
||||
# AppKey(在开放平台创建应用后获取)
|
||||
app-key: 23046693
|
||||
# AppSecret
|
||||
app-secret: sxsSY3lfL001LB6lLhpO
|
||||
|
||||
spring:
|
||||
application:
|
||||
name: hikvision-video-api
|
||||
jackson:
|
||||
default-property-inclusion: non_null
|
||||
time-zone: GMT+8
|
||||
|
||||
# 抓图本地保存目录(相对于启动目录,或使用绝对路径)
|
||||
app:
|
||||
capture:
|
||||
save-path: ./captures
|
||||
|
||||
logging:
|
||||
level:
|
||||
com.hikvision: DEBUG
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue