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

Flutter 组件 tavily_dart 的适配 鸿蒙Harmony 实战 - 驾驭 AI 搜索引擎集成、实现鸿蒙端互联网知识精密获取与语义增强方案

Flutter 组件 tavily_dart 的适配 鸿蒙Harmony 实战 - 驾驭 AI 搜索引擎集成、实现鸿蒙端互联网知识精密获取与语义增强方案

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.ZEEKLOG.net Flutter 组件 tavily_dart 的适配 鸿蒙Harmony 实战 - 驾驭 AI 搜索引擎集成、实现鸿蒙端互联网知识精密获取与语义增强方案 前言 在鸿蒙(OpenHarmony)生态的智能个人助理、行业垂直类知识中枢以及需要实时获取互联网最新动态并进行 AI 语义加工的各种前沿应用开发中,“信息的有效检索与精准抽取”是决定 AI 应用是否具备“生命感”的关键泵口。面对浩如烟海且充满噪声的互联网网页。如果仅仅依靠传统的关键词匹配。那么不仅会导致应用返回大量无关紧要的垃圾信息。更会因为无法将网页内容转化为 AI 易于理解的结构化上下文(Context),引发严重的 LLM(大语言模型)幻觉风险。 我们需要一种“AI 驱动、语义过滤”的搜索艺术。 tavily_dart 是一套专为 AI

旧电脑秒变 AI 员工:OpenClaw 本地部署教程(含环境配置 + 插件开发 + 常见坑)

旧电脑秒变 AI 员工:OpenClaw 本地部署教程(含环境配置 + 插件开发 + 常见坑)

前言 本文基于最新OpenClaw版本编写,适配电脑低配置场景(最低2vCPU+2GiB内存+40GiB SSD),兼容Windows 10/11(优先WSL2)、Ubuntu 20.04+系统,全程纯操作指令,覆盖环境配置、本地部署、插件开发、高频坑排查。核心解决部署卡顿、国内网络适配、插件开发无思路、报错无法排查四大痛点,全程适配国内网络(国内镜像源)、国内大模型(通义千问、阿里云百炼等),无需海外代理,可稳定运行实现自动化办公(文件处理、IM对接、任务调度等)。 一、前置准备(适配优化) 1.1 硬件要求(最低适配) * CPU:Intel i3 4代+/AMD Ryzen 3 2000+(支持虚拟化,

人工智能:自然语言处理在医疗健康领域的应用与实战

人工智能:自然语言处理在医疗健康领域的应用与实战

人工智能:自然语言处理在医疗健康领域的应用与实战 学习目标 💡 理解自然语言处理(NLP)在医疗健康领域的应用场景和重要性 💡 掌握医疗健康领域NLP应用的核心技术(如电子病历分析、医学文本分类、疾病预测) 💡 学会使用前沿模型(如BERT、GPT-3)进行医疗健康文本分析 💡 理解医疗健康领域的特殊挑战(如医学术语、数据隐私、数据质量) 💡 通过实战项目,开发一个电子病历分析应用 重点内容 * 医疗健康领域NLP应用的主要场景 * 核心技术(电子病历分析、医学文本分类、疾病预测) * 前沿模型(BERT、GPT-3)在医疗健康领域的使用 * 医疗健康领域的特殊挑战 * 实战项目:电子病历分析应用开发 一、医疗健康领域NLP应用的主要场景 1.1 电子病历分析 1.1.1 电子病历分析的基本概念 电子病历分析是对电子病历文本进行分析和处理的过程。在医疗健康领域,电子病历分析的主要应用场景包括: * 病历结构化:将非结构化的电子病历文本转换为结构化数据 * 病历检索:检索相关的电子病历 * 病历质量评估:

AI 大模型落地系列|Eino 组件核心篇:Embedding 到底解决了什么

AI 大模型落地系列|Eino 组件核心篇:Embedding 到底解决了什么

Embedding 使用说明 * 有啥用?! * 他能干嘛? * 它不能直接干嘛? * 总结: * 浅用之法 * 食用之法 * 一、最基本用法:直接调用 `EmbedStrings` * 1. 创建 embedder * 2. 调用 `EmbedStrings` * 3. 向量拿来干嘛 * 二、完整demo * 三、带 Option 怎么用 * 四、在编排中怎么用 * 在 Chain 中使用 * 在 Graph 中使用 * 五、带 Callback 怎么用 * 六、真实场景 * 场景:做知识库问答 * 第一步:把知识库切块 * 第二步:给每个 chunk 生成向量 * 第三步:存起来