<!--视频直播-->
|
<template>
|
<div
|
class="live-video"
|
v-loading="loading"
|
element-loading-background="rgb(33 33 33 / 80%)"
|
>
|
<video
|
:currentLiveUrl="videoUrl"
|
ref="liveVideoRef"
|
class="video-player"
|
:style="{objectFit: liveFullScreen ? 'cover' : 'contain' }"
|
:class="{ 'is-border-radius': isBorderRadius }"
|
:controls="props.controls"
|
autoplay
|
muted
|
playsinline
|
@click="videoClick"
|
>
|
您的浏览器太旧,不支持HTML5视频,请升级浏览器。
|
</video>
|
<!-- <canvas
|
v-if="hasRightMenu"
|
@dblclick="moveToPoint"
|
:class="{ 'draw-canvas': true, 'crosshair': openBoxSelection }"
|
:width="canvasWidth"
|
:height="canvasHeight"
|
@click.right.prevent="rightClick"
|
ref="myCanvas"
|
/>-->
|
</div>
|
<teleport defer to="body">
|
<div v-if="visible" :style="{ top: `${position.y}px`, left: `${position.x}px` }" class="rightMenu">
|
<div @click="switchCanvas" v-if="hasFrameZoom">{{ openBoxSelection ? '关闭框选' : '开启框选' }}</div>
|
<template v-if="!hideFlyToHere">
|
<div @click="flyToHereFun" v-if="hasFlyToHereBtn">飞向此处</div>
|
</template>
|
<div @click="consoleSwitch">{{ showFunBlock ? '隐藏控制台' : '显示控制台' }}</div>
|
</div>
|
</teleport>
|
|
<div class="flyToHere" v-if="showInputHeightBox">
|
<div>目标点真高</div>
|
<div class="heightInputBox">
|
<el-input-number v-focusDisableKeys :controls="false" v-model="flyToHereHeight" />
|
<div>m</div>
|
</div>
|
<div class="btnList">
|
<div class="btnItem" @click="flyToHereClose">取消</div>
|
<div class="btnItem" @click="flyToHereExecution">开始执行</div>
|
</div>
|
</div>
|
</template>
|
|
<script setup>
|
import { flyToHereApi, frameSelectionZoomApi, postPayloadAIM } from '@/api/payload'
|
import { ElMessage } from 'element-plus'
|
import getBaseConfig from '@/buildConfig/config'
|
import { useStore } from 'vuex'
|
|
const { hideFlyToHere } = getBaseConfig()
|
|
const props = defineProps({
|
// 播放url
|
videoUrl: {
|
type: String,
|
default: '',
|
},
|
// 显示控制台
|
controls: {
|
type: Boolean,
|
default: true,
|
},
|
isBorderRadius: {
|
type: Boolean,
|
default: false,
|
},
|
hasRightMenu: {
|
type: Boolean,
|
default: false,
|
},
|
liveFullScreen:{
|
type: Boolean,
|
default: true,
|
}
|
})
|
let webrtcPlayer = null
|
let liveVideoRef = ref(null)
|
let tryTimer = null
|
const visible = ref(false)
|
const position = ref({ x: 0, y: 0 })
|
const openBoxSelection = ref(false) //框选变焦
|
const canvasWidth = ref(0)
|
const canvasHeight = ref(0)
|
const myCanvas = ref(null)
|
const loading = ref(true)
|
const showInputHeightBox = ref(false)
|
let curElement = null
|
let flyToHereHeight = ref(0)
|
let lastTime = 0
|
let stallTimer = null
|
|
const store = useStore()
|
const emit = defineEmits(['videoClick'])
|
const droneSn = inject('droneSn')
|
const droneStatus = inject('droneStatus')
|
const showFunBlock = inject('showFunBlock')
|
const droneModel = inject('droneModel')
|
const hasPayloadControl = inject('hasPayloadControl')
|
const dockSn = inject('dockSn')
|
const cameraParams = inject('cameraParams')
|
const isAutoControl = inject('isAutoControl')
|
const camera_type = computed(() => cameraParams?.value?.camera_type)
|
const modeCode = computed(() => droneStatus?.value?.mode_code)
|
|
const hasFlyToHereBtn = computed(
|
() => [3, 16].includes(modeCode?.value) && hasPayloadControl?.value && droneModel?.value !== '91'
|
)
|
// 有框选变焦功能
|
const hasFrameZoom = computed(
|
() =>
|
droneSn?.value !== '1581F5BMD22CK0014H2U'
|
&& hasPayloadControl?.value
|
&& ![10, 11, 0, 1, 2].includes(modeCode.value)
|
&& !isAutoControl?.value
|
)
|
|
// 开启框选变焦
|
function switchCanvas() {
|
if (!hasFrameZoom.value){
|
closeDrawBox()
|
return ElMessage.warning('当前状态无法操作')
|
}
|
openBoxSelection.value = !openBoxSelection.value
|
nextTick(openBoxSelection.value ? drawBox : closeDrawBox)
|
}
|
|
// 控制台显示切换
|
function consoleSwitch() {
|
showFunBlock.value = !showFunBlock?.value
|
}
|
|
// 飞向此处弹框
|
function flyToHereFun() {
|
showInputHeightBox.value = true
|
flyToHereHeight.value = 30
|
}
|
|
// 飞向此处执行
|
function flyToHereExecution() {
|
const e = curElement
|
flyToHereApi({
|
sn: dockSn.value,
|
x: e.offsetX / e.target.offsetWidth,
|
y: e.offsetY / e.target.offsetHeight,
|
camera_type: camera_type?.value,
|
height: flyToHereHeight.value,
|
})
|
.then(res => {
|
showInputHeightBox.value = false
|
})
|
.finally(() => {})
|
}
|
|
// 飞到此处关闭
|
function flyToHereClose() {
|
showInputHeightBox.value = false
|
}
|
|
// 视频单机
|
function videoClick(e) {
|
e.preventDefault()
|
e.stopPropagation()
|
emit('videoClick')
|
}
|
|
// 双击画面到双击点
|
function moveToPoint(e) {
|
if (!hasPayloadControl?.value) return
|
if ([9, 10, 11, 0, 1, 2].includes(modeCode.value)) {
|
return
|
}
|
postPayloadAIM({
|
sn: dockSn.value,
|
x: e.offsetX / e.target.offsetWidth,
|
y: e.offsetY / e.target.offsetHeight,
|
camera_type: camera_type?.value,
|
})
|
}
|
|
// 右键菜单
|
function rightClick(e) {
|
console.log('负载控制' + hasPayloadControl?.value)
|
curElement = e
|
visible.value = true
|
position.value = { x: e.clientX, y: e.clientY }
|
}
|
|
// 绘制框选变焦
|
function drawBox() {
|
const cav = myCanvas.value
|
const ctx = cav.getContext('2d') //返回一个对象,该对象提供了用于在画布上绘图的方法和属性
|
let sX = 0 // 起点X坐标
|
let sY = 0 // 起点Y坐标
|
let eX = 0 // 终点X坐标
|
let eY = 0 // 终点Y坐标
|
cav.onmousedown = function (e) {
|
if (!hasFrameZoom.value || !openBoxSelection.value) {
|
closeDrawBox()
|
return ElMessage.warning('当前状态无法操作')
|
}
|
sX = e.offsetX
|
sY = e.offsetY
|
cav.onmousemove = function (e) {
|
ctx.clearRect(0, 0, cav.width, cav.height)
|
ctx.fillStyle = 'rgba(81,168,255,0.2)' // 填充颜色,透明度为0.2
|
ctx.strokeStyle = 'rgba(81,168,255,1)' // 边框颜色,不透明
|
ctx.beginPath() // 新的绘制路径
|
ctx.fillRect(sX, sY, e.offsetX - sX, e.offsetY - sY) // 填充矩形
|
ctx.strokeRect(sX, sY, e.offsetX - sX, e.offsetY - sY) // 绘制边框
|
}
|
cav.onmouseup = function (e) {
|
cav.onmousemove = null
|
cav.onmouseup = null
|
eX = e.offsetX // 终点X坐标
|
eY = e.offsetY // 终点Y坐标
|
ctx.clearRect(0, 0, cav.width, cav.height)
|
if (!hasFrameZoom.value || !openBoxSelection.value) {
|
closeDrawBox()
|
return ElMessage.warning('当前状态无法操作')
|
}
|
let refuse = Math.abs(eX - sX) < 8 && Math.abs(eY - sY) < 8 //绘制路径小于8不做任何动作
|
if (refuse) return
|
frameSelectionZoomApi({
|
x: Math.min(sX, eX) / canvasWidth.value,
|
y: Math.min(sY, eY) / canvasHeight.value,
|
sn: dockSn.value,
|
camera_type: camera_type?.value,
|
width: Math.abs(sX - eX) / canvasWidth.value,
|
height: Math.abs(sY - eY) / canvasHeight.value,
|
})
|
}
|
}
|
}
|
|
// 关闭绘制
|
function closeDrawBox() {
|
const cav = myCanvas.value
|
openBoxSelection.value = false
|
cav.onmousedown = null
|
cav.onmousemove = null
|
cav.onmouseup = null
|
}
|
|
// 点击其他地方关闭菜单
|
function closeMenu() {
|
visible.value = false
|
}
|
|
// 监听卡顿
|
function monitorLag() {
|
if (stallTimer) clearInterval(stallTimer)
|
stallTimer = setInterval(() => {
|
const video = liveVideoRef.value
|
if (!video) return
|
// currentTime 长时间不变,说明画面卡住了
|
if (video.currentTime === lastTime) {
|
console.log('检测到视频卡顿尝试刷新')
|
playVideo()
|
}
|
lastTime = video.currentTime
|
}, 5000)
|
}
|
|
// 移除监听卡顿
|
function stopMonitorLog() {
|
if (stallTimer) clearInterval(stallTimer)
|
}
|
|
// webrtc播放
|
const playVideo = () => {
|
loading.value = true
|
pause()
|
// https://gitee.com/xiongguangjie/zlmrtcclient.js/blob/dev/demo/index.html
|
|
const RTCPeerConnection = window.RTCPeerConnection || window.mozRTCPeerConnection || window.webkitRTCPeerConnection;
|
if (!RTCPeerConnection) {
|
alert('您的浏览器不支持WebRTC,请使用Chrome/Firefox/Edge等现代浏览器')
|
}
|
|
webrtcPlayer = new window.ZLMRTCClient.Endpoint({
|
element: liveVideoRef.value, // video 标签
|
debug: true, // 是否打印日志
|
zlmsdpUrl: props.videoUrl, //流地址
|
simulecast: false,
|
useCamera: false,
|
audioEnable: true,
|
videoEnable: true,
|
recvOnly: true,
|
usedatachannel: false,
|
})
|
webrtcPlayer.on(window.ZLMRTCClient.Events.WEBRTC_ON_REMOTE_STREAMS, e => {
|
liveVideoRef.value.onplaying = () => {
|
getVideoSize()
|
loading.value = false
|
monitorLag()
|
}
|
})
|
webrtcPlayer.on(window.ZLMRTCClient.Events.WEBRTC_OFFER_ANWSER_EXCHANGE_FAILED, e => {
|
console.error('offer anwser 交换失败', e)
|
if (e.code === -400) {
|
console.log('流不存在,即将重试')
|
tryTimer = setTimeout(() => {
|
playVideo()
|
}, 100)
|
}
|
})
|
webrtcPlayer.on(ZLMRTCClient.Events.WEBRTC_ICE_CANDIDATE_ERROR,event => {
|
// ICE 协商出错
|
console.log('ICE 协商出错');
|
});
|
webrtcPlayer.on(ZLMRTCClient.Events.WEBRTC_ON_DATA_CHANNEL_MSG,event => {
|
console.log('rtc datachannel 消息 :',event.data);
|
});
|
webrtcPlayer.on(ZLMRTCClient.Events.WEBRTC_ON_DATA_CHANNEL_ERR,event => {
|
console.log('rtc datachannel 错误 :',event);
|
});
|
}
|
|
|
const handleMetadata = () => {
|
const video = document.querySelector('video')
|
console.log(`原始视频分辨率: ${video.videoWidth}x${video.videoHeight}`)
|
store.commit('setLiveResolution', {width: video.videoWidth, height: video.videoHeight})
|
if (props.liveFullScreen){
|
canvasWidth.value = liveVideoRef.value.offsetWidth
|
canvasHeight.value = (canvasWidth.value / video.videoWidth) * video.videoHeight
|
}else{
|
canvasHeight.value = liveVideoRef.value.offsetHeight
|
canvasWidth.value = (canvasHeight.value / video.videoHeight) * video.videoWidth
|
}
|
}
|
|
|
|
// 获取视频分辨率
|
function getVideoSize() {
|
const video = document.querySelector('video')
|
video.removeEventListener('resize', handleMetadata)
|
video.addEventListener('resize', handleMetadata)
|
// 初始调用一次(如果视频元数据已加载)
|
if (video.readyState >= HTMLMediaElement.HAVE_METADATA) {
|
handleMetadata()
|
}
|
}
|
|
// 销毁webrtc
|
const pause = () => {
|
if (webrtcPlayer) {
|
webrtcPlayer.close()
|
webrtcPlayer = null
|
}
|
}
|
|
// 监听画面变化重置cavas画布
|
let resizeObserver = new ResizeObserver(getVideoSize)
|
|
watch(()=> props.liveFullScreen,getVideoSize)
|
|
watch(
|
() => props.videoUrl,
|
newValue => {
|
if (!newValue) return
|
nextTick(() => {
|
playVideo()
|
})
|
},
|
{
|
immediate: true,
|
}
|
)
|
|
onMounted(() => {
|
document.addEventListener('click', closeMenu)
|
resizeObserver.observe(liveVideoRef.value)
|
})
|
|
onBeforeUnmount(() => {
|
pause()
|
stopMonitorLog()
|
clearTimeout(tryTimer)
|
document.removeEventListener('click', closeMenu)
|
resizeObserver.unobserve(liveVideoRef.value) // 停止监听某个元素
|
resizeObserver = null
|
})
|
</script>
|
|
<style lang="scss" scoped>
|
.live-video {
|
position: relative;
|
width: 100%;
|
height: 100%;
|
color: #fff;
|
background: rgba(0, 0, 0, 0.5);
|
z-index: 1;
|
|
.video-player {
|
width: 100%;
|
height: 100%;
|
|
&.is-border-radius {
|
// 保持16:9比例
|
aspect-ratio: 16/9;
|
object-fit: contain;
|
margin: auto;
|
display: block;
|
position: absolute;
|
top: 50%;
|
left: 50%;
|
transform: translate(-50%, -50%);
|
}
|
}
|
|
.draw-canvas {
|
position: absolute;
|
left: 50%;
|
top: 50%;
|
transform: translate(-50%, -50%);
|
|
&.crosshair {
|
cursor: crosshair;
|
}
|
}
|
|
:deep() {
|
.el-loading-mask {
|
.el-loading-spinner {
|
transform: translateY(-50%);
|
}
|
}
|
}
|
}
|
|
.rightMenu {
|
z-index: 9999;
|
position: fixed;
|
width: 94px;
|
background: rgba(0, 0, 0, 0.4);
|
border-radius: 8px;
|
cursor: pointer;
|
font-weight: 500;
|
font-size: 14px;
|
font-family: Source Han Sans CN, Source Han Sans CN, serif;
|
backdrop-filter: blur(5px);
|
|
> div {
|
height: 32px;
|
color: #ffffff;
|
display: flex;
|
align-items: center;
|
justify-content: center;
|
border-radius: 8px;
|
|
&:hover {
|
background: rgba(255, 255, 255, 0.78) !important;
|
color: #3d3b3b;
|
border: 1px solid rgba(255, 255, 255, 0.99);
|
}
|
}
|
}
|
|
.flyToHere {
|
position: fixed;
|
z-index: 5;
|
left: 50%;
|
top: 50%;
|
transform: translate(-50%, -50%);
|
width: 164px;
|
background: rgba(29, 29, 29, 0.25);
|
border-radius: 8px 8px 8px 8px;
|
backdrop-filter: blur(5px);
|
padding: 16px;
|
color: #ffffff;
|
display: flex;
|
flex-direction: column;
|
gap: 8px;
|
|
.heightInputBox {
|
display: flex;
|
align-items: center;
|
gap: 6px;
|
|
.el-input-number {
|
height: 25px;
|
border-radius: 4px 4px 4px 4px;
|
|
:deep() {
|
.el-input__inner {
|
color: #ffffff;
|
}
|
|
.el-input__wrapper {
|
--el-input-focus-border-color: #ffffff;
|
background: rgba(36, 36, 36, 0.54) !important;
|
}
|
}
|
}
|
}
|
|
.btnList {
|
display: flex;
|
align-items: center;
|
gap: 0 6px;
|
font-size: 13px;
|
|
.btnItem {
|
height: 32px;
|
line-height: 32px;
|
padding: 0 9px;
|
background: rgba(58, 58, 58, 0.75);
|
box-shadow: 0px 4px 72px 0px rgba(0, 0, 0, 0.25);
|
border-radius: 8px 8px 8px 8px;
|
border: 1px solid transparent;
|
cursor: pointer;
|
|
&:hover {
|
background: #3a3a3a;
|
box-shadow: 0px 4px 72px 0px rgba(0, 0, 0, 0.25);
|
border-radius: 8px 8px 8px 8px;
|
border: 1px solid rgba(255, 255, 255, 0.99);
|
}
|
}
|
}
|
}
|
</style>
|