前端在浏览器显示摄像头传回的RTSP视频流,前端采用的技术有VUE+video.js+flv.js
在前端使用Vue.js框架,结合video.js和flv.js播放RTSP视频流,需要经过一系列步骤,因为浏览器并不能直接播放RTSP流,所以通常需要一个服务器来将RTSP流转为HLS或FLV格式,然后前端再通过flv.js播放。以下是一个基于此思路的基本实现指南:
确保你已经安装了Vue.js、video.js、flv.js相关的依赖。
npm install video.js flv.js --save
Vue组件实现
<template> <div> <video id="myPlayer" class="video-js vjs-default-skin" controls></video> </div> </template> <script> import videojs from 'video.js'; import 'video.js/dist/video-js.css'; import flvjs from 'flv.js'; export default { mounted() { this.player = videojs('myPlayer', { techOrder: ['html5', 'flvjs'], sources: [{ src: 'YOUR_FLV_STREAM_URL', // 这里换成实际的FLV流URL type: 'video/x-flv' }], controlBar: { // 自定义控制栏 fullscreenToggle: true, }, }); // 检查flvjs是否可用并加载流 if (flvjs.isSupported()) { const tech = flvjs.createPlayer({ type: 'flv', url: 'YOUR_FLV_STREAM_URL', // 与sources.src相同,确保一致 }); this.player.tech_.hls = tech; tech.attachMediaElement(this.player.el().querySelector('video')); tech.load(); } else { console.error('FLV.js is not supported in this browser.'); } }, beforeDestroy() { if (this.player && this.player.dispose) { this.player.dispose(); } }, }; </script>
以上代码提供了一个基础框架,实际应用中可能需要根据具体需求和环境进行调整。
如果streamUrl从后端获取,并隔一段时间推送一次呢?
如果streamUrl
需要从后端动态获取,并且这个URL可能会随着时间推移而变化(例如,每隔一段时间后端推送新的流地址),你可以采用Vue的响应式特性以及WebSocket或轮询的方式来动态更新视频流地址。下面是一个简化版的实现思路:
使用WebSocket接收实时更新
-
安装WebSocket客户端库: 首先,确保你的项目中安装了WebSocket客户端库,如
socket.io-client
。npm install socket.io-client
-
建立WebSocket连接: 在Vue组件中建立与后端的WebSocket连接,并设置消息监听器来接收新的
streamUrl
。
import io from "socket.io-client"; data() { return { player: null, streamUrl: '', // 初始化为空或默认值 socket: null, // 用于存放WebSocket实例 }; }, mounted() { this.initWebSocket(); this.initPlayer(); }, methods: { initWebSocket() { this.socket = io("http://your-backend-url"); // 替换为你的后端WebSocket服务器地址 this.socket.on("newStreamUrl", (newUrl) => { console.log("New stream URL received:", newUrl); this.streamUrl = newUrl; // 当收到新的URL时,重新设置播放器的源 if (this.player && this.player.src) { this.player.src({ type: 'application/x-flv', src: this.streamUrl, }); // 重新加载播放器 this.player.load(); // 如果需要,还可以在这里调用play()方法开始播放 } }); }, // ...其他代码保持不变 }, beforeDestroy() { // 清理WebSocket连接 if (this.socket) { this.socket.disconnect(); } // ...其他清理工作 }
使用定时轮询
步骤 1: 定义数据属性和方法
在Vue组件中,定义需要的数据属性,比如存放视频流URL的变量,以及一个用于发起请求的方法。
export default { data() { return { videoStreamUrl: '', // 初始视频流URL pollInterval: null, // 用于存储轮询的定时器引用 }; }, methods: { fetchVideoStreamUrl() { // 发起请求到后台获取视频流URL axios.get('/api/getVideoStream') .then(response => { if (response.data.success && response.data.url) { this.videoStreamUrl = response.data.url; // 如果是首次获取成功,或者需要在每次获取后重新加载播放器,这里可以添加逻辑 // 例如,如果使用video.js,可能需要重新初始化播放器或更新源 } }) .catch(error => { console.error('Error fetching video stream URL:', error); }); }, }, };
步骤 2: 创建轮询逻辑
在Vue组件的生命周期钩子中启动轮询,通常在mounted
钩子启动,在beforeDestroy
钩子中清理轮询,避免内存泄漏。
mounted() { // 启动轮询,例如每10秒检查一次 this.pollInterval = setInterval(() => { this.fetchVideoStreamUrl(); }, 10000); // 10000毫秒即10秒 }, beforeDestroy() { // 清理轮询,当组件销毁时停止 clearInterval(this.pollInterval); },
步骤 3: 动态更新视频播放
当从后台获取到新的视频流URL时,如果你使用的是像video.js这样的播放器库,你可能需要根据新的URL更新播放器的源。具体实现取决于你使用的播放器库的API。
对于video.js,你可能需要重新创建播放器实例或使用其提供的API来更新视频源。例如:
methods: { // ...其他方法 updateVideoSource(newUrl) { if (this.player) { // 假设this.player是video.js播放器实例 this.player.src({ src: newUrl, type: 'video/x-flv', // 根据实际情况调整类型 }); // 可能需要重新加载或播放 this.player.load(); this.player.play(); } }, }, // 在fetchVideoStreamUrl方法内部调用updateVideoSource if (response.data.success && response.data.url !== this.videoStreamUrl) { this.updateVideoSource(response.data.url); }
这样,你就实现了根据后台动态提供的视频流URL,使用轮询方式在前端进行定期检查和更新的功能。记得根据实际应用场景调整轮询间隔,避免过于频繁的请求导致服务器压力。
以上是ai生成,实际项目代码如下:
1 <template> 2 <div class="planningStyle"> 3 <div class="top"> 4 <div class="top-title"> 5 实时视频 6 </div> 7 <el-select 8 v-model="devSnValue" 9 class="planselectes" 10 @change="changePlanning" 11 placeholder="请选择" 12 > 13 <el-option 14 v-for="item in cameraList" 15 :key="item.devSn" 16 :label="item.devName" 17 :value="item.devSn" 18 > 19 </el-option> 20 </el-select> 21 </div> 22 23 <div class="content" v-loading="loading"> 24 <video 25 class="video" 26 id="videoElement" 27 :src="streamUrl" 28 :autoplay="true" 29 :fluid="true" 30 :controls="false" 31 muted 32 ></video> 33 </div> 34 </div> 35 </template> 36 37 <script> 38 import { getCameraListWithGb, getStreamInfo } from '@/api/cockpit'; 39 export default { 40 name: 'RealtimeVideo', 41 data() { 42 return { 43 loading: false, 44 cameraList: [], 45 currentVideoObj: {}, 46 devSnValue: '', 47 streamUrl: '', 48 flvPlayer: null, 49 pollIntervalTimer: null // 定时器 50 }; 51 }, 52 53 mounted() { 54 this.getDataList(); 55 }, 56 watch: { 57 streamUrl(newVal, oldVal) { 58 if (newVal && newVal !== oldVal) { 59 this.$nextTick(() => { 60 this.createPlayer(); 61 }); 62 } 63 } 64 }, 65 methods: { 66 getDataList() { 67 const form = new FormData(); 68 form.append('token', this.$store.getters.token); 69 form.append('pageNum', 1); 70 form.append('pageSize', -1); 71 form.append('status', '1'); 72 getCameraListWithGb(form).then(ret => { 73 this.cameraList = ret.data.body.list; 74 this.currentVideoObj = this.cameraList[0]; 75 this.devSnValue = this.currentVideoObj.devSn; 76 this.getStreamInfo(); // 获取设备码流地址 77 }); 78 }, 79 // 获取设备码流地址 80 getStreamInfo() { 81 this.loading = true; 82 const form = new FormData(); 83 form.append('token', this.$store.getters.token); 84 form.append('productIdentifier', this.currentVideoObj.productIdentifier); 85 form.append('devSn', this.devSnValue); 86 form.append('channelNum', 1); 87 form.append('streamMode', 'sub'); 88 getStreamInfo(form).then(ret => { 89 if (ret.data.status === 0) { 90 let res = ret.data.body; 91 this.streamUrl = res.streamUrl; 92 } 93 }); 94 }, 95 createPlayer() { 96 if (!this.streamUrl) return; 97 // 判断当前浏览器是否支持播放 98 if (flvjs.isSupported()) { 99 let videoElement = document.getElementById('videoElement'); 100 // 创建一个播放实例 101 this.flvPlayer = flvjs.createPlayer({ 102 type: 'flv', 103 isLive: true, 104 hasAudio: false, 105 cros: true, 106 enableWorker: true, 107 enableStashBuffer: false, 108 stashInitialSize: 128, 109 url: this.streamUrl 110 }); 111 // 将播放实例注册到video节点 112 this.loading = false; 113 this.flvPlayer.attachMediaElement(videoElement); 114 this.flvPlayer.load(); // 加载数据流 115 this.flvPlayer.play(); // 播放数据流 116 } 117 }, 118 changePlanning(value) { 119 this.currentVideoObj = this.cameraList.find(item => { 120 return item.devSn === value; 121 }); 122 this.stopPlay(); 123 this.getStreamInfo(); 124 }, 125 // 停止播放 126 stopPlay() { 127 if (!this.flvPlayer) return false; 128 this.clearPolling(); 129 this.flvPlayer.pause(); // 暂停播放数据流 130 this.flvPlayer.unload(); // 取消数据流加载 131 this.flvPlayer.detachMediaElement(); // 将播放实例从节点中取出 132 this.flvPlayer.destroy(); // 销毁播放实例 133 this.flvPlayer = null; 134 }, 135 // 开始轮询,每两分钟获取一次视频流 136 polling() { 137 if (this.pollIntervalTimer) { 138 this.clearPolling(); 139 return; 140 } 141 this.pollIntervalTimer = setInterval(() => { 142 this.getStreamInfo(); 143 }, 1000 * 15); 144 }, 145 clearPolling() { 146 clearInterval(this.pollIntervalTimer); 147 } 148 }, 149 beforeDestroy() { 150 clearInterval(this.pollIntervalTimer); 151 this.pollIntervalTimer = null; 152 }, 153 destroyed() { 154 this.stopPlay(); 155 } 156 }; 157 </script> 158 <style lang="scss" scoped> 159 .fade-enter { 160 opacity: 0; 161 } 162 .fade-enter-active { 163 transition: opacity 2s; 164 } 165 .fade-leave-to { 166 opacity: 0; 167 } 168 .fade-leave-active { 169 transition: opacity 2s; 170 } 171 172 .planningStyle { 173 width: 100%; 174 height: 100%; 175 padding: 13px 9px 24px 9px; 176 177 .top { 178 width: 100%; 179 height: 32px; 180 background: url('~@/assets/images/bigScreen/title-figure.gif') no-repeat; 181 background-size: 100% 100%; 182 text-align: center; 183 display: flex; 184 flex-direction: row; 185 font-size: 20px; 186 color: #1af1e9; 187 .top-title { 188 width: 100%; 189 text-align: center; 190 margin-left: 85px; 191 line-height: 22px; 192 } 193 } 194 .planselectes { 195 position: relative; 196 margin-top: 13px; 197 width: 153px; 198 height: 20px; 199 margin-right: 10px; 200 font-size: 14px; 201 color: #fff; 202 line-height: 20px; 203 background: #05488c; 204 border-radius: 4px; 205 :deep(.el-input__suffix) { 206 display: none; 207 } 208 :deep(.el-input__inner) { 209 padding: 0px; 210 text-align: center; 211 height: 20px; 212 line-height: 20px; 213 background: #05488c !important; 214 font-size: 14px; 215 color: #ffffff; 216 border-radius: 4px; 217 max-width: 100px; 218 display: inline-block; 219 overflow: hidden; 220 text-overflow: ellipsis; 221 white-space: nowrap; 222 } 223 /*将小箭头的样式去去掉*/ 224 .el-icon-arrow-up:before { 225 content: ''; 226 } 227 } 228 .content { 229 height: 244px; 230 .video { 231 width: 100%; 232 height: 100%; 233 object-fit: fill; 234 } 235 } 236 .el-select__caret :before { 237 content: ''; 238 } 239 /deep/ .el-loading-mask { 240 background: transparent; 241 } 242 } 243 </style>