云台联调

This commit is contained in:
bb_pan 2025-01-23 17:15:09 +08:00
parent 117aa3c224
commit 6f2f047ede
6 changed files with 343 additions and 77 deletions

View File

@ -44,6 +44,7 @@
"echarts": "5.4.0",
"element-ui": "2.15.14",
"file-saver": "2.0.5",
"flv.js": "^1.6.2",
"fuse.js": "6.4.3",
"highlight.js": "9.18.5",
"js-beautify": "1.13.0",

View File

@ -1,6 +1,6 @@
module.exports = {
printWidth: 120,
tabWidth: 4,
tabWidth: 2,
semi: false,
vueIndentScriptAndStyle: false,
singleQuote: true,

View File

@ -64,3 +64,18 @@ export const delPlatformConfiguration = (id) => {
export const changeStatus = (data) => {
return request.post('/smart-site/platform_configuration/changeStatus', data);
};
// 获取清新token
export const getQxToken = (params) => {
return request.get('/smart-site/video_equipment/getQxToken', { params });
};
// 获取清新视频设备列表
export const getVideoEquipment = (params) => {
return request.get('/smart-site/video_equipment/getVideoEquipment', { params });
};
// 更新水印
export const updateWaterMark = (data) => {
return request.post('/smart-site/video_equipment/updateWaterMark', data);
};

View File

@ -230,7 +230,7 @@ export default {
wsUrl: [{ required: true, message: '请输入视频连接地址-公网', trigger: 'blur' }],
epid: [
{ required: true, message: '请输入epid', trigger: 'blur' },
{ pattern: /^[0-9]*$/, message: '只能输入正整数', trigger: 'blur' },
{ pattern: /^[A-Za-z0-9]+$/, message: '只能输入字和字母', trigger: 'blur' },
],
bfix: [{ required: true, message: '请输入bfix', trigger: 'blur' }],
},

View File

@ -190,7 +190,7 @@
</el-dialog>
<el-dialog title="调试" :visible.sync="videoDebug" width="70%" height="80%">
<MonitoringAndDebug />
<MonitoringAndDebug v-if="videoDebug" :row="this.row"/>
</el-dialog>
</div>
</template>
@ -249,6 +249,7 @@ export default {
qrData: {
devName: '',
},
row: {}, //
dialogTitle: '新增',
isAdd: true,
dialogVisible: false,
@ -418,6 +419,7 @@ export default {
//
handleMonitoringAndDebug(row) {
console.log('监控调试', row)
this.row = row
this.videoDebug = true
},
//

View File

@ -2,16 +2,18 @@
<div class="monitor-page">
<!-- 左侧视频监控区 -->
<div class="video-section">
<div class="video-title">视频播放</div>
<div class="video-title">
视频播放
<el-select v-model="ballIndex" placeholder="请选择" style="width: 80px">
<el-option v-for="item in ballIndexOpts" :key="item.value" :label="item.label" :value="item.value">
</el-option>
</el-select>
</div>
<div class="video-container">
<div class="video-overlay">
<div class="timestamp">{{ currentTime }}</div>
<div class="coordinates">
<div>{{ coordinates.line1 }}</div>
<div>{{ coordinates.line2 }}</div>
</div>
<div class="signal-info">电池: {{ batteryInfo }}</div>
</div>
<video class="cell-player-1" ref="videosmallone" preload="auto" muted type="rtmp/flv" :id="videoId">
<source src="" />
</video>
<canvas ref="canvas" style="display: none"></canvas>
</div>
</div>
@ -23,34 +25,30 @@
<!-- 方向控制 -->
<div class="direction-control">
<div class="direction-pad">
<el-button icon="el-icon-arrow-up" circle @click="control('up')">
</el-button>
<el-button icon="el-icon-arrow-up" circle @click="control('up')"> </el-button>
<div class="horizontal-buttons">
<el-button icon="el-icon-arrow-left" circle @click="control('left')">
</el-button>
<el-button icon="el-icon-arrow-right" circle @click="control('right')">
</el-button>
<el-button icon="el-icon-arrow-left" circle @click="control('left')"> </el-button>
<el-button icon="el-icon-arrow-right" circle @click="control('right')"> </el-button>
</div>
<el-button icon="el-icon-arrow-down" circle @click="control('down')">
</el-button>
<el-button icon="el-icon-arrow-down" circle @click="control('down')"> </el-button>
</div>
<!-- 缩放控制 -->
<div class="zoom-controls">
<div class="zoom-column">
<el-button size="small" @click="control('zoomIn')">
<el-button size="small" @click="control('focusIn')">
<i class="el-icon-plus"></i>
</el-button>
<el-button size="small" @click="control('zoomOut')">
<el-button size="small" @click="control('focusOut')">
<i class="el-icon-minus"></i>
</el-button>
<div class="column-label">聚焦</div>
</div>
<div class="zoom-column">
<el-button size="small" @click="control('focusIn')">
<el-button size="small" @click="control('zoomIn')">
<i class="el-icon-plus"></i>
</el-button>
<el-button size="small" @click="control('focusOut')">
<el-button size="small" @click="control('zoomOut')">
<i class="el-icon-minus"></i>
</el-button>
<div class="column-label">缩放</div>
@ -72,9 +70,10 @@
<div class="view-row">
<div class="view-item">
<i class="el-icon-video-camera"></i>
<span>本地录像</span>
<span v-if="!isRecording" @click="startRecording">本地录像</span>
<span v-else @click="stopRecording">停止录像</span>
</div>
<div class="view-item">
<div class="view-item" @click="takeSnapshot">
<i class="el-icon-picture"></i>
<span>本地抓拍</span>
</div>
@ -104,15 +103,9 @@
<!-- 水印设置 -->
<div class="watermark-section">
<div class="watermark-title">应用水印<i class="el-icon-s-operation"></i></div>
<el-input
v-model="watermark.line1"
placeholder="设置第一行视频水印文字"
></el-input>
<el-input
v-model="watermark.line2"
placeholder="设置第二行视频水印文字"
></el-input>
<div class="watermark-title" @click="updateWaterMark">应用水印<i class="el-icon-s-operation"></i></div>
<el-input v-model="watermark.oneText" placeholder="设置第一行视频水印文字"></el-input>
<el-input v-model="watermark.twoText" placeholder="设置第二行视频水印文字"></el-input>
</div>
<div class="control-title">快捷键设置</div>
@ -122,13 +115,13 @@
<div class="shortcut-grid">
<div class="shortcut-row" v-for="(row, rowIndex) in shortcutSettings" :key="rowIndex">
<div class="shortcut-item" v-for="(item, colIndex) in row" :key="colIndex">
<span>{{item.label}}</span>
<span>{{ item.label }}</span>
<input
v-model="item.key"
@keydown="handleKeyPress($event, rowIndex, colIndex)"
:maxlength="1"
class="key"
></input>
/>
</div>
</div>
</div>
@ -138,66 +131,123 @@
</template>
<script>
import flvjs from 'flv.js'
import axios from 'axios'
import { getQxToken, getVideoEquipment, updateWaterMark } from '@/api/deviceManagement'
export default {
props: {
row: {
type: Object,
default: () => ({}),
},
},
name: 'VideoMonitor',
data() {
return {
currentTime: '2023-01-07 13:19:38',
coordinates: {
line1: '116.13263L 000KPH',
line2: '033.50462N 000D/G'
},
batteryInfo: '64% 4G1:96% 4G2:96%',
qxToken: '',
qxInfo: {},
videoId: '',
player: null,
ballIndex: 0,
ballIndexOpts: [],
isDown: false,
watermark: {
line1: '',
line2: ''
oneText: '',
twoText: '',
},
//
shortcutSettings: [
[
{ label: '上', key: 'W', action: 'up' },
{ label: '放大', key: 'Q', action: 'zoomIn' }
{ label: '放大', key: 'Q', action: 'zoomIn' },
],
[
{ label: '下', key: 'S', action: 'down' },
{ label: '缩小', key: 'E', action: 'zoomOut' }
{ label: '缩小', key: 'E', action: 'zoomOut' },
],
[
{ label: '左', key: 'A', action: 'left' },
{ label: '远焦', key: 'R', action: 'focusIn' }
{ label: '远焦', key: 'R', action: 'focusIn' },
],
[
{ label: '右', key: 'D', action: 'right' },
{ label: '近焦', key: 'T', action: 'focusOut' }
{ label: '近焦', key: 'T', action: 'focusOut' },
],
[
{ label: '抓拍', key: 'F', action: 'capture' },
{ label: '录像', key: 'G', action: 'record' }
]
]
{ label: '录像', key: 'G', action: 'record' },
],
],
mediaRecorder: null,
recordedChunks: [],
isRecording: false,
}
},
methods: {
control(action) {
console.log('Control action:', action)
this.isDown = true
switch (action) {
case 'up':
console.log('Up')
this.requestPost(
'PTZ/C_PTZ_Turn?token=' + this.qxToken,
{
puid: this.row.puId,
idx: this.ballIndex,
motion: 'up',
},
async (rv) => {},
)
this.handleStop()
break
case 'down':
console.log('Down')
this.requestPost(
'PTZ/C_PTZ_Turn?token=' + this.qxToken,
{
puid: this.row.puId,
idx: this.ballIndex,
motion: 'down',
},
async (rv) => {},
)
this.handleStop()
break
case 'left':
console.log('Left')
this.requestPost(
'PTZ/C_PTZ_Turn?token=' + this.qxToken,
{
puid: this.row.puId,
idx: this.ballIndex,
motion: 'left',
},
async (rv) => {},
)
this.handleStop()
break
case 'right':
console.log('Right')
this.requestPost(
'PTZ/C_PTZ_Turn?token=' + this.qxToken,
{
puid: this.row.puId,
idx: this.ballIndex,
motion: 'right',
},
async (rv) => {},
)
this.handleStop()
break
case 'zoomIn':
console.log('Zoom In')
this.handleZoomIn()
break
case 'zoomOut':
console.log('Zoom Out')
this.handleZoomOut()
break
case 'focusIn':
console.log('Focus In')
@ -219,7 +269,6 @@ export default {
break
default:
console.log('Unknown action:', action)
}
},
toggleCloud() {
@ -231,51 +280,250 @@ export default {
//
handleKeyPress(event, rowIndex, colIndex) {
event.preventDefault();
const key = event.key.toUpperCase();
event.preventDefault()
const key = event.key.toUpperCase()
//
if (/^[A-Z]$/.test(key)) {
//
const isDuplicate = this.shortcutSettings.some(row =>
row.some(item => item.key === key && !(item.key === this.shortcutSettings[rowIndex][colIndex].key))
);
const isDuplicate = this.shortcutSettings.some((row) =>
row.some((item) => item.key === key && !(item.key === this.shortcutSettings[rowIndex][colIndex].key)),
)
if (isDuplicate) {
this.$message.error('该键位已被使用,请选择其他键位');
return;
this.$message.error('该键位已被使用,请选择其他键位')
return
}
//
const oldKey = this.shortcutSettings[rowIndex][colIndex].key;
const oldKey = this.shortcutSettings[rowIndex][colIndex].key
//
this.shortcutSettings[rowIndex][colIndex].key = key;
this.shortcutSettings[rowIndex][colIndex].key = key
}
}
},
// token
async getQxToken() {
try {
const res = await getQxToken()
console.log('qxToken:', res)
this.qxToken = res.msg
} catch (error) {
console.log('getQxToken error:', error)
}
},
//
async getQxInfo() {
try {
const res = await getVideoEquipment()
console.log('qxInfo:', res)
this.qxInfo = res.data
} catch (error) {
console.log('getQxInfo error:', error)
}
},
init() {
setTimeout(() => {
//使mountedDOM
var videoElement = this.$refs.videosmallone // htmlvideo
if (videoElement && flvjs.isSupported()) {
// playerTCP
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:
this.qxInfo.videoUrl +
'stream.flv?puid=' +
this.row.puId +
'&idx=' +
this.ballIndex +
'&stream=0&token=' +
this.qxToken, //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() //
//!!!!!!loadsettimeout this.player.play()
setTimeout(() => {
this.player.play()
}, 500) // Delay to ensure the player is ready
}
}, 1000)
},
requestPost(url, data, callback) {
console.log('requestPost----------------', JSON.stringify(data))
url = this.qxInfo.videoUrl + url
axios.post(url, data).then((rv) => {
// console.log(rv)
// console.log(JSON.stringify(rv))
callback(rv)
})
},
handleStop() {
console.log('handleStop')
setTimeout(() => {
this.requestPost(
'PTZ/C_PTZ_Turn?token=' + this.qxToken,
{
puid: this.row.puId,
idx: this.ballIndex,
motion: 'stop',
},
async (rv) => {},
)
this.isDown = false
}, 700)
},
//
handleStopZoom() {
const params = {
puid: this.row.puId,
idx: this.ballIndex,
}
setTimeout(() => {
this.requestPost('PTZ/C_PTZ_StopPictureZoom?token=' + this.qxToken, params, (rv) => {})
}, 700)
},
//
handleZoomIn() {
const params = {
puid: this.row.puId,
idx: this.ballIndex,
}
this.requestPost('PTZ/C_PTZ_ZoomInPicture?token=' + this.qxToken, params, (rv) => {})
this.handleStopZoom()
},
//
handleZoomOut() {
const params = {
puid: this.row.puId,
idx: this.ballIndex,
}
this.requestPost('PTZ/C_PTZ_ZoomOutPicture?token=' + this.qxToken, params, (rv) => {})
this.handleStopZoom()
},
//
async updateWaterMark() {
try {
const params = {
puId: this.row.puId,
oneText: this.watermark.oneText,
twoText: this.watermark.twoText,
videoPath: this.qxInfo.videoUrl,
}
console.log('更新水印 params:', params)
const res = await updateWaterMark(params)
console.log('updateWaterMark:', res)
//
this.$message.success('操作成功')
} catch (error) {
console.log('updateWaterMark error:', error)
}
},
//
startRecording() {
this.isRecording = true;
const video = this.$refs.videosmallone;
const stream = video.captureStream();
this.mediaRecorder = new MediaRecorder(stream);
this.mediaRecorder.ondataavailable = (event) => {
if (event.data.size > 0) {
this.recordedChunks.push(event.data);
}
};
this.mediaRecorder.start();
},
//
stopRecording() {
this.isRecording = false
this.mediaRecorder.stop()
this.mediaRecorder.onstop = () => {
const blob = new Blob(this.recordedChunks, { type: 'video/webm' })
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = `本地录像_${new Date().getTime()}.mp4`
a.click()
URL.revokeObjectURL(url)
this.recordedChunks = []
}
},
//
takeSnapshot() {
console.log('Take snapshot')
const video = this.$refs.videosmallone
const canvas = this.$refs.canvas
canvas.width = video.videoWidth
canvas.height = video.videoHeight
const context = canvas.getContext('2d')
context.drawImage(video, 0, 0, canvas.width, canvas.height)
const dataURL = canvas.toDataURL('image/png')
const a = document.createElement('a')
a.href = dataURL
a.download = 'snapshot.png'
a.click()
},
},
computed: {
keyMap() {
return this.shortcutSettings.reduce((map, row) => {
row.forEach(item => {
map[item.key] = item.action;
});
return map;
}, {});
}
row.forEach((item) => {
map[item.key] = item.action
})
return map
}, {})
},
},
mounted() {
//
window.addEventListener('keydown', (e) => {
const key = e.key.toUpperCase();
//
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') {
console.log('Input focused')
return
}
if (this.isDown) return
const key = e.key.toUpperCase()
if (this.keyMap[key]) {
this.control(this.keyMap[key])
}
})
}
this.ballIndexOpts = this.row.videoIndex.split(',').map((item) => {
return {
value: item,
label: item,
}
})
this.getQxToken()
this.getQxInfo()
this.init()
},
}
</script>
<style scoped>
<style lang="scss" scoped>
.cell-player-1 {
width: 100%;
height: 100%;
box-sizing: border-box;
}
.monitor-page {
display: flex;
gap: 20px;
@ -386,7 +634,7 @@ export default {
margin: 12px 0;
}
.watermark-title{
.watermark-title {
cursor: pointer;
font-size: 13px;
display: flex;
@ -427,7 +675,7 @@ export default {
.key {
width: 21px;
height: 21px;
border: 0px solid #DCDFE6;
border: 0px solid #dcdfe6;
background: #f5f7fa;
color: rgb(25, 190, 107);
font-weight: bold;
@ -436,7 +684,7 @@ export default {
.control-title {
font-size: 14px;
font-family: "微软雅黑 Bold", "微软雅黑 Regular", 微软雅黑, sans-serif;
font-family: '微软雅黑 Bold', '微软雅黑 Regular', 微软雅黑, sans-serif;
font-weight: 700;
font-style: normal;
color: rgb(102, 177, 255);
@ -446,11 +694,11 @@ export default {
padding: 8px 9px;
}
.el-button [class^="el-icon-"] {
.el-button [class^='el-icon-'] {
margin: 0;
}
.column-label{
.column-label {
display: flex;
flex-direction: column;
justify-content: center;