vue2纯前端对接海康威视摄像头实现实时视频预览

vue2纯前端对接海康威视摄像头实现实时视频预览

vue2纯前端对接海康威视摄像头实现实时视频预览

实现实时对海康威视摄像头进行取流的大致思路:摄像头做端口映射(安装摄像头的师傅一般都会),做了映射之后就可以通过IP+端口的形式在浏览器中进行摄像头的实时浏览,这种是海康威视自己就带有的方式,不能嵌入到自研的系统,视频流画面实现嵌入自研系统,需要在满足以上的前提下,使用webrtc-streamer进行推流,然后在vue2中进行接流,渲染到页面中

一、环境准备

需要具备的前提条件,设备可在网页端进行浏览,且做以下设置

登录进行设置


在这里插入图片描述
设置视频编码格式


设置RTSP协议端口


至此摄像头设置已完成,接下来需要获取摄像头设备所在IP的rtsp链接,海康摄像头的rtsp链接获取见官方说明:海康威视摄像头取流说明
可以使用VLC取流软件进行验证rtsp链接是否是通的VLC官方下载地址

VLC官网


打开网络串流


输入取流地址


在这里插入图片描述


至此准备工作就完成了,接下来就是敲代码进行集成阶段了

二、代码集成

1.1 准备webrtcstreamer.js,粘贴即用,不用做任何修改

var WebRtcStreamer =(function(){/** * Interface with WebRTC-streamer API * @constructor * @param {string} videoElement - id of the video element tag * @param {string} srvurl - url of webrtc-streamer (default is current location) */varWebRtcStreamer=functionWebRtcStreamer(videoElement, srvurl){if(typeof videoElement ==="string"){this.videoElement = document.getElementById(videoElement);}else{this.videoElement = videoElement;}this.srvurl = srvurl || location.protocol+"//"+window.location.hostname+":"+window.location.port;this.pc =null;this.mediaConstraints ={ offerToReceiveAudio:true, offerToReceiveVideo:true};this.iceServers =null;this.earlyCandidates =[];}WebRtcStreamer.prototype._handleHttpErrors=function(response){if(!response.ok){throwError(response.statusText);}return response;}/** * Connect a WebRTC Stream to videoElement * @param {string} videourl - id of WebRTC video stream * @param {string} audiourl - id of WebRTC audio stream * @param {string} options - options of WebRTC call * @param {string} stream - local stream to send * @param {string} prefmime - prefered mime */WebRtcStreamer.prototype.connect=function(videourl, audiourl, options, localstream, prefmime){this.disconnect();// getIceServers is not already receivedif(!this.iceServers){ console.log("Get IceServers");fetch(this.srvurl +"/api/getIceServers").then(this._handleHttpErrors).then((response)=>(response.json())).then((response)=>this.onReceiveGetIceServers(response, videourl, audiourl, options, localstream, prefmime)).catch((error)=>this.onError("getIceServers "+ error ))}else{this.onReceiveGetIceServers(this.iceServers, videourl, audiourl, options, localstream, prefmime);}}/** * Disconnect a WebRTC Stream and clear videoElement source */WebRtcStreamer.prototype.disconnect=function(){if(this.videoElement?.srcObject){this.videoElement.srcObject.getTracks().forEach(track=>{ track.stop()this.videoElement.srcObject.removeTrack(track);});}if(this.pc){fetch(this.srvurl +"/api/hangup?peerid="+this.pc.peerid).then(this._handleHttpErrors).catch((error)=>this.onError("hangup "+ error ))try{this.pc.close();}catch(e){ console.log("Failure close peer connection:"+ e);}this.pc =null;}}WebRtcStreamer.prototype.filterPreferredCodec=function(sdp, prefmime){const lines = sdp.split('\n');const[prefkind, prefcodec]= prefmime.toLowerCase().split('/');let currentMediaType =null;let sdpSections =[];let currentSection =[];// Group lines into sections lines.forEach(line=>{if(line.startsWith('m=')){if(currentSection.length){ sdpSections.push(currentSection);} currentSection =[line];}else{ currentSection.push(line);}}); sdpSections.push(currentSection);// Process each sectionconst processedSections = sdpSections.map(section=>{const firstLine = section[0];if(!firstLine.startsWith('m='+ prefkind)){return section.join('\n');}// Get payload types for preferred codecconst rtpLines = section.filter(line=> line.startsWith('a=rtpmap:'));const preferredPayloads = rtpLines .filter(line=> line.toLowerCase().includes(prefcodec)).map(line=> line.split(':')[1].split(' ')[0]);if(preferredPayloads.length ===0){return section.join('\n');}// Modify m= line to only include preferred payloadsconst mLine = firstLine.split(' ');const newMLine =[...mLine.slice(0,3),...preferredPayloads].join(' ');// Filter related attributesconst filteredLines = section.filter(line=>{if(line === firstLine)returnfalse;if(line.startsWith('a=rtpmap:')){return preferredPayloads.some(payload=> line.startsWith(`a=rtpmap:${payload}`));}if(line.startsWith('a=fmtp:')|| line.startsWith('a=rtcp-fb:')){return preferredPayloads.some(payload=> line.startsWith(`a=${line.split(':')[0].split('a=')[1]}:${payload}`));}returntrue;});return[newMLine,...filteredLines].join('\n');});return processedSections.join('\n');}/* * GetIceServers callback */WebRtcStreamer.prototype.onReceiveGetIceServers=function(iceServers, videourl, audiourl, options, stream, prefmime){this.iceServers = iceServers;this.pcConfig = iceServers ||{"iceServers":[]};try{this.createPeerConnection();let callurl =this.srvurl +"/api/call?peerid="+this.pc.peerid +"&url="+encodeURIComponent(videourl);if(audiourl){ callurl +="&audiourl="+encodeURIComponent(audiourl);}if(options){ callurl +="&options="+encodeURIComponent(options);}if(stream){this.pc.addStream(stream);}// clear early candidatesthis.earlyCandidates.length =0;// create Offerthis.pc.createOffer(this.mediaConstraints).then((sessionDescription)=>{ console.log("Create offer:"+JSON.stringify(sessionDescription)); console.log(`video codecs:${Array.from(newSet(RTCRtpReceiver.getCapabilities("video")?.codecs?.map(codec=> codec.mimeType)))}`) console.log(`audio codecs:${Array.from(newSet(RTCRtpReceiver.getCapabilities("audio")?.codecs?.map(codec=> codec.mimeType)))}`)if(prefmime !=undefined){//set prefered codeclet[prefkind]= prefmime.split('/');if(prefkind !="video"&& prefkind !="audio"){ prefkind ="video"; prefmime = prefkind +"/"+ prefmime;} console.log("sdp:"+ sessionDescription.sdp); sessionDescription.sdp =this.filterPreferredCodec(sessionDescription.sdp, prefmime); console.log("sdp:"+ sessionDescription.sdp);}this.pc.setLocalDescription(sessionDescription).then(()=>{fetch(callurl,{ method:"POST", body:JSON.stringify(sessionDescription)}).then(this._handleHttpErrors).then((response)=>(response.json())).catch((error)=>this.onError("call "+ error )).then((response)=>this.onReceiveCall(response)).catch((error)=>this.onError("call "+ error ))},(error)=>{ console.log("setLocalDescription error:"+JSON.stringify(error));});},(error)=>{alert("Create offer error:"+JSON.stringify(error));});}catch(e){this.disconnect();alert("connect error: "+ e);}}WebRtcStreamer.prototype.getIceCandidate=function(){fetch(this.srvurl +"/api/getIceCandidate?peerid="+this.pc.peerid).then(this._handleHttpErrors).then((response)=>(response.json())).then((response)=>this.onReceiveCandidate(response)).catch((error)=>this.onError("getIceCandidate "+ error ))}/* * create RTCPeerConnection */WebRtcStreamer.prototype.createPeerConnection=function(){ console.log("createPeerConnection config: "+JSON.stringify(this.pcConfig));this.pc =newRTCPeerConnection(this.pcConfig);let pc =this.pc; pc.peerid = Math.random(); pc.onicecandidate=(evt)=>this.onIceCandidate(evt); pc.onaddstream=(evt)=>this.onAddStream(evt); pc.oniceconnectionstatechange=(evt)=>{ console.log("oniceconnectionstatechange state: "+ pc.iceConnectionState);if(this.videoElement){if(pc.iceConnectionState ==="connected"){this.videoElement.style.opacity ="1.0";}elseif(pc.iceConnectionState ==="disconnected"){this.videoElement.style.opacity ="0.25";}elseif((pc.iceConnectionState ==="failed")||(pc.iceConnectionState ==="closed")){this.videoElement.style.opacity ="0.5";}elseif(pc.iceConnectionState ==="new"){this.getIceCandidate();}}} pc.ondatachannel=function(evt){ console.log("remote datachannel created:"+JSON.stringify(evt)); evt.channel.onopen=function(){ console.log("remote datachannel open");this.send("remote channel openned");} evt.channel.onmessage=function(event){ console.log("remote datachannel recv:"+JSON.stringify(event.data));}}try{let dataChannel = pc.createDataChannel("ClientDataChannel"); dataChannel.onopen=function(){ console.log("local datachannel open");this.send("local channel openned");} dataChannel.onmessage=function(evt){ console.log("local datachannel recv:"+JSON.stringify(evt.data));}}catch(e){ console.log("Cannor create datachannel error: "+ e);} console.log("Created RTCPeerConnnection with config: "+JSON.stringify(this.pcConfig));return pc;}/* * RTCPeerConnection IceCandidate callback */WebRtcStreamer.prototype.onIceCandidate=function(event){if(event.candidate){if(this.pc.currentRemoteDescription){this.addIceCandidate(this.pc.peerid, event.candidate);}else{this.earlyCandidates.push(event.candidate);}}else{ console.log("End of candidates.");}}WebRtcStreamer.prototype.addIceCandidate=function(peerid, candidate){fetch(this.srvurl +"/api/addIceCandidate?peerid="+peerid,{ method:"POST", body:JSON.stringify(candidate)}).then(this._handleHttpErrors).then((response)=>(response.json())).then((response)=>{console.log("addIceCandidate ok:"+ response)}).catch((error)=>this.onError("addIceCandidate "+ error ))}/* * RTCPeerConnection AddTrack callback */WebRtcStreamer.prototype.onAddStream=function(event){ console.log("Remote track added:"+JSON.stringify(event));this.videoElement.srcObject = event.stream;let promise =this.videoElement.play();if(promise !==undefined){ promise.catch((error)=>{ console.warn("error:"+error);this.videoElement.setAttribute("controls",true);});}}/* * AJAX /call callback */WebRtcStreamer.prototype.onReceiveCall=function(dataJson){ console.log("offer: "+JSON.stringify(dataJson));let descr =newRTCSessionDescription(dataJson);this.pc.setRemoteDescription(descr).then(()=>{ console.log("setRemoteDescription ok");while(this.earlyCandidates.length){let candidate =this.earlyCandidates.shift();this.addIceCandidate(this.pc.peerid, candidate);}this.getIceCandidate()},(error)=>{ console.log("setRemoteDescription error:"+JSON.stringify(error));});}/* * AJAX /getIceCandidate callback */WebRtcStreamer.prototype.onReceiveCandidate=function(dataJson){ console.log("candidate: "+JSON.stringify(dataJson));if(dataJson){for(let i=0; i<dataJson.length; i++){let candidate =newRTCIceCandidate(dataJson[i]); console.log("Adding ICE candidate :"+JSON.stringify(candidate));this.pc.addIceCandidate(candidate).then(()=>{ console.log("addIceCandidate OK");},(error)=>{ console.log("addIceCandidate error:"+JSON.stringify(error));});}this.pc.addIceCandidate();}}/* * AJAX callback for Error */WebRtcStreamer.prototype.onError=function(status){ console.log("onError:"+ status);}return WebRtcStreamer;})();if(typeof window !=='undefined'&&typeof window.document !=='undefined'){ window.WebRtcStreamer = WebRtcStreamer;}if(typeof module !=='undefined'&&typeof module.exports !=='undefined'){ module.exports = WebRtcStreamer;}

1.2 封装视频组件,在需要视频的地方引入此封装的视频组件即可,也是粘贴即用,注意其中import的webrtcstreamer.js的地址替换为自己的

<template><div class="rtsp_video_container"><div v-if="videoUrls.length === 1"class="rtsp_video single-video"><video :id="'video_0'" controls autoPlay muted width="100%" height="100%" style="object-fit: fill"></video></div><div v-if="videoUrls.length >1" v-for="(videoUrl, index) in videoUrls":key="index"class="rtsp_video"><video :id="'video_' + index" controls autoPlay muted width="100%" height="100%" style="object-fit: fill"></video></div></div></template><script>import WebRtcStreamer from'../untils/webrtcstreamer';// 注意此处替换为webrtcstreamer.js所在的路径exportdefault{ name:'RtspVideo', props:{ videoUrls:{ type: Array, required:true,}},data(){return{ cameraIp:'localhost:8000',// 这里的IP固定为本地,不要修改,是用来与本地的webrtc-streamer插件进行通讯的,见文章1.3 webRtcServers:[],// 存储 WebRtcStreamer 实例};},mounted(){this.initializeStreams();}, watch:{// 监听 videoUrls 或 cameraIp 的变化,重新初始化流 videoUrls:{handler(newUrls, oldUrls){if(newUrls.length !== oldUrls.length ||!this.isSameArray(newUrls, oldUrls)){this.resetStreams();this.initializeStreams();}}, deep:true,},cameraIp(newIp, oldIp){if(newIp !== oldIp){this.resetStreams();this.initializeStreams();}}}, methods:{// 初始化视频流连接initializeStreams(){if(this.webRtcServers.length ===0){this.videoUrls.forEach((videoUrl, index)=>{const videoElement = document.getElementById(`video_${index}`);const webRtcServer =newWebRtcStreamer(videoElement,`http://${this.cameraIp}`);this.webRtcServers.push(webRtcServer); webRtcServer.connect(videoUrl,null,'rtptransport=tcp',null);});}},// 检查新旧数组是否相同isSameArray(arr1, arr2){return arr1.length === arr2.length && arr1.every((value, index)=> value === arr2[index]);},// 清除 WebRtcStreamer 实例resetStreams(){this.webRtcServers.forEach((webRtcServer)=>{if(webRtcServer){ webRtcServer.disconnect();// 断开连接}});this.webRtcServers =[];// 清空实例},},beforeDestroy(){this.resetStreams();// 页面销毁时清理 WebRtcStreamer 实例,避免内存泄漏},};</script><style lang="less" scoped>.rtsp_video_container { display: flex; flex-wrap: wrap; gap:10px; justify-content: space-between;}.rtsp_video { flex:1148%; height:225px; max-width:48%; background: #000; border-radius:8px; overflow: hidden;}.single-video { flex:11100%; height:100%; max-width:100%; background: #000;} video { width:100%; height:100%; object-fit: cover;}</style>

父组件中进行此视频组件的引用示例:

<template><div style="margin-top: 10px;width: 100%;height: 100%;"><rtsp-video :videoUrls="selectedUrls":key="selectedUrls.join(',')"></rtsp-video></div></template>import RtspVideo from"../views/video"; components:{ RtspVideo }data(){return{ selectedUrls:['rtsp://user:[email protected]:xxxx/Streaming/Channels/101','rtsp://user:[email protected]:xxxx/Streaming/Channels/201'],}}

1.3 以上完成之后,需要观看视频的本地PC设备启动webrtc-streamer插件

webrtc-streamer插件下载webrtc-streamer

下载图中的版本,标题1.1中对应的js版本就是此版本


下载解压完成之后,其中的exe和js是配套的,插件脚本在webrtc-streamer-v0.8.13-dirty-Windows-AMD64-Release\bin目录下,对应的webrtcstreamer.js在webrtc-streamer-v0.8.13-dirty-Windows-AMD64-Release\share\webrtc-streamer\html目录下,只需要webrtc-streamer.exe和webrtcstreamer.js即可,也可以直接用博主在上面提供的,切记一定要配套,不然可能画面取不出。

实现效果图见下:

在这里插入图片描述

至此海康威视实时视频预览功能已完成,写作不易,如果对您有帮助,恳请保留一个赞。

补充:
如果启动webrtc-streamer.exe导致客户端卡顿 或者 需要更改webrtc-streamer.exe的端口号,可参考下图

在这里插入图片描述
在这里插入图片描述


视频监控观看插件.bat:
@echo off
cd C:
start webrtc-streamer.exe -o -H 0.0.0.0:8124
exit

Read more

Vivado使用教程:图解说明管脚分配全过程

Vivado管脚分配实战指南:从原理到避坑全解析 你有没有遇到过这样的情况?逻辑代码写得完美无缺,仿真波形也完全正确,结果下载到FPGA板子上——灯不亮、通信失败、甚至芯片发热异常。排查半天,最后发现是某个引脚接错了电压标准? 别笑,这在FPGA开发中太常见了。 尤其是在初学阶段,很多人把注意力都放在Verilog或VHDL的语法和状态机设计上,却忽略了 一个比代码更底层、更关键的环节:管脚分配 。 今天我们就来彻底拆解这个“隐形杀手”——用最贴近工程实践的方式,带你一步步搞懂 Vivado中的管脚分配全过程 ,不只是点几下鼠标那么简单,而是理解背后的电气规则、约束机制与系统级影响。 为什么管脚分配不是“随便连一下”? FPGA不像MCU那样有固定的外设映射。它的每个IO引脚都是可编程的,这意味着你可以自由定义哪个引脚做时钟输入、哪个输出控制LED。但自由的背后是责任: 每一个引脚配置都必须符合物理世界的电气法则 。 举个真实案例: 某工程师将一个来自3.3V系统的复位信号接入Bank 14(VCCO=1.8V),没有加电平转换。虽然一开始功能似乎正常,但在高温环境下

HarmonyOS6 底部导航栏组件 rc_concave_tabbar 使用指南

HarmonyOS6 底部导航栏组件 rc_concave_tabbar 使用指南

文章目录 * 前言 * 组件特性 * 适用场景 * 使用说明 * 安装组件 * 安装步骤 * 步骤一:引入相关依赖 * 步骤二:创建菜单数据 * 步骤三:使用导航组件 * 运行效果 * 参数介绍 * TabsConcaveCircle 组件参数 * TabMenusInterfaceIRequired 菜单项配置 * 进阶使用 * 自定义单个菜单项颜色 * 调整动画速度 * 自定义高度和颜色 * 注意事项 * 总结 前言 rc_concave_tabbar 是一个功能强大、样式精美的 HarmonyOS 底部导航栏组件库,提供凹陷圆形动画效果样式,适用于多种场景。本篇将介绍 rc_concave_tabbar 的使用方法以及其相关的设计理念。 组件特性 * 流畅动画:支持流畅的凹陷圆形切换动画效果 * 高度定制:支持自定义背景色、字体颜色、高度等多种样式配置 * 灵活配置:支持全局配置和单项配置,满足不同场景需求

积木报表快速入门指南:零基础轻松上手数据可视化【低代码报表设计器】

积木报表快速入门指南:零基础轻松上手数据可视化【低代码报表设计器】

文章目录 * 前言 * 一、积木报表简介 * 二、环境准备 * 1. 下载积木报表 * 2. 运行环境要求 * 3. 快速启动(以Docker方式为例) * 三、第一个报表创建实战 * 1. 登录系统 * 2. 选择数据源 * 3. 设计报表 * 四、进阶功能快速上手 * 1. 图表集成 * 2. 参数传递 * 3. 分组与汇总 * 4. 导出与打印 * 五、实用技巧与最佳实践 * 1. 性能优化: * 2. 模板复用: * 3. 移动端适配: * 4. 定时任务: * 六、常见问题解答 * Q1:积木报表支持哪些数据库? * Q2:如何实现复杂的中国式报表? * Q3:能否集成到自己的系统中? * Q4:

RISC-V开源处理器实战:从Verilog RTL设计到FPGA原型验证

RISC-V开源处理器实战:从Verilog RTL设计到FPGA原型验证

引言:开源浪潮下的RISC-V处理器设计 在芯片设计领域,RISC-V架构正以其开源免授权、模块化扩展和极简指令集三大优势重塑行业格局。与传统闭源架构不同,RISC-V允许开发者自由定制处理器核,从嵌入式微控制器到高性能服务器芯片均可覆盖。本文以Xilinx Vivado 2025工具链和蜂鸟E203处理器为核心,完整呈现从Verilog RTL设计到FPGA原型验证的全流程,为嵌入式工程师和硬件爱好者提供一套可复现的实战指南。 项目目标与技术栈 * 核心目标:基于RISC-V RV32I指令集,设计支持五级流水线的32位处理器核,实现基础算术运算、逻辑操作及访存功能,并在Xilinx Artix-7 FPGA开发板验证。 * 工具链:Xilinx Vivado 2025(逻辑设计、综合实现)、ModelSim(功能仿真)、Xilinx Artix-7 XC7A35T FPGA开发板(硬件验证)。 * 参考案例:蜂鸟E203处理器(芯来科技开源RISC-V核,已在Xilinx FPGA上完成移植验证,最高运行频率50MHz)。 一、数字系统设计流程:从需求到架构 1.