<template>
|
<div class="drone-pilot-details" :style="{ width: screenWidth,height: screenHeight}">
|
<!-- 横屏模式 -->
|
<div id="landscapeBox" class="landscape-box" :style="{ width: screenWidth + 'px', height: (screenHeight) + 'px'}">
|
<div id="videoModule" class="l-video" :style="{ width: screenWidthVideo + 'px', height: (screenHeightVideo) + 'px'}">
|
<video v-show="videoUrl" ref="videoPlayerBig" :style="{ width: screenWidthVideo + 'px', height: screenHeightVideo + 'px'}" controls autoplay muted playsinline style="text-align: left; object-fit: fill">
|
Your browser is too old which doesn't support HTML5 video.
|
</video>
|
<el-empty v-show="videoUrl == ''" description="当前设备已关机,无法进行直播" :image="imageUrl"></el-empty>
|
</div>
|
<div class="l-map" :style="{ width: screenWidthMap + 'px', height: (screenHeightMap) + 'px'}">
|
<div id="cesiumContainerBigMap"></div>
|
<div class="l-zp" id="lZp">
|
<comPass :cesiumViewe="globalViewer"/>
|
</div>
|
<div class="l-zoom" id="lZoom">
|
<div class="zoom">
|
<img v-if="!isBigMap" src="@/assets/images/fang.png" @touchstart="changeModelMap" />
|
<img v-else src="@/assets/images/suo.png" @touchstart="changeModelMap" />
|
</div>
|
</div>
|
</div>
|
<div class="right-box">
|
<ptzControl :sn="sn" :osdVisible="sbInfo" />
|
<controlConsole :sn="sn" :osdVisible="sbInfo" :cesiumViewe="globalViewer" />
|
</div>
|
</div>
|
<!-- 竖屏 -->
|
<!-- <div class="portrait-screen" v-else>
|
<div class="l-video" :style="{ width: screenWidth + 'px', height: (screenHeight/2)+40 + 'px'}">
|
<video v-show="videoUrl" ref="videoPlayerBig" :style="{ width: screenWidth + 'px', height: (screenHeight/2)+40 + 'px'}" controls autoplay muted playsinline style="text-align: left; object-fit: fill">
|
Your browser is too old which doesn't support HTML5 video.
|
</video>
|
<el-empty v-show="videoUrl == ''" description="当前设备已关机,无法进行直播" :image="imageUrl"></el-empty>
|
</div>
|
<div class="l-map" :style="{ width: screenWidth + 'px', height: (screenHeight/2)-40 + 'px'}">
|
<div id="cesiumContainerMap"></div>
|
<div class="l-zp" id="lZp">
|
<comPass :cesiumViewe="globalViewer"/>
|
</div>
|
</div>
|
<div class="right-box">
|
<ptzControl :sn="sn" :osdVisible="sbInfo" />
|
<controlConsole :sn="sn" :osdVisible="sbInfo" />
|
</div>
|
</div> -->
|
</div>
|
</template>
|
|
<script lang="ts" setup>
|
import { ref, onMounted, onBeforeUnmount, nextTick, reactive, computed, watch } from 'vue';
|
import { EDeviceTypeName, EHmsLevel } from '@/types/enums'
|
import { getRoot } from '../../root'
|
import { startLivestream, getLiveVideoUrl } from '@/api/manage'
|
import { CURRENT_CONFIG as config } from '@/api/http/config'
|
// import videojs from 'video.js'
|
// import 'video.js/dist/video-js.css';
|
import { getLnglatDist, cartesian3Convert, getLnglatAltitude, createCircleBillboard } from '@/utils/cesium/mapUtil'
|
import cesiumOperation from '@/utils/cesium-tsa.js';
|
const { _init, viewerDestory } = cesiumOperation();
|
import * as Cesium from 'cesium';
|
|
// import { cesiumOperation } from '@/hooks/use-cesium-tsa'
|
// const { _init } = cesiumOperation();
|
import { useMyStore } from '@/store'
|
import { getWaylineFile } from '@/api/manage'
|
import comPass from './components/comPass.vue'
|
import ptzControl from './components/ptzControl.vue'
|
import controlConsole from './components/controlConsole.vue'
|
import { useConnectWebSocket } from '@/utils/websocket/connect-websocket'
|
import { getWebsocketUrl } from '@/websocket/util/config'
|
import { EBizCode, ELocalStorageKey, ERouterName } from '@/types'
|
import VConsole from 'vconsole';
|
import { any } from 'video.js/dist/types/utils/events';
|
import { analyzeKmzFile, XMLToJSON } from '@/utils/cesium/kmz.js'
|
import { initPointWayLine } from './hooks/initPointWayline'
|
import { initPlanarWayline } from './hooks/initPlanarWayline'
|
import { droneFly } from './hooks/droneFly'
|
import flvjs from 'flv.js';
|
const { pointWayline } = initPointWayLine()
|
const { planarWayline } = initPlanarWayline()
|
const { initDock } = droneFly()
|
|
const {
|
removeAllPoint,
|
removeAllDataSource,
|
globalCesium,
|
getEntityById,
|
removeById,
|
loadLAYER,
|
} = cesiumOperation()
|
|
// const vConsole = new VConsole();
|
|
let globalViewer = null
|
|
// 使用 ref 来存储屏幕宽度和高度
|
const screenWidth = ref(window.innerWidth);
|
const screenHeight = ref(window.innerHeight);
|
// 地图切换
|
const screenWidthMap = ref(window.innerWidth);
|
const screenHeightMap = ref((window.innerHeight/2)+40);
|
// 视频切换
|
const screenWidthVideo = ref(window.innerWidth);
|
const screenHeightVideo = ref(window.innerHeight/2);
|
const isBigMap = ref(false)
|
|
// 更新屏幕尺寸的函数
|
const updateScreenSize = () => {
|
screenWidth.value = window.innerWidth;
|
screenHeight.value = window.innerHeight;
|
if (isLandscape.value) { //横屏
|
screenWidthMap.value = window.innerWidth/2
|
screenHeightMap.value = window.innerHeight
|
screenWidthVideo.value = window.innerWidth/2
|
screenHeightVideo.value = window.innerHeight
|
} else {//竖屏
|
screenWidthMap.value = window.innerWidth
|
screenHeightMap.value = (window.innerHeight/2)-40
|
screenWidthVideo.value = window.innerWidth
|
screenHeightVideo.value = (window.innerHeight/2)+40
|
}
|
};
|
|
const store = useMyStore()
|
// 获取路由
|
const root = getRoot()
|
|
interface deviceInfo {
|
dockSn: string;
|
latitude?:number;// 机场经纬度
|
longitude?:number;
|
height?:number; // 机场高度
|
isOnline: boolean;
|
name: string; // 可选字段
|
sn: string;
|
workspaceId:string;
|
children?: { };
|
}
|
// 获取机场/遥控器sn
|
let localData = ref<any>(window.localStorage.getItem('sbInfo'))
|
const sbInfo: deviceInfo = JSON.parse(localData.value) as deviceInfo;
|
// console.log('哒哒哒', sbInfo)
|
// 无人机sn
|
const sn = ref<String>(sbInfo.sn)
|
// 机场sn
|
const dockSn = ref<String>(sbInfo.dockSn)
|
|
// 记录横竖屏模式
|
const isLandscape = ref(false);
|
|
// 记录视频地址
|
const videoUrl = ref<String>('');
|
|
// 设置图片地址
|
const imageUrl = new URL('@/assets/images/norecord.png', import.meta.url).href
|
|
const imageMapSrc = new URL('@/assets/images/mapdock.png', import.meta.url).href
|
|
const mapswitching = new URL('@/assets/images/mapswitching.png', import.meta.url).href
|
|
const videoWH = ref<any>(null)
|
|
// 视频宽高
|
const drawCanvasWidth = ref<Number>(0)
|
const drawCanvasHeight = ref<Number>(0)
|
|
// 鹰眼视频
|
const timer = null;
|
const videoPlayer = ref<any>(null); // 视频播放器实例
|
let webrtcPlayer = null; // video.js 播放器
|
|
// 大屏幕视频
|
const timerBig = null;
|
const videoPlayerBig = ref<any>(null); // 视频播放器实例
|
let webrtcPlayerBig = null
|
|
let player = null; // video.js 播放器
|
|
// 无人机实时飞行链接
|
let ssLinePath = ref(null)
|
|
// 实时定位
|
let viewDroneInfo = {}
|
// 机场高度
|
let droneHeight = ref(0)
|
|
// 监听
|
let connectWs = ref<any>(null)
|
|
let workspaceId = ref<String>(sbInfo.workspaceId)
|
localStorage.setItem('bs_workspace_id', workspaceId.value)
|
// store.commit('SET_SELECTED_WORKSPACE_ID', workspaceId.value);
|
|
// RTCWEB 加载视频
|
const playBig = () => {
|
webrtcPlayerBig = new window.ZLMRTCClient.Endpoint({
|
element: videoPlayerBig.value, // video 标签
|
debug: true, // 是否打印日志
|
zlmsdpUrl: videoUrl.value, //流地址
|
simulecast: false,
|
useCamera: false,
|
audioEnable: true,
|
videoEnable: true,
|
recvOnly: true,
|
usedatachannel: false,
|
})
|
webrtcPlayerBig.on(
|
window.ZLMRTCClient.Events.WEBRTC_ICE_CANDIDATE_ERROR,
|
(e) => {
|
// ICE 协商出错
|
console.error('ICE 协商出错')
|
// this.eventcallbacK('ICE ERROR', 'ICE 协商出错')
|
},
|
)
|
|
webrtcPlayerBig.on(
|
window.ZLMRTCClient.Events.WEBRTC_ON_REMOTE_STREAMS,
|
(e) => {
|
//获取到了远端流,可以播放
|
console.log('播放成功', e.streams)
|
// this.eventcallbacK('playing', '播放成功')
|
},
|
)
|
|
webrtcPlayerBig.on(
|
window.ZLMRTCClient.Events.WEBRTC_OFFER_ANWSER_EXCHANGE_FAILED,
|
(e) => {
|
// offer anwser 交换失败
|
console.error('offer anwser 交换失败', e)
|
// this.eventcallbacK('OFFER ANSWER ERROR ', 'offer anwser 交换失败')
|
if (e.code == -400 && e.msg == '流不存在') {
|
console.log('流不存在')
|
timerBig = setTimeout(() => {
|
webrtcPlayerBig.close()
|
playBig()
|
}, 100)
|
}
|
},
|
)
|
|
webrtcPlayerBig.on(
|
window.ZLMRTCClient.Events.WEBRTC_ON_LOCAL_STREAM,
|
(s) => {
|
// 获取到了本地流
|
|
// document.getElementById('selfVideo').srcObject=s;
|
// this.eventcallbacK('LOCAL STREAM', '获取到了本地流')
|
},
|
)
|
}
|
// const play = () => {
|
// webrtcPlayer = new window.ZLMRTCClient.Endpoint({
|
// element: videoPlayer.value, // video 标签
|
// debug: true, // 是否打印日志
|
// zlmsdpUrl: videoUrl.value, //Rtc地址
|
// simulecast: false,
|
// useCamera: false,
|
// audioEnable: true,
|
// videoEnable: true,
|
// recvOnly: true,
|
// usedatachannel: false,
|
// })
|
// webrtcPlayer.on(
|
// window.ZLMRTCClient.Events.WEBRTC_ICE_CANDIDATE_ERROR,
|
// (e) => {
|
// // ICE 协商出错
|
// console.error('ICE 协商出错')
|
// // this.eventcallbacK('ICE ERROR', 'ICE 协商出错')
|
// },
|
// )
|
|
// webrtcPlayer.on(
|
// window.ZLMRTCClient.Events.WEBRTC_ON_REMOTE_STREAMS,
|
// (e) => {
|
// //获取到了远端流,可以播放
|
// console.log('播放成功', e.streams)
|
// // this.eventcallbacK('playing', '播放成功')
|
// },
|
// )
|
|
// webrtcPlayer.on(
|
// window.ZLMRTCClient.Events.WEBRTC_OFFER_ANWSER_EXCHANGE_FAILED,
|
// (e) => {
|
// // offer anwser 交换失败
|
// console.error('offer anwser 交换失败', e)
|
// // this.eventcallbacK('OFFER ANSWER ERROR ', 'offer anwser 交换失败')
|
// if (e.code == -400 && e.msg == '流不存在') {
|
// console.log('流不存在')
|
// timer = setTimeout(() => {
|
// webrtcPlayer.close()
|
// play()
|
// }, 100)
|
// }
|
// },
|
// )
|
|
// webrtcPlayer.on(
|
// window.ZLMRTCClient.Events.WEBRTC_ON_LOCAL_STREAM,
|
// (s) => {
|
// },
|
// )
|
// }
|
const videoUrlFlv = ref<String>('')
|
// 加载该设备的视频信息
|
const loadVideo = async () => {
|
await getLiveVideoUrl(sn.value).then((res) => {
|
if (res.data.code != 0) return
|
// 获取高度
|
if (videoWH.value) {
|
drawCanvasWidth.value = videoWH.value.offsetWidth;
|
drawCanvasHeight.value = videoWH.value.offsetHeight;
|
}
|
videoUrl.value = res.data.data.rtcs_url
|
videoUrlFlv.value = res.data.data.url
|
// 播放
|
// play()
|
playBig()
|
// flv
|
// initFlvPlayer()
|
})
|
}
|
|
// flv
|
const videoPlayerFlv = ref(null); // 引用 video 元素
|
let flvPlayer = null; // 用于存储 flv.js 播放器实例
|
// 初始化 flv.js 播放器
|
|
const initFlvPlayer = () => {
|
if (flvjs.isSupported()) {
|
const videoElement = videoPlayerFlv.value; // 获取 video 元素
|
flvPlayer = flvjs.createPlayer({
|
type: 'flv', // 视频格式
|
url: videoUrlFlv.value, // FLV 视频地址
|
});
|
flvPlayer.attachMediaElement(videoElement); // 绑定 video 元素
|
flvPlayer.load(); // 加载视频
|
flvPlayer.play(); // 播放视频
|
} else {
|
console.error('Your browser does not support flv.js.');
|
}
|
};
|
|
//===============================
|
|
// 监听ws
|
const messageHandler = async (payload: any) => {
|
// if (!payload) {
|
// return
|
// }
|
switch (payload.biz_code) {
|
case EBizCode.GatewayOsd: { // 遥控器
|
store.commit('SET_GATEWAY_INFO', payload.data)
|
break
|
}
|
case EBizCode.DeviceOsd: {
|
// console.log('低调点',payload)
|
store.commit('SET_DEVICE_INFO', payload.data)
|
store.commit('SET_WS_MESSAGE', payload)
|
break
|
}
|
case EBizCode.DockOsd: { // 机场
|
store.commit('SET_DOCK_INFO', payload.data)
|
break
|
}
|
}
|
|
}
|
|
const webSorketUrl = getWebsocketUrl() + '&workspace-id=' + workspaceId.value
|
// 监听ws 消息
|
useConnectWebSocket(messageHandler, webSorketUrl)
|
|
|
let savedMapState = null; // 保存地图状态
|
// 销毁之前先保存状态
|
const destroyViewer = () => {
|
if ( window.cesiumViewer) {
|
// 保存当前地图状态(如相机位置)
|
// savedMapState = window.cesiumViewer.camera.view;
|
window.cesiumViewer.destroy();
|
window.cesiumViewer = null;
|
}
|
}
|
|
// 地图全屏切换
|
const changeModelMap = () => {
|
isBigMap.value = !isBigMap.value
|
const cesiumContainer = document.getElementById('cesiumContainerBigMap');
|
const lZp = document.getElementById('lZp');
|
const lZoom = document.getElementById('lZoom');
|
|
if (isBigMap.value) { // 全屏
|
cesiumContainer.style.position = 'fixed';
|
cesiumContainer.style.top = '0';
|
cesiumContainer.style.left = '0';
|
cesiumContainer.style.width = '100%';
|
cesiumContainer.style.height = '100%';
|
lZp.style.right = '40%';
|
lZoom.style.left = '0';
|
} else { // 缩放
|
cesiumContainer.style.position = 'static';
|
cesiumContainer.style.width = `${screenWidthMap.value}`
|
cesiumContainer.style.height = `${screenHeight.value}`
|
lZp.style.right = '16%';
|
lZoom.style.left = 'auto';
|
|
}
|
}
|
|
const checkOrientation = async () => {
|
const landscapeBox = document.getElementById('landscapeBox');
|
const cesiumContainer = document.getElementById('cesiumContainerBigMap');
|
const videoModule = document.getElementById('videoModule');
|
const lZp = document.getElementById('lZp');
|
const lZoom = document.getElementById('lZoom');
|
// 横屏再加载视频请求
|
// destroyViewer()
|
|
// loadVideo()
|
if (window.innerWidth > window.innerHeight) {
|
isLandscape.value = true;
|
updateScreenSize()
|
landscapeBox.style.display = 'flex';
|
lZp.style.right = '16%';
|
lZoom.style.display = 'block';
|
lZoom.style.position = 'fixed';
|
// await nextTick();
|
// await _init('cesiumContainerBigMap');
|
// await getWrjSsLx();
|
} else {
|
console.log('是否进来',landscapeBox)
|
isLandscape.value = false;
|
updateScreenSize()
|
landscapeBox.style.display = 'inherit';
|
lZp.style.right = '28%';
|
lZoom.style.display = 'none';
|
// landscapeBox.style.width = `${screenWidth}`
|
// landscapeBox.style.height = `${screenHeight}`
|
// videoModule.style.width = `${screenWidth}`
|
// videoModule.style.height = `${screenHeight}`
|
// await nextTick();
|
// await _init('cesiumContainerMap');
|
// await getWrjSsLx();
|
}
|
};
|
|
// 获取实时航线
|
const getWrjSsLx = () => {
|
getWaylineFile(sbInfo.sn).then((res:any) => {
|
if (res.code != 200) return
|
ssLinePath.value = res.data
|
globalViewer = window.cesiumViewer
|
// 先有航线,再飞行
|
generateCourse()
|
})
|
}
|
|
// 切换航线轨迹
|
const courseTrack = (data:any) => {
|
if (window.cesiumViewer== null) return
|
const currentSn = dockSn.value
|
const longitude = data.dockInfo[currentSn]?.basic_osd?.longitude || null
|
const latitude = data.dockInfo[currentSn]?.basic_osd?.latitude || null
|
const height = data.dockInfo[currentSn]?.basic_osd?.height || null
|
let getLongOk = ref(0)
|
if (!getEntityById('drone_dock')) {
|
getLnglatAltitude(longitude, latitude, window.cesiumViewer).then(res => {
|
removeById('drone_dock')
|
window.cesiumViewer.entities.add({
|
position: globalCesium.Cartesian3.fromDegrees(
|
longitude,
|
latitude,
|
res.height,
|
),
|
id: 'drone_dock',
|
billboard: {
|
image: imageMapSrc,
|
outlineWidth: 0,
|
width: 36,
|
height: 36,
|
scale: 1.0,
|
}
|
})
|
window.cesiumViewer.scene.camera.setView({
|
destination: globalCesium.Cartesian3.fromDegrees(
|
Number(longitude),
|
Number(latitude),
|
10000.0,
|
),
|
})
|
droneHeight.value = res.height
|
})
|
} else {
|
// 当机场坐标存在时,判断获取的位置与机场坐标位置是否一致,若不是,重新更新位置
|
let dornePoint = cartesian3Convert(
|
getEntityById('drone_dock')._position._value,
|
window.cesiumViewer,
|
)
|
getLongOk = getLnglatDist(
|
dornePoint.longitude,
|
dornePoint.latitude,
|
longitude,
|
latitude,
|
)
|
if (getLongOk.value > 100) {
|
removeById('drone_dock')
|
}
|
}
|
if (getLongOk.value > 100) return
|
initDock(data, sn.value, dockSn.value, workspaceId.value, ssLinePath.value, viewDroneInfo)
|
}
|
|
// 监听
|
watch(() => store.state.deviceState, (newValue, oldValue) => {
|
// 试试
|
if (newValue) {
|
nextTick()
|
courseTrack(newValue)
|
}
|
}, {deep: true})
|
|
watch(() => store.state.wsMessage, (newValue, oldValue) => {
|
viewDroneInfo.longitude = newValue?.longitude
|
viewDroneInfo.latitude = newValue?.latitude
|
viewDroneInfo.elevation = Number(newValue?.elevation.toFixed(2)) + droneHeight.value
|
}, {deep: true})
|
|
|
|
// 生成航线轨迹
|
const generateCourse = async () => {
|
if (!ssLinePath.value) return
|
const { fileInfoObj } = await analyzeKmzFile(`${ssLinePath.value}?_t=${new Date().getTime()}`)
|
const xmlStr = await fileInfoObj['wpmz/template.kml']
|
const xmlJson = XMLToJSON(xmlStr)?.['Document']
|
const placemark = xmlJson.Folder.Placemark
|
if (placemark?.Polygon) {
|
// return 'planar'
|
planarWayline(ssLinePath.value, sbInfo.longitude, sbInfo.latitude)
|
} else {
|
// return 'point'
|
pointWayline(ssLinePath.value, sbInfo.longitude, sbInfo.latitude)
|
}
|
}
|
|
onMounted(async () => {
|
window.addEventListener('orientationchange', checkOrientation);
|
window.addEventListener('resize', checkOrientation); // 兼容某些设备
|
checkOrientation(); // 初始化时检查一次
|
loadVideo()
|
await nextTick();
|
await _init('cesiumContainerBigMap');
|
await getWrjSsLx();
|
|
});
|
|
onBeforeUnmount(() => {
|
window.removeEventListener('orientationchange', checkOrientation);
|
window.removeEventListener('resize', checkOrientation);
|
if (player) {
|
player.dispose();
|
}
|
if (flvPlayer) {
|
flvPlayer.pause(); // 暂停播放
|
flvPlayer.unload(); // 卸载视频
|
flvPlayer.detachMediaElement(); // 解绑 video 元素
|
flvPlayer.destroy(); // 销毁播放器
|
flvPlayer = null;
|
}
|
});
|
|
|
|
</script>
|
|
<style lang="scss" scoped>
|
.drone-pilot-details {
|
.landscape-box {
|
position: relative;
|
display: flex;
|
.l-map {
|
.l-zp {
|
position: fixed;
|
right: 16%;
|
bottom: 0;
|
}
|
.l-zoom {
|
position: fixed;
|
bottom: 0;
|
.zoom {
|
width: 2rem;
|
height: 2rem;
|
border-radius: 3px;
|
background-color: rgba(0, 0, 0, 0.8);
|
display: flex;
|
align-items: center;
|
justify-content: center;
|
overflow: hidden;
|
cursor: pointer;
|
pointer-events: all;
|
img {
|
width: 1.6rem;
|
height: 1.6rem;
|
}
|
}
|
}
|
}
|
}
|
.l-video {
|
.el-empty {
|
width: 100%;
|
height: 100%;
|
padding: 0;
|
::v-deep(.el-empty__image) {
|
width: 50%;
|
height: 50%;
|
}
|
::v-deep(.el-empty__description) {
|
margin: 0;
|
}
|
}
|
}
|
#cesiumContainerBigMap {
|
width: 100%;
|
height: 100%;
|
overflow: hidden;
|
|
:deep() {
|
.cesium-viewer {
|
width: 100%;
|
height: 100%;
|
|
.cesium-viewer-cesiumWidgetContainer {
|
width: 100%;
|
height: 100%;
|
|
.cesium-widget {
|
width: 100%;
|
height: 100%;
|
|
canvas {
|
width: 100%;
|
height: 100%;
|
}
|
}
|
}
|
}
|
}
|
}
|
#cesiumContainerMap {
|
width: 100%;
|
height: 100%;
|
overflow: hidden;
|
|
:deep() {
|
.cesium-viewer {
|
width: 100%;
|
height: 100%;
|
|
.cesium-viewer-cesiumWidgetContainer {
|
width: 100%;
|
height: 100%;
|
|
.cesium-widget {
|
width: 100%;
|
height: 100%;
|
|
canvas {
|
width: 100%;
|
height: 100%;
|
}
|
}
|
}
|
}
|
}
|
}
|
.portrait-screen {
|
.l-map {
|
.l-zp {
|
position: fixed;
|
right: 28%;
|
bottom: 0;
|
}
|
.l-zoom {
|
position: fixed;
|
bottom: 0;
|
.zoom {
|
width: 2rem;
|
height: 2rem;
|
border-radius: 3px;
|
background-color: rgba(0, 0, 0, 0.8);
|
display: flex;
|
align-items: center;
|
justify-content: center;
|
overflow: hidden;
|
cursor: pointer;
|
pointer-events: all;
|
img {
|
width: 1.6rem;
|
height: 1.6rem;
|
}
|
}
|
}
|
}
|
}
|
}
|
</style>
|