前端接入 海康 SDK WEB无插件开发包 V3.2 实现双通道预览及云台反控功能

86 阅读3分钟

前端接入 海康 SDK WEB无插件开发包 V3.2

先看效果:

Snipaste_2025-03-27_19-53-34.png

Snipaste_2025-03-27_19-53-48.png

因为是分拆组件实现反控功能,后面会贴代码

1、前期工作准备

海康 SDK 下载链接 下载需要注册海康账号,注册下就好了

Snipaste_2025-03-27_19-38-23.png

1-1、解压文件,找到webs文件夹

Snipaste_2025-03-27_19-41-38.png

1-2、找到codebase文件夹将文件复制到工程项目public文件夹下

Snipaste_2025-03-27_19-42-40.png

Snipaste_2025-03-27_19-44-54.png 自己取个名字,在html文件里引入

Snipaste_2025-03-27_19-46-36.png

2、项目中使用

在utils文件夹里新增一个 webVideo.js文件

实现双通道 主要代码:

init方法里面设置 此属性 iWndowType: 2, //分屏

在start方法里面 遍历所有通道 (self.channels)
self.channels.forEach((channel, index) => { 
    var oWndInfo = WebVideoCtrl.I_GetWindowStatus(index); // 获取当前窗口状态 
    var startRealPlay = function () { 
        console.log(`调用播放: 通道=${channel.id}, 窗口=${index}`); 
        WebVideoCtrl.I_StartRealPlay(self.szDeviceIdentify, { 
            iChannelID: channel.id, 
            bZeroChannel: false, 
            iStreamType: 2, 
            iWndIndex: index, // 指定窗口索引 
            success: function () { 
                console.log(`通道 ${channel.id} 播放成功, 分配到窗口 ${index}`); }, 
            error: function (status) { 
                console.log(`通道 ${channel.id} 播放失败`, status); } }); 
            }; 
            
            if (oWndInfo != null) { 
                // 如果窗口已经有播放内容,先停止 console.log(`尝试停止窗口 ${index}`); 
                WebVideoCtrl.I_Stop({ 
                    iWndIndex: index, 
                    success: function () { 
                        setTimeout(() => startRealPlay(), 500); 
                    }, 
                    error: function () { 
                        console.log(`停止窗口 ${index} 失败`); } }); 
            } else { 
                startRealPlay(); // 直接播放 
            } 
        });
    

完整代码

export function WebVideo() {
    this.g_iWndIndex = 0//窗口索引
    this.szDeviceIdentify = ''//设备标识(通道)
    this.deviceport = ''//设备端口
    this.rtspPort = ''//rtsp端口
    this.channels = []//通道数组
    this.ip = ''//ip地址
    this.port = ''//端口号
    this.username = ''//海康提供的监控登入用户名
    this.password = ''//登入密码
    this.init = function(ip, username, password, port) {
        this.ip = ip
        this.username = username
        this.password = password
        this.port = port
        // 初始化插件参数及插入插件
        WebVideoCtrl.I_InitPlugin(1160, 630, {
            szColorProperty: 'plugin-background:#102749; sub-background:#102749; sub-border:#18293c; sub-border-select:red',
            bWndFull: true, // 全屏
            // iPackageType: 2,
            iWndowType: 2, //分屏
            bNoPlugin: true, // 支持无插件
            cbInitPluginComplete: function () {
                WebVideoCtrl.I_InsertOBJECTPlugin("divPlugin");
            }
        });
        // console.log('ip和端口', ip, username, password,);
    }
    // 登录
    this.clickLogin = function () {
        let self = this
        if ("" == self.ip || "" == self.port) {
            return console.log('请输入ip和端口', WebVideoCtrl);
        }
        self.szDeviceIdentify = self.ip + "_" + self.port;
        console.log(self.szDeviceIdentify,'----self.szDeviceIdentify-----');
        WebVideoCtrl.I_Login(self.ip, 1, self.port, self.username, self.password, {//1 代表http协议,2 https
            success: function (xmlDoc) {        
                setTimeout(function () {
                    //   console.log('登录成功');
                    // Store.setIpProt(self.szDeviceIdentify)//存储已登入ip和端口
                    self.getChannelInfo();
                    self.getDevicePort();
                }, 10);
                setTimeout(function() {
                    self.clickStartRealPlay()
                }, 500)
            },
            error: function (status, xmlDoc) {
                console.log(status,'登录失败');
                
            }
        });
    }
    // 退出
    this.clickLogout = function() {
        var self = this
        self.channels = []
    
        if (null == self.szDeviceIdentify) {
            return;
        }
        var iRet = WebVideoCtrl.I_Logout(self.szDeviceIdentify);
        if (0 == iRet) {
            self.getChannelInfo();
            self.getDevicePort();
        }
        console.log(self.szDeviceIdentify,'退出成功');
    }
    // 获取通道
    this.getChannelInfo = function(e) {
        var self = this
        self.channels = []
        if (null == self.szDeviceIdentify) {
            return;
        }
        // 模拟通道
        WebVideoCtrl.I_GetAnalogChannelInfo(self.szDeviceIdentify, {
            async: false,
            success: function (xmlDoc) {
                var oChannels = $(xmlDoc).find("VideoInputChannel");
                $.each(oChannels, function (i) {
                    var id = $(this).find("id").eq(0).text(),
                        name = $(this).find("name").eq(0).text();
                    if ("" == name) {
                        name = "Camera " + (i < 9 ? "0" + (i + 1) : (i + 1));
                    }
                    self.channels.push({
                        id: id,
                        name: name
                    })
                });
    
            },
            error: function (status, xmlDoc) {
            
                console.log(self.channels,'获取模拟通道号失败')
            }
        });
        //   数字通道
        WebVideoCtrl.I_GetDigitalChannelInfo(self.szDeviceIdentify, {
            async: false,
            success: function (xmlDoc) {
                var oChannels = $(xmlDoc).find("InputProxyChannelStatus");
    
                $.each(oChannels, function (i) {
                    var id = $(this).find("id").eq(0).text(),
                        name = $(this).find("name").eq(0).text(),
                        online = $(this).find("online").eq(0).text();
                    if ("" == name) {
                        name = "IPCamera " + (i < 9 ? "0" + (i + 1) : (i + 1));
                    }
    
                    self.channels.push({
                        id: id,
                        name: name,
                        online:online
                    })
                });
                console.log(self.szDeviceIdentify +'--'+self.channels[0]+ "--szDeviceIdentify--channels[0]--- 获取数字通道成功!");
    
            },
            error: function (status, xmlDoc) {
                console.log(self.szDeviceIdentify +'--'+self.channels[0] + " ---获取数字通道失---", status, xmlDoc);
            }
        });
    }
    // 获取端口
    this.getDevicePort = function() {
        var self = this
        if (null == self.szDeviceIdentify) {
            return;
        }
        var oPort = WebVideoCtrl.I_GetDevicePort(self.szDeviceIdentify);
        if (oPort != null) {
            self.deviceport = oPort.iDevicePort;
            self.rtspPort = oPort.iRtspPort;
        }
        console.log('获取端口号成功')
    }
    
    // 开始预览
    this.clickStartRealPlay = function() {
        var self = this
            console.log(self.channels, "---开始预览--通道号----", self.szDeviceIdentify);
        if (null == self.szDeviceIdentify) {
            console.log(self.szDeviceIdentify + ' --szDeviceIdentify--为空---');
            return;
        }

        var result = WebVideoCtrl.I_ChangeWndNum(2);
        console.log(`分屏设置结果: ${result}`);
        
        self.channels.forEach((channel, index) => {
            var oWndInfo = WebVideoCtrl.I_GetWindowStatus(index); // 获取当前窗口状态
            var startRealPlay = function () {
                console.log(`调用播放: 通道=${channel.id}, 窗口=${index}`);
                WebVideoCtrl.I_StartRealPlay(self.szDeviceIdentify, {
                    iChannelID: channel.id,
                    bZeroChannel: false,
                    iStreamType: 2,
                    iWndIndex: index,  // 指定窗口索引
                    success: function () {
                        console.log(`通道 ${channel.id} 播放成功, 分配到窗口 ${index}`);
                    },
                    error: function (status) {
                        console.log(`通道 ${channel.id} 播放失败`, status);
                    }
                });
            };
    

            if (oWndInfo != null) { // 如果窗口已经有播放内容,先停止
                console.log(`尝试停止窗口 ${index}`);
                WebVideoCtrl.I_Stop({
                    iWndIndex: index,
                    success: function () {
                        setTimeout(() => startRealPlay(), 500);
                    },
                    error: function () {
                        console.log(`停止窗口 ${index} 失败`);
                    }
                });
            } else {
                startRealPlay(); // 直接播放
            }
        });
    }
    // 停止预览
    this.clickStopRealPlay = function() {
        var self = this
        if (self.channels.length === 0) {
            console.log('没有可停止的通道');
            return;
        }
    
        self.channels.forEach((channel, index) => {
            var oWndInfo = WebVideoCtrl.I_GetWindowStatus(index);
            if (oWndInfo != null) {
                WebVideoCtrl.I_Stop({
                    iWndIndex: index, // 停止对应窗口
                    success: function () {
                        console.log(`通道 ${channel.id} 预览停止成功`);
                    },
                    error: function () {
                        console.log(`通道 ${channel.id} 预览停止失败`);
                    }
                });
            }
        });
    }
    /*  云平台控制 */
    // PTZ控制 9为自动,1,2,3,4,5,6,7,8为方向PTZ
    let g_bPTZAuto = false;
    this.mouseDownPTZControl=function (iPTZIndex) {
        var self = this
        var oWndInfo = WebVideoCtrl.I_GetWindowStatus(self.g_iWndIndex),
            bZeroChannel = self.channels[0].id ,//通道id
            iPTZSpeed = 4;//切换速度 $("#ptzspeed").val()
        if (bZeroChannel==0) {// 零通道不支持云台
            // return console.log(bZeroChannel+"-----云台");
        }
        if (oWndInfo != null) {
            if (9 == iPTZIndex && g_bPTZAuto) {
                iPTZSpeed = 0;// 自动开启后,速度置为0可以关闭自动
            } else {
                self.g_bPTZAuto = false;// 点击其他方向,自动肯定会被关闭
            }
            WebVideoCtrl.I_PTZControl(iPTZIndex, false, {
                iPTZSpeed: iPTZSpeed,
                success: function (xmlDoc) {
                    if (9 == iPTZIndex && g_bPTZAuto) {

                    } else {
                    }
                    if (9 == iPTZIndex) {
                        g_bPTZAuto = !g_bPTZAuto;
                    }
                },
                error: function (status, xmlDoc) {
                }
            });
        }
    }
    // 方向PTZ停止
    this.mouseUpPTZControl=function () {
        let self = this
        var oWndInfo = WebVideoCtrl.I_GetWindowStatus(self.g_iWndIndex);

        if (oWndInfo != null) {
            WebVideoCtrl.I_PTZControl(1, true, {
                success: function (xmlDoc) {
                },
                error: function (status, xmlDoc) {
                }
            });
        }
    }

}

控制器代码:

<!-- 控制器 -->
<template>
  <div>
    <el-form-item label="控制模式" v-if="props.isRadio">
      <el-radio-group v-model="radio">
        <el-radio :value="1">任务模式</el-radio>
        <el-radio :value="2">后台遥控</el-radio>
        <el-radio :value="3">手持遥控</el-radio>
      </el-radio-group>
    </el-form-item>
    <div class="w-full h-12.5" v-else></div>
    <div class="flex justify-between items-center mb-8">
      <div class="opacity-0 w-30% h-20 flex justify-center items-center border border-solid border-#d1d5db rounded-2">
      </div>
      <!-- 上 -->
      <div 
        class="
          w-30% h-20 flex justify-center items-center
          border border-solid border-#d1d5db rounded-2 cursor-pointer
          hover:text-#ff4500
        "
        @mousedown="handleOperate('mouseDownPTZControl')" 
        @mouseup="handleOperate('mouseUpPTZControl')"
      >
        <el-icon><ArrowUpBold /></el-icon>
      </div>
      <!-- 播放状态 -->
      <div 
        class="
          w-30% h-20 flex justify-center items-center
          border border-solid border-#d1d5db rounded-2 cursor-pointer
          hover:text-#ff4500
          opacity-0
        "
        @click="handlePause"
      >
        <img v-if="!isPause" src="@/assets/images/pause.svg" class="w-10 h-10" alt="">
        <el-icon class="text-4xl" v-else><CaretRight /></el-icon>
      </div>
    </div>
    <!-- 左 -->
    <div class="flex justify-between items-center">
      <div 
        class="
          w-30% h-20 flex justify-center items-center
          border border-solid border-#d1d5db rounded-2 cursor-pointer
          hover:text-#ff4500
        "
        @mousedown="handleOperate('mouseDownPTZControl3')" 
        @mouseup="handleOperate('mouseUpPTZControl3')"
      >
        <el-icon><ArrowLeftBold /></el-icon>
      </div>
      <!-- 下 -->
      <div 
        class="
          w-30% h-20 flex justify-center items-center
          border border-solid border-#d1d5db rounded-2 cursor-pointer
          hover:text-#ff4500
        "
        @mousedown="handleOperate('mouseDownPTZControl2')" 
        @mouseup="handleOperate('mouseUpPTZControl2')"
      >
        <el-icon><ArrowDownBold /></el-icon>
      </div>
      <!-- 右 -->
      <div 
        class="
          w-30% h-20 flex justify-center items-center
          border border-solid border-#d1d5db rounded-2 cursor-pointer
          hover:text-#ff4500
        "
        @mousedown="handleOperate('mouseDownPTZControl4')" 
        @mouseup="handleOperate('mouseUpPTZControl4')"
      >
        <el-icon><ArrowRightBold /></el-icon>
      </div>
    </div>
  </div>
</template>

<script setup>
import { ref } from 'vue'
import { ArrowUpBold, ArrowDownBold, ArrowLeftBold, ArrowRightBold, CaretRight } from '@element-plus/icons-vue'

const emit = defineEmits([
  'clickStartRealPlay1',
  'clickStopRealPlay',
  'mouseDownPTZControl',
  'mouseUpPTZControl',
  'mouseDownPTZControl2',
  'mouseUpPTZControl2',
  'mouseDownPTZControl3',
  'mouseUpPTZControl3',
  'mouseDownPTZControl4',
  'mouseUpPTZControl4',
])

const radio = ref(1)

const isPause = ref(false)

const props = defineProps({
  isRadio: {
    type: Boolean,
    default: false
  }
})

const handlePause = () => {
  isPause.value = !isPause.value
  if(isPause.value) {
    emit('clickStopRealPlay')
  } else {
    emit('clickStartRealPlay1')
  }
}

const handleOperate = (type) => {
  emit(type)
}

</script>

重点: 拆分摄像头逻辑写了个中间件

最重要的是拿到摄像头的ip、端口、账号密码

import { ref, nextTick } from "vue";
import { WebVideo } from '@/utils/webVideo.js'

/** 海康逻辑 */
export const useIndexHaikang = ()=>{
  // 海康摄像头----------------------------------------
  //登入信息
  const loginInfo = ref({
    ip: "xxx.xxx.x.x",
    portNumber: "xx",
    userName: "xxx",
    password: "xxx",
  })

  const webVideo = new WebVideo()
  // 登入
  function initVideoPlay() {   
    try{
      nextTick(() => {
        webVideo.init(loginInfo.value.ip,  loginInfo.value.userName, loginInfo.value.password, loginInfo.value.portNumber)
        webVideo.clickLogin()
      })
    }catch(e){
      console.error(e);
    }
  }
  // 登出
  function stopVideoPlay() {
    webVideo.clickStopRealPlay()
    webVideo.clickLogout()
  }

  // 控制器------------------------------------------
  // 播放
  function clickStartRealPlay1() {
    initVideoPlay()
    webVideo.clickStartRealPlay()
  }
  // 暂停播放
  function clickStopRealPlay() {
    webVideo.clickStopRealPlay()
    stopVideoPlay()
  }
  // 开始移动控制 传入方向控制1-8
  function mouseDownPTZControl() {
    // console.log('鼠标按下了');
    webVideo.mouseDownPTZControl(1)
  }
  // 结束移动控制
  function mouseUpPTZControl() {
    // console.log('鼠标抬起了');
    webVideo.mouseUpPTZControl(1)
  }
  //向下移动
  function mouseDownPTZControl2() {
    webVideo.mouseDownPTZControl(2)
  }
  function mouseUpPTZControl2() {

    webVideo.mouseUpPTZControl(2)
  }
  // 向左移动
  function mouseDownPTZControl3() {
    webVideo.mouseDownPTZControl(3)
  }
  function mouseUpPTZControl3() {
    webVideo.mouseUpPTZControl(3)
  }
  // 向右移动
  function mouseDownPTZControl4() {
    webVideo.mouseDownPTZControl(4)
  }
  function mouseUpPTZControl4() {
    webVideo.mouseUpPTZControl(4)
  }
  // 控制器------------------------------------------

  // 海康摄像头----------------------------------------


  return {
    loginInfo,
    initVideoPlay,
    stopVideoPlay,
    clickStartRealPlay1,
    clickStopRealPlay,
    mouseDownPTZControl,
    mouseUpPTZControl,
    mouseDownPTZControl2,
    mouseUpPTZControl2,
    mouseDownPTZControl3,
    mouseUpPTZControl3,
    mouseDownPTZControl4,
    mouseUpPTZControl4,
  }
}

/** websocket逻辑 */
export const useIndexWS = () => {
  let socket

  // 连接 WebSocket
  const connectWebSocket = () => {
    const wsUrl = import.meta.env.VITE_APP_BASE_WS;
    socket = new WebSocket(wsUrl);

    // WebSocket 打开事件
    socket.onopen = () => {
      console.log('建立连接成功');
    };

    // 接收消息事件
    socket.onmessage = (event) => {
      // console.log(`收到的消息: ${event.data}`);
      // socket.send(message); 
    };

    // WebSocket 关闭事件
    socket.onclose = () => {
      console.log('连接关闭');
    };

    // WebSocket 错误事件
    socket.onerror = (error) => {
      console.error('错误链接:', error);
    };
  };


  return {
    socket,
    connectWebSocket,
  }
}

主页面使用 删除了部分业务代码

<template>
  <div class="w-full h-full">
    <div class="flex justify-between p-4">
      
      <div class="flex-1 h-full mx-4">
      

        <div class="p-4 rounded-2 shadow-lg bg-white">
          <cardTitle :title="'相机视图'" />
          <div class="flex justify-between items-center w-full h-80 ">
            <!-- 相机dom -->
            <div class="w-full h-full " id="divPlugin"></div>
          </div>
        </div>
      </div>
      <div class="w-23.5% h-full ">
        <div class="rounded-2 p-4 mb-4 shadow-lg bg-white  ">
          <cardTitle :title="'实时控制'" />
          <el-tabs v-model="activeName" class="demo-tabs" @tab-click="handleClick">
            <el-tab-pane label="云台控制" name="1">
              <controller 
                @clickStartRealPlay1="clickStartRealPlay1"
                @clickStopRealPlay="clickStopRealPlay"
                @mouseDownPTZControl="mouseDownPTZControl"
                @mouseUpPTZControl="mouseUpPTZControl"
                @mouseDownPTZControl2="mouseDownPTZControl2"
                @mouseUpPTZControl2="mouseUpPTZControl2"
                @mouseDownPTZControl3="mouseDownPTZControl3"
                @mouseUpPTZControl3="mouseUpPTZControl3"
                @mouseDownPTZControl4="mouseDownPTZControl4"
                @mouseUpPTZControl4="mouseUpPTZControl4"
              />
            </el-tab-pane>
            <el-tab-pane label="移动控制" name="2">
              <controller :isRadio="true" />
            </el-tab-pane>
          </el-tabs>
        </div>
        <div class="rounded-2 p-4 shadow-lg bg-white  ">
          <cardTitle :title="'实时警报'" />
          <el-scrollbar class="h-156">
            <warnItems v-for="i in 10" :key="i" />
          </el-scrollbar>
        </div>
      </div>
    </div>
  </div>
</template>

<script setup>
import { ref, onMounted, nextTick, onBeforeUnmount } from "vue";
import { ElMessage } from 'element-plus'
import { CaretRight } from "@element-plus/icons-vue";
import cardTitle from "@/components/cardTitle.vue";
import controller from '@/components/controller.vue'

import { useIndexHaikang, useIndexWS } from "@/middleware/index/index.js";


// 海康摄像头----------------------------------------
const { 
  initVideoPlay,
  clickStartRealPlay1,
  clickStopRealPlay,
  mouseDownPTZControl,
  mouseUpPTZControl,
  mouseDownPTZControl2,
  mouseUpPTZControl2,
  mouseDownPTZControl3,
  mouseUpPTZControl3,
  mouseDownPTZControl4,
  mouseUpPTZControl4,} = useIndexHaikang()
// 海康摄像头----------------------------------------


// websocket----------------------------------------
const { 
  socket,
  connectWebSocket, 
  } = useIndexWS()

// websocket----------------------------------------


onMounted(() => {
  initVideoPlay()
})


onBeforeUnmount(()=>{
  if(socket){
    socket.close()
  }
  clearInterval(timer.value)
})

</script>

3、nginx配置

代码写完开发环境是没法测试的,必须打包在生产环境下测试!

下载的解压包里面有个nginx的文件夹 start.bat 是启动 stop.bat是停止

Snipaste_2025-03-28_09-25-37.png 把打包好的文件扔到ng同级目录或者上级目录(对应ng配置不一样),看你自己心情 我是直接扔到上级目录,nginx.conf配置如下:

Snipaste_2025-03-28_09-31-23.png

4、测试

用ng配置的ip+端口测试就行,调试过程需要频繁打包

tip:网上有很多博客说网段必须一致,其实是不正确的,摄像头网段和你本地网段不一定要一致,ping的通就行,重点在测试之前先ping一下摄像头ip,ping不通代码写得再好也没用