基于萤石云实现的九宫格视频监控效果
萤石云九宫格监控实现流程
说在最前面
将海康录像机添加到萤石云控制台
开始进行开发
代码中所用接口
获取accessToken
获取设备列表
获取摄像头(录像机的通道)列表
获取当前摄像头的监控地址
实现完整代码
展示效果(出于隐私不显示视频)
额外总结
1、上、下、左、右、放大、缩小是用来操作球机或者可以进行操作的摄像机,截图和全屏显示功能均可使用;
2、获取录像机下的通道列表,每一条信息中的`status`为`1`时,代表此通道有摄像机,为`-1`时,代表此通道未连接摄像机;
3、`ipcSerial`字段的说明
4、上面代码说明
说在最前面
本笔记使用的摄像头为海康摄像头,摄像头连接的是海康录像机。小于等于9个摄像机可以直接使用。
将海康录像机添加到萤石云控制台
- 在萤石云开放平台创建萤石云账号(注册并登录)
萤石云开放平台链接 - 进入控制台并进行将设备进行绑定
-
进入控制台
-
进行认证,个人身份权限较少,企业的比个人的认证要方便一些 。
-
创建应用,创建应用之后就可以看到调用接口所需要的参数,我这里创建的是web应用
-
其中后续调用接口所需要的参数appKey和appSecret分别为应用秘钥模块中的AppKey和Secret,经常还需要一个accessToken,需要调用接口获取,最好不要用这里的AccessToken,毕竟有一定的有效期(7天)。
开始进行开发
- 安装并引入
安装: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。