JS视频插件二开指北(萤石云,海康威视ISC,直连)

687 阅读1分钟

前言

  一直在处理这个海康视频二开的插件,现在弄的差不多了,今儿的分享出来,里面有不少坑,我应该基本都跳过了就希望大家不要再跳了。基于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>