Vue实现摄像头视频分屏, 使用flv.js接收rtmp/flv视频流

这篇具有很好参考价值的文章主要介绍了Vue实现摄像头视频分屏, 使用flv.js接收rtmp/flv视频流。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

一、业务需求和调研

1. 现有的平台系统播放实时视频。

因为用户电脑都是Linux系统,无法直接使用海康前端SDK,讨论决定由后台推视频流,简单调研后发现最流行的是flv,而且有B站开源的flv.js适配。前期后台推给我RTMP前缀的视频流,我尝试使用video.js,西瓜视频等都失败了,后来后端改为http前缀的,对接成功。这里还要讲一下flv.js的文档, 不知道是我理解有误, 还是文档没有更新, 还是让人一身冷汗的:

Vue实现摄像头视频分屏, 使用flv.js接收rtmp/flv视频流,业务需求解决方案,javascript,vue.js,音视频,前端

第二句讲: FLV实时流在所有浏览器无法工作

但是点进去livestream.md:

Vue实现摄像头视频分屏, 使用flv.js接收rtmp/flv视频流,业务需求解决方案,javascript,vue.js,音视频,前端

这里又讲: 根据IO限制, flv.js目前在各类新版浏览器支持HTTP FLV实时流

总而言之,即便是chrome已经不支持flash,但是用B站这款flv.js还是可以实现在现代浏览器播放HTTP FLV视频流的。

2. 分屏,先点击分屏,然后选择需要播放的视频设备,在该分屏播放对应的视频流。

3. 开启新的视频的同时,以及离开本页面时要关闭之前的视频流,以减轻服务器压力。这一点跟主流需求还是很不同的,因为通常都会理解为在分屏可以同时观看多个摄像头的实时画面,所以即使我已经实现了需求,但还是感觉分屏在这里是有些鸡肋的。

二、实现效果

这里展示4屏和6屏,1屏就不用展示了,下面代码中还有9屏和16屏可选,目前我这里用不到,就先注释掉了。

Vue实现摄像头视频分屏, 使用flv.js接收rtmp/flv视频流,业务需求解决方案,javascript,vue.js,音视频,前端

Vue实现摄像头视频分屏, 使用flv.js接收rtmp/flv视频流,业务需求解决方案,javascript,vue.js,音视频,前端

三、鸣谢

感谢二位大佬的解决方案,这是我实现本业务需求的基础:

ID: 抄一下你代码

全网最详细!vue中使用flv.js 播放直播监控视频流

ID: 三体人1379号

vue实现视频播放1,4,6,9,16宫格布局文章来源地址https://www.toymoban.com/news/detail-769118.html

四、代码实现

1. 子组件, 也就是视频播放器,您也可以根据不同的视频流资源配置不同的播放器:
<template>
  <div :class="{ player: true, selected: isSelected }" @click="handlePlayerClick">
    <!-- {{ title }}号窗口 -->
    <video
      class="cell-player-1"
      ref="videosmallone"
      preload="auto"
      muted
      controls
      autoplay
      type="rtmp/flv"
    >
      <source src="" />
    </video>
  </div>
</template>

<script>
import flvjs from 'flv.js'

export default {
  props: {
    title: {
      type: Number,
      default: 1
    },
    activePlayer: {
      type: Number,
      default: null
    }
  },
  data() {
    return {
      player: null,
      loading: false,
      videoUrl: '',
      videoToken: ''
    }
  },
  beforeUnmount() {
    if (this.player) {
      this.player.pause()
      this.player.unload()
      this.player.detachMediaElement()
      this.player.destroy()
      this.player = null
    }
  },
  computed: {
    // Use a computed property to determine if the player is active
    isSelected() {
      return this.activePlayer === this.title
    },
    playerClass() {
      return ['player', `cell-player-1`, { active: this.title === this.activePlayer }]
    }
  },
  methods: {
    handlePlayerClick() {
      // 在点击事件中调用父组件的方法,传递数据
      this.$emit('playerClick', this.title)
      // console.log('class', this.playerClass)
    },
    openVideo(data) {
      // Implement this method to update the data in the player component
      // Use the passed data to update the player's state or perform other operations
      // console.log(`Setting data for player ${this.title}:`, data)
      this.init(data.data.url)
    },
    init(val) {
      //这个val 就是一个地址,例如: http://192.168.2.201:85/live/9311272c49b845baa2b2810ad9bf3f68.flv 这是个服务器返回给我的一个监控视频流地址
      setTimeout(() => {
        //使用定时器是因为,在mounted声明周期里调用,可能会出现DOM没加载出来的原因
        var videoElement = this.$refs.videosmallone // 获取到html中的video标签
        if (flvjs.isSupported()) {
          //因为我这个是复用组件,进来先判断 player是否存在,如果存在,销毁掉它,不然会占用TCP名额
          if (this.player !== null) {
            this.player.pause()
            this.player.unload()
            this.player.detachMediaElement()
            this.player.destroy()
            this.player = null
          }
          this.player = flvjs.createPlayer(
            //创建直播流,加载到DOM中去
            {
              type: 'flv',
              url: val, //你的url地址
              isLive: true, //数据源是否为直播流
              hasAudio: false, //数据源是否包含有音频
              hasVideo: true, //数据源是否包含有视频
              enableStashBuffer: true //是否启用缓存区
            },
            {
              enableWorker: false, //不启用分离线程
              enableStashBuffer: false, //关闭IO隐藏缓冲区
              autoCleanupSourceBuffer: true, //自动清除缓存
              lazyLoad: false
            }
          )
          this.player.attachMediaElement(videoElement) //放到dom中去
          this.player.load() //准备完成
          //!!!!!!这里需要注意,有的时候load加载完成不一定可以播放,要是播放不成功,用settimeout 给下面的this.player.play() 延时几百毫秒再播放
          this.player.play() //播放
        }
      }, 1000)
    }
  }
}
</script>

<style scoped>
.player {
  background-color: black;
  height: 100%;
  border: 1px solid grey;
  color: white;
  text-align: center;
}
.selected {
  background-color: black;
  height: 100%;
  border: 2px solid green;
  color: white;
  text-align: center;
}

.cell-player-1 {
  width: 100%;
  height: 100%;
  box-sizing: border-box;
}
</style>
2. 父组件结构:
<template>
  <div style="height: 100%">
    <a-form layout="inline" class="header">
      <a-form-item>
        <div class="cell-tool">
          <div class="bk-button-group">
            <a-button
              :class="{ active: cellCount === 1 }"
              @click="cellCount = 1"
              style="margin-right: 5px"
              >1屏</a-button
            >
            <a-button
              :class="{ active: cellCount === 4 }"
              @click="cellCount = 4"
              style="margin-right: 5px"
              >4屏</a-button
            >
            <a-button
              :class="{ active: cellCount === 6 }"
              @click="cellCount = 6"
              style="margin-right: 5px"
              >6屏</a-button
            >
            <!-- <button @click="cellCount = 9" size="small">9</button>
            <button @click="cellCount = 16" size="small">16</button> -->
          </div>
        </div>
      </a-form-item>
      <a-form-item label="选择设备:">
        <a-tree-select
          v-model="value"
          style="width: 200px"
          :dropdown-style="{ maxHeight: '600px', overflow: 'auto' }"
          :tree-data="treeData"
          placeholder="请选择设备"
          :treeDefaultExpandAll="true"
        >
        </a-tree-select>
      </a-form-item>

      <a-form-item>
        <div style="display: inline-block">
          <SavaButton type="search" @click="playRealtimeVideo">播放</SavaButton>
          <SavaButton type="delete" @click="resetSearchForm()" style="margin-left: 8px"
            >重置</SavaButton
          >
        </div>
      </a-form-item>
    </a-form>
    <div class="main-body">
      <div class="left">
        <div class="left-upper"></div>
        <div class="left-lower"></div>
      </div>
      <div class="right">
        <!-- 然后在这里添加分屏的布局 -->
        <div class="cell">
          <div class="cell-player">
            <div :class="cellClass(i)" v-for="i in cellCount" :key="i">
              <player
                :title="i"
                @playerClick="handlePlayerClick"
                v-if="cellCount != 6"
                :activePlayer="activePlayer"
                :ref="`player${i}`"
              ></player>
              <player
                :title="i"
                @playerClick="handlePlayerClick"
                v-if="cellCount == 6 && i != 2 && i != 3"
                :activePlayer="activePlayer"
                :ref="`player${i}`"
              ></player>
              <template v-if="cellCount == 6 && i == 2">
                <div class="cell-player-6-2-cell">
                  <player
                    :title="i"
                    @playerClick="handlePlayerClick"
                    :activePlayer="activePlayer"
                    :ref="`player${i}`"
                  ></player>
                  <!-- original config is ++i -->
                  <player
                    :title="i + 1"
                    @playerClick="handlePlayerClick"
                    :activePlayer="activePlayer"
                    :ref="`player${i + 1}`"
                  ></player>
                </div>
              </template>
            </div>
          </div>
        </div>
        <div class="right-lower"></div>
      </div>
    </div>
  </div>
</template>
3. 核心业务逻辑:
<script>
import player from './player/player.vue'
import { reqStationAndCamera, reqGetRealtimeVideo, reqCloseVideo1 } from '@/api/camera'
export default {
  components: { player },
  data() {
    return {
      queryParam: {
        id: ''
      },
      cellCount: 1,
      value: '',
      treeData: [],
      activePlayer: 1,
      oldToken: '', // 保存已经开启视频的token, 用于关闭视频
      oldTokensArray: []
    }
  },
  created() {
    this.getStationAndCamera()
  },
  mounted() {
    // Add the beforeunload event listener when the component is mounted
    window.addEventListener('beforeunload', this.closeOldVideos)
  },
  beforeUnmount() {
    // This method will be called before the component is unmounted or the page is unloaded
    this.closeOldVideos()
    // Remove the beforeunload event listener before the component is unmounted
    window.removeEventListener('beforeunload', this.closeOldVideos)
  },
  watch: {
    value(value) {
      console.log(value)
    }
  },
  methods: {
    changeScreen() {
      // 处理切换分屏的逻辑
    },
    // 这里是整理数据用于下拉框选择播放视频源的设备
    getStationAndCamera() {
      reqStationAndCamera({ city: '', camera: 1 }).then((res) => {
        // 创建一个空数组用于存储treeData
        const treeData = []

        // 遍历后台返回的数组
        res.forEach((station) => {
          // 提取一级菜单的信息
          const firstLevelNode = {
            title: station.stationName,
            value: station.id,
            key: `level1-${station.id}`, // 使用id作为key
            disabled: true, // 设置一级菜单为不可选
            children: [] // 用于存储二级菜单
          }

          // 遍历devices数组,提取二级菜单的信息
          station.devices.forEach((device) => {
            const secondLevelNode = {
              title: device.deviceName,
              value: device.id,
              key: `level2-${device.id}` // 使用id作为key
              // 如果有三级菜单,可以在这里继续处理
            }

            // 将二级菜单添加到一级菜单的children数组中
            firstLevelNode.children.push(secondLevelNode)
          })

          // 将一级菜单添加到treeData数组中
          treeData.push(firstLevelNode)
        })

        // 打印加工后的treeData
        console.log('Processed treeData:', treeData)
        this.treeData = treeData
      })
    },

    async playRealtimeVideo() {
      if (!this.value) {
        this.$message.error('请选择设备')

        // 中止程序,可以使用return或者throw语句,根据您的需求选择
        return // 中止程序执行
      } else {
        this.queryParam = {
          id: this.value
        }
      }

      // console.log('realtime video param', this.queryParam)

      const RealtimeVideoParams = this.queryParam
      const playerRef = `player${this.activePlayer}`

      // 使用 $refs 引用 player 组件实例
      const playerInstance = this.$refs[playerRef]
      // console.log('playerInstance:', playerInstance)

      try {
        const res = await this.getRealtimeVideo(RealtimeVideoParams)

        // Check if 'res' is undefined or not
        if (res !== undefined) {
          console.log('new data res', res)
          this.$message.success('获取视频成功, 正在打开', 5)

          const newDataForClickedPlayer = res

          console.log('newDataForClickedPlayer:', newDataForClickedPlayer)

          if (playerInstance) {
            // Pass data to the newly clicked player
            playerInstance[0].openVideo(newDataForClickedPlayer)

            // Check if there was a previously clicked player
            if (this.activePlayer !== null) {
              // console.log('active player', this.activePlayer)
              // Perform any operations specific to the previously clicked player
              // playerInstance[0].closeVideo(historyVideoData)
            }
          }

          this.closeOldVideos()
        }
        this.oldToken = res.data.token
      } catch (error) {
        console.error('Error in play realtime video:', error)
      }
    },
    resetSearchForm() {
      this.value = ''
      this.queryParam = {
        id: ''
      }
    },
    getRealtimeVideo(queryParam) {
      return new Promise((resolve, reject) => {
        reqGetRealtimeVideo(queryParam)
          .then((res) => {
            console.log('realtime video', res)
            resolve(res)
          })
          .catch((error) => {
            console.error('Error fetching realtime video:', error)
            reject(error)
          })
      })
    },
    handlePlayerClick(title) {
      // console.log('clicked window', title)

      // Update the active player in the parent component
      this.activePlayer = title
      // console.log('active player', this.activePlayer)
    },
    closeOldVideos() {
      if (this.oldToken) {
        this.oldTokensArray.push(this.oldToken)

        // Map old tokens array to an array of promises
        const closePromises = this.oldTokensArray.map((oldToken) =>
          reqCloseVideo1(oldToken)
            .then((resc) => {
              console.log('close old video', resc)
              this.$message.warn('已关闭其他视频')
            })
            .catch((e) => {
              console.log('close error', e)
            })
        )

        // Use Promise.all to wait for all promises to resolve
        Promise.all(closePromises)
          .then(() => {
            // All videos closed successfully
            console.log('All videos closed successfully')
          })
          .catch((error) => {
            // Handle errors if any of the requests fail
            console.log('Error closing videos:', error)
          })
      }
    }
  },
  computed: {
    cellClass() {
      return function (index) {
        switch (this.cellCount) {
          case 1:
            return ['cell-player-1']
          case 4:
            return ['cell-player-4']
          case 6:
            if (index == 1) return ['cell-player-6-1']
            if (index == 2) return ['cell-player-6-2']
            if (index == 3) return ['cell-player-6-none']
            return ['cell-player-6']
          case 9:
            return ['cell-player-9']
          case 16:
            return ['cell-player-16']
          default:
            break
        }
      }
    }
  }
}
</script>
4. 样式, 这里有些ant D穿透样式, 可以去掉:
<style lang="less" scoped>
.header {
  background-color: #034d94;
  padding: 10px 25px;
  border-radius: 10px;
}
.main-body {
  width: 100%;
  height: 90%;
  display: flex;

  .right {
    width: 100%;
    height: 100%;
    .cell {
      margin-top: 0.5%;
      display: flex;
      flex-direction: column;
      height: 100%;
    }
  }
}
.bk-button-group .active {
  background-color: skyblue;
  color: #fff;
  /* Add any other styles for the active button */
}
.cell-tool {
  height: 40px;
  line-height: 40px;
  margin-top: -1px;
  // padding: 0 7px;
}
.cell-player {
  flex: 1;
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  width: 100%;
  height: 100%;
}
.cell-player-4 {
  width: 50%;
  height: 50% !important;
  box-sizing: border-box;
}
.cell-player-1 {
  width: 100%;
  height: 100%;
  box-sizing: border-box;
}
.cell-player-6-1 {
  width: 66.66%;
  height: 66.66% !important;
  box-sizing: border-box;
}
.cell-player-6-2 {
  width: 33.33%;
  height: 66.66% !important;
  box-sizing: border-box;
  display: flex;
  flex-direction: column;
}
.cell-player-6-none {
  display: none;
}
.cell-player-6-2-cell {
  width: 100%;
  height: 50% !important;
  box-sizing: border-box;
}
.cell-player-6 {
  width: 33.33%;
  height: 33.33% !important;
  box-sizing: border-box;
}
.cell-player-9 {
  width: 33.33%;
  height: 33.33% !important;
  box-sizing: border-box;
}
.cell-player-16 {
  width: 25%;
  height: 25% !important;
  box-sizing: border-box;
}

.ant-select {
  width: 180px;
}
/deep/.ant-time-picker-input {
  background-color: #034d94;
  border: 1px solid rgba(255, 255, 255, 0.4);
  color: #fff;
  &::placeholder {
    color: #bfbfb5;
  }
}
/deep/ .ant-select-selection--single {
  background-color: #034d94;
  border: 1px solid rgba(255, 255, 255, 0.4);
  color: #fff;
  &::placeholder {
    color: #bfbfb5;
  }
}
/deep/ .ant-select-arrow {
  color: white;
}
/deep/.page-search-none {
  padding: 0;
}
/deep/.ant-svg {
  color: #fff;
}
/deep/.ant-time-picker-icon .ant-time-picker-clock-icon,
.ant-time-picker-clear .ant-time-picker-clock-icon {
  color: #fff;
}
li.ant-select-tree-treenode-disabled > span:not(.ant-select-tree-switcher),
li.ant-select-tree-treenode-disabled > .ant-select-tree-node-content-wrapper,
li.ant-select-tree-treenode-disabled > .ant-select-tree-node-content-wrapper span {
  color: red !important;
}
</style>

到了这里,关于Vue实现摄像头视频分屏, 使用flv.js接收rtmp/flv视频流的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包赞助服务器费用

相关文章

  • VUE+Django实现前端开启摄像头录制存储视频并直接上传后端

    VUE+Django实现前端开启摄像头录制存储视频并直接上传后端

    1.录制前 2.录制中 3.录制结束下载文件至本地 4.视频文件同时上传至后端接口 参考博客https://blog.csdn.net/wuchenlhy/article/details/79311234?spm=1001.2014.3001.5506 博主在后端这块写的十分简洁明了,可以直接参考实现开设后端简单文件上传接口的方法 参考文章: https://blog.csdn.net/XH_jing/a

    2024年02月14日
    浏览(52)
  • 使用手机摄像头实现视频监控实时播放

    视频监控实时播放的原理与目前较为流行的直播是一致的,所以采用直播的架构实现视频监控实时播放,流程图如下: 目前实时视频流的传输协议有以下几种:RTSP、RTMP、HLS、Http-flv。 安卓APP开发使用HBuilder,而HBuilder内置了LivePusher直播推流控件,该控件使用了RTMP协议,所以

    2023年04月08日
    浏览(15)
  • vue2使用rtsp视频流接入海康威视摄像头(纯前端)

    vue2使用rtsp视频流接入海康威视摄像头(纯前端)

    海康威视官方的RTSP最新取流格式如下: rtsp://用户名:密码@IP:554/Streaming/Channels/101 用户名和密码 IP就是登陆摄像头时候的IP(笔者这里IP是192.168.1.210) 所以笔者的rtsp流地址就是 rtsp://用户名:密码@192.168.1.210:554/Streaming/Channels/101 1.1关闭 萤石云的接入 1.2 调整视频编码为H.264 在此下载

    2024年04月26日
    浏览(15)
  • 使用 MFC 和 OpenCV 实现实时摄像头视频显示

    1、引言 MFC 是一个在 Windows 平台上编写 C++ 应用程序的库,提供了丰富的用户界面功能。OpenCV(Open Source Computer Vision Library)是一个开源的计算机视觉库,包含了丰富的图像处理和计算机视觉功能。本文将向大家展示如何将这两个库结合起来,实现一个实时显示摄像头画面的简

    2024年02月13日
    浏览(91)
  • LiveGBS流媒体平台GB/T28181功能-如何获取接入的海康大华宇视华为摄像头硬件NVR设备通道视频直播流地址HLS/HTTP-FLV/WS-FLV/WebRTC/RTMP/RTSP

    LiveGBS流媒体平台GB/T28181功能-如何获取接入的海康大华宇视华为摄像头硬件NVR设备通道视频直播流地址HLS/HTTP-FLV/WS-FLV/WebRTC/RTMP/RTSP

    LiveGBS国标GB/T28181流媒体服务器软件,支持设备|平台GB28181注册接入、向上级联第三方国标平台, 可视化的WEB页面管理(页面源码开源);支持云台控制、设备录像检索、回放,支持语音对讲,用户管理, 多种协议流输出,实现浏览器无插件直播。 在项目过程中,需要播放视频

    2024年03月25日
    浏览(19)
  • JAVACV 读取摄像头流将rtsp转flv 通过http-flv和flv.js播放 无插件 纯代码

    1、pom  2、摄像头类 3、服务实现  4、拉流转码 5、前端(vue) 安装flv.js   npm install --save flv.js

    2024年02月12日
    浏览(18)
  • 吸烟检测从零开始使用YOLOv5+PyQt5+OpenCV实现(支持图片、视频、摄像头实时检测)

    吸烟检测从零开始使用YOLOv5+PyQt5+OpenCV实现(支持图片、视频、摄像头实时检测)

    全流程 教程,从数据采集到模型使用到最终展示。若有任何疑问和建议欢迎评论区讨论。 先放上最终实现效果 检测效果 由上图我们可以看到,使用YOLOV5完成了吸烟的目标识别检测,可以达到mAP可达85.38%。通过对吸烟的自动检测可以方便商场、医院、疗养院等公共场合进行禁

    2024年02月09日
    浏览(9)
  • 大华摄像头实时预览(spring boot+websocket+flv.js)Java开发

    大华摄像头实时预览(spring boot+websocket+flv.js)Java开发

    1.大华NetSDK_JAVA; 这里使用的是 Linux64的架包 2.websocket 前端使用的vue框架    3.flv.js的播放插件     4.大华摄像头提供的平台(后面称为官方平台) 根据大华《NetSDK_JAVA编程指导手册》的流程图 根据图可以得知关键流程为: 初始化sdk——登录设备——打开实时预览——设置视

    2024年02月04日
    浏览(20)
  • OpenCV(视频加载与摄像头使用)

    OpenCV(视频加载与摄像头使用)

    目录 1、VideoCapture类 2、视频属性get()  3、视屏文件保存

    2024年02月12日
    浏览(12)
  • vue2 对接 海康摄像头插件 (视频WEB插件 V1.5.2)

    vue2 对接 海康摄像头插件 (视频WEB插件 V1.5.2)

    前言 海康视频插件v.1.5.2版本运行环境需要安装插件VideoWebPlugin.exe,对浏览器也有兼容性要求,具体看官方文档 对应下载插件 去海康官网下载插件 里面有dome等其他需要用到的 地址: 安装插件 打开下载的文件里的bin文件 安装一下VideoWebPlugin vue脚手架中集成插件 把官方资源

    2024年02月03日
    浏览(15)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包