优化云端录像播放结构

This commit is contained in:
lin 2026-06-12 10:55:04 +08:00
parent c59bacdc2c
commit d3e89786c2
11 changed files with 284 additions and 604 deletions

View File

@ -180,8 +180,6 @@ import devicePlayer from '@/views/common/channelPlayer/index.vue'
import Edit from './edit.vue'
import ChooseCivilCode from '../dialog/chooseCivilCode.vue'
import ChooseGroup from '@/views/dialog/chooseGroup.vue'
import { MessageBox } from 'element-ui'
import store from '@/store'
export default {
name: 'ChannelList',

View File

@ -1,452 +0,0 @@
<template>
<div id="cloudRecordPlayer" style="height: 100%">
<div class="cloud-record-playBox" :style="playBoxStyle">
<jessibucaPlayer
v-if="playerType === 'Jessibuca'"
ref="recordVideoPlayer"
:height="'calc(100% - 250px)'"
:show-button="false"
@playTimeChange="showPlayTimeChange"
@playStatusChange="playingChange"
fluent
autoplay
live
/>
<rtcPlayer
v-if="playerType === 'WebRTC'"
ref="recordVideoPlayer"
:has-audio="true"
:show-controls="false"
style="height: calc(100% - 250px)"
autoplay
@playTimeChange="showPlayTimeChange"
@playStatusChange="playingChange"
/>
<h265web v-if="playerType === 'H265web'" ref="recordVideoPlayer" :height="'calc(100% - 250px)'" :show-button="false" @playTimeChange="showPlayTimeChange" @playStatusChange="playingChange"/>
</div>
<div class="cloud-record-player-option-box">
<div class="cloud-record-show-time">
{{showPlayTimeValue}}
</div>
<div class="cloud-record-time-process" ref="timeProcess" @click="timeProcessClick($event)"
@mouseenter="timeProcessMouseEnter($event)" @mousemove="timeProcessMouseMove($event)"
@mouseleave="timeProcessMouseLeave($event)">
<div v-if="streamInfo">
<div class="cloud-record-time-process-value" :style="playTimeValue"></div>
<transition name="el-fade-in-linear">
<div v-show="showTimeLeft" class="cloud-record-time-process-title" :style="playTimeTitleStyle" >{{showPlayTimeTitle}}</div>
</transition>
</div>
</div>
<div class="cloud-record-show-time">
{{showPlayTimeTotal}}
</div>
</div>
<div style="height: 40px; background-color: #383838; display: grid; grid-template-columns: 1fr auto 1fr">
<div style="text-align: left;">
<div class="cloud-record-record-play-control" style="background-color: transparent; box-shadow: 0 0 10px transparent">
<a v-if="showListCallback" target="_blank" class="cloud-record-record-play-control-item iconfont icon-list" title="列表" @click="sidebarControl()" />
<a target="_blank" class="cloud-record-record-play-control-item iconfont icon-camera1196054easyiconnet" title="截图" @click="snap()" />
<!-- <a target="_blank" class="cloud-record-record-play-control-item iconfont icon-shuaxin11" title="刷新" @click="refresh()" />-->
<!-- <a target="_blank" class="cloud-record-record-play-control-item iconfont icon-xiazai011" title="下载" />-->
</div>
</div>
<div style="text-align: center;">
<div class="cloud-record-record-play-control">
<a v-if="!lastDiable" target="_blank" class="cloud-record-record-play-control-item iconfont icon-diyigeshipin" title="上一个" @click="playLast()" />
<a v-else style="color: #acacac; cursor: not-allowed" target="_blank" class="cloud-record-record-play-control-item iconfont icon-diyigeshipin" title="上一个" />
<a target="_blank" class="cloud-record-record-play-control-item iconfont icon-kuaijin" title="快退五秒" @click="seekBackward()" />
<a target="_blank" class="cloud-record-record-play-control-item iconfont icon-stop1" style="font-size: 14px" title="停止" @click="stopPLay()" />
<a v-if="playing" target="_blank" class="cloud-record-record-play-control-item iconfont icon-zanting" title="暂停" @click="pausePlay()" />
<a v-if="!playing" target="_blank" class="cloud-record-record-play-control-item iconfont icon-kaishi" title="播放" @click="play()" />
<a target="_blank" class="cloud-record-record-play-control-item iconfont icon-houtui" title="快进五秒" @click="seekForward()" />
<a v-if="!nextDiable" target="_blank" class="cloud-record-record-play-control-item iconfont icon-zuihouyigeshipin" title="下一个" @click="playNext()" />
<a v-else style="color: #acacac; cursor: not-allowed" target="_blank" class="cloud-record-record-play-control-item iconfont icon-zuihouyigeshipin" title="下一个" @click="playNext()" />
<el-dropdown @command="changePlaySpeed" :popper-append-to-body='false' >
<a target="_blank" class="cloud-record-record-play-control-item record-play-control-speed" title="倍速播放">{{ playSpeed }}X</a>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item
v-for="item in playSpeedRange"
:key="item"
:command="item"
>{{ item }}X</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</div>
</div>
<div style="text-align: right;">
<div class="cloud-record-record-play-control" style="background-color: transparent; box-shadow: 0 0 10px transparent">
<div class="cloud-record-record-play-control-item record-play-control-player">
<el-dropdown @command="changePlayerType" :popper-append-to-body='false' >
<a target="_blank" class="cloud-record-record-play-control-item record-play-control-speed" title="选择播放器">{{ playerLabel }}</a>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item command="Jessibuca" >Jessibuca</el-dropdown-item>
<el-dropdown-item command="WebRTC" >WebRTC</el-dropdown-item>
<el-dropdown-item command="H265web" >H265web</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</div>
<a v-if="!isFullScreen" target="_blank" class="cloud-record-record-play-control-item iconfont icon-fangdazhanshi" title="全屏" @click="fullScreen()" />
<a v-else target="_blank" class="cloud-record-record-play-control-item iconfont icon-suoxiao1" title="全屏" @click="fullScreen()" />
</div>
</div>
</div>
</div>
</template>
<script>
import h265web from '../common/h265web.vue'
import jessibucaPlayer from '@/views/common/jessibuca.vue'
import rtcPlayer from '../common/rtcPlayer.vue'
import moment from 'moment'
import momentDurationFormatSetup from 'moment-duration-format'
import screenfull from 'screenfull'
momentDurationFormatSetup(moment)
export default {
name: 'CloudRecordPlayer',
components: {
jessibucaPlayer, rtcPlayer, h265web
},
props: ['showListCallback', 'showNextCallback', 'showLastCallback', 'lastDiable', 'nextDiable'],
data() {
return {
showSidebar: false,
videoUrl: null,
streamInfo: null,
timeLen: null,
startTime: null,
showTimeLeft: null,
isMousedown: false,
loading: false,
playerTime: null,
playSpeed: 1,
playLoading: false,
isFullScreen: false,
playing: false,
initTime: null,
playerType: 'Jessibuca',
playerUrls: {
Jessibuca: ['ws_flv', 'wss_flv'],
WebRTC: ['rtc', 'rtcs'],
H265web: ['ws_flv', 'wss_flv']
},
playSpeedRange: [1, 2, 4, 6, 8, 16, 20]
}
},
computed: {
playBoxStyle() {
return this.isFullScreen ? { height: 'calc(100vh - 61px)' } : { height: '100%' }
},
showPlayTimeValue() {
return this.streamInfo === null ? '--:--:--' : moment.duration(this.playerTime, 'milliseconds').format('hh:mm:ss', {
trim: false
})
},
playTimeValue() {
return { width: this.playerTime/this.streamInfo.duration * 100 + '%' }
},
showPlayTimeTotal() {
if (this.streamInfo === null) {
return '--:--:--'
}else {
return moment.duration(this.streamInfo.duration, 'milliseconds').format('hh:mm:ss', {
trim: false
})
}
},
playTimeTotal() {
return { left: `calc(${this.playerTime/this.streamInfo.duration * 100}% - 6px)` }
},
playTimeTitleStyle() {
return { left: (this.showTimeLeft - 16) + 'px' }
},
showPlayTimeTitle() {
if (this.showTimeLeft) {
let time = this.showTimeLeft / this.$refs.timeProcess.clientWidth * this.streamInfo.duration
let realTime = this.timeLen/this.streamInfo.duration * time + this.startTime
return `${moment(time).format('mm:ss')}(${moment(realTime).format('HH:mm:ss')})`
}else {
return ''
}
},
playerLabel() {
const labels = { Jessibuca: 'Jessibuca', WebRTC: 'WebRTC', H265web: 'H265Web' }
return labels[this.playerType] || 'Jessibuca'
}
},
created() {
document.addEventListener('mousemove', this.timeProcessMousemove)
document.addEventListener('mouseup', this.timeProcessMouseup)
},
mounted() {},
destroyed() {
this.$destroy('recordVideoPlayer')
},
methods: {
timeProcessMouseup(event) {
this.isMousedown = false
},
timeProcessMousemove(event) {
},
timeProcessClick(event) {
let x = event.offsetX
let clientWidth = this.$refs.timeProcess.clientWidth
this.seekRecord(x / clientWidth * this.streamInfo.duration)
},
timeProcessMousedown(event) {
this.isMousedown = true
},
timeProcessMouseEnter(event) {
this.showTimeLeft = event.offsetX
},
timeProcessMouseMove(event) {
this.showTimeLeft = event.offsetX
},
timeProcessMouseLeave(event) {
this.showTimeLeft = null
},
sidebarControl() {
this.showSidebar = !this.showSidebar
this.showListCallback(this.showSidebar)
},
snap() {
this.$refs.recordVideoPlayer.screenshot()
},
refresh() {
this.$refs.recordVideoPlayer.destroy()
this.$refs.recordVideoPlayer.playBtnClick()
},
playLast() {
this.showLastCallback()
},
playNext() {
this.showNextCallback()
},
changePlaySpeed(speed) {
//
this.playSpeed = speed
this.$store.dispatch('cloudRecord/speed', {
mediaServerId: this.streamInfo.mediaServerId,
app: this.streamInfo.app,
stream: this.streamInfo.stream,
key: this.streamInfo.key,
speed: this.playSpeed,
schema: 'ts'
})
this.$refs.recordVideoPlayer.setPlaybackRate(this.playSpeed)
},
changePlayerType(playerType) {
if (this.playerType === playerType) {
return
}
this.playerType = playerType
if (this.streamInfo) {
this.videoUrl = this.getUrlByStreamInfo()
this.$nextTick(() => {
if (this.$refs.recordVideoPlayer) {
this.$refs.recordVideoPlayer.play(this.videoUrl)
}
})
}
},
seekBackward() {
// 退
this.seekRecord(this.playerTime - 5 * 1000)
},
seekForward() {
//
this.seekRecord(this.playerTime + 5 * 1000)
},
stopPLay() {
//
if (this.$refs.recordVideoPlayer) {
this.$refs.recordVideoPlayer.destroy()
}
this.streamInfo = null
this.playerTime = null
this.playSpeed = 1
},
pausePlay() {
//
this.$refs.recordVideoPlayer.pause()
// TODO
},
play() {
if (this.$refs.recordVideoPlayer.loaded) {
this.$refs.recordVideoPlayer.unPause()
} else {
this.playRecord()
}
},
fullScreen() {
//
if (this.isFullScreen) {
screenfull.exit()
this.isFullScreen = false
return
}
const playerWidth = this.$refs.recordVideoPlayer.playerWidth
const playerHeight = this.$refs.recordVideoPlayer.playerHeight
screenfull.request(document.getElementById('cloudRecordPlayer'))
screenfull.on('change', (event) => {
this.$refs.recordVideoPlayer.resize(playerWidth, playerHeight)
this.isFullScreen = screenfull.isFullscreen
})
this.isFullScreen = true
},
setStreamInfo(streamInfo, timeLen, startTime) {
const keys = this.playerUrls[this.playerType]
if (location.protocol === 'https:') {
this.videoUrl = streamInfo[keys[1]]
} else {
this.videoUrl = streamInfo[keys[0]]
}
console.log(location.protocol)
this.streamInfo = streamInfo
this.timeLen = timeLen
this.startTime = startTime
this.$nextTick(() => {
if (this.$refs.recordVideoPlayer) {
this.$refs.recordVideoPlayer.play(this.videoUrl)
}
})
},
getUrlByStreamInfo() {
if (!this.streamInfo) return ''
const keys = this.playerUrls[this.playerType]
if (location.protocol === 'https:') {
this.videoUrl = this.streamInfo[keys[1]]
} else {
this.videoUrl = this.streamInfo[keys[0]]
}
return this.videoUrl
},
seekRecord(playSeekValue, callback) {
this.$store.dispatch('cloudRecord/seek', {
mediaServerId: this.streamInfo.mediaServerId,
app: this.streamInfo.app,
stream: this.streamInfo.stream,
seek: playSeekValue,
schema: 'fmp4'
})
.then((data) => {
this.playerTime = playSeekValue
if (callback) {
callback(playSeekValue)
}
})
.catch((error) => {
console.log(error)
})
},
showPlayTimeChange(val) {
console.log(val)
if (Number(val)) {
this.playerTime = Number(val)
}
},
playingChange(val) {
this.playing = val
if (!val) {
this.stopPLay()
}
}
}
}
</script>
<style>
.cloud-record-playBox {
width: 100%;
background-color: #000000;
display: flex;
align-items: center;
justify-content: center;
}
.cloud-record-record-play-control {
height: 32px;
line-height: 32px;
display: inline-block;
width: fit-content;
padding: 0 10px;
-webkit-box-shadow: 0 0 10px #262626;
box-shadow: 0 0 10px #262626;
background-color: #262626;
margin: 4px 0;
}
.cloud-record-record-play-control-item {
display: inline-block;
padding: 0 10px;
color: #fff;
margin-right: 2px;
}
.cloud-record-record-play-control-item:hover {
color: #1f83e6;
}
.cloud-record-record-play-control-speed {
font-weight: bold;
color: #fff;
user-select: none;
}
.cloud-record-player-option-box {
height: 20px;
width: 100%;
display: grid;
grid-template-columns: 70px auto 70px;
background-color: rgb(0, 0, 0);
}
.cloud-record-time-process {
width: 100%;
height: 8px;
margin: 6px 0 ;
border-radius: 4px;
border: 1px solid #505050;
background-color: rgb(56, 56, 56);
cursor: pointer;
}
.cloud-record-show-time {
color: #FFFFFF;
text-align: center;
font-size: 14px;
line-height: 20px
}
.cloud-record-time-process-value {
width: 100%;
height: 6px;
background-color: rgb(162, 162, 162);
}
.cloud-record-time-process-value1::after {
content: '';
display: block;
width: 12px;
height: 12px;
background-color: rgb(192 190 190);
border-radius: 5px;
position: relative;
top: -3px;
right: -6px;
float: right;
}
.cloud-record-time-process-title {
width: fit-content;
text-align: center;
position: relative;
top: -35px;
color: rgb(217, 217, 217);
font-size: 14px;
text-shadow:
-1px -1px 0 black, /* 左上角阴影 */
1px -1px 0 black, /* 右上角阴影 */
-1px 1px 0 black, /* 左下角阴影 */
1px 1px 0 black; /* 右下角阴影 */
}
.record-play-control-player {
width: fit-content;
height: 32px;
}
</style>

View File

@ -58,7 +58,7 @@
import moment from 'moment'
import momentDurationFormatSetup from 'moment-duration-format'
import screenfull from 'screenfull'
import cloudRecordPlayer from './cloudRecordPlayer.vue'
import cloudRecordPlayer from './player.vue'
momentDurationFormatSetup(moment)
@ -284,12 +284,15 @@ export default {
this.$refs.cloudRecordPlayer.setStreamInfo(data, this.detailFiles[this.chooseFileIndex].timeLen, this.detailFiles[this.chooseFileIndex].startTime)
})
.catch((error) => {
console.log(error)
this.$message({
showClose: true,
message: error,
type: 'error'
})
})
.finally(() => {
this.playLoading = false
})
},
downloadFile(file) {
this.$store.dispatch('cloudRecord/getPlayPath', file.id)

View File

@ -21,7 +21,7 @@
<script>
import elDragDialog from '@/directive/el-drag-dialog'
import cloudRecordPlayer from './cloudRecordPlayer.vue'
import cloudRecordPlayer from './player.vue'
export default {
name: 'PlayerDialog',

View File

@ -1,9 +1,7 @@
<template>
<div class="player-tabs-wrapper" ref="playerWrapper">
<el-tabs v-if="showTab && playerCount > 1" v-model="activePlayer" type="card" :stretch="true" @tab-click="changePlayer">
<el-tab-pane label="Jessibuca" name="jessibuca"></el-tab-pane>
<el-tab-pane label="WebRTC" name="webRTC"></el-tab-pane>
<el-tab-pane label="h265web" name="h265web"></el-tab-pane>
<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-tabs>
<div class="player-video-area">
<jessibucaPlayer
@ -13,6 +11,8 @@
:has-audio="hasAudio"
:show-button="showButton"
fluent autoplay live
@playTimeChange="$emit('playTimeChange', $event)"
@playStatusChange="$emit('playStatusChange', $event)"
/>
<rtc-player
v-if="activePlayer === 'webRTC'"
@ -21,6 +21,8 @@
:has-audio="hasAudio"
:show-button="showButton"
fluent autoplay live
@playTimeChange="$emit('playTimeChange', $event)"
@playStatusChange="$emit('playStatusChange', $event)"
/>
<h265web
v-if="activePlayer === 'h265web'"
@ -29,6 +31,8 @@
:has-audio="hasAudio"
:show-button="showButton"
fluent autoplay live
@playTimeChange="$emit('playTimeChange', $event)"
@playStatusChange="$emit('playStatusChange', $event)"
/>
</div>
</div>
@ -44,38 +48,59 @@ export default {
components: { jessibucaPlayer, rtcPlayer, h265web },
props: {
hasAudio: { type: Boolean, default: false },
showButton: { type: Boolean, default: true }
showButton: { type: Boolean, default: true },
showTab: { type: Boolean, default: true }
},
data() {
return {
showTab: true,
streamInfo: null,
activePlayer: 'jessibuca',
player: { jessibuca: ['ws_flv', 'wss_flv'], webRTC: ['rtc', 'rtcs'], h265web: ['ws_flv', 'wss_flv'] }
player: { jessibuca: ['ws_flv', 'wss_flv'], webRTC: ['rtc', 'rtcs'], h265web: ['ws_flv', 'wss_flv'] },
allPlayerList: [
{ key: 'jessibuca', label: 'Jessibuca' },
{ key: 'webRTC', label: 'WebRTC' },
{ key: 'h265web', label: 'H265web' }
]
}
},
computed: {
playerList() {
return this.allPlayerList
},
playerCount() {
return Object.keys(this.player).length
return this.playerList.length
}
},
created() {
if (this.playerCount === 1) {
this.activePlayer = Object.keys(this.player)[0]
this.activePlayer = this.playerList[0].key
}
},
methods: {
getPlayerList() {
return this.playerList
},
getActivePlayer() {
return this.activePlayer
},
switchPlayer(key) {
if (this.activePlayer === key) return
this.activePlayer = key
if (this.streamInfo) {
this.play()
}
},
getUrlByStreamInfo() {
if (!this.streamInfo) return ''
const src = this.streamInfo.transcodeStream || this.streamInfo
if (location.protocol === 'https:') {
return src[this.player[this.activePlayer][1]]
return this.streamInfo[this.player[this.activePlayer][1]]
}
return src[this.player[this.activePlayer][0]]
return this.streamInfo[this.player[this.activePlayer][0]]
},
changePlayer(tab) {
this.activePlayer = tab.name
this.play()
this.$emit('player-changed', this.activePlayer)
},
setStreamInfo(streamInfo) {
this.streamInfo = streamInfo
@ -103,6 +128,30 @@ export default {
this.$refs[this.activePlayer].pause()
}
},
destroy() {
const player = this.$refs[this.activePlayer]
if (player && player.destroy) {
player.destroy()
}
},
setPlaybackRate(rate) {
const player = this.$refs[this.activePlayer]
if (player && player.setPlaybackRate) {
player.setPlaybackRate(rate)
}
},
resize(width, height) {
const player = this.$refs[this.activePlayer]
if (player && player.resize) {
player.resize(width, height)
}
},
screenshot() {
const player = this.$refs[this.activePlayer]
if (player && player.screenshot) {
return player.screenshot()
}
},
getVideoRect() {
const player = this.$refs[this.activePlayer]
return player && player.getVideoRect ? player.getVideoRect() : null

View File

@ -16,10 +16,6 @@
<i class="iconfont icon-slider-right" style="margin-right: 6px" />
<span>线性扫描</span>
</el-menu-item>
<el-menu-item index="wiper">
<i class="el-icon-umbrella" style="margin-right: 6px" />
<span>雨刷</span>
</el-menu-item>
<el-menu-item index="switch">
<i class="el-icon-s-tools" style="margin-right: 6px" />
<span>辅助开关</span>
@ -34,7 +30,6 @@
<ptzPresetConfig v-if="activeTab === 'preset'" :device-id="deviceId" :channel-device-id="channelDeviceId" />
<ptzCruiseConfig v-if="activeTab === 'cruise'" :device-id="deviceId" :channel-device-id="channelDeviceId" />
<ptzScanConfig v-if="activeTab === 'scan'" :device-id="deviceId" :channel-device-id="channelDeviceId" />
<ptzWiperConfig v-if="activeTab === 'wiper'" :device-id="deviceId" :channel-device-id="channelDeviceId" />
<ptzSwitchConfig v-if="activeTab === 'switch'" :device-id="deviceId" :channel-device-id="channelDeviceId" />
</div>
</div>
@ -47,12 +42,11 @@ import playerPtzPanel from '../common/playerPtzPanel.vue'
import ptzPresetConfig from '../common/ptzPresetConfig.vue'
import ptzCruiseConfig from '../common/ptzCruiseConfig.vue'
import ptzScanConfig from '../common/ptzScanConfig.vue'
import ptzWiperConfig from '../common/ptzWiperConfig.vue'
import ptzSwitchConfig from '../common/ptzSwitchConfig.vue'
export default {
name: 'PtzConfigPage',
components: { playerPtzPanel, ptzPresetConfig, ptzCruiseConfig, ptzScanConfig, ptzWiperConfig, ptzSwitchConfig },
components: { playerPtzPanel, ptzPresetConfig, ptzCruiseConfig, ptzScanConfig, ptzSwitchConfig },
props: {
deviceId: { type: String, default: null },
channelDeviceId: { type: String, default: null }

View File

@ -44,7 +44,7 @@ export default {
this.hasAudio = data.hasAudio
this.$nextTick(() => {
if (this.$refs.playerTabs) {
this.$refs.playerTabs.setStreamInfo(data)
this.$refs.playerTabs.setStreamInfo(data.transcodeStream || data)
}
})
})

View File

@ -1,28 +1,59 @@
<template>
<div style="height: 100%; display: flex; flex-direction: column;">
<el-form size="small" inline style="margin-bottom: 12px; padding: 16px 8px; border: 1px solid #e6e6e6; border-radius: 4px;">
<el-form-item label="扫描组号" style="margin-bottom: 0;">
<el-input-number v-model="scanId" :min="1" :max="255" controls-position="right" style="width: 140px" />
</el-form-item>
</el-form>
<div style="margin-bottom: 8px;">
<el-button size="small" :loading="leftLoading" :disabled="leftLoading" @click="setLeft">设置左边界</el-button>
<el-button size="small" :loading="rightLoading" :disabled="rightLoading" @click="setRight">设置右边界</el-button>
<div id="ptzScanConfig" style="height: 100%; display: flex; flex-direction: column;">
<div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 6px;">
<div>
<el-button type="primary" :loading="adding" :disabled="adding" @click="addLineScan">添加线扫</el-button>
<el-button @click="clearAll">清空</el-button>
</div>
<el-button icon="el-icon-refresh-right" circle />
</div>
<el-form v-if="showSpeedInput" size="mini" inline style="margin-bottom: 8px;">
<el-form-item label="扫描速度" style="margin-bottom: 0;">
<el-input-number v-model="scanSpeed" :min="1" :max="255" controls-position="right" style="width: 120px" />
</el-form-item>
<el-form-item style="margin-bottom: 0;">
<el-button type="primary" @click="setSpeed">确定</el-button>
<el-button @click="cancelSpeed">取消</el-button>
</el-form-item>
</el-form>
<el-button v-else size="small" style="margin-bottom: 8px;" @click="showSpeedInput = true">设置扫描速度</el-button>
<div style="margin-top: 8px;">
<el-button size="small" type="primary" :loading="starting" :disabled="starting" @click="startScan">开始自动扫描</el-button>
<el-button size="small" :loading="stopping" :disabled="stopping" @click="stopScan">停止自动扫描</el-button>
<div v-if="scanAreas.length > 0" style="flex: 1; overflow: auto;">
<el-table :data="scanAreas" max-height="100%" stripe border highlight-current-row height="100%">
<el-table-column label="序号" min-width="50">
<template v-slot="{ row }">{{ row.index }}</template>
</el-table-column>
<el-table-column label="名称" min-width="80">
<template v-slot="{ row }">{{ row.name }}</template>
</el-table-column>
<el-table-column label="左边界" min-width="90">
<template v-slot="{ row }">
<el-button type="text"
:style="{ color: row.leftBoundary ? '#67C23A' : '#409EFF' }"
:loading="boundaryLoading.index === row.index && boundaryLoading.side === 'Left'"
:disabled="operatingId !== null"
@click="setBoundary(row, 'Left')">
{{ row.leftBoundary ? '重新保存' : '待保存' }}
</el-button>
</template>
</el-table-column>
<el-table-column label="右边界" min-width="90">
<template v-slot="{ row }">
<el-button type="text"
:style="{ color: row.rightBoundary ? '#67C23A' : '#409EFF' }"
:loading="boundaryLoading.index === row.index && boundaryLoading.side === 'Right'"
:disabled="operatingId !== null"
@click="setBoundary(row, 'Right')">
{{ row.rightBoundary ? '重新保存' : '待保存' }}
</el-button>
</template>
</el-table-column>
<el-table-column label="速度" min-width="90">
<template v-slot="{ row }">
<el-select v-model="row.speed" :disabled="speedSaving === row.index" @change="onSpeedChange(row)">
<el-option v-for="s in 8" :key="s" :label="s" :value="s" />
</el-select>
</template>
</el-table-column>
<el-table-column label="操作" min-width="120">
<template v-slot="{ row, $index }">
<el-button v-if="$index === cruisingScanIndex" type="text" style="color: #F56C6C" :loading="operatingId === row.index" :disabled="operatingId !== null" @click="stopScan(row)">停用</el-button>
<el-button v-else type="text" style="color: #409EFF" :disabled="operatingId !== null" :loading="operatingId === row.index" @click="startScan(row, $index)">启用</el-button>
<el-button type="text" style="color: #F56C6C" :disabled="operatingId !== null" @click="deleteScan(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
</div>
<div v-else style="color: #909399; font-size: 12px; margin-bottom: 8px;">暂无线扫区域</div>
</div>
</template>
@ -35,72 +66,112 @@ export default {
},
data() {
return {
scanId: 1,
showSpeedInput: false,
scanSpeed: 5,
leftLoading: false,
rightLoading: false,
starting: false,
stopping: false
scanAreas: [],
cruisingScanIndex: null,
operatingId: null,
adding: false,
boundaryLoading: { index: null, side: null },
speedSaving: null
}
},
methods: {
setLeft() {
this.leftLoading = true
this.$store.dispatch('frontEnd/setLeftForScan', [this.deviceId, this.channelDeviceId, this.scanId])
getNextAvailableIndex() {
const used = new Set(this.scanAreas.filter(a => a.name && a.name.trim()).map(a => a.index))
for (let i = 0; i <= 255; i++) {
if (!used.has(i)) return i
}
return 0
},
addLineScan() {
const nextIndex = this.getNextAvailableIndex()
const name = '线扫' + nextIndex
this.adding = true
this.scanAreas.push({
index: nextIndex,
name: name,
leftBoundary: false,
rightBoundary: false,
speed: 5
})
this.$nextTick(() => { this.adding = false })
},
setBoundary(row, boundary) {
this.boundaryLoading = { index: row.index, side: boundary }
const action = boundary === 'Left' ? 'setLeftForScan' : 'setRightForScan'
this.$store.dispatch('frontEnd/' + action, [this.deviceId, this.channelDeviceId, row.index])
.then(() => {
this.$message({ showClose: true, message: '左边界设置成功', type: 'success' })
}).catch(error => {
this.$message({ showClose: true, message: error, type: 'error' })
this.$message({ showClose: true, message: (boundary === 'Left' ? '左' : '右') + '边界设置成功', type: 'success' })
if (boundary === 'Left') {
row.leftBoundary = true
} else {
row.rightBoundary = true
}
}).catch(() => {
this.$message({ showClose: true, message: '边界设置失败', type: 'error' })
}).finally(() => {
this.leftLoading = false
this.boundaryLoading = { index: null, side: null }
})
},
setRight() {
this.rightLoading = true
this.$store.dispatch('frontEnd/setRightForScan', [this.deviceId, this.channelDeviceId, this.scanId])
onSpeedChange(row) {
this.speedSaving = row.index
this.$store.dispatch('frontEnd/setSpeedForScan', [this.deviceId, this.channelDeviceId, row.index, row.speed])
.then(() => {
this.$message({ showClose: true, message: '右边界设置成功', type: 'success' })
}).catch(error => {
this.$message({ showClose: true, message: error, type: 'error' })
this.$message({ showClose: true, message: '速度已保存', type: 'success' })
}).catch(() => {
this.$message({ showClose: true, message: '速度保存失败', type: 'error' })
}).finally(() => {
this.rightLoading = false
this.speedSaving = null
})
},
setSpeed() {
this.$store.dispatch('frontEnd/setSpeedForScan', [this.deviceId, this.channelDeviceId, this.scanId, this.scanSpeed])
startScan(row, index) {
this.operatingId = row.index
this.$store.dispatch('frontEnd/startScan', [this.deviceId, this.channelDeviceId, row.index])
.then(() => {
this.showSpeedInput = false
this.$message({ showClose: true, message: '速度设置成功', type: 'success' })
}).catch(error => {
this.$message({ showClose: true, message: error, type: 'error' })
})
},
cancelSpeed() {
this.showSpeedInput = false
this.scanSpeed = 5
},
startScan() {
this.starting = true
this.$store.dispatch('frontEnd/startScan', [this.deviceId, this.channelDeviceId, this.scanId])
.then(() => {
this.$message({ showClose: true, message: '扫描启动成功', type: 'success' })
}).catch(error => {
this.$message({ showClose: true, message: error, type: 'error' })
this.$message({ showClose: true, message: '启用成功', type: 'success' })
this.cruisingScanIndex = index
}).catch(() => {
this.$message({ showClose: true, message: '启用失败', type: 'error' })
}).finally(() => {
this.starting = false
this.operatingId = null
})
},
stopScan() {
this.stopping = true
this.$store.dispatch('frontEnd/stopScan', [this.deviceId, this.channelDeviceId, this.scanId])
stopScan(row) {
this.operatingId = row.index
this.$store.dispatch('frontEnd/stopScan', [this.deviceId, this.channelDeviceId, row.index])
.then(() => {
this.$message({ showClose: true, message: '扫描停止成功', type: 'success' })
}).catch(error => {
this.$message({ showClose: true, message: error, type: 'error' })
this.$message({ showClose: true, message: '停用成功', type: 'success' })
this.cruisingScanIndex = null
}).catch(() => {
this.$message({ showClose: true, message: '停用失败', type: 'error' })
}).finally(() => {
this.stopping = false
this.operatingId = null
})
},
deleteScan(row) {
this.$confirm('确定删除线扫 ' + row.index + '?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
const idx = this.scanAreas.indexOf(row)
if (idx !== -1) this.scanAreas.splice(idx, 1)
if (this.cruisingScanIndex !== null && this.scanAreas[this.cruisingScanIndex] === undefined) {
this.cruisingScanIndex = null
}
this.$message({ showClose: true, message: '删除成功(仅本地列表,设备端配置需手动清除)', type: 'success' })
}).catch(() => {})
},
clearAll() {
if (this.scanAreas.length === 0) return
this.$confirm('确定清空所有线扫区域?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
this.scanAreas = []
this.cruisingScanIndex = null
this.$message({ showClose: true, message: '清空成功(仅本地列表,设备端配置需手动清除)', type: 'success' })
}).catch(() => {})
}
}
}

View File

@ -1,12 +1,18 @@
<template>
<div>
<el-form size="mini" inline>
<el-form inline label-width="120px" size="small">
<el-form-item label="开关编号" style="margin-bottom: 0;">
<el-input-number v-model="switchId" :min="1" :max="255" controls-position="right" style="width: 140px" />
</el-form-item>
<el-form-item style="margin-bottom: 0;">
<el-button size="small" :loading="loading" :disabled="loading" @click="control('on')">开启</el-button>
<el-button size="small" :loading="loading" :disabled="loading" @click="control('off')">关闭</el-button>
<el-button type="primary" :loading="loading" :disabled="loading" @click="control('on')">开启</el-button>
<el-button :loading="loading" :disabled="loading" @click="control('off')">关闭</el-button>
</el-form-item>
<el-divider />
<el-form-item style="margin-bottom: 0;" label="雨刷">
<el-button type="primary" :loading="wiperLoading" :disabled="wiperLoading" @click="wiperControl('on')">开启</el-button>
<el-button :loading="wiperLoading" :disabled="wiperLoading" @click="wiperControl('off')">关闭</el-button>
</el-form-item>
</el-form>
</div>
@ -22,10 +28,22 @@ export default {
data() {
return {
switchId: 1,
loading: false
loading: false,
wiperLoading: false
}
},
methods: {
wiperControl(command) {
this.wiperLoading = true
this.$store.dispatch('frontEnd/wiper', [this.deviceId, this.channelDeviceId, command])
.then(() => {
this.$message({ showClose: true, message: command === 'on' ? '雨刷已开启' : '雨刷已关闭', type: 'success' })
}).catch(error => {
this.$message({ showClose: true, message: error, type: 'error' })
}).finally(() => {
this.wiperLoading = false
})
},
control(command) {
this.loading = true
this.$store.dispatch('frontEnd/auxiliary', [this.deviceId, this.channelDeviceId, command, this.switchId])

View File

@ -38,55 +38,54 @@
</el-tab-pane>
<el-tab-pane label="实时视频" name="media">
<div v-if="tabActiveName === 'media'" class="media-info-content">
<div class="media-row">
<span class="media-label">播放地址</span>
<el-input v-model="playerUrlInfo.playerUrl" :disabled="true">
<template slot="append">
<i class="cpoy-btn el-icon-document-copy" title="点击拷贝" style="cursor: pointer" @click="copyUrl(playerUrlInfo.playerUrl)" />
</template>
</el-input>
</div>
<div class="media-row">
<span class="media-label">iframe</span>
<el-input v-model="sharedIframe" :disabled="true">
<template slot="append">
<i class="cpoy-btn el-icon-document-copy" title="点击拷贝" style="cursor: pointer" @click="copyUrl(sharedIframe)" />
</template>
</el-input>
</div>
<div class="media-row">
<span class="media-label">资源地址</span>
<el-input v-model="playerUrlInfo.playUrl" :disabled="true">
<el-button slot="append" icon="el-icon-document-copy" title="点击拷贝" style="cursor: pointer" @click="copyUrl(playerUrlInfo.playUrl)" />
<el-dropdown v-if="streamInfo" slot="prepend" trigger="click" @command="copyUrl">
<el-button>更多地址<i class="el-icon-arrow-down el-icon--right" /></el-button>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item v-if="streamInfo.flv" :command="streamInfo.flv"><el-tag>FLV:</el-tag><span>{{ streamInfo.flv }}</span></el-dropdown-item>
<el-dropdown-item v-if="streamInfo.https_flv" :command="streamInfo.https_flv"><el-tag>FLV(https):</el-tag><span>{{ streamInfo.https_flv }}</span></el-dropdown-item>
<el-dropdown-item v-if="streamInfo.ws_flv" :command="streamInfo.ws_flv"><el-tag>FLV(ws):</el-tag><span>{{ streamInfo.ws_flv }}</span></el-dropdown-item>
<el-dropdown-item v-if="streamInfo.wss_flv" :command="streamInfo.wss_flv"><el-tag>FLV(wss):</el-tag><span>{{ streamInfo.wss_flv }}</span></el-dropdown-item>
<el-dropdown-item v-if="streamInfo.fmp4" :command="streamInfo.fmp4"><el-tag>FMP4:</el-tag><span>{{ streamInfo.fmp4 }}</span></el-dropdown-item>
<el-dropdown-item v-if="streamInfo.https_fmp4" :command="streamInfo.https_fmp4"><el-tag>FMP4(https):</el-tag><span>{{ streamInfo.https_fmp4 }}</span></el-dropdown-item>
<el-dropdown-item v-if="streamInfo.ws_fmp4" :command="streamInfo.ws_fmp4"><el-tag>FMP4(ws):</el-tag><span>{{ streamInfo.ws_fmp4 }}</span></el-dropdown-item>
<el-dropdown-item v-if="streamInfo.wss_fmp4" :command="streamInfo.wss_fmp4"><el-tag>FMP4(wss):</el-tag><span>{{ streamInfo.wss_fmp4 }}</span></el-dropdown-item>
<el-dropdown-item v-if="streamInfo.hls" :command="streamInfo.hls"><el-tag>HLS:</el-tag><span>{{ streamInfo.hls }}</span></el-dropdown-item>
<el-dropdown-item v-if="streamInfo.https_hls" :command="streamInfo.https_hls"><el-tag>HLS(https):</el-tag><span>{{ streamInfo.https_hls }}</span></el-dropdown-item>
<el-dropdown-item v-if="streamInfo.ws_hls" :command="streamInfo.ws_hls"><el-tag>HLS(ws):</el-tag><span>{{ streamInfo.ws_hls }}</span></el-dropdown-item>
<el-dropdown-item v-if="streamInfo.wss_hls" :command="streamInfo.wss_hls"><el-tag>HLS(wss):</el-tag><span>{{ streamInfo.wss_hls }}</span></el-dropdown-item>
<el-dropdown-item v-if="streamInfo.ts" :command="streamInfo.ts"><el-tag>TS:</el-tag><span>{{ streamInfo.ts }}</span></el-dropdown-item>
<el-dropdown-item v-if="streamInfo.https_ts" :command="streamInfo.https_ts"><el-tag>TS(https):</el-tag><span>{{ streamInfo.https_ts }}</span></el-dropdown-item>
<el-dropdown-item v-if="streamInfo.ws_ts" :command="streamInfo.ws_ts"><el-tag>TS(ws):</el-tag><span>{{ streamInfo.ws_ts }}</span></el-dropdown-item>
<el-dropdown-item v-if="streamInfo.wss_ts" :command="streamInfo.wss_ts"><el-tag>TS(wss):</el-tag><span>{{ streamInfo.wss_ts }}</span></el-dropdown-item>
<el-dropdown-item v-if="streamInfo.rtc" :command="streamInfo.rtc"><el-tag>RTC:</el-tag><span>{{ streamInfo.rtc }}</span></el-dropdown-item>
<el-dropdown-item v-if="streamInfo.rtcs" :command="streamInfo.rtcs"><el-tag>RTCS:</el-tag><span>{{ streamInfo.rtcs }}</span></el-dropdown-item>
<el-dropdown-item v-if="streamInfo.rtmp" :command="streamInfo.rtmp"><el-tag>RTMP:</el-tag><span>{{ streamInfo.rtmp }}</span></el-dropdown-item>
<el-dropdown-item v-if="streamInfo.rtmps" :command="streamInfo.rtmps"><el-tag>RTMPS:</el-tag><span>{{ streamInfo.rtmps }}</span></el-dropdown-item>
<el-dropdown-item v-if="streamInfo.rtsp" :command="streamInfo.rtsp"><el-tag>RTSP:</el-tag><span>{{ streamInfo.rtsp }}</span></el-dropdown-item>
<el-dropdown-item v-if="streamInfo.rtsps" :command="streamInfo.rtsps"><el-tag>RTSPS:</el-tag><span>{{ streamInfo.rtsps }}</span></el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</el-input>
</div>
<el-form label-width="90px" size="small">
<el-form-item label="播放地址">
<el-input v-model="playerUrlInfo.playerUrl" :disabled="true">
<template slot="append">
<i class="cpoy-btn el-icon-document-copy" title="点击拷贝" style="cursor: pointer" @click="copyUrl(playerUrlInfo.playerUrl)" />
</template>
</el-input>
</el-form-item>
<el-form-item label="iframe">
<el-input v-model="sharedIframe" :disabled="true" >
<template slot="append">
<i class="cpoy-btn el-icon-document-copy" title="点击拷贝" style="cursor: pointer" @click="copyUrl(sharedIframe)" />
</template>
</el-input>
</el-form-item>
<el-form-item label="资源地址">
<el-input v-model="playerUrlInfo.playUrl" :disabled="true" size="mini">
<el-button slot="append" icon="el-icon-document-copy" title="点击拷贝" style="cursor: pointer" @click="copyUrl(playerUrlInfo.playUrl)" />
<el-dropdown v-if="streamInfo" slot="prepend" trigger="click" @command="copyUrl">
<el-button>更多地址<i class="el-icon-arrow-down el-icon--right" size="mini"/></el-button>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item v-if="streamInfo.flv" :command="streamInfo.flv"><el-tag>FLV:</el-tag><span>{{ streamInfo.flv }}</span></el-dropdown-item>
<el-dropdown-item v-if="streamInfo.https_flv" :command="streamInfo.https_flv"><el-tag>FLV(https):</el-tag><span>{{ streamInfo.https_flv }}</span></el-dropdown-item>
<el-dropdown-item v-if="streamInfo.ws_flv" :command="streamInfo.ws_flv"><el-tag>FLV(ws):</el-tag><span>{{ streamInfo.ws_flv }}</span></el-dropdown-item>
<el-dropdown-item v-if="streamInfo.wss_flv" :command="streamInfo.wss_flv"><el-tag>FLV(wss):</el-tag><span>{{ streamInfo.wss_flv }}</span></el-dropdown-item>
<el-dropdown-item v-if="streamInfo.fmp4" :command="streamInfo.fmp4"><el-tag>FMP4:</el-tag><span>{{ streamInfo.fmp4 }}</span></el-dropdown-item>
<el-dropdown-item v-if="streamInfo.https_fmp4" :command="streamInfo.https_fmp4"><el-tag>FMP4(https):</el-tag><span>{{ streamInfo.https_fmp4 }}</span></el-dropdown-item>
<el-dropdown-item v-if="streamInfo.ws_fmp4" :command="streamInfo.ws_fmp4"><el-tag>FMP4(ws):</el-tag><span>{{ streamInfo.ws_fmp4 }}</span></el-dropdown-item>
<el-dropdown-item v-if="streamInfo.wss_fmp4" :command="streamInfo.wss_fmp4"><el-tag>FMP4(wss):</el-tag><span>{{ streamInfo.wss_fmp4 }}</span></el-dropdown-item>
<el-dropdown-item v-if="streamInfo.hls" :command="streamInfo.hls"><el-tag>HLS:</el-tag><span>{{ streamInfo.hls }}</span></el-dropdown-item>
<el-dropdown-item v-if="streamInfo.https_hls" :command="streamInfo.https_hls"><el-tag>HLS(https):</el-tag><span>{{ streamInfo.https_hls }}</span></el-dropdown-item>
<el-dropdown-item v-if="streamInfo.ws_hls" :command="streamInfo.ws_hls"><el-tag>HLS(ws):</el-tag><span>{{ streamInfo.ws_hls }}</span></el-dropdown-item>
<el-dropdown-item v-if="streamInfo.wss_hls" :command="streamInfo.wss_hls"><el-tag>HLS(wss):</el-tag><span>{{ streamInfo.wss_hls }}</span></el-dropdown-item>
<el-dropdown-item v-if="streamInfo.ts" :command="streamInfo.ts"><el-tag>TS:</el-tag><span>{{ streamInfo.ts }}</span></el-dropdown-item>
<el-dropdown-item v-if="streamInfo.https_ts" :command="streamInfo.https_ts"><el-tag>TS(https):</el-tag><span>{{ streamInfo.https_ts }}</span></el-dropdown-item>
<el-dropdown-item v-if="streamInfo.ws_ts" :command="streamInfo.ws_ts"><el-tag>TS(ws):</el-tag><span>{{ streamInfo.ws_ts }}</span></el-dropdown-item>
<el-dropdown-item v-if="streamInfo.wss_ts" :command="streamInfo.wss_ts"><el-tag>TS(wss):</el-tag><span>{{ streamInfo.wss_ts }}</span></el-dropdown-item>
<el-dropdown-item v-if="streamInfo.rtc" :command="streamInfo.rtc"><el-tag>RTC:</el-tag><span>{{ streamInfo.rtc }}</span></el-dropdown-item>
<el-dropdown-item v-if="streamInfo.rtcs" :command="streamInfo.rtcs"><el-tag>RTCS:</el-tag><span>{{ streamInfo.rtcs }}</span></el-dropdown-item>
<el-dropdown-item v-if="streamInfo.rtmp" :command="streamInfo.rtmp"><el-tag>RTMP:</el-tag><span>{{ streamInfo.rtmp }}</span></el-dropdown-item>
<el-dropdown-item v-if="streamInfo.rtmps" :command="streamInfo.rtmps"><el-tag>RTMPS:</el-tag><span>{{ streamInfo.rtmps }}</span></el-dropdown-item>
<el-dropdown-item v-if="streamInfo.rtsp" :command="streamInfo.rtsp"><el-tag>RTSP:</el-tag><span>{{ streamInfo.rtsp }}</span></el-dropdown-item>
<el-dropdown-item v-if="streamInfo.rtsps" :command="streamInfo.rtsps"><el-tag>RTSPS:</el-tag><span>{{ streamInfo.rtsps }}</span></el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</el-input>
</el-form-item>
</el-form>
</div>
</el-tab-pane>
<el-tab-pane label="编码信息" name="codec">
@ -171,7 +170,7 @@ export default {
this.showVideoDialog = true
this.$nextTick(() => {
if (this.$refs.playerTabs) {
this.$refs.playerTabs.setStreamInfo(streamInfo)
this.$refs.playerTabs.setStreamInfo(streamInfo.transcodeStream || streamInfo)
}
})
},
@ -217,7 +216,7 @@ export default {
.player-side { flex: 3; min-width: 0; }
.player-container { width: 100%; }
.control-side { flex: 2; min-width: 340px; display: flex; flex-direction: column; }
.control-tabs { flex: 1; display: flex; flex-direction: column; min-height: 180px}
.control-tabs { flex: 1; display: flex; flex-direction: column; min-height: 220px}
.control-tabs .el-tabs__content { flex: 1; overflow: auto; }
.media-info-content { overflow: auto; }
.media-row { display: flex; margin-bottom: 0.5rem; height: 2.5rem; }

View File

@ -114,7 +114,7 @@ export default {
this.showPlayer = true
this.$nextTick(() => {
if (this.$refs.playerTabs) {
this.$refs.playerTabs.setStreamInfo(data)
this.$refs.playerTabs.setStreamInfo(data.transcodeStream || data)
}
})
})