前端在浏览器显示摄像头传回的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接收实时更新

  1. 安装WebSocket客户端库: 首先,确保你的项目中安装了WebSocket客户端库,如socket.io-client。 npm install socket.io-client

  2. 建立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>

 

posted @ 2024-04-23 15:27  鼓舞飞扬  阅读(2838)  评论(0编辑  收藏  举报