1: 文档地址: 这里有api列表
2:运管中心的 api网关有api列表点击名称可以看到接口详情,可以在线测试接口数据 ,api 列表跟上面网页的api 列表差不多
接口详情:
在线测试: 填写分页数据, key 和 secret 就可以了
3:springboot 海康签名工具类, 来自海康SDK包
package com.tang.object.utils;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.lang3.StringUtils;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.io.UnsupportedEncodingException;
import java.net.MalformedURLException;
import java.util.*;
public class HaiKangSignUtil {
public static String CONTENTTYPE = "application/json";
public HaiKangSignUtil() {
}
public static String sign(String secret, String method, String path, Map<String, Object> headers, Map<String, Object> querys, Map<String, Object> bodys, List<String> signHeaderPrefixList) {
try {
Mac hmacSha256 = Mac.getInstance("HmacSHA256");
byte[] keyBytes = secret.getBytes("UTF-8");
hmacSha256.init(new SecretKeySpec(keyBytes, 0, keyBytes.length, "HmacSHA256"));
String stringToSign = buildStringToSign(method, path, headers, querys, bodys, signHeaderPrefixList);
return new String(Base64.encodeBase64(hmacSha256.doFinal(stringToSign.getBytes("UTF-8"))), "UTF-8");
} catch (Exception var14) {
throw new RuntimeException(var14);
}
}
private static String buildStringToSign(String method, String path, Map<String, Object> headers, Map<String, Object> querys, Map<String, Object> bodys, List<String> signHeaderPrefixList) {
StringBuilder sb = new StringBuilder();
sb.append(method.toUpperCase()).append("\n");
if (null != headers) {
if (null != headers.get("Accept")) {
sb.append((String)headers.get("Accept"));
sb.append("\n");
}
if (null != headers.get("Content-MD5")) {
sb.append((String)headers.get("Content-MD5"));
sb.append("\n");
}
if (null != headers.get("Content-Type")) {
String contentType = (String)headers.get("Content-Type");
if (contentType.contains("boundary")) {
String[] strings = contentType.split(";");
String[] var9 = strings;
int var10 = strings.length;
for(int var11 = 0; var11 < var10; ++var11) {
String string = var9[var11];
if (!string.contains("boundary")) {
sb.append(string);
sb.append(";");
}
}
sb.deleteCharAt(sb.toString().length() - 1);
} else {
sb.append(contentType);
}
sb.append("\n");
}
if (null != headers.get("Date")) {
sb.append((String)headers.get("Date"));
sb.append("\n");
}
if (StringUtils.isNotEmpty((CharSequence)headers.get("x-ca-path"))) {
path = (String)headers.get("x-ca-path");
}
}
sb.append(buildHeaders(headers, signHeaderPrefixList));
sb.append(buildResource(path, querys, bodys));
return sb.toString();
}
private static String buildResource(String path, Map<String, Object> querys, Map<String, Object> bodys) {
StringBuilder sb = new StringBuilder();
if (!StringUtils.isBlank(path)) {
sb.append(path);
}
Map<Object, Object> sortMap = new TreeMap();
Iterator var5;
Map.Entry body;
if (null != querys) {
var5 = querys.entrySet().iterator();
while(var5.hasNext()) {
body = (Map.Entry)var5.next();
if (!StringUtils.isBlank((CharSequence)body.getKey())) {
sortMap.put(body.getKey(), body.getValue());
}
}
}
if (null != bodys) {
var5 = bodys.entrySet().iterator();
while(var5.hasNext()) {
body = (Map.Entry)var5.next();
if (!StringUtils.isBlank((CharSequence)body.getKey())) {
sortMap.put(body.getKey(), body.getValue());
}
}
}
StringBuilder sbParam = new StringBuilder();
Iterator var9 = sortMap.entrySet().iterator();
while(var9.hasNext()) {
Map.Entry<String, Object> item = (Map.Entry)var9.next();
if (!StringUtils.isBlank((CharSequence)item.getKey())) {
if (0 < sbParam.length()) {
sbParam.append("&");
}
sbParam.append((String)item.getKey());
sbParam.append("=").append(item.getValue());
}
}
if (0 < sbParam.length()) {
sb.append("?");
sb.append(sbParam);
}
return sb.toString();
}
private static String buildHeaders(Map<String, Object> headers, List<String> signHeaderPrefixList) {
StringBuilder sb = new StringBuilder();
if (null != signHeaderPrefixList) {
signHeaderPrefixList.remove("x-ca-signature");
signHeaderPrefixList.remove("Accept");
signHeaderPrefixList.remove("Content-MD5");
signHeaderPrefixList.remove("Content-Type");
signHeaderPrefixList.remove("Date");
Collections.sort(signHeaderPrefixList);
}
if (null != headers) {
Map<String, Object> sortMap = new TreeMap();
sortMap.putAll(headers);
StringBuilder signHeadersStringBuilder = new StringBuilder();
Iterator var5 = sortMap.entrySet().iterator();
while(var5.hasNext()) {
Map.Entry<String, String> header = (Map.Entry)var5.next();
if (isHeaderToSign((String)header.getKey(), signHeaderPrefixList)) {
sb.append((String)header.getKey());
sb.append(":");
if (!StringUtils.isBlank((CharSequence)header.getValue())) {
sb.append((String)header.getValue());
}
sb.append("\n");
if (0 < signHeadersStringBuilder.length()) {
signHeadersStringBuilder.append(",");
}
signHeadersStringBuilder.append((String)header.getKey());
}
}
headers.put("x-ca-signature-headers", signHeadersStringBuilder.toString());
}
return sb.toString();
}
private static boolean isHeaderToSign(String headerName, List<String> signHeaderPrefixList) {
if (StringUtils.isBlank(headerName)) {
return false;
} else if ("x-ca-path".equals(headerName)) {
return false;
} else if (headerName.startsWith("x-ca-")) {
return true;
} else {
if (null != signHeaderPrefixList) {
Iterator var2 = signHeaderPrefixList.iterator();
while(var2.hasNext()) {
String signHeaderPrefix = (String)var2.next();
if (headerName.equalsIgnoreCase(signHeaderPrefix)) {
return true;
}
}
}
return false;
}
}
public static String utf8ToIso88591(String str) {
if (str == null) {
return str;
} else {
try {
return new String(str.getBytes("UTF-8"), "ISO-8859-1");
} catch (UnsupportedEncodingException var2) {
throw new RuntimeException(var2.getMessage(), var2);
}
}
}
public static Map<String, Object> initialBasicHeader(String method, String path, Map<String, Object> querys, Map<String, Object> bodys, String contentType, List<String> signHeaderPrefixList, String appKey, String appSecret) throws MalformedURLException {
Map<String, Object> headers = new HashMap();
headers.put("x-ca-timestamp", String.valueOf((new Date()).getTime()));
headers.put("x-ca-nonce", UUID.randomUUID().toString());
headers.put("x-ca-key", appKey);
headers.put("Accept", "*/*");
headers.put("Content-Type", contentType);
headers.put("x-ca-signature", sign(appSecret, method, path, headers, querys, bodys, signHeaderPrefixList));
return headers;
}
}
4:拉取点位
接口名称 :
private final static String previewURLsApi = "/artemis/api/resource/v1/cameras";
代码:
private final static String previewURLsApi = "/artemis/api/resource/v1/cameras";
public static String CONTENTTYPE = "application/json";
// 请求方法
public JSONObject requestByPostAddHeader(Object o, String url, MediaType mediaType, Map<String, Object> header) throws Exception {
return CompletableFuture.supplyAsync(() -> {
Mono<String> stringMono = ignoreSSLWebClient.post().uri(url)
.headers(httpHeaders -> {
header.entrySet().stream().forEach(e->{
httpHeaders.add(e.getKey(), e.getValue().toString());
});
})
// 类型
.contentType(mediaType)
//参数
.bodyValue(o)
.retrieve()
//返回值类型
.bodyToMono(String.class)
.timeout(Duration.ofSeconds(webclientTimeOut));
JSONObject jsonObject = JSON.parseObject(stringMono.block(Duration.ofSeconds(webclientTimeOut)));
if (null == jsonObject) {
log.error(ExceptionEnum.WEBCLIENT_RESPONSE_IS_NULL.getMsg() + " 原因: " + jsonObject);
throw new BusinessException(ExceptionEnum.WEBCLIENT_RESPONSE_IS_NULL);
}
return jsonObject;
}, executorService)
//异常处理
.exceptionally((error) -> {
// 处理任务 的返回值或异常
error.printStackTrace();
throw new BusinessException(ExceptionEnum.WEBCLIENT_EXCEPTION);
}).get(webclientTimeOut, TimeUnit.SECONDS);
}
// 拉取点位
public viod test() {
JSONObject body = new JSONObject();
body.put("pageNo", 1);
body.put("pageSize", 500);
Map<String, Object> headers = HaiKangSignUtil.initialBasicHeader("POST", previewURLsApi, null, null, HaiKangSignUtil.CONTENTTYPE, null, wgHttpXyPO.getHtAppId(), wgHttpXyPO.getHtKey());
for (Map.Entry<String, Object> entry : headers.entrySet()) {
headers.put(entry.getKey(), HaiKangSignUtil.utf8ToIso88591(entry.getValue().toString()));
}
// 返回数据,获取分页总数,计算是否有下一页,重复调用,直到数据拉取完毕, 根据设备编号cameraIndexCode 监控点唯一标识进行增加或者修改。 saveOrUpdate 可以保存或更新。必要的几个字段
JSONObject jsonObject = webclientService.requestByPostAddHeader(body, ip+端口 + previewURLsApi, MediaType.APPLICATION_JSON, headers);
}
返回数据中 监控唯一标识,播放用到此字段, 监控点名称(cameraame)后面状态更新时用到。
5: 状态更新
接口名称:
private final static String previewURLsApi = "/artemis/api/nms/v1/online/encode_device/get";
private final static String previewURLsApi = "/artemis/api/nms/v1/online/encode_device/get";
// 工具类已包含
public static String CONTENTTYPE = "application/json";
// 请求方法上面接口 拿出来公共类使用就行
public void test(){
JSONObject body = new JSONObject();
body.put("pageNo", 1);
body.put("pageSize", 500);
Map<String, Object> headers = HaiKangSignUtil.initialBasicHeader("POST", previewURLsApi, null, null, HaiKangSignUtil.CONTENTTYPE, null, wgHttpXyPO.getHtAppId(), wgHttpXyPO.getHtKey());
for (Map.Entry<String, Object> entry : headers.entrySet()) {
headers.put(entry.getKey(), HaiKangSignUtil.utf8ToIso88591(entry.getValue().toString()));
}
// 返回值
JSONObject jsonObject = webclientService.requestByPostAddHeader(body, wgHttpXyPO.getHtHttpUrl().trim() + previewURLsApi, MediaType.APPLICATION_JSON, headers);
}
接口返回,这个接口没有返回监控点唯一标识,但是有监控点名称。根据监控点名称更新就行
6:vue3 视频播放, 下载视频web插件, 这个页面有点慢,要等一会才出来
安装bin 里面的插件, 可以做成zip 放到服务器 提供下载
public 目录下, 新建一个 common目录,复制js 到 common目录
三个文件复制
vue3 代码:公共模块: videoInit.vue
采用 el-dialog, 打开播放, 关闭断开销毁。
<template>
<el-dialog
:close-on-click-modal="false"
:close-on-press-escape="false"
:model-value="videoDialogVisible"
append-to-body
@close="cancel"
@opened="handleDialogOpened"
:title="titleName"
:lock-scroll="false"
width="85%"
destroy-on-close
class="video-dialog"
center
>
<div class="dialog-content">
<!-- 视频窗口 -->
<div id="playWnd" ref="playWndRef" class="play-wnd"></div>
</div>
</el-dialog>
</template>
<script setup>
import {ref, onMounted, onUnmounted, nextTick} from 'vue'
import {debounce} from 'lodash-es'
import formUtils from "@/utils/formUtils"
import Type from "@/utils/typeEnum"
const props = defineProps(['videoDialogVisible'])
const emit = defineEmits(["update:videoDialogVisible"])
const titleName = ref('')
const oWebControl = ref(null)
const pubKey = ref('')
const playWndRef = ref(null)
const resizeObserver = ref(null)
const secrt = ref('')
const k = ref('')
const host = ref('')
const shb = ref('')
// 暴露方法
const init = (name, number, app, key, ip) => {
titleName.value = name
shb.value = number // 监控点唯一标识
// 下面三个是 需要提供的,可以从后端返回,将appSecret 加密返回,前端解密 ,aes
secrt.value = key // appSecret
k.value = app // appId
host.value = ip // 连接ip
initPlugin()
}
defineExpose({init})
// 加载脚本
const loadScript = (src) => {
return new Promise((resolve, reject) => {
const script = document.createElement('script')
script.src = src
script.onload = resolve
script.onerror = reject
document.body.appendChild(script)
})
}
// 对话框打开后初始化
const handleDialogOpened = async () => {
await nextTick()
initResizeObserver()
adjustPlayerSize()
}
// 初始化ResizeObserver监听
const initResizeObserver = () => {
// 先断开之前的观察
if (resizeObserver.value) {
resizeObserver.value.disconnect()
}
const dialogElement = document.querySelector('.video-dialog')
if (dialogElement) {
resizeObserver.value = new ResizeObserver(debounce(() => {
adjustPlayerSize()
}, 100))
resizeObserver.value.observe(dialogElement)
}
}
// 调整播放器尺寸
const adjustPlayerSize = () => {
if (!playWndRef.value) return
const dialogBody = document.querySelector('.video-dialog .el-dialog__body')
if (!dialogBody) return
// 计算可用高度
const dialogStyle = window.getComputedStyle(dialogBody)
const paddingTop = parseFloat(dialogStyle.paddingTop)
const paddingBottom = parseFloat(dialogStyle.paddingBottom)
// 可用高度 = 对话框内容高度 - 内边距 - 按钮区域高度(50px)
const availableHeight = dialogBody.clientHeight - paddingTop - paddingBottom - 50
const width = playWndRef.value.clientWidth
const height = Math.max(600, availableHeight) // 设置最小高度300px
// 更新DOM尺寸
playWndRef.value.style.height = `${height}px`
// 更新插件窗口尺寸
if (oWebControl.value) {
oWebControl.value.JS_Resize(width, height)
setWndCover()
}
}
const initPlugin = async () => {
await loadScript('/common/jquery-1.12.4.min.js')
await loadScript('/common/jsencrypt.min.js')
await loadScript('/common/web-control_1.2.7.min.js')
oWebControl.value = new WebControl({
szPluginContainer: "playWnd", // 指定容器id
iServicePortStart: 15900, // 指定起止端口号,建议使用该值
iServicePortEnd: 15900,
szClassId: "23BF3B0A-2C56-4D97-9C03-0CB103AA8F11", // 用于IE10使用ActiveX的clsid
cbConnectSuccess: function () { // 创建WebControl实例成功
oWebControl.value.JS_StartService("window", { // WebControl实例创建成功后需要启动服务
dllPath: "./VideoPluginConnect.dll" // 值"./VideoPluginConnect.dll"写死
}).then(function () { // 启动插件服务成功
oWebControl.value.JS_SetWindowControlCallback({ // 设置消息回调
cbIntegrationCallBack: cbIntegrationCallBack
});
const width = playWndRef.value?.clientWidth || 1000
const height = playWndRef.value?.clientHeight || 600
oWebControl.value.JS_CreateWnd("playWnd", width, height).then(function () { //JS_CreateWnd创建视频播放窗口,宽高可设定
getPubKey(() => {
initSettings()
adjustPlayerSize()
})
});
}, function () { // 启动插件服务失败
});
},
cbConnectError: function () { // 创建WebControl实例失败
// 没安装的时候这里也没进来,不生效
formUtils.confirm(
Type.WARNING,
'插件初始化失败,请确认是否已安装插件;如果未安装,请先下载插件后进行安装!',
() => window.open(`${location.protocol}//${location.host}/download/plugin.zip`)
)
}
});
}
const initSettings = () => {
var appkey = k.value; //综合安防管理平台提供的appkey,必填
var secret = setEncrypt(secrt.value); //综合安防管理平台提供的secret,必填
var ip = host.value; //综合安防管理平台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.value.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) {
adjustPlayerSize()
startPreview()
});
}
// 获取公钥
const getPubKey = (callback) => {
oWebControl.value.JS_RequestInterface({
funcName: "getRSAPubKey",
argument: JSON.stringify({
keyLength: 1024
})
}).then(function (oData) {
console.log(oData);
if (oData.responseMsg.data) {
pubKey.value = oData.responseMsg.data;
callback()
}
})
}
// RSA加密
const setEncrypt = (value) => {
const encrypt = new JSEncrypt()
encrypt.setPublicKey(pubKey.value)
return encrypt.encrypt(value)
}
// 消息回调
const cbIntegrationCallBack = (oData) => {
}
// 开始预览
const startPreview = () => {
if (!oWebControl.value) return
const streamMode = 0
const transMode = 1
const gpuMode = 0
const wndId = -1
oWebControl.value.JS_RequestInterface({
funcName: "startPreview",
argument: JSON.stringify({
cameraIndexCode: shb.value,
streamMode: streamMode,
transMode: transMode,
gpuMode: gpuMode,
wndId: wndId
})
})
}
// 停止预览
const stopAllPreview = () => {
oWebControl.value?.JS_RequestInterface({
funcName: "stopAllPreview"
})
}
// 窗口裁剪
const setWndCover = () => {
if (!oWebControl.value || !playWndRef.value) return
const rect = playWndRef.value.getBoundingClientRect()
const viewportWidth = window.innerWidth
const viewportHeight = window.innerHeight
let coverLeft = Math.max(0, -rect.left)
let coverTop = Math.max(0, -rect.top)
let coverRight = Math.max(0, rect.right - viewportWidth)
let coverBottom = Math.max(0, rect.bottom - viewportHeight)
const width = playWndRef.value.clientWidth
const height = playWndRef.value.clientHeight
coverLeft = Math.min(coverLeft, width)
coverTop = Math.min(coverTop, height)
coverRight = Math.min(coverRight, width)
coverBottom = Math.min(coverBottom, height)
oWebControl.value.JS_RepairPartWindow(0, 0, width + 1, height)
if (coverLeft > 0) oWebControl.value.JS_CuttingPartWindow(0, 0, coverLeft, height)
if (coverTop > 0) oWebControl.value.JS_CuttingPartWindow(0, 0, width + 1, coverTop)
if (coverRight > 0) oWebControl.value.JS_CuttingPartWindow(width - coverRight, 0, coverRight, height)
if (coverBottom > 0) oWebControl.value.JS_CuttingPartWindow(0, height - coverBottom, width, coverBottom)
}
// 处理窗口大小变化
const handleResize = debounce(() => {
adjustPlayerSize()
}, 200)
// 清理资源
const stop = () => {
if (resizeObserver.value) {
resizeObserver.value.disconnect()
resizeObserver.value = null
}
window.removeEventListener('resize', handleResize)
if (oWebControl.value) {
stopAllPreview()
oWebControl.value.JS_HideWnd()
oWebControl.value.JS_DestroyWnd()
oWebControl.value.JS_Disconnect().then(
() => console.log("插件连接已断开"),
() => console.log("插件断开连接失败")
)
oWebControl.value = null
}
}
// 关闭对话框
const cancel = () => {
stop()
emit("update:videoDialogVisible", false)
}
// 生命周期
onMounted(() => {
window.addEventListener('resize', handleResize)
})
onUnmounted(() => {
stop()
})
</script>
<style scoped>
.video-dialog {
top: 5vh;
}
.video-dialog :deep(.el-dialog) {
display: flex;
flex-direction: column;
max-height: 90vh;
}
.video-dialog :deep(.el-dialog__body) {
flex: 1;
padding: 20px;
overflow: hidden;
display: flex;
flex-direction: column;
}
.dialog-content {
flex: 1;
display: flex;
flex-direction: column;
height: 80vh;
}
.play-wnd {
flex: 1;
min-height: 300px;
border: 1px solid #ebeef5;
border-radius: 4px;
margin-top: 10px;
background-color: #000;
}
</style>
调用播放:
导入
配置:
事件点击播放
效果: