一、業(yè)務(wù)需求和調(diào)研
1. 現(xiàn)有的平臺(tái)系統(tǒng)播放實(shí)時(shí)視頻。
因?yàn)橛脩綦娔X都是Linux系統(tǒng),無法直接使用??登岸薙DK,討論決定由后臺(tái)推視頻流,簡單調(diào)研后發(fā)現(xiàn)最流行的是flv,而且有B站開源的flv.js適配。前期后臺(tái)推給我RTMP前綴的視頻流,我嘗試使用video.js,西瓜視頻等都失敗了,后來后端改為http前綴的,對接成功。這里還要講一下flv.js的文檔, 不知道是我理解有誤, 還是文檔沒有更新, 還是讓人一身冷汗的:
第二句講: FLV實(shí)時(shí)流在所有瀏覽器無法工作
但是點(diǎn)進(jìn)去livestream.md:
這里又講: 根據(jù)IO限制, flv.js目前在各類新版瀏覽器支持HTTP FLV實(shí)時(shí)流
總而言之,即便是chrome已經(jīng)不支持flash,但是用B站這款flv.js還是可以實(shí)現(xiàn)在現(xiàn)代瀏覽器播放HTTP FLV視頻流的。
2. 分屏,先點(diǎn)擊分屏,然后選擇需要播放的視頻設(shè)備,在該分屏播放對應(yīng)的視頻流。
3. 開啟新的視頻的同時(shí),以及離開本頁面時(shí)要關(guān)閉之前的視頻流,以減輕服務(wù)器壓力。這一點(diǎn)跟主流需求還是很不同的,因?yàn)橥ǔ6紩?huì)理解為在分屏可以同時(shí)觀看多個(gè)攝像頭的實(shí)時(shí)畫面,所以即使我已經(jīng)實(shí)現(xiàn)了需求,但還是感覺分屏在這里是有些雞肋的。
二、實(shí)現(xiàn)效果
這里展示4屏和6屏,1屏就不用展示了,下面代碼中還有9屏和16屏可選,目前我這里用不到,就先注釋掉了。
三、鳴謝
感謝二位大佬的解決方案,這是我實(shí)現(xiàn)本業(yè)務(wù)需求的基礎(chǔ):
ID:?抄一下你代碼
全網(wǎng)最詳細(xì)!vue中使用flv.js 播放直播監(jiān)控視頻流
ID:?三體人1379號(hào)文章來源:http://www.zghlxwxcb.cn/news/detail-769118.html
vue實(shí)現(xiàn)視頻播放1,4,6,9,16宮格布局文章來源地址http://www.zghlxwxcb.cn/news/detail-769118.html
四、代碼實(shí)現(xiàn)
1. 子組件, 也就是視頻播放器,您也可以根據(jù)不同的視頻流資源配置不同的播放器:
<template>
<div :class="{ player: true, selected: isSelected }" @click="handlePlayerClick">
<!-- {{ title }}號(hào)窗口 -->
<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() {
// 在點(diǎn)擊事件中調(diào)用父組件的方法,傳遞數(shù)據(jù)
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) {
//這個(gè)val 就是一個(gè)地址,例如: http://192.168.2.201:85/live/9311272c49b845baa2b2810ad9bf3f68.flv 這是個(gè)服務(wù)器返回給我的一個(gè)監(jiān)控視頻流地址
setTimeout(() => {
//使用定時(shí)器是因?yàn)?,在mounted聲明周期里調(diào)用,可能會(huì)出現(xiàn)DOM沒加載出來的原因
var videoElement = this.$refs.videosmallone // 獲取到html中的video標(biāo)簽
if (flvjs.isSupported()) {
//因?yàn)槲疫@個(gè)是復(fù)用組件,進(jìn)來先判斷 player是否存在,如果存在,銷毀掉它,不然會(huì)占用TCP名額
if (this.player !== null) {
this.player.pause()
this.player.unload()
this.player.detachMediaElement()
this.player.destroy()
this.player = null
}
this.player = flvjs.createPlayer(
//創(chuàng)建直播流,加載到DOM中去
{
type: 'flv',
url: val, //你的url地址
isLive: true, //數(shù)據(jù)源是否為直播流
hasAudio: false, //數(shù)據(jù)源是否包含有音頻
hasVideo: true, //數(shù)據(jù)源是否包含有視頻
enableStashBuffer: true //是否啟用緩存區(qū)
},
{
enableWorker: false, //不啟用分離線程
enableStashBuffer: false, //關(guān)閉IO隱藏緩沖區(qū)
autoCleanupSourceBuffer: true, //自動(dòng)清除緩存
lazyLoad: false
}
)
this.player.attachMediaElement(videoElement) //放到dom中去
this.player.load() //準(zhǔn)備完成
//!!!!!!這里需要注意,有的時(shí)候load加載完成不一定可以播放,要是播放不成功,用settimeout 給下面的this.player.play() 延時(shí)幾百毫秒再播放
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. 父組件結(jié)構(gòu):
<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="選擇設(shè)備:">
<a-tree-select
v-model="value"
style="width: 200px"
:dropdown-style="{ maxHeight: '600px', overflow: 'auto' }"
:tree-data="treeData"
placeholder="請選擇設(shè)備"
: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. 核心業(yè)務(wù)邏輯:
<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: '', // 保存已經(jīng)開啟視頻的token, 用于關(guān)閉視頻
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() {
// 處理切換分屏的邏輯
},
// 這里是整理數(shù)據(jù)用于下拉框選擇播放視頻源的設(shè)備
getStationAndCamera() {
reqStationAndCamera({ city: '', camera: 1 }).then((res) => {
// 創(chuàng)建一個(gè)空數(shù)組用于存儲(chǔ)treeData
const treeData = []
// 遍歷后臺(tái)返回的數(shù)組
res.forEach((station) => {
// 提取一級(jí)菜單的信息
const firstLevelNode = {
title: station.stationName,
value: station.id,
key: `level1-${station.id}`, // 使用id作為key
disabled: true, // 設(shè)置一級(jí)菜單為不可選
children: [] // 用于存儲(chǔ)二級(jí)菜單
}
// 遍歷devices數(shù)組,提取二級(jí)菜單的信息
station.devices.forEach((device) => {
const secondLevelNode = {
title: device.deviceName,
value: device.id,
key: `level2-${device.id}` // 使用id作為key
// 如果有三級(jí)菜單,可以在這里繼續(xù)處理
}
// 將二級(jí)菜單添加到一級(jí)菜單的children數(shù)組中
firstLevelNode.children.push(secondLevelNode)
})
// 將一級(jí)菜單添加到treeData數(shù)組中
treeData.push(firstLevelNode)
})
// 打印加工后的treeData
console.log('Processed treeData:', treeData)
this.treeData = treeData
})
},
async playRealtimeVideo() {
if (!this.value) {
this.$message.error('請選擇設(shè)備')
// 中止程序,可以使用return或者throw語句,根據(jù)您的需求選擇
return // 中止程序執(zhí)行
} else {
this.queryParam = {
id: this.value
}
}
// console.log('realtime video param', this.queryParam)
const RealtimeVideoParams = this.queryParam
const playerRef = `player${this.activePlayer}`
// 使用 $refs 引用 player 組件實(shí)例
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('已關(guān)閉其他視頻')
})
.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>
到了這里,關(guān)于Vue實(shí)現(xiàn)攝像頭視頻分屏, 使用flv.js接收rtmp/flv視頻流的文章就介紹完了。如果您還想了解更多內(nèi)容,請?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!