前言
一直在处理这个海康视频二开的插件,现在弄的差不多了,今儿的分享出来,里面有不少坑,我应该基本都跳过了就希望大家不要再跳了。基于Vue开发的,在看这篇文档的你手上都有肯定开发包了,海康插件地址,自己下,如果是用Vue记得自己对着demo封装下子插件【js语法转Vue,我想你应该会,这里不说了好吧】。
这里主要讲三类:
1、直连协议类型视频,对应的开发包文档是:Web3.2_控件开发包编程指南.pdf
2、ISC类型的视频,对应的开发包文档是:Web3.0_控件开发包编程指南.pdf
3、萤石云视频,对应的开发文档是:在线文档
再补一嘴使用场景:公司想要的效果是:希望多类型视频混合播放,所以一类视频的多窗格播放就不太适用了,所以就是说,一路视频就要渲染一次插件,并默认一路视频就是单窗格。
哎说了很多废话,开整开整!
视频组件
先上我封装的组件,这里我命名为VideoCompo.vue
<template>
<div class="cameraCompoContainer">
<!-- 这里是展示默认图片,触发条件:默认展示 + 用户关闭某路视频 -->
<div v-if="isWaiting" style="height: 100%">
<img :src="videoCoverImg" class="default-pic"/>
<img src="@/assets/img/play-btn.png" class="play-btn" v-if="videoInfo" @click="playAnyTypeVideo"/>
<div class="camera-name" v-if="videoInfo">{{videoInfo.cameraName}}</div>
</div>
<div v-else>
<hikvision-video v-if="videoInfo.dockingMode == '0'" @getISCVideoInfo="getISCVideoInfo" ref="ISCVideoRef" :id="videoInfo.channelNumber"></hikvision-video>
<div v-else-if="videoInfo.dockingMode == '3'" >
<div class="iframeTitle">
<span>{{videoInfo.cameraName}}</span>
<a-icon type="close" @click="closeZLTypeVideo(videoInfo)"/>
</div>
<iframe ref="videoFrame" :src="zlVideoIframeUrl" :id="videoInfo.cameraCode" @load="onLoad(videoInfo)" marginwidth="0" marginwheight="0" frameborder="0" :width="videoBoxWidth" :height="videoBoxHeight - 21"></iframe>
</div>
<ys-video v-else-if="videoInfo.dockingMode == '4' && videoInfo.playerSrc" :ysData="videoInfo" @ysyVideoClose="ysyVideoClose"></ys-video>
</div>
</div>
</template>
<script>
import HikvisionVideo from "./hikvisionVideo";
import YsVideo from './yingshiVideo.vue';
import $ from 'jquery';
export default {
props:[ 'playMode'], //是选择回放还是实时视频..
components:{
HikvisionVideo, YsVideo
},
watch:{
},
computed:{
videoCoverImg(){
let imgUrl = require("@/assets/img/default_video.png");
if(this.videoInfo?.defaultPicUrl){
imgUrl = this.videoInfo?.defaultPicUrl;
}
return imgUrl
},
zlVideoIframeUrl(){
// 路径视个人项目而定
let url = process.env.NODE_ENV === 'development' ?
`${window.location.protocol}//${window.location.hostname}:1100/hikVideoSeries/camera.html`
:
'/hikVideoSeries/camera.html';
return url;
}
},
data(){
return {
isWaiting: true,
videoInfo: null,
// 控制ISC视频的元对象
iscVideoMetaArray: [],
videoBoxHeight: 0,
videoBoxWidth: 0
}
},
mounted(){
let that = this;
window.addEventListener('resize', function(){
that.handleResizeCamera();
})
},
destroyed(){
this.iscVideoMetaArray.length && this.destroyISCTypeVideo();
window.removeEventListener('resize');
},
methods:{
handleResizeCamera(){
this.closeZLTypeVideo();
this.$nextTick(() => {
this.playAnyTypeVideo();
})
},
playAnyTypeVideo(){
if(this.videoInfo){
let rootVideoRef = this.handleRootVideoRef(this);
if(this.videoInfo.dockingMode == 0){
this.openISCTypeVideo({ ...this.videoInfo, width: rootVideoRef[0].offsetWidth, height: rootVideoRef[0].offsetHeight });
}else if(this.videoInfo.dockingMode == 3){
this.openZLTypeVideo({ ...this.videoInfo, width: rootVideoRef[0].offsetWidth, height: rootVideoRef[0].offsetHeight })
}else if(this.videoInfo.dockingMode == 4){
this.openYSTypeVideo({ ...this.videoInfo, width: rootVideoRef[0].offsetWidth, height: rootVideoRef[0].offsetHeight });
}else if(this.videoInfo.dockingMode == 1){
this.openNVRTypeVideo({ ...this.videoInfo, width: rootVideoRef[0].offsetWidth, height: rootVideoRef[0].offsetHeight });
}
}else{
this.isWaiting = false;
this.middleState = true;
}
},
// 这里的视频组件调用的时候一定要写明确,不然容易找不到父组件,至于为什么这么写,见仁见智
handleRootVideoRef(rootRef){
if(rootRef.$parent && rootRef.$parent.$refs != {} && rootRef.$parent.$refs.videoBoxRef){
console.log(rootRef.$parent.$refs.videoBoxRef)
return rootRef.$parent.$refs.videoBoxRef;
}else{
if(rootRef.$parent){
return this.handleRootVideoRef(rootRef.$parent);
}
}
},
openISCTypeVideo( params ){
this.isWaiting = false;
this.videoInfo = params;
this.$nextTick(()=>{
this.$refs.ISCVideoRef.open({
videoInfo: params,
videoCommonInfo:{
width: params.width,
height: params.height,
playMode: this.playMode
},
iscVideoMetaObj: this.iscVideoMetaArray
});
})
},
destroyISCTypeVideo(metaData = this.iscVideoMetaArray[0], clear){
metaData && metaData.JS_DestroyWnd().then(function(){
},function(){
});
if( clear ) this.videoInfo = null;
this.iscVideoMetaArray = [];
this.isWaiting = true;
},
// ISC视频窗口状态获取,全靠它了..
getISCVideoInfo(factor){
this.factorInfo = factor;
let thisArrow = this;
if(factor.msg.result === 816){
// 消息推送 --- 关闭信息
// 需要到父组件关闭插件...
if(factor.type === 2 || factor.type === 1 ){
thisArrow.$emit("destroyIscVideo", factor);
}
}else if(factor.msg.result == 769 && factor.type == 2){
// 播放失败逻辑
this.$message.warning('播放失败')
let marker = factor.msg.cameraIndexCode ? factor.msg.cameraIndexCode : factor.domID;
if(marker === thisArrow.iscVideoMetaArray[0].oOptions.szPluginContainer){
thisArrow.destroyISCTypeVideo()
}
thisArrow.$emit("playNextVideo");
}
else if(factor.type === 2 && factor.msg.result === 768){
// 开始播放逻辑
thisArrow.isWaiting = false;
thisArrow.$emit("playNextVideo");
}
},
openZLTypeVideo( params ){
this.isWaiting = false;
this.videoInfo = params;
this.videoBoxHeight = params.height;
this.videoBoxWidth = params.width;
this.$nextTick(()=> {
sessionStorage.setItem('iframeWidth', params.width);
sessionStorage.setItem('iframeHeight', params.height);
this.$emit("playNextVideo");
})
},
// 这里是改过源码了的,但我想你应该看得懂,我对开发包的源码干了啥子...
onLoad( data ){
this.$nextTick(()=>{
let iframeWindow = this.$refs.videoFrame.contentWindow;
let judge = true;
// 约定:默认是多IP的形式,否则【一个ip多通道号的方式】,用户名 = 用户名 + 通道号
// 同时,通道号明确要有D,否则视为乱写的通道号
if(data.channelNumber.startsWith("D")) judge = false;
iframeWindow.document.getElementById('loginip').value = data.ip;
iframeWindow.document.getElementById('port').value = data.port;
iframeWindow.document.getElementById('username').value = judge ? data.accountInfo : data.accountInfo + data.channelNumber;
iframeWindow.document.getElementById('password').value = data.pwd;
iframeWindow.document.getElementById('_loginButton').click();
iframeWindow.document.getElementById('netsPreach').value = '3';
Object.defineProperty(iframeWindow.document, 'loginStatus', {
set: function(value){
// 这个值肯定是布尔值
if(value){
iframeWindow.document.getElementById('_previewButton').click();
}
}
})
})
},
closeZLTypeVideo(){
this.isWaiting = true;
},
destroyZLTypeVideo(){
this.videoInfo = null;
this.isWaiting = true;
},
openYSTypeVideo( params ){
this.videoInfo = params;
let that = this;
$.ajax({
url: 'https://open.ys7.com/api/lapp/v2/live/address/get',
contentType: "application/x-www-form-urlencoded;charset=utf-8",
dataType: "json",
type: "post",
data:{
deviceSerial: params.field2,
accessToken: params.field3,
type: 1,
protocol:2,
quality: 2
},
success: function(res) {
let { code,data } = res;
if(code == 200){
params.playerSrc = data.url;
that.isWaiting = false;
}
that.$emit("playNextVideo");
},
error: function(){
that.$message.warning('播放失败');
that.isWaiting = false;
that.$emit("playNextVideo");
}
});
},
ysyVideoClose(){
this.isWaiting = true;
}
}
}
</script>
关于直连类型的处理
代码已经在上面了,就讲一下大致思路。通过开发包给的demo用iframe嵌入至页面中,注意数据加载的时机,iframe在执行onload方法之前就要获取到数据,然后将设备信息填入到表单中,使用iframe的中的按钮来执行demo.js中的函数。
domo.js上的改动
// 在零、模拟、数字通道任一通道获取到了通道号,即:
document.loginStatus = true;
// 就会触发组件的 Object.defineProperty(iframeWindow.document, 'loginStatus'......状态值变化,然后再开始执行播放操作。【ctrl+f】
关于ISC类型的处理
这个也是需要自己封装组件的,这里我也假设你已经封装过组件了,
// 找到ISCVideoRef.open【ctrl+f】,这里的open执行了两步操作,一个是创建插件对象,一个是初始化插件
// video.js 创建插件对象
function createHikVerson(videoInfo, videoCommonInfo){ //dom元素 播放模式( 0视频预览 1视频回放) 视频的宽高
this.appkey = process.env.VUE_APP_APPKEY; // 合作方Appkey
this.secret = process.env.VUE_APP_SECRET; //合作方Secret
this.ishttps = 1; // 是否启用https 0->不启用 1->启用
this.ip = videoInfo.ip, //平台IP地址
this.port = videoInfo.port; //平台端口,默认
this.snapDir = 'D:\\SnapDir'; //抓图存储路径
this.videoDir = 'D:\\VideoDir'; //录像存储路径
this.layout = '1x1'; //窗口布局
this.isShowToolbar = 1; //显示工具栏 0->隐藏 1->显示
this.isShowSmart = 1; //显示智能信息 0->隐藏 1->显示
this.reconnectTimes = 5; //重连次数
this.duration = 1; //重连间隔
this.btId = '0,16,256,257,258,259,260,512,513,514,515,516,517,768,769'; //工具条按钮ID集
this.streamMode = 0; //主子码流标识 0->主码流 1->子码流
this.transMode = 1; //传输协议: 0->UDP 1->TCP
this.gpuMode = 0; //是否启用GPU硬解: 0->不启用 1->启用
this.isDirectEzviz = 0; //是否直连萤石预览: 0->不启用 1->启用
this.PlayType = 0; //预览模式: 0->空闲窗口预览 1->选中窗口预览 2->指定窗口预览
this.SnapType = 0; //抓图模式: 0->选中窗口抓图 1->指定窗口抓图
this.SnapWndId = 0; //窗口ID
this.videoBoxId = 0; //预览窗口id
this.snapName = 'd:\\SnapDir\\test.jpg'; //图片绝对路径名称
this.SetOSDType = 0; //叠加模式 0->选中窗口字符串叠加 1->指定窗口字符串叠加
this.Xsite = 0; //起点X坐标 0~1000
this.Ysite = 0; //起点Y坐标 0~1000
this.RColor = 255; //字体RGB颜色 R
this.GColor = 0; //字体RGB颜色 R
this.BColor = 0; //字体RGB颜色 R
this.OSDText = 20; //待叠加字符串
this.cameraIndexCode = videoInfo.channelNumber; //监控点编号
this.recordLocation = 1; //0-> 中心存储 1->设备存储
this.initCount = 0;
this.dom = videoInfo.channelNumber;//dom元素
this.box_width = videoCommonInfo.width;//视频的宽
this.box_height = videoCommonInfo.height;//视频的高
this.playMode = videoCommonInfo.playMode; //视频播放模式 0视频预览 1视频回放
this.msg = '';
}
// let hikverson = new createHikVerson( videoInfo, videoCommonInfo );
// hikverson.initPlugin( iscVideoMetaObj )
// video.js 初始化插件
createHikVerson.prototype.initPlugin = function(metaObj){
var _this = this;
oWebControl = new WebControl({
szPluginContainer: _this.dom,
iServicePortStart: 15900,
iServicePortEnd: 15909,
szClassId:"23BF3B0A-2C56-4D97-9C03-0CB103AA8F11", // 用于IE10使用ActiveX的clsid
cbConnectSuccess: function () {
oWebControl.JS_StartService("window", {
dllPath: "./VideoPluginConnect.dll"
}).then(function () {
this_arrow = _this;
setCallbacks();
oWebControl.JS_CreateWnd(_this.dom,_this.box_width,_this.box_height).then(function () {
_this.init(_this.playMode,_this.box_width,_this.box_height); //调用视频初始化
});
}, function () {
});
},
cbConnectError: function () {
oWebControl = null;
$("#videoBox").html("插件未启动,正在尝试启动,请稍候...");
WebControl.JS_WakeUp("VideoWebPlugin://");
_this.initCount ++;
if (_this.initCount < 3) {
setTimeout(function () {
_this.initPlugin(_this.playMode,_this.box_width,_this.box_height);
}, 3000)
} else {
// $("#videoBox").html("插件启动失败,请检查插件是否安装");
// _this.alert_tips();
}
},
cbConnectClose: function (bNormalClose) {
// 异常断开:bNormalClose = false
// JS_Disconnect正常断开:bNormalClose = true
if (!bNormalClose) {
}
_this.oWebControl = null;
},
});
metaObj.push(oWebControl);
}
至于iscVideoMetaObj,它是一个数组,其作用就是为了在初始化插件之后存储元对象,有了这些对象就可以直接使用开发包里面提供的方法。这里注意的要点是一个时间内最好只加载一次插件,否则会出现视频崩溃错位或者干脆没画面等问题。插件的状态与控制显示隐藏就全靠回调函数的信息来判断..
关于萤石云视频的处理
这个就没什么说的,按照API的说法来就行了..组件代码如下,维护较少将就看,如果你有觉得有需要改进的地方烦请指出,虽然不定会听..
<template>
<div class="videoContainer" ref="videoContainerRef">
<header v-show="toolState" @mousemove="toolState = true">
<span>{{ysData.cameraName}}</span>
<span @click="close"><a-icon type="close-circle" /></span>
</header>
<video @mousemove="toolState = true" ref="videoRef" @mouseleave="toolState = false" id="YSvideo" crossOrigin="anonymous" class="video-js vjs-default-skin vjs-big-play-centered" preload="auto">
<source :src="ysData.playerSrc" type="application/x-mpegURL"/>
</video>
<div class="controllerContainer" v-if="!fullScreenState">
<div class="controllerModel">
<!-- 右,右下....上,右上 -->
<p @click="ysyControl(3)"></p>
<p @click="ysyControl(7)"></p>
<p @click="ysyControl(1)"></p>
<p @click="ysyControl(5)"></p>
<p @click="ysyControl(2)"></p>
<p @click="ysyControl(4)"></p>
<p @click="ysyControl(0)"></p>
<p @click="ysyControl(6)"></p>
<div class="stopButton" @click="stop">停止<br>移动</div>
</div>
<div class="changeJiaoJu">
<span @click="ysyControl(11)">-</span>
<span>焦距</span>
<span @click="ysyControl(10)">+</span>
</div>
</div>
<div class="toolContainer" v-show="toolState" @mousemove="toolState = true">
<span @click="playerPalseOrPlay"><a-icon :type="playerStateText" /></span>
<span class="toolArr">
<span @click="fullScreen"><a-icon :type="fullScreenText" /></span>
</span>
</div>
</div>
</template>
<script>
import "video.js/dist/video-js.css";
import videojs from 'video.js'
import $ from 'jquery';
export default {
props:['ysData'],
data(){
return {
player: null,
playState: true,
playerStateText: 'pause-circle',
fullScreenState: true,
fullScreenText: 'fullscreen',
toolState: false,
}
},
created(){
},
mounted(){
this.player = videojs('YSvideo',{
autoplay: true,
muted: true,
bigPlayButton: false,
controlBar: true,
errorDisplay: false,
posterImage: true,
textTrackDisplay: false,
hls: {
withCreadentials: true,
}
})
},
beforeDestroy(){
this.player.dispose();
},
methods:{
ysyControl(val){
let deviceSerial = this.ysData.field2;
let accessToken = this.ysData.field3;
$.ajax({
url: 'https://open.ys7.com/api/lapp/device/ptz/start',
contentType: "application/x-www-form-urlencoded;charset=utf-8",
dataType: "json",
type: "post",
data:{
deviceSerial,
accessToken,
channelNo: 1,
direction:val,
speed:1
},
success: function(res) {
},
error: function(err){
}
});
},
stop(){
let deviceSerial = this.ysData.field2;
let accessToken = this.ysData.field3;
$.ajax({
url: 'https://open.ys7.com/api/lapp/device/ptz/stop',
contentType: "application/x-www-form-urlencoded;charset=utf-8",
dataType: "json",
type: "post",
data:{
deviceSerial,
accessToken,
channelNo: 1
},
success: function(res){
},
error:function(err){
}
})
},
close(){
this.$emit('ysyVideoClose');
this.player.dispose();
},
playerPalseOrPlay(){
if(this.playState){
this.playState = false;
this.playerStateText = 'pause-circle';
this.$refs.videoRef.play();
}else{
this.playState = true;
this.playerStateText = 'play-circle';
this.$refs.videoRef.pause();
}
},
fullScreen(){
let container = this.$refs.videoContainerRef;
if(this.fullScreenState){
this.fullScreenText = "fullscreen-exit";
container.requestFullscreen
? container.requestFullscreen()
: container.webkitRequestFullscreen
? container.webkitRequestFullscreen()
: container.mozRequestFullScreen
? container.mozRequestFullScreen()
: container.msRequestFullscreen && container.msRequestFullscreen();
this.fullScreenState = false;
}else{
this.fullScreenText = "fullscreen"
document.exitFullscreen
? document.exitFullscreen()
: document.webkitExitFullscreen
? document.webkitExitFullscreen()
: document.mozCancelFullScreen
? document.mozCancelFullScreen()
: document.msExitFullscreen && document.msExitFullscreen();
this.fullScreenState = true;
}
},
}
}
</script>
<style lang="less" scoped>
.videoContainer{
height: 100%;
width: 100%;
position: relative;
header{
position: absolute;
top: 0;
width: 100%;
background: rgba(0,0,0,0.5);
color: white;
font-size: 15px;
line-height: 20px;
display: flex;
justify-content: space-between;
z-index: 10;
span:nth-child(1){
padding-left: 5px;
}
span:nth-child(2){
padding-right: 5px;
font-size: 20px;
cursor: pointer;
}
}
#YSvideo{
width: 100%;
height: 100%;
}
.controllerContainer{
width: 230px;
height: 240px;
background: #343651;
position: absolute;
bottom: 40px;
right: 40px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
.controllerModel{
height: 158px;
width: 158px;
border-radius: 50%;
background: linear-gradient(45deg, #87A9EE,#FFFFFF);
position: relative;
p{
position: absolute;
height: 0;
line-height: 0;
font-size: 0;
width: 0;
border-style: solid;
border-width: 14px;
border-color: transparent transparent transparent #343651;
cursor: pointer;
}
// 正右
p:nth-child(1){
top: calc(50% - 7px);
right: -10px;
}
// 右下
p:nth-child(2){
transform: rotate(45deg);
top: 75%;
right: 14px;
}
// 正下
p:nth-child(3){
transform: rotate(90deg);
bottom: -8px;
right: calc(50% - 18px);
}
// 左下
p:nth-child(4){
transform: rotate(135deg);
top: 75%;
left: 14px;
}
// 正左
p:nth-child(5){
transform: rotate(180deg);
top: calc(50% - 7px);
left: -10px;
}
// 左上
p:nth-child(6){
transform: rotate(-135deg);
top: 9%;
left: 8px;
}
// 正上
p:nth-child(7){
transform: rotate(-90deg);
top: -8px;
right: calc(50% - 13px);
}
// 右上
p:nth-child(8){
transform: rotate(-45deg);
top: 10%;
right: 12px;
}
.stopButton{
height: 56px;
width: 56px;
border-radius: 50%;
background: linear-gradient(45deg, #87A9EE,#FFFFFF);
position: absolute;
top: calc(50% - 28px);
left: calc(50% - 28px);
border: 3px solid #343651;
font-size: 14px;
font-weight: bolder;
color: #343651;
text-align: center;
cursor: pointer;
}
}
.changeJiaoJu{
span{
display: inline-block;
cursor: pointer;
}
span:nth-child(1),span:nth-child(3){
height: 26px;
width: 26px;
color: #343651;
font-size: 26px;
line-height: 20px;
background: linear-gradient(45deg, #87A9EE,#FFFFFF);
font-weight: bolder;
border-radius: 50%;
text-align: center;
}
span:nth-child(2){
padding: 0 10px;
color: #B7C3FF;
font-size: 20px;
}
}
}
.toolContainer{
width: 100%;
background: rgba(0,0,0,0.5);
height: 40px;
position: absolute;
bottom: 0;
color: white;
display: flex;
justify-content: space-between;
z-index: 10;
line-height: 40px;
span:nth-child(1){
padding-left: 5px;
font-size: 30px;
cursor: pointer;
}
span:nth-child(2){
padding-right: 5px;
span{
font-size: 30px;
cursor: pointer;
}
}
}
}
</style>