云台联调

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", "echarts": "5.4.0",
"element-ui": "2.15.14", "element-ui": "2.15.14",
"file-saver": "2.0.5", "file-saver": "2.0.5",
"flv.js": "^1.6.2",
"fuse.js": "6.4.3", "fuse.js": "6.4.3",
"highlight.js": "9.18.5", "highlight.js": "9.18.5",
"js-beautify": "1.13.0", "js-beautify": "1.13.0",

View File

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

View File

@ -64,3 +64,18 @@ export const delPlatformConfiguration = (id) => {
export const changeStatus = (data) => { export const changeStatus = (data) => {
return request.post('/smart-site/platform_configuration/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' }], wsUrl: [{ required: true, message: '请输入视频连接地址-公网', trigger: 'blur' }],
epid: [ epid: [
{ required: true, message: '请输入epid', trigger: 'blur' }, { 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' }], bfix: [{ required: true, message: '请输入bfix', trigger: 'blur' }],
}, },

View File

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

View File

@ -2,16 +2,18 @@
<div class="monitor-page"> <div class="monitor-page">
<!-- 左侧视频监控区 --> <!-- 左侧视频监控区 -->
<div class="video-section"> <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-container">
<div class="video-overlay"> <video class="cell-player-1" ref="videosmallone" preload="auto" muted type="rtmp/flv" :id="videoId">
<div class="timestamp">{{ currentTime }}</div> <source src="" />
<div class="coordinates"> </video>
<div>{{ coordinates.line1 }}</div> <canvas ref="canvas" style="display: none"></canvas>
<div>{{ coordinates.line2 }}</div>
</div>
<div class="signal-info">电池: {{ batteryInfo }}</div>
</div>
</div> </div>
</div> </div>
@ -23,34 +25,30 @@
<!-- 方向控制 --> <!-- 方向控制 -->
<div class="direction-control"> <div class="direction-control">
<div class="direction-pad"> <div class="direction-pad">
<el-button icon="el-icon-arrow-up" circle @click="control('up')"> <el-button icon="el-icon-arrow-up" circle @click="control('up')"> </el-button>
</el-button>
<div class="horizontal-buttons"> <div class="horizontal-buttons">
<el-button icon="el-icon-arrow-left" circle @click="control('left')"> <el-button icon="el-icon-arrow-left" circle @click="control('left')"> </el-button>
</el-button> <el-button icon="el-icon-arrow-right" circle @click="control('right')"> </el-button>
<el-button icon="el-icon-arrow-right" circle @click="control('right')">
</el-button>
</div> </div>
<el-button icon="el-icon-arrow-down" circle @click="control('down')"> <el-button icon="el-icon-arrow-down" circle @click="control('down')"> </el-button>
</el-button>
</div> </div>
<!-- 缩放控制 --> <!-- 缩放控制 -->
<div class="zoom-controls"> <div class="zoom-controls">
<div class="zoom-column"> <div class="zoom-column">
<el-button size="small" @click="control('zoomIn')"> <el-button size="small" @click="control('focusIn')">
<i class="el-icon-plus"></i> <i class="el-icon-plus"></i>
</el-button> </el-button>
<el-button size="small" @click="control('zoomOut')"> <el-button size="small" @click="control('focusOut')">
<i class="el-icon-minus"></i> <i class="el-icon-minus"></i>
</el-button> </el-button>
<div class="column-label">聚焦</div> <div class="column-label">聚焦</div>
</div> </div>
<div class="zoom-column"> <div class="zoom-column">
<el-button size="small" @click="control('focusIn')"> <el-button size="small" @click="control('zoomIn')">
<i class="el-icon-plus"></i> <i class="el-icon-plus"></i>
</el-button> </el-button>
<el-button size="small" @click="control('focusOut')"> <el-button size="small" @click="control('zoomOut')">
<i class="el-icon-minus"></i> <i class="el-icon-minus"></i>
</el-button> </el-button>
<div class="column-label">缩放</div> <div class="column-label">缩放</div>
@ -72,9 +70,10 @@
<div class="view-row"> <div class="view-row">
<div class="view-item"> <div class="view-item">
<i class="el-icon-video-camera"></i> <i class="el-icon-video-camera"></i>
<span>本地录像</span> <span v-if="!isRecording" @click="startRecording">本地录像</span>
<span v-else @click="stopRecording">停止录像</span>
</div> </div>
<div class="view-item"> <div class="view-item" @click="takeSnapshot">
<i class="el-icon-picture"></i> <i class="el-icon-picture"></i>
<span>本地抓拍</span> <span>本地抓拍</span>
</div> </div>
@ -104,15 +103,9 @@
<!-- 水印设置 --> <!-- 水印设置 -->
<div class="watermark-section"> <div class="watermark-section">
<div class="watermark-title">应用水印<i class="el-icon-s-operation"></i></div> <div class="watermark-title" @click="updateWaterMark">应用水印<i class="el-icon-s-operation"></i></div>
<el-input <el-input v-model="watermark.oneText" placeholder="设置第一行视频水印文字"></el-input>
v-model="watermark.line1" <el-input v-model="watermark.twoText" placeholder="设置第二行视频水印文字"></el-input>
placeholder="设置第一行视频水印文字"
></el-input>
<el-input
v-model="watermark.line2"
placeholder="设置第二行视频水印文字"
></el-input>
</div> </div>
<div class="control-title">快捷键设置</div> <div class="control-title">快捷键设置</div>
@ -128,7 +121,7 @@
@keydown="handleKeyPress($event, rowIndex, colIndex)" @keydown="handleKeyPress($event, rowIndex, colIndex)"
:maxlength="1" :maxlength="1"
class="key" class="key"
></input> />
</div> </div>
</div> </div>
</div> </div>
@ -138,66 +131,123 @@
</template> </template>
<script> <script>
import flvjs from 'flv.js'
import axios from 'axios'
import { getQxToken, getVideoEquipment, updateWaterMark } from '@/api/deviceManagement'
export default { export default {
props: {
row: {
type: Object,
default: () => ({}),
},
},
name: 'VideoMonitor', name: 'VideoMonitor',
data() { data() {
return { return {
currentTime: '2023-01-07 13:19:38', qxToken: '',
coordinates: { qxInfo: {},
line1: '116.13263L 000KPH', videoId: '',
line2: '033.50462N 000D/G' player: null,
}, ballIndex: 0,
batteryInfo: '64% 4G1:96% 4G2:96%', ballIndexOpts: [],
isDown: false,
watermark: { watermark: {
line1: '', oneText: '',
line2: '' twoText: '',
}, },
// //
shortcutSettings: [ shortcutSettings: [
[ [
{ label: '上', key: 'W', action: 'up' }, { label: '上', key: 'W', action: 'up' },
{ label: '放大', key: 'Q', action: 'zoomIn' } { label: '放大', key: 'Q', action: 'zoomIn' },
], ],
[ [
{ label: '下', key: 'S', action: 'down' }, { label: '下', key: 'S', action: 'down' },
{ label: '缩小', key: 'E', action: 'zoomOut' } { label: '缩小', key: 'E', action: 'zoomOut' },
], ],
[ [
{ label: '左', key: 'A', action: 'left' }, { label: '左', key: 'A', action: 'left' },
{ label: '远焦', key: 'R', action: 'focusIn' } { label: '远焦', key: 'R', action: 'focusIn' },
], ],
[ [
{ label: '右', key: 'D', action: 'right' }, { label: '右', key: 'D', action: 'right' },
{ label: '近焦', key: 'T', action: 'focusOut' } { label: '近焦', key: 'T', action: 'focusOut' },
], ],
[ [
{ label: '抓拍', key: 'F', action: 'capture' }, { label: '抓拍', key: 'F', action: 'capture' },
{ label: '录像', key: 'G', action: 'record' } { label: '录像', key: 'G', action: 'record' },
] ],
] ],
mediaRecorder: null,
recordedChunks: [],
isRecording: false,
} }
}, },
methods: { methods: {
control(action) { control(action) {
console.log('Control action:', action) console.log('Control action:', action)
this.isDown = true
switch (action) { switch (action) {
case 'up': case 'up':
console.log('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 break
case 'down': case 'down':
console.log('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 break
case 'left': case 'left':
console.log('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 break
case 'right': case 'right':
console.log('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 break
case 'zoomIn': case 'zoomIn':
console.log('Zoom In') console.log('Zoom In')
this.handleZoomIn()
break break
case 'zoomOut': case 'zoomOut':
console.log('Zoom Out') console.log('Zoom Out')
this.handleZoomOut()
break break
case 'focusIn': case 'focusIn':
console.log('Focus In') console.log('Focus In')
@ -219,7 +269,6 @@ export default {
break break
default: default:
console.log('Unknown action:', action) console.log('Unknown action:', action)
} }
}, },
toggleCloud() { toggleCloud() {
@ -231,51 +280,250 @@ export default {
// //
handleKeyPress(event, rowIndex, colIndex) { handleKeyPress(event, rowIndex, colIndex) {
event.preventDefault(); event.preventDefault()
const key = event.key.toUpperCase(); const key = event.key.toUpperCase()
// //
if (/^[A-Z]$/.test(key)) { if (/^[A-Z]$/.test(key)) {
// //
const isDuplicate = this.shortcutSettings.some(row => const isDuplicate = this.shortcutSettings.some((row) =>
row.some(item => item.key === key && !(item.key === this.shortcutSettings[rowIndex][colIndex].key)) row.some((item) => item.key === key && !(item.key === this.shortcutSettings[rowIndex][colIndex].key)),
); )
if (isDuplicate) { if (isDuplicate) {
this.$message.error('该键位已被使用,请选择其他键位'); this.$message.error('该键位已被使用,请选择其他键位')
return; 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: { computed: {
keyMap() { keyMap() {
return this.shortcutSettings.reduce((map, row) => { return this.shortcutSettings.reduce((map, row) => {
row.forEach(item => { row.forEach((item) => {
map[item.key] = item.action; map[item.key] = item.action
}); })
return map; return map
}, {}); }, {})
} },
}, },
mounted() { mounted() {
// //
window.addEventListener('keydown', (e) => { 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]) { if (this.keyMap[key]) {
this.control(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> </script>
<style scoped> <style lang="scss" scoped>
.cell-player-1 {
width: 100%;
height: 100%;
box-sizing: border-box;
}
.monitor-page { .monitor-page {
display: flex; display: flex;
gap: 20px; gap: 20px;
@ -427,7 +675,7 @@ export default {
.key { .key {
width: 21px; width: 21px;
height: 21px; height: 21px;
border: 0px solid #DCDFE6; border: 0px solid #dcdfe6;
background: #f5f7fa; background: #f5f7fa;
color: rgb(25, 190, 107); color: rgb(25, 190, 107);
font-weight: bold; font-weight: bold;
@ -436,7 +684,7 @@ export default {
.control-title { .control-title {
font-size: 14px; font-size: 14px;
font-family: "微软雅黑 Bold", "微软雅黑 Regular", 微软雅黑, sans-serif; font-family: '微软雅黑 Bold', '微软雅黑 Regular', 微软雅黑, sans-serif;
font-weight: 700; font-weight: 700;
font-style: normal; font-style: normal;
color: rgb(102, 177, 255); color: rgb(102, 177, 255);
@ -446,7 +694,7 @@ export default {
padding: 8px 9px; padding: 8px 9px;
} }
.el-button [class^="el-icon-"] { .el-button [class^='el-icon-'] {
margin: 0; margin: 0;
} }