萤石云九宫格监控实现流程
说在最前面
本笔记使用的摄像头为海康摄像头,摄像头连接的是海康录像机。小于等于9个摄像机可以直接使用。
将海康录像机添加到萤石云控制台
- 在萤石云开放平台创建萤石云账号(注册并登录)
萤石云开放平台链接 - 进入控制台并进行将设备进行绑定
-
进入控制台
-
进行认证,不然什么都做不了,企业的比个人的认证要方便一些
-
创建应用,创建应用之后就可以看到调用接口所需要的参数,我这里创建的是web应用
-
其中后续调用接口所需要的参数
appKey
和appSecret
分别为应用秘钥模块中的AppKey
和Secret
,经常还需要一个accessToken
,需要调用接口获取,最好不要用这里的AccessToken,毕竟有一定的有效期
-
进行设备绑定
(1)要在海康平台进行配置,接入的平台为萤石云
(2)在萤石云的设备列表添加自己的设备,没有添加过是没有设备的。我这里的设备是录像机,添加设备需要的九位设备序列号和设备验证码在设备后面的标签上(添加设备可以根据需求选择使用接口添加)
-
开始进行开发
- 安装并引入
安装:npm install ezuikit-js
main.js中引入:import EZUIKit from 'ezuikit-js'; Vue.use(EZUIKit );
- 萤石云接口基址:
https://open.ys7.com/
- 萤石云的请求参数都是formData型数据
代码中所用接口
获取accessToken
url: '/api/lapp/token/get'
type: 'post'
data: {
appKey: 应用信息页面的appKey,
appSecret: 应用信息页面的appSecret
}
获取设备列表
url: '/api/lapp/device/list'
type: 'post'
data: {
accessToken: 获取到的accessToken,
pageSize: 每页多少条数据,
pageStart: 开始页数
}
获取摄像头(录像机的通道)列表
url: '/api/lapp/device/camera/list'
type: 'post'
data: {
accessToken: 获取到的accessToken,
deviceSerial: 设备的序列号
}
获取当前摄像头的监控地址
url: '/api/lapp/v2/live/address/get'
type: 'post'
data: {
accessToken: 获取到的accessToken,
deviceSerial: 设备的序列号,
channelNo: 当前摄像头在录像机中的通道号
}
实现完整代码
<template>
<div class="main">
<div class="app-container">
<!-- 左边视频窗口 -->
<div class="left" id="divPlugin">
<div class="hello-ezuikit-js" ref="videoBox">
<!-- 最多9格 -->
<div
v-for="item in 9"
v-show="
(select == 1 && selectVideoFirst == item) ||
(select == 2 &&
item >= selectVideoFirst &&
item < selectVideoFirst + 4) ||
select == 3
"
:key="item"
:class="select == 1 ? 'width' : select == 2 ? 'width2' : 'width3'"
style="position:relative;"
>
<!-- 最多16格 -->
<!-- <div
v-for="item in 16"
v-show="
(select == 1 && selectVideoFirst == item) ||
(select == 2 &&
item >= selectVideoFirst &&
item < selectVideoFirst + 4) ||
(select == 3 &&
item >= selectVideoFirst &&
item < selectVideoFirst + 9) ||
select == 4
"
:key="item"
:class="
select == 1
? 'width'
: select == 2
? 'width2'
: select == 3
? 'width3'
: 'width4'
"
style="position:relative;"
> -->
<div
:id="'video-cover' + item"
class="video-cover"
:class="{
'video-active': selectVideo == item
}"
></div>
<div :id="'video-container' + item"></div>
</div>
</div>
</div>
<!-- 右边操作区 -->
<div class="right">
<el-input
style="width:15.625rem;position: relative;left:1.875rem"
placeholder="请输入设备名称"
prefix-icon="el-icon-search"
v-model="search"
clearable
></el-input>
<div
v-if="searchList.length"
style="width:91%;position: relative;left:1.25rem;height:50%;overflow:auto;top:.3125rem"
>
<div
v-for="(camera, index) in searchList"
@click="selectCamera2(camera)"
:key="index"
:style="
cameraList[selectVideo - 1].ipcSerial == camera.ipcSerial
? 'color:#0079e0'
: ''
"
style="width: 100%;height: 2rem;cursor: pointer"
>
<span
v-if="camera.status"
style="width:.5rem;height:.5rem;borderRadius:50%;background:#0cdc8c;display:inline-block;margin-right:.9375rem"
></span>
<span
v-else
style="width:.5rem;height:.5rem;borderRadius:50%;background:#aaa;display:inline-block;margin-right:.9375rem"
></span>
{{ camera.channelName }}
</div>
</div>
<div
v-else
style="width:91%;position: relative;left:1.25rem;height:50%;overflow:auto;top:.3125rem"
>
<div
v-for="(camera, index) in cameraList"
@click="selectCamera(camera, index)"
:key="index"
:style="
cameraList[selectVideo - 1].ipcSerial == camera.ipcSerial
? 'color:#0079e0'
: ''
"
style="width: 100%;height: 2rem;cursor: pointer"
>
<span
v-if="camera.status"
style="width:.5rem;height:.5rem;borderRadius:50%;background:#0cdc8c;display:inline-block;margin-right:.9375rem"
></span>
<span
v-else
style="width:.5rem;height:.5rem;borderRadius:50%;background:#aaa;display:inline-block;margin-right:.9375rem"
></span>
{{ camera.channelName }}
</div>
</div>
<div class="btns">
<div class="wheel">
<div @click="deviceCapture" class="camera">
<i class="el-icon-camera"></i>
</div>
<div class="top">
<div
@click="startPTZCtrl('0')"
class="triangle triangle-top"
></div>
</div>
<div class="center">
<div class="center-left">
<div
@click="startPTZCtrl('2')"
class="triangle triangle-left"
></div>
</div>
<div class="center-right">
<div
@click="startPTZCtrl('3')"
class="triangle triangle-right"
></div>
</div>
</div>
<div class="bottom">
<div
@click="startPTZCtrl('1')"
class="triangle triangle-bottom"
></div>
</div>
</div>
<div class="two-btn">
<el-button @click="startPTZCtrl('9')" size="mini" type="primary"
>-</el-button
>
<el-button @click="startPTZCtrl('8')" size="mini" type="primary"
>+</el-button
>
</div>
<el-button
class="right-btn"
@click="showAllScreen"
size="small"
type="primary"
>全屏显示</el-button
>
</div>
</div>
</div>
<!-- 底部切屏按钮 -->
<div
@click="select = 1"
style="position: absolute;left:1.875rem;top:94.5vh;cursor: pointer;"
>
<img
class="rect"
v-if="select == 1"
src="../../assets/images1/one_1.png"
alt
/>
<img class="rect" v-else src="../../assets/images1/one.png" alt />
</div>
<div
@click="select = 2"
style="position: absolute;left:4rem;top:94.5vh;cursor: pointer;"
>
<img
class="rect"
v-if="select == 2"
src="../../assets/images1/four_1.png"
alt
/>
<img class="rect" v-else src="../../assets/images1/four.png" alt />
</div>
<div
@click="select = 3"
style="position: absolute;left:6.125rem;top:94.5vh;cursor: pointer;"
>
<img
class="rect"
v-if="select == 3"
src="../../assets/images1/nine_1.png"
alt
/>
<img class="rect" v-else src="../../assets/images1/nine.png" alt />
</div>
<!-- <div
@click="select = 4"
style="position: absolute;left:8.25rem;top:94.5vh;cursor: pointer;"
>
<img
class="rect"
v-if="select == 4"
src="../../assets/images1/nine_1.png"
alt
/>
<img class="rect" v-else src="../../assets/images1/nine.png" alt />
</div> -->
</div>
</template>
<script>
import EZUIKit from "ezuikit-js";
import axios from "axios";
axios.defaults.baseURL = "/yingshiyun";
export default {
name: "Project",
data() {
return {
count: 0,
selectPlayer: "", // 选中的监控
deviceList: [], // 录像机个数
cameraList: [], // 摄像头个数
selectChannelNo: 1, // 选中的通道号
select: 1, // 选中的网格数
accessToken: "", // 用appKey和APPSecret请求回来的token
selectVideo: 1, // 当前选中的video序号
selectVideoFirst: 1,
search: "", // 搜索框
searchList: []
};
},
created() {},
async mounted() {
// 获得萤石云的token
this.getDeviceToken();
},
watch: {
// 选择显示video数量
select(value) {
this.select = value;
this.cameraList.forEach((item, index) => {
if (value == 1) {
if (item.code == 200)
item.player.reSize(
this.$refs.videoBox.offsetWidth,
this.$refs.videoBox.offsetHeight
);
} else if (value == 2) {
if (item.code == 200)
item.player.reSize(
this.$refs.videoBox.offsetWidth / 2 - 2,
this.$refs.videoBox.offsetHeight / 2 - 2
);
} else if (value == 3) {
if (item.code == 200)
item.player.reSize(
this.$refs.videoBox.offsetWidth / 3,
this.$refs.videoBox.offsetHeight / 3
);
}
// else {
// if (item.code == 200)
// item.player.reSize(
// this.$refs.videoBox.offsetWidth / 4,
// this.$refs.videoBox.offsetHeight / 4
// );
// }
});
},
search(value) {
if (value) {
this.searchList = this.cameraList.filter(
item => item.channelName.indexOf(value) > -1
);
} else {
this.searchList = [];
}
}
},
beforeDestroy() {
this.players.forEach(item => {
item.stop();
});
},
methods: {
// 获取token
async getDeviceToken() {
const data = new FormData();
data.append("appKey", "萤石云账户的appKey");
data.append("appSecret", "萤石云账户的appSecret");
var res = await axios({
headers: {
"Content-Type": "application/x-www-form-urlencoded"
},
method: "post",
url: "/api/lapp/token/get",
data: data
});
if (res.data.code == 200) {
this.accessToken = res.data.data.accessToken;
// TODO:deviceList(设备列表)需要利用获取设备列表接口获取
if (this.deviceList.length) {
this.deviceList.forEach(item => {
this.getChannelList(item);
});
}
}
},
// 获取摄像头(通道)列表
async getChannelList(device) {
const data = new FormData();
data.append("accessToken", this.accessToken);
data.append("deviceSerial", device.deviceSerial);
var res = await axios({
headers: {
"Content-Type": "application/x-www-form-urlencoded"
},
method: "post",
url: "/api/lapp/device/camera/list",
data: data
});
this.count++;
if (res.data.code == 200) {
var canUseList = [];
canUseList = res.data.data.filter(
item => item.deviceSerial != item.ipcSerial
);
this.cameraList = [...this.cameraList, ...canUseList];
if (this.count >= this.deviceList.length) {
this.cameraList.forEach((item, index) => {
this.getEzuikitUrl(item, index);
});
this.selectPlayer = this.cameraList[0];
}
}
},
// 获取监控地址
async getEzuikitUrl(item, index, select) {
const data = new FormData();
data.append("accessToken", this.accessToken);
data.append("deviceSerial", item.deviceSerial);
data.append("channelNo", item.channelNo);
var res = await axios({
headers: {
"Content-Type": "application/x-www-form-urlencoded"
},
url: "/api/lapp/v2/live/address/get",
method: "post",
data: data
});
if (res.data.code == 200) {
var url = res.data.data.url;
item.url = url;
item.code = 200;
item.msg = res.data.msg;
// 渲染视频播放
this.StructureEZUIKitPlayer(url, item, index, select);
} else {
var ref = document.querySelector("#video-cover" + (index + 1));
ref.innerText = res.data.msg;
}
this.$set(this.cameraList, index, item);
},
// 渲染视频播放
StructureEZUIKitPlayer(url, item, index, select) {
if (select) {
var player = new EZUIKit.EZUIKitPlayer({
autoplay: false,
audio: "0",
id: "video-container", // 视频容器ID
accessToken: this.accessToken,
url: url, // 初始化写死一个离线或者找不到的设备,避免初始化无法创建播放器;
template: "simple",
width: this.$refs.videoBox.offsetWidth / 3,
height: this.$refs.videoBox.offsetHeight / 3
});
this.selectPlayer.player = player;
} else {
var player = new EZUIKit.EZUIKitPlayer({
autoplay: false,
audio: "0",
id: `video-container${index + 1}`, // 视频容器ID
accessToken: this.accessToken,
url: url, // 初始化写死一个离线或者找不到的设备,避免初始化无法创建播放器;
template: "simple",
width: this.$refs.videoBox.offsetWidth / 3,
height: this.$refs.videoBox.offsetHeight / 3
});
item.player = player;
}
this.select = 3;
},
// 开始云台控制
async startPTZCtrl(direction) {
// 放大缩小:8 放大 9 缩小
// 方向:0-上,1-下,2-左,3-右,4-左上,5-左下,6-右上,7-右下
const data = new FormData();
data.append("accessToken", this.accessToken);
data.append("deviceSerial", this.selectPlayer.deviceSerial);
data.append("channelNo", this.selectPlayer.channelNo);
data.append("direction", direction);
data.append("speed", "1");
var res = await axios({
headers: {
"Content-Type": "application/x-www-form-urlencoded"
},
method: "post",
url: "/api/lapp/device/ptz/start",
data: data
});
if (res.data.code != 200) {
this.$message(res.data.msg);
} else {
this.stopPTZCtrl(direction);
}
},
// 停止云台控制
async stopPTZCtrl() {
const data = new FormData();
data.append("accessToken", this.accessToken);
data.append("deviceSerial", this.selectPlayer.deviceSerial);
data.append("channelNo", this.selectPlayer.channelNo);
data.append("direction", "0");
var res = await axios({
headers: {
"Content-Type": "application/x-www-form-urlencoded"
},
method: "post",
url: "/api/lapp/device/ptz/stop",
data: data
});
this.$message(res.data.msg);
},
// 设备抓拍图片
async deviceCapture() {
this.cameraList[this.selectVideo - 1].player.capturePicture();
},
// 全屏显示
showAllScreen() {
this.cameraList[this.selectVideo - 1].player.cancelFullScreen();
this.cameraList[this.selectVideo - 1].player.fullScreen();
},
selectCamera(item, index) {
this.selectPlayer = item;
this.selectVideo = index + 1;
this.selectVideoFirst = index + 1;
},
selectCamera2(camera) {
var index = this.cameraList.findIndex(
item => item.ipcSerial == camera.ipcSerial
);
this.selectPlayer = item;
this.selectVideo = index + 1;
this.selectVideoFirst = index + 1;
}
}
};
</script>
<style lang="scss" scoped>
.main {
position: fixed;
.app-container {
width: 94.375rem;
height: 71vh;
// border: .125rem solid rgb(116, 228, 24);
left: 1.875rem;
background-color: rgb(255, 255, 255);
margin: 0rem auto;
position: relative;
top: 13.125rem;
// border-radius:.625rem;
overflow: hidden;
.left {
overflow: hidden;
width: 80%;
border: 0.125rem solid rgb(226, 181, 33);
height: 100%;
position: absolute;
left: 0;
top: 0rem;
.title {
position: absolute;
top: 1rem;
left: 0.75rem;
font-size: 1rem;
font-weight: 600;
color: #000;
}
.time {
position: absolute;
top: 1rem;
right: 1.25rem;
font-size: 1rem;
font-weight: 600;
color: #000;
}
}
.left1 {
width: 80%;
border: 0.125rem solid rgb(37, 43, 102);
height: 100%;
top: 0rem;
position: absolute;
left: 0;
overflow: hidden;
}
.left2 {
top: 0rem;
width: 80%;
border: 0.125rem solid rgb(37, 43, 102);
height: 100%;
position: absolute;
overflow: hidden;
left: 0;
}
.right {
display: flex;
flex-direction: column;
justify-content: space-between;
background: rgb(255, 255, 255);
width: 20%;
height: 100%;
top: 0;
// border: .0625rem solid rgb(22, 21, 27);
position: absolute;
right: 0;
.right1 {
width: 100%;
text-align: start;
line-height: 2.5rem;
color: #6e727a;
margin: 0.3125rem auto;
height: 2.5rem;
padding: 0 1.25rem;
cursor: pointer;
}
.right1:hover {
background: #1393fc;
color: rgb(255, 255, 255);
border: none;
}
.right2 {
width: 100%;
padding: 0 1.25rem;
height: 2.5rem;
margin: 0.3125rem auto;
line-height: 2.5rem;
background: #1393fc;
color: rgb(255, 255, 255);
cursor: pointer;
}
}
}
}
.hello-ezuikit-js {
width: 100%;
height: 100%;
display: flex;
flex-wrap: wrap;
overflow: hidden;
background: #ccc;
}
.width {
width: 100%;
height: 100%;
}
.width2 {
width: 50%;
height: 50%;
}
.width3 {
width: 33.3%;
height: 33.3%;
}
.width4 {
width: 25%;
height: 25%;
}
.video-active {
border: 0.125rem solid rgb(255, 133, 62) !important;
}
.rect {
width: 1.625rem;
height: 1.625rem;
}
.video-cover {
display: flex;
justify-content: center;
align-items: center;
font-size: 12px;
color: rgb(153, 0, 0);
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 5;
border-top: 0.0313rem solid #fff;
border-right: 0.0313rem solid #fff;
}
::v-deep .el-dialog__wrapper {
display: flex;
justify-content: center;
align-items: center;
}
::v-deep .el-dialog__header {
background: #efefef;
}
::v-deep .el-dialog {
width: 36rem;
}
::v-deep .el-dialog__body {
padding-top: 3.75rem;
display: flex;
justify-content: center;
}
::v-deep .el-form {
width: 28.125rem;
}
::v-deep .el-form-item__label {
width: 6.875rem !important;
}
::v-deep .el-input {
width: 20rem;
}
.tabs {
width: 100%;
height: 2.4375rem;
display: flex;
position: absolute;
top: 3.375rem;
left: 0;
border-top: 1px solid #ccc;
.tab-item {
width: 50%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
}
.tab-active {
background: #ccc;
color: #fff;
}
}
.wheel {
position: relative;
width: 9.375rem;
height: 9.375rem;
border-radius: 50%;
background: rgb(77, 77, 77);
.camera {
position: absolute;
left: 3.75rem;
top: 3.75rem;
z-index: 5;
width: 1.875rem;
height: 1.875rem;
text-align: center;
line-height: 1.875rem;
font-size: 1.25rem;
color: #fff;
cursor: pointer;
}
.top {
height: 33.3%;
display: flex;
justify-content: center;
align-items: center;
}
.center {
height: 33.3%;
display: flex;
justify-content: space-between;
align-items: center;
.center-left,
.center-right {
width: 33.3%;
display: flex;
justify-content: center;
}
}
.bottom {
height: 33.3%;
display: flex;
justify-content: center;
align-items: center;
}
.triangle {
width: 0;
height: 0;
border: 0.625rem solid transparent;
cursor: pointer;
}
.triangle-top {
border-bottom: 0.9375rem solid #fff;
}
.triangle-bottom {
border-top: 0.9375rem solid #fff;
}
.triangle-left {
border-right: 0.9375rem solid #fff;
}
.triangle-right {
border-left: 0.9375rem solid #fff;
}
}
.bg-black {
display: flex;
justify-content: center;
align-items: center;
background: #000;
color: rgb(151, 0, 0);
font-size: 12px;
}
.btns {
display: flex;
flex-direction: column;
align-items: center;
.two-btn {
width: 11.25rem;
margin: 0.9375rem auto;
.el-button {
width: 50%;
margin: 0;
}
}
}
::v-deep .el-button {
background: rgb(77, 77, 77);
border-color: rgb(77, 77, 77);
}
::v-deep .el-button.right-btn {
width: 11.25rem;
margin: 0 auto 0.625rem;
}
</style>
展示效果(出于隐私不显示视频)
额外总结
1、上、下、左、右、放大、缩小是用来操作球机或者可以进行操作的摄像机,截图和全屏显示功能均可使用;
2、获取录像机下的通道列表,每一条信息中的status
为1
时,代表此通道有摄像机,为-1
时,代表此通道未连接摄像机;
3、ipcSerial
字段的说明
利用设备序列号获取通道列表时:
(1)status
字段为1
时,且ipcSerial
和deviceSerial
相同时,代表当前设备是直连的摄像头,摄像头的;
(2)status
字段为-1
时,且ipcSerial
和deviceSerial
相同时,代表此通道未连接摄像机,连接萤石云的设备为硬盘录像机;
4、上面代码说明
(1)因为我的设备使用接口添加的,设备列表数据库存了一份,所以应用中没有使用接口获取设备列表;
(2)代码中判断当前通道是否接入摄像头是判断ipcSerial
和deviceSerial
是否相等;如果萤石云直接连的摄像头,需要改变判断方式;
(3)代码中注掉的部分是16格视频,使用16格视频时需要解除注释,渲染视频播放
方法中的this.select=4
,宽高比也应为/4
。