<template>
|
<div class="drone-pilot-details" :style="{ width: screenWidth,height: screenHeight}">
|
<!-- 横屏模式 -->
|
<div class="landscape-box" :style="{ width: screenWidth + 'px', height: (screenHeight) + 'px'}" v-if="isLandscape">
|
<div v-show="qhModelBig =='地图'" class="big-box" id="cesiumContainerBigMap"></div>
|
<div v-show="qhModelBig =='无人机'" class="big-box" id="cesiumContainerVideo">
|
<video v-show="videoUrl" ref="videoPlayerBig" :style="{ width: screenWidth + 'px', height: (screenHeight) + '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="landscape-map" v-show="qhModel=='地图'" @click="changeModel('无人机')">
|
<div class="map-box" id="cesiumContainerSmallMap"></div>
|
</div>
|
<!-- 转盘 -->
|
<div class="landscape-zp">
|
<comPass :cesiumViewe="globalViewer"/>
|
</div>
|
<!-- 鹰眼 -->
|
<div ref="videoWH" v-show="qhModel=='无人机'" class="landscape-video">
|
<div class="mapswitching" @click="changeModel('地图')"><img :src="mapswitching" alt=""></div>
|
<div>
|
<video v-show="videoUrl" ref="videoPlayer" :style="{ width: drawCanvasWidth + 'px', height: drawCanvasHeight + '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>
|
<div class="right-box">
|
<ptzControl :sn="sn" :osdVisible="sbInfo" />
|
<controlConsole :sn="sn" :osdVisible="sbInfo" />
|
</div>
|
</div>
|
<!-- 竖屏 -->
|
<div class="portrait-screen" v-else>当前为竖屏模式,请切换至横屏才有更好的体验</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'
|
const { pointWayline } = initPointWayLine()
|
const { planarWayline } = initPlanarWayline()
|
const { initDock } = droneFly()
|
|
const toggleVisibility = () => {
|
const element = document.querySelector('#cesiumContainerBigMap');
|
element.classList.toggle('active');
|
}
|
|
const {
|
removeAllPoint,
|
removeAllDataSource,
|
globalCesium,
|
getEntityById,
|
removeById,
|
} = cesiumOperation()
|
|
const vConsole = new VConsole();
|
|
let globalViewer = null
|
|
// 使用 ref 来存储屏幕宽度和高度
|
const screenWidth = ref(window.innerWidth);
|
const screenHeight = ref(window.innerHeight);
|
|
// 更新屏幕尺寸的函数
|
const updateScreenSize = () => {
|
screenWidth.value = window.innerWidth;
|
screenHeight.value = window.innerHeight;
|
};
|
console.log(screenWidth.value, '宽度')
|
console.log(screenHeight.value, '高度')
|
|
|
// import cesiumOperation from '@/utils/cesium-tsa.js';
|
// const { _init, viewerDestory } = cesiumOperation();
|
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;
|
|
// 无人机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)
|
|
// 切换地图和无人机出现在大屏
|
let qhModelBig = ref('地图')
|
let qhModel = ref('无人机')
|
|
// 鹰眼视频
|
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) => {
|
// 获取到了本地流
|
|
// document.getElementById('selfVideo').srcObject=s;
|
// this.eventcallbacK('LOCAL STREAM', '获取到了本地流')
|
},
|
)
|
}
|
|
// 加载该设备的视频信息
|
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
|
// 播放
|
play()
|
playBig()
|
})
|
}
|
|
// 监听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 changeModel = async (value:any) => {
|
// 销毁当前 Cesium 实例
|
destroyViewer()
|
updateScreenSize()
|
if (value == '地图') {
|
qhModel.value = '地图'
|
qhModelBig.value = '无人机'
|
console.log('横屏执行一次')
|
await nextTick()
|
await _init('cesiumContainerSmallMap')
|
await getWrjSsLx();
|
// 手动调整画布大小
|
// window.cesiumViewer.resize();
|
} else {
|
qhModel.value = '无人机'
|
qhModelBig.value = '地图'
|
console.log('竖屏执行一次')
|
await nextTick()
|
await _init('cesiumContainerBigMap')
|
await getWrjSsLx();
|
}
|
|
}
|
|
const checkOrientation = async () => {
|
// 使用 CSS 媒体查询检测横屏
|
// isLandscape.value = window.matchMedia('(orientation: landscape)').matches;
|
// 横屏再加载视频请求
|
if (window.innerWidth > window.innerHeight) {
|
isLandscape.value = true;
|
updateScreenSize()
|
loadVideo()
|
await nextTick();
|
await _init('cesiumContainerBigMap');
|
await getWrjSsLx();
|
} else {
|
isLandscape.value = false;
|
}
|
};
|
|
// 获取实时航线
|
const getWrjSsLx = () => {
|
getWaylineFile(sbInfo.sn).then((res:any) => {
|
if (res.code != 200) return
|
ssLinePath = 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) return
|
const { fileInfoObj } = await analyzeKmzFile(`${ssLinePath}?_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, sbInfo.longitude, sbInfo.latitude)
|
} else {
|
// return 'point'
|
pointWayline(ssLinePath, sbInfo.longitude, sbInfo.latitude)
|
}
|
}
|
|
onMounted(() => {
|
window.addEventListener('orientationchange', checkOrientation);
|
window.addEventListener('resize', checkOrientation); // 兼容某些设备
|
checkOrientation(); // 初始化时检查一次
|
|
});
|
|
onBeforeUnmount(() => {
|
window.removeEventListener('orientationchange', checkOrientation);
|
window.removeEventListener('resize', checkOrientation);
|
if (player) {
|
player.dispose();
|
}
|
});
|
|
|
|
</script>
|
|
<style lang="scss" scoped>
|
.drone-pilot-details {
|
.fade-element {
|
width: 300px;
|
height: 300px;
|
background-color: lightcoral;
|
display: flex;
|
justify-content: center;
|
align-items: center;
|
transform: scale(0);
|
|
}
|
|
.fade-element.active {
|
transform: scale(1);
|
opacity: 1; /* 显示时透明度为 1 */
|
}
|
.landscape-box {
|
position: relative;
|
#cesiumContainerVideo {
|
// opacity: 0; /* 默认透明度为 0 */
|
// transition: transform 0.8s ease-in-out, opacity 0.8s ease-in-out;
|
}
|
#cesiumContainerVide.active {
|
// transform: scale(1);
|
// opacity: 1; /* 显示时透明度为 1 */
|
}
|
#cesiumContainerBigMap {
|
// transform: scale(0);
|
// transition: transform 0.8s ease-in-out, opacity 0.8s ease-in-out;
|
}
|
#cesiumContainerBigMap.active {
|
// transform: scale(1); /* 放大到原始大小 */
|
// opacity: 1; /* 显示时透明度为 1 */
|
}
|
.big-box {
|
#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%;
|
}
|
}
|
}
|
}
|
}
|
}
|
}
|
.landscape-map {
|
position: fixed;
|
|
width: 30%;
|
height: 34%;// calc(100vh - 60%);
|
left: 10%;
|
bottom: 0;
|
// transition: all 0.5s ease-in-out; /* 平滑过渡效果 */
|
#cesiumContainerSmallMap {
|
border-radius: 6px;
|
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%;
|
}
|
}
|
}
|
}
|
}
|
}
|
}
|
.landscape-zp {
|
position: fixed;
|
left: 50%;
|
bottom: 0;
|
// background: url('@/assets/images/newcon_box.png') no-repeat center / 100% 100%;
|
}
|
.landscape-video {
|
// background: url('@/assets/images/newcon_box.png') no-repeat center / 100% 100%;
|
background-color: rgba(0, 0, 0, 0.5);
|
position: fixed;
|
border-radius: 6px;
|
width: 30%;
|
height: 34%;
|
left: 10%;
|
// right: 2.6rem;
|
bottom: 0;
|
.mapswitching {
|
display: flex;
|
justify-content: flex-end;
|
img {
|
width: 1rem;
|
height: 1rem;
|
}
|
}
|
.el-empty {
|
width: 100%;
|
height: 100%;
|
padding: 0;
|
::v-deep(.el-empty__image) {
|
width: 50%;
|
height: 50%;
|
}
|
::v-deep(.el-empty__description) {
|
margin: 0;
|
}
|
}
|
}
|
.right-box {}
|
}
|
.portrait-screen {
|
color: red;
|
}
|
}
|
</style>
|