支持多屏播放的播放器切换

This commit is contained in:
lin 2026-06-13 07:27:17 +08:00
parent 1b7661039b
commit 9a52d29ded
4 changed files with 83 additions and 97 deletions

View File

@ -1,6 +1,6 @@
<template> <template>
<div id="h265Player" ref="container" style="background-color: #000000; position: relative; display: flex; align-items: center; justify-content: center;" @dblclick="fullscreenSwich" @mouseenter="showBar = true" @mouseleave="showBar = false"> <div :id="'h265Player-' + _uid" ref="container" style="background-color: #000000; position: relative; display: flex; align-items: center; justify-content: center;" @dblclick="fullscreenSwich" @mouseenter="showBar = true" @mouseleave="showBar = false">
<div id="glplayer" ref="playerBox" style="width: 100%; height: 100%; margin: 0 auto;"> <div :id="'glplayer-' + _uid" ref="playerBox" style="width: 100%; height: 100%; margin: 0 auto;">
<div v-if="playerLoading" class="play-loading"> <div v-if="playerLoading" class="play-loading">
<i class="el-icon-loading" /> <i class="el-icon-loading" />
<span style="margin-left: 5px">视频加载中</span> <span style="margin-left: 5px">视频加载中</span>
@ -122,7 +122,7 @@ export default {
const options = {} const options = {}
h265webPlayer[this._uid] = new window.new265webjs(url, Object.assign( h265webPlayer[this._uid] = new window.new265webjs(url, Object.assign(
{ {
player: 'glplayer', // id player: 'glplayer-' + this._uid,
width: this.playerWidth, width: this.playerWidth,
height: this.playerHeight, height: this.playerHeight,
token: token, token: token,

View File

@ -3,7 +3,7 @@
<el-tabs v-if="showTab && playerList.length > 1" v-model="activePlayer" type="card" :stretch="true" @tab-click="changePlayer"> <el-tabs v-if="showTab && playerList.length > 1" v-model="activePlayer" type="card" :stretch="true" @tab-click="changePlayer">
<el-tab-pane v-for="p in playerList" :key="p.key" :label="p.label" :name="p.key"></el-tab-pane> <el-tab-pane v-for="p in playerList" :key="p.key" :label="p.label" :name="p.key"></el-tab-pane>
</el-tabs> </el-tabs>
<div class="player-video-area"> <div class="player-video-area" :style="{ height: showTab ? 'calc(100% - 36px)' : '100%' }">
<jessibucaPlayer <jessibucaPlayer
v-if="activePlayer === 'jessibuca'" v-if="activePlayer === 'jessibuca'"
ref="jessibuca" ref="jessibuca"
@ -179,7 +179,7 @@ export default {
} }
.player-video-area { .player-video-area {
width: 100%; width: 100%;
height:calc(100% - 36px);; height: 100%;
background: #000; background: #000;
} }
</style> </style>

View File

@ -1,13 +1,13 @@
<template> <template>
<div id="rtcPlayer"> <div :id="'rtcPlayer-' + _uid" class="rtc-player-wrapper">
<video id="webRtcPlayerBox" :controls="showControls" autoplay style="text-align:left;"> <video :id="'webRtcPlayerBox-' + _uid" class="rtc-player-video" :controls="showControls" autoplay style="text-align:left;">
Your browser is too old which doesn't support HTML5 video. Your browser is too old which doesn't support HTML5 video.
</video> </video>
</div> </div>
</template> </template>
<script> <script>
let webrtcPlayer = null const webrtcPlayer = {}
import dragZoom from '../../mixins/dragZoom' import dragZoom from '../../mixins/dragZoom'
export default { export default {
name: 'RtcPlayer', name: 'RtcPlayer',
@ -26,16 +26,17 @@ export default {
mounted() {}, mounted() {},
destroyed() { destroyed() {
clearTimeout(this.timer) clearTimeout(this.timer)
this.pause()
}, },
methods: { methods: {
play: function(url) { play: function(url) {
if (webrtcPlayer != null) { if (webrtcPlayer[this._uid]) {
this.pause() this.pause()
} }
webrtcPlayer = new ZLMRTCClient.Endpoint({ webrtcPlayer[this._uid] = new ZLMRTCClient.Endpoint({
element: document.getElementById('webRtcPlayerBox'), // video element: document.getElementById('webRtcPlayerBox-' + this._uid),
debug: true, // debug: true,
zlmsdpUrl: url, // zlmsdpUrl: url,
simulecast: false, simulecast: false,
useCamera: false, useCamera: false,
audioEnable: true, audioEnable: true,
@ -43,37 +44,37 @@ export default {
recvOnly: true, recvOnly: true,
usedatachannel: false usedatachannel: false
}) })
webrtcPlayer.on(ZLMRTCClient.Events.WEBRTC_ICE_CANDIDATE_ERROR, (e) => { // ICE const player = webrtcPlayer[this._uid]
player.on(ZLMRTCClient.Events.WEBRTC_ICE_CANDIDATE_ERROR, (e) => {
console.error('ICE 协商出错') console.error('ICE 协商出错')
this.eventcallbacK('ICE ERROR', 'ICE 协商出错') this.eventcallbacK('ICE ERROR', 'ICE 协商出错')
}) })
webrtcPlayer.on(ZLMRTCClient.Events.WEBRTC_ON_REMOTE_STREAMS, (e) => { // player.on(ZLMRTCClient.Events.WEBRTC_ON_REMOTE_STREAMS, (e) => {
console.log('播放成功', e.streams) console.log('播放成功', e.streams)
this.eventcallbacK('playing', '播放成功') this.eventcallbacK('playing', '播放成功')
}) })
webrtcPlayer.on(ZLMRTCClient.Events.WEBRTC_OFFER_ANWSER_EXCHANGE_FAILED, (e) => { // offer anwser player.on(ZLMRTCClient.Events.WEBRTC_OFFER_ANWSER_EXCHANGE_FAILED, (e) => {
console.error('offer anwser 交换失败', e) console.error('offer anwser 交换失败', e)
this.eventcallbacK('OFFER ANSWER ERROR ', 'offer anwser 交换失败') this.eventcallbacK('OFFER ANSWER ERROR ', 'offer anwser 交换失败')
if (e.code == -400 && e.msg == '流不存在') { if (e.code == -400 && e.msg == '流不存在') {
console.log('流不存在') console.log('流不存在')
this.timer = setTimeout(() => { this.timer = setTimeout(() => {
this.webrtcPlayer.close() player.close()
this.play(url) this.play(url)
}, 100) }, 100)
} }
}) })
webrtcPlayer.on(ZLMRTCClient.Events.WEBRTC_ON_LOCAL_STREAM, (s) => { // player.on(ZLMRTCClient.Events.WEBRTC_ON_LOCAL_STREAM, (s) => {
// document.getElementById('selfVideo').srcObject=s;
this.eventcallbacK('LOCAL STREAM', '获取到了本地流') this.eventcallbacK('LOCAL STREAM', '获取到了本地流')
}) })
}, },
pause: function() { pause: function() {
if (webrtcPlayer != null) { if (webrtcPlayer[this._uid]) {
webrtcPlayer.close() webrtcPlayer[this._uid].close()
webrtcPlayer = null webrtcPlayer[this._uid] = null
} }
}, },
stop: function() { stop: function() {
@ -85,7 +86,7 @@ export default {
console.log(message) console.log(message)
}, },
getVideoElement() { getVideoElement() {
return document.getElementById('webRtcPlayerBox') return document.getElementById('webRtcPlayerBox-' + this._uid)
}, },
getVideoRect() { getVideoRect() {
const video = this.getVideoElement() const video = this.getVideoElement()
@ -121,12 +122,12 @@ export default {
.LodingTitle { .LodingTitle {
min-width: 70px; min-width: 70px;
} }
#rtcPlayer{ .rtc-player-wrapper{
width: 100%; width: 100%;
height: 100%; height: 100%;
position: relative; position: relative;
} }
#webRtcPlayerBox{ .rtc-player-video{
width: 100%; width: 100%;
height: 100%; height: 100%;
max-height: 100%; max-height: 100%;

View File

@ -13,6 +13,14 @@
<i class="iconfont icon-a-mti-6fenpingshi btn" :class="{active: spiltIndex === 2}" @click="spiltIndex=2" /> <i class="iconfont icon-a-mti-6fenpingshi btn" :class="{active: spiltIndex === 2}" @click="spiltIndex=2" />
<i class="iconfont icon-a-mti-9fenpingshi btn" :class="{active: spiltIndex === 3}" @click="spiltIndex=3" /> <i class="iconfont icon-a-mti-9fenpingshi btn" :class="{active: spiltIndex === 3}" @click="spiltIndex=3" />
</div> </div>
<div class="global-player-control">
播放器:
<el-select v-model="globalPlayer" size="mini" style="width: 120px">
<el-option label="Jessibuca" value="jessibuca" />
<el-option label="WebRTC" value="webRTC" />
<el-option label="H265web" value="h265web" />
</el-select>
</div>
<div class="fullscreen-control"> <div class="fullscreen-control">
<i class="el-icon-full-screen btn" @click="fullScreen()" /> <i class="el-icon-full-screen btn" @click="fullScreen()" />
</div> </div>
@ -30,15 +38,12 @@
:class="getPlayerClass(spiltIndex, i)" :class="getPlayerClass(spiltIndex, i)"
@click="playerIdx = (i-1)" @click="playerIdx = (i-1)"
> >
<div v-if="!videoUrl[i-1]" class="no-signal">{{ videoTip[i-1]?videoTip[i-1]:"无信号" }}</div> <div v-if="!streamInfo[i-1]" class="no-signal">{{ videoTip[i-1]?videoTip[i-1]:"无信号" }}</div>
<player <PlayerTabs
v-else v-else
:ref="'player' + i" :ref="'playerTabs' + i"
fluent :show-tab="false"
autoplay
:show-button="true" :show-button="true"
@screenshot="shot"
@destroy="destroy"
/> />
</div> </div>
</div> </div>
@ -49,20 +54,21 @@
</template> </template>
<script> <script>
import player from '../common/jessibuca.vue' import PlayerTabs from '../common/playerTabs.vue'
import DeviceTree from '../common/DeviceTree.vue' import DeviceTree from '../common/DeviceTree.vue'
import screenFull from 'screenfull' import screenFull from 'screenfull'
export default { export default {
name: 'Live', name: 'Live',
components: { components: {
player, DeviceTree PlayerTabs, DeviceTree
}, },
data() { data() {
return { return {
videoUrl: [''], streamInfo: [null],
videoTip: [''], videoTip: [''],
globalPlayer: 'jessibuca',
spiltIndex: 2, // spiltIndex: 2, //
playerIdx: 0, // playerIdx: 0, //
@ -122,23 +128,33 @@ export default {
} }
}, },
watch: { watch: {
spilt(newValue) { spiltIndex(newValue) {
console.log('切换画幅;' + newValue) console.log('切换画幅;' + newValue)
const that = this const that = this
for (let i = 1; i <= newValue; i++) { for (let i = 1; i <= this.layout[newValue].spilt; i++) {
if (!that.$refs['player' + i]) { if (!that.$refs['playerTabs' + i]) {
continue continue
} }
this.$nextTick(() => { this.$nextTick(() => {
if (that.$refs['player' + i] instanceof Array) { const ref = that.$refs['playerTabs' + i]
that.$refs['player' + i][0].resize() const instance = ref instanceof Array ? ref[0] : ref
} else { if (instance && instance.resize) {
that.$refs['player' + i].resize() instance.resize()
} }
}) })
} }
window.localStorage.setItem('split', newValue) window.localStorage.setItem('split', newValue)
}, },
globalPlayer(newKey) {
for (let i = 1; i <= this.layout[this.spiltIndex].spilt; i++) {
const ref = this.$refs['playerTabs' + i]
if (ref) {
const instance = ref instanceof Array ? ref[0] : ref
instance.switchPlayer(newKey)
}
}
window.localStorage.setItem('globalPlayer', newKey)
},
'$route.fullPath': 'checkPlayByParam' '$route.fullPath': 'checkPlayByParam'
}, },
mounted() { mounted() {
@ -156,27 +172,18 @@ export default {
}, },
methods: { methods: {
handleResize() { handleResize() {
// Force update to recalculate responsive layout
this.$forceUpdate() this.$forceUpdate()
// Resize any active players
this.$nextTick(() => { this.$nextTick(() => {
for (let i = 0; i < this.layout[this.spiltIndex].spilt; i++) { for (let i = 0; i < this.layout[this.spiltIndex].spilt; i++) {
const playerRef = this.$refs[`player${i + 1}`] const ref = this.$refs[`playerTabs${i + 1}`]
if (playerRef) { if (ref) {
if (playerRef instanceof Array) { const instance = ref instanceof Array ? ref[0] : ref
playerRef[0].resize && playerRef[0].resize() instance.resize && instance.resize()
} else {
playerRef.resize && playerRef.resize()
}
} }
} }
}) })
}, },
destroy(idx) {
console.log(idx)
this.clear(idx.substring(idx.length - 1))
},
clickEvent: function(channelId) { clickEvent: function(channelId) {
this.sendDevicePush(channelId) this.sendDevicePush(channelId)
}, },
@ -194,17 +201,11 @@ export default {
sendDevicePush: function(channelId) { sendDevicePush: function(channelId) {
this.save(channelId) this.save(channelId)
const idxTmp = this.playerIdx const idxTmp = this.playerIdx
this.setPlayUrl('', idxTmp) this.$set(this.streamInfo, idxTmp, null)
this.$set(this.videoTip, idxTmp, '正在拉流...') this.$set(this.videoTip, idxTmp, '正在拉流...')
this.$store.dispatch('commonChanel/playChannel', channelId) this.$store.dispatch('commonChanel/playChannel', channelId)
.then(data => { .then(data => {
let videoUrl this.setPlayStream(data.transcodeStream || data, idxTmp)
if (location.protocol === 'https:') {
videoUrl = data.wss_flv
} else {
videoUrl = data.ws_flv
}
this.setPlayUrl(videoUrl, idxTmp)
}) })
.catch(err => { .catch(err => {
this.$set(this.videoTip, idxTmp, '播放失败: ' + err) this.$set(this.videoTip, idxTmp, '播放失败: ' + err)
@ -213,18 +214,15 @@ export default {
this.loading = false this.loading = false
}) })
}, },
setPlayUrl(url, idx) { setPlayStream(streamInfo, idx) {
this.$set(this.videoUrl, idx, url) this.$set(this.streamInfo, idx, streamInfo)
const _this = this
setTimeout(() => {
window.localStorage.setItem('videoUrl', JSON.stringify(_this.videoUrl))
}, 100)
this.$nextTick(() => { this.$nextTick(() => {
const refName = 'player' + (idx + 1) const refName = 'playerTabs' + (idx + 1)
if (this.$refs[refName]) { const ref = this.$refs[refName]
const player = this.$refs[refName] instanceof Array ? this.$refs[refName][0] : this.$refs[refName] if (ref) {
if (player && player.play) { const instance = ref instanceof Array ? ref[0] : ref
player.play(url) if (instance && instance.setStreamInfo) {
instance.setStreamInfo(streamInfo)
} }
} }
}) })
@ -235,28 +233,7 @@ export default {
this.sendDevicePush(query.channelId) this.sendDevicePush(query.channelId)
} }
}, },
shot(e) {
var base64ToBlob = function(code) {
const parts = code.split(';base64,')
const contentType = parts[0].split(':')[1]
const raw = window.atob(parts[1])
const rawLength = raw.length
const uInt8Array = new Uint8Array(rawLength)
for (let i = 0; i < rawLength; ++i) {
uInt8Array[i] = raw.charCodeAt(i)
}
return new Blob([uInt8Array], {
type: contentType
})
}
const aLink = document.createElement('a')
const blob = base64ToBlob(e) // new Blob([content]);
const evt = document.createEvent('HTMLEvents')
evt.initEvent('click', true, true) // initEvent FF
aLink.download = '截图'
aLink.href = URL.createObjectURL(blob)
aLink.click()
},
save(item) { save(item) {
const dataStr = window.localStorage.getItem('playData') || '[]' const dataStr = window.localStorage.getItem('playData') || '[]'
const data = JSON.parse(dataStr) const data = JSON.parse(dataStr)
@ -340,6 +317,13 @@ export default {
padding-right: 10px; padding-right: 10px;
} }
.global-player-control {
display: flex;
align-items: center;
gap: 6px;
font-size: 14px;
}
.player-container { .player-container {
flex: 1; flex: 1;
display: flex; display: flex;
@ -354,6 +338,7 @@ export default {
height: 100%; height: 100%;
max-height: calc(100vh - 180px); max-height: calc(100vh - 180px);
aspect-ratio: 16/9; aspect-ratio: 16/9;
border: 4px solid rgb(169, 168, 168);
} }
.btn { .btn {
@ -370,7 +355,7 @@ export default {
} }
.redborder { .redborder {
border: 4px solid rgb(0, 198, 255) !important; outline: 4px solid rgb(0, 198, 255);
} }
.play-box { .play-box {