Sfoglia il codice sorgente

更新交互方法

gr 5 mesi fa
parent
commit
10ef3a93ed
10 ha cambiato i file con 2126 aggiunte e 5781 eliminazioni
  1. 123 4
      package-lock.json
  2. 2 0
      package.json
  3. 0 3519
      src/adapter.js
  4. 159 60
      src/components/UeVideo.vue
  5. 1 0
      src/main.js
  6. 681 47
      src/utils/UIInteractions.js
  7. 924 0
      src/utils/peer-stream.js
  8. 236 92
      src/views/Home.vue
  9. 0 303
      src/webRtcPlayer.js
  10. 0 1756
      src/webRtcVideo.js

+ 123 - 4
package-lock.json

@@ -9,6 +9,8 @@
       "version": "0.0.0",
       "dependencies": {
         "@vueuse/core": "^11.1.0",
+        "axios": "^1.7.7",
+        "bootstrap": "^5.3.3",
         "echarts": "^5.5.1",
         "element-plus": "^2.8.3",
         "mitt": "^3.0.1",
@@ -509,10 +511,9 @@
       "license": "MIT"
     },
     "node_modules/@popperjs/core": {
-      "name": "@sxzz/popperjs-es",
-      "version": "2.11.7",
-      "resolved": "https://r.cnpmjs.org/@sxzz/popperjs-es/-/popperjs-es-2.11.7.tgz",
-      "integrity": "sha512-Ccy0NlLkzr0Ex2FKvh2X+OyERHXJ88XJ1MXtsI9y9fGexlaXaVTPzBCRBwIxFkORuOb+uBqeu+RqnpgYTEZRUQ==",
+      "version": "2.11.8",
+      "resolved": "https://registry.npmmirror.com/@popperjs/core/-/core-2.11.8.tgz",
+      "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==",
       "license": "MIT",
       "funding": {
         "type": "opencollective",
@@ -979,6 +980,42 @@
       "integrity": "sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg==",
       "license": "MIT"
     },
+    "node_modules/asynckit": {
+      "version": "0.4.0",
+      "resolved": "https://registry.npmmirror.com/asynckit/-/asynckit-0.4.0.tgz",
+      "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
+      "license": "MIT"
+    },
+    "node_modules/axios": {
+      "version": "1.7.7",
+      "resolved": "https://registry.npmmirror.com/axios/-/axios-1.7.7.tgz",
+      "integrity": "sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==",
+      "license": "MIT",
+      "dependencies": {
+        "follow-redirects": "^1.15.6",
+        "form-data": "^4.0.0",
+        "proxy-from-env": "^1.1.0"
+      }
+    },
+    "node_modules/bootstrap": {
+      "version": "5.3.3",
+      "resolved": "https://registry.npmmirror.com/bootstrap/-/bootstrap-5.3.3.tgz",
+      "integrity": "sha512-8HLCdWgyoMguSO9o+aH+iuZ+aht+mzW0u3HIMzVu7Srrpv7EBBxTnrFlSCskwdY1+EOFQSm7uMJhNQHkdPcmjg==",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/twbs"
+        },
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/bootstrap"
+        }
+      ],
+      "license": "MIT",
+      "peerDependencies": {
+        "@popperjs/core": "^2.11.8"
+      }
+    },
     "node_modules/chokidar": {
       "version": "4.0.1",
       "resolved": "https://r.cnpmjs.org/chokidar/-/chokidar-4.0.1.tgz",
@@ -995,6 +1032,18 @@
         "url": "https://paulmillr.com/funding/"
       }
     },
+    "node_modules/combined-stream": {
+      "version": "1.0.8",
+      "resolved": "https://registry.npmmirror.com/combined-stream/-/combined-stream-1.0.8.tgz",
+      "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
+      "license": "MIT",
+      "dependencies": {
+        "delayed-stream": "~1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
     "node_modules/csstype": {
       "version": "3.1.3",
       "resolved": "https://r.cnpmjs.org/csstype/-/csstype-3.1.3.tgz",
@@ -1007,6 +1056,15 @@
       "integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==",
       "license": "MIT"
     },
+    "node_modules/delayed-stream": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmmirror.com/delayed-stream/-/delayed-stream-1.0.0.tgz",
+      "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.4.0"
+      }
+    },
     "node_modules/echarts": {
       "version": "5.5.1",
       "resolved": "https://r.cnpmjs.org/echarts/-/echarts-5.5.1.tgz",
@@ -1200,6 +1258,40 @@
       "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
       "license": "MIT"
     },
+    "node_modules/follow-redirects": {
+      "version": "1.15.9",
+      "resolved": "https://registry.npmmirror.com/follow-redirects/-/follow-redirects-1.15.9.tgz",
+      "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==",
+      "funding": [
+        {
+          "type": "individual",
+          "url": "https://github.com/sponsors/RubenVerborgh"
+        }
+      ],
+      "license": "MIT",
+      "engines": {
+        "node": ">=4.0"
+      },
+      "peerDependenciesMeta": {
+        "debug": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/form-data": {
+      "version": "4.0.1",
+      "resolved": "https://registry.npmmirror.com/form-data/-/form-data-4.0.1.tgz",
+      "integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==",
+      "license": "MIT",
+      "dependencies": {
+        "asynckit": "^0.4.0",
+        "combined-stream": "^1.0.8",
+        "mime-types": "^2.1.12"
+      },
+      "engines": {
+        "node": ">= 6"
+      }
+    },
     "node_modules/fsevents": {
       "version": "2.3.3",
       "resolved": "https://r.cnpmjs.org/fsevents/-/fsevents-2.3.3.tgz",
@@ -1260,6 +1352,27 @@
       "integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==",
       "license": "MIT"
     },
+    "node_modules/mime-db": {
+      "version": "1.52.0",
+      "resolved": "https://registry.npmmirror.com/mime-db/-/mime-db-1.52.0.tgz",
+      "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/mime-types": {
+      "version": "2.1.35",
+      "resolved": "https://registry.npmmirror.com/mime-types/-/mime-types-2.1.35.tgz",
+      "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+      "license": "MIT",
+      "dependencies": {
+        "mime-db": "1.52.0"
+      },
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
     "node_modules/mitt": {
       "version": "3.0.1",
       "resolved": "https://r.cnpmjs.org/mitt/-/mitt-3.0.1.tgz",
@@ -1345,6 +1458,12 @@
         "postcss": ">=5.0.2"
       }
     },
+    "node_modules/proxy-from-env": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmmirror.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
+      "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
+      "license": "MIT"
+    },
     "node_modules/readdirp": {
       "version": "4.0.1",
       "resolved": "https://r.cnpmjs.org/readdirp/-/readdirp-4.0.1.tgz",

+ 2 - 0
package.json

@@ -10,6 +10,8 @@
   },
   "dependencies": {
     "@vueuse/core": "^11.1.0",
+    "axios": "^1.7.7",
+    "bootstrap": "^5.3.3",
     "echarts": "^5.5.1",
     "element-plus": "^2.8.3",
     "mitt": "^3.0.1",

File diff suppressed because it is too large
+ 0 - 3519
src/adapter.js


+ 159 - 60
src/components/UeVideo.vue

@@ -1,79 +1,154 @@
 <template>
-  <div ref="video" id="player">
-    <i id="overlayButton" style="display: none;"></i>
-    <i id="qualityStatus" style="display: none;"></i>
-    <div id="stats" style="display: none;"></div>
+  <div class="ue-wrapper">
+    <div class="connect-status" v-show="connectStatus!=='连接成功'">连接状态:{{ connectStatus }}</div>
+    <div ref="videoContainerRef" class="video-container"></div>
+    <dialog id="login-dialog" data-bs-theme="dark">
+      <h2 class="mb-3 fs-6">登录以获取场景资源</h2>
+      <form :model="form" v-on:submit="handleLogin">
+        <div class="mb-3">
+          <label htmlFor="userName" class="form-label">用户名</label>
+          <input v-model="form.userName" id="userName" type="text" class="form-control" />
+        </div>
+        <div class="mb-3">
+          <label htmlFor="password" class="form-label">密码</label>
+          <input v-model="form.password" id="password" type="password" class="form-control" />
+        </div>
+        <div class="text-center">
+          <button type="submit" class="btn btn-primary">登录</button>
+        </div>
+      </form>
+      <div v-if="loginMsg" class="mt-3 text-danger">{{ loginMsg }}</div>
+    </dialog>
   </div>
 </template>
 
-<script>
+<script setup>
 import { onMounted, ref, getCurrentInstance } from 'vue'
-import { initLoad, addResponseEventListener } from '../webRtcVideo.js'
+import axios from 'axios';
 
-export default {
-  name: 'UeVideo',
+const { proxy } = getCurrentInstance()
 
-  setup(props, context) {
+const connectStatus = ref("未连接"); // State to store the input content
 
-    const { proxy } = getCurrentInstance();
+const peerStreamRef = ref(null)
+// 用来存储视频容器的引用
+const videoContainerRef = ref(null);
 
-    let video = ref(null)
-    let videoInstance = ref(null)
-
-    onMounted(() => {
-
-      // console.log('video.value', video.value);
-
-      videoInstance = initLoad({
-        context,
-        autoConnection: true,
-        showPlayOverlay: false,
-        // serverUrl: 'http://192.168.10.52:60001/',
-        serverUrl: 'http://127.0.0.1:60001/',
-        qualityControl: true,
-        inputOptions: {
-          controlScheme: 1, // 鼠标:0是锁定,1是滑过
-          suppressBrowserKeys: false,
-        },
-        matchViewportResolution: true,
-      })
+// 关闭场景的函数
+const closeScene = () => {
+  if (videoContainerRef.value) {
+    videoContainerRef.value.removeChild(peerStreamRef.value);
+    console.log("Scene closed");
+  }
+};
+
+// 连接场景
+const wsConnect = (sceneId, view_mode, token) => {
+  // 确保 videoContainerRef 指向一个有效的容器
+  if (videoContainerRef.value) {
+    // 清空容器
+    videoContainerRef.value.innerHTML = '';
+    // 创建 PeerStream 元素
+    peerStreamRef.value = document.createElement('video', { is: 'peer-stream' });
+    peerStreamRef.value.dataset.sceneId = sceneId;
+    peerStreamRef.value.dataset.viewMode = view_mode;
+    peerStreamRef.value.dataset.token = token;
+    peerStreamRef.value.style.width = '100%';
+    videoContainerRef.value.appendChild(peerStreamRef.value);
+
+    //场景事件
+    peerStreamRef.value.addEventListener("playing", () => {
+      connectStatus.value = ("连接成功");
+      proxy.$bus.emit('ueConnected')
+    });
+    peerStreamRef.value.addEventListener("disConnected", () => connectStatus.value = ("连接断开"));
+    peerStreamRef.value.addEventListener("message", (e) => {
+      const message = JSON.parse(e.detail);
+      console.log("收到消息:", message)
+      // connectStatus.value = ("收到消息:" + message);
+    });
+  }
+}
 
-      // videoInstance = load()
+const sendDataChannelCommand = (command) => {
+  if (peerStreamRef.value) {
+    // console.error((peerStreamRef.value), (peerStreamRef.value).dc);
+    // Call the send method on the custom peer-stream element
+    (peerStreamRef.value).emitMessage((JSON.parse(command)));
+    console.log('Sending command:', command);
+  }
+}
 
-      //添加UE的消息监听
-      addResponseEventListener('play-video', async (data) => {
-        console.log('接收信息-->', data)
-        let dataObj
-        try {
-          dataObj = JSON.parse(data)
-        } catch (e) {
-          console.log(e)
+const form = ref({
+  userName: 'admin',
+  password: 'citygis1613'
+})
+
+const loginMsg = ref('')
+
+function handleLogin(e) {
+  e.preventDefault();
+  console.log(form.value)
+  const baseUrl = 'http://10.1.161.53:3000'
+  axios.post(`${baseUrl}/auth/login`, form.value)
+  .then(response => {
+    if (response.status === 201) {
+      localStorage.setItem('token', response.data.access_token);
+      // 获取场景列表
+      axios({
+        method: 'get',
+        url: baseUrl + '/scenePermission',
+        headers: {
+          'Authorization': `Bearer ${localStorage.getItem('token')}`
         }
-        if (dataObj) {
-          switch (dataObj.action) {
-            case 'boat_guiji':
-              proxy.$bus.emit('ueRec_boatGuiji', dataObj.data)
-              break
+      }).then(res => {
+        if(res.data) {
+          const targetScene = res.data.find(row => row.name === "低空经济")
+          if(!targetScene) {
+            loginMsg.value = '未发现场景资源'
+            return
           }
-
-        } else {
-
+          wsConnect(targetScene.scene_id, targetScene.permission, localStorage.getItem('token'));
+          // 添加全局调用监听
+          proxy.$bus.on('callUE', (command) => {
+            sendDataChannelCommand(command)
+          })
+          // 关闭弹窗和页面loading
+          const dialog = document.getElementById('login-dialog')
+          dialog.close()
+          proxy.$bus.emit('toggleLoading', false)
         }
-
       })
-
-    })
-
-    return {
-      video,
-      //向UE发送一个字符串.打印在游戏屏幕上
+    } else {
+      loginMsg.value = '登录失败'
     }
-  }
+  })
+  .catch((e) => {
+    console.log(e)
+    loginMsg.value = '登录失败'
+  })
 }
+
+onMounted(() => {
+  const script = document.createElement('script');
+  script.src = '/src/utils/peer-stream.js'; // 自定义元素文件的路径
+  script.async = true;
+  script.onload = () => {
+    console.log('PeerStream custom element script loaded');
+  };
+  document.body.appendChild(script);
+  const dialog = document.getElementById('login-dialog')
+  dialog.showModal()
+
+  // setTimeout(() => {
+  //   proxy.$bus.emit('toggleLoading', false)
+  // }, 1000);
+})
+
 </script>
 
 <style lang="scss" scoped>
-#player {
+.ue-wrapper {
   box-sizing: border-box;
   position: absolute;
   top: 0;
@@ -81,11 +156,35 @@ export default {
   width: 100%;
   height: 100%;
   overflow: hidden;
-  // background-color: #041637;
 
-  .inter-btn {
-    margin-top: 15vh;
+  .connect-status {
+    position: absolute;
+    top: 50%;
+    width: 100%;
+    text-align: center;
+    z-index: 200
   }
 
+  .video-container {
+    position: absolute;
+    top: 0;
+    left: 0;
+    width: 100%;
+    height: 100%;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+  }
+
+  #login-dialog {
+    position: absolute;
+    top: 50%;
+    left: 50%;
+    transform: translateX(-50%) translateY(-50%);
+    padding: 30px 40px;
+    border: 2px solid #003e58;
+    border-radius: 15px;
+    color: #888;
+  }
 }
-</style>
+</style>

+ 1 - 0
src/main.js

@@ -1,6 +1,7 @@
 import { createApp } from 'vue'
 import App from './App.vue'
 import './assets/styles/index.scss'
+import 'bootstrap/dist/css/bootstrap.css'
 import mitt from "mitt";
 
 const app = createApp(App);

+ 681 - 47
src/utils/UIInteractions.js

@@ -1,58 +1,692 @@
-import { callUIInteraction } from "@/webRtcVideo";
-
-
-function callUIInteractionFormat(action) {
-  if(action['Parameters']) {
-    action['Parameters'] = JSON.stringify(action['Parameters'])
-  }
-  console.log('发送信息-->',action)
-  callUIInteraction(action)
-}
-
 /**
- * 仅有 ActionName 的方法
- * @param {String} ActionName 
+ * 获取操作
+ * @param {string} cName 操作对应的自定义名称
+ * @param {object} Params 自定义参数,覆盖 Params 中对应的部分
+ * @returns JSON String
  */
-export function ueCallByName(ActionName) {
-  callUIInteractionFormat({ ActionName })
+function getFn(cName, cParams = {}) {
+  const target = fnList.find(i => i.cName === cName)
+  if (!target) return ''
+  const targetDup = JSON.parse(JSON.stringify(target))
+  const paramKeys = Object.keys(cParams)
+  if (paramKeys.length !== 0) {
+    paramKeys.forEach(key => {
+      targetDup.content.Params[key] = cParams[key]
+    })
+  }
+  return JSON.stringify(targetDup.content)
 }
 
-//example
-export function ueCallxxx() {
-  callUIInteractionFormat({
-    "ActionName":"netLookDown",
-    'Parameters': {}
-  })
-}
+const fnList = [
+  {
+    cName: "toggleGrid",
+    remark: '网格显示/隐藏',
+    content: {
+      "ModuleName": "BluePrint",
+      "ActionName": "",
+      "Params": {
+        "ModuleName": "AirGrid",
+        "ActionName": "ShowHidden",
+        "Id": "Green",  // 'Green' | 'Red'
+        "Visible": true
+      }
+    }
 
+  },
+  {
+    cName: 'toggleRoute',
+    remark: '航路显示/隐藏',
+    content: {
+      "ModuleName": "BluePrint",
+      "ActionName": "",
+      "Params": {
+        "ModuleName": "AirRoute",
+        "ActionName": "ShowHidden",
+        "Id": "1",
+        "Visible": true
+      }
+    }
+
+  },
+  {
+    cName: 'toggleLine',
+    remark: '航线显示/隐藏(Block--碰撞,Normal--无碰撞,Back--返回)',
+    content: {
+      "ModuleName": "BluePrint",
+      "ActionName": "",
+      "Params": {
+        "ModuleName": "AirLine",
+        "ActionName": "ShowHidden",
+        "Id": "Block",   // 'Normal' | 'Block' | 'Back'
+        "Visible": true
+      }
+    }
+  },
+  {
+    cName: 'initUavNormal',
+    remark: '初始化无人机飞行路线--无碰撞路线',
+    content: {
+      "ModuleName": "BluePrint",
+      "ActionName": "",
+      "Params": {
+        "ModuleName": "Uav",
+        "ActionName": "InitUav",
+        "Id": "UavId",
+        "Speed": 2000.0,
+        "SplinePoints": [
+          {
+            "Location": "X=443230,Y=-736980,Z=5250",
+            "Rotation": "Pitch=89,Yaw=71,Roll=0",
+            "Scale": "X=1,Y=1,Z=1"
+          },
+          {
+            "Location": "X=443240,Y=-736952,Z=6610",
+            "Rotation": "Pitch=26,Yaw=101,Roll=0",
+            "Scale": "X=1,Y=1,Z=1"
+          },
+          {
+            "Location": "X=442020,Y=-730610,Z=9720",
+            "Rotation": "Pitch=-3,Yaw=68,Roll=0",
+            "Scale": "X=1,Y=1,Z=1"
+          },
+          {
+            "Location": "X=449660,Y=-711980,Z=8820",
+            "Rotation": "Pitch=-1,Yaw=73,Roll=0",
+            "Scale": "X=1,Y=1,Z=1"
+          },
+          {
+            "Location": "X=454810,Y=-695130,Z=8650",
+            "Rotation": "Pitch=0,Yaw=70,Roll=0",
+            "Scale": "X=1,Y=1,Z=1"
+          },
+          {
+            "Location": "X=466190,Y=-664600,Z=8830",
+            "Rotation": "Pitch=-1,Yaw=49,Roll=0",
+            "Scale": "X=1,Y=1,Z=1"
+          },
+          {
+            "Location": "X=471450,Y=-658450,Z=8710",
+            "Rotation": "Pitch=1,Yaw=33,Roll=0",
+            "Scale": "X=1,Y=1,Z=1"
+          },
+          {
+            "Location": "X=474820,Y=-656240,Z=8780",
+            "Rotation": "Pitch=3,Yaw=-14,Roll=0",
+            "Scale": "X=1,Y=1,Z=1"
+          },
+          {
+            "Location": "X=480660,Y=-657700,Z=9130",
+            "Rotation": "Pitch=0,Yaw=-21,Roll=0",
+            "Scale": "X=1,Y=1,Z=1"
+          },
+          {
+            "Location": "X=489870,Y=-661280,Z=9200",
+            "Rotation": "Pitch=-2,Yaw=-22,Roll=0",
+            "Scale": "X=1,Y=1,Z=1"
+          },
+          {
+            "Location": "X=502730,Y=-666350,Z=8830",
+            "Rotation": "Pitch=0,Yaw=-17,Roll=0",
+            "Scale": "X=1,Y=1,Z=1"
+          },
+          {
+            "Location": "X=517900,Y=-671020,Z=8910",
+            "Rotation": "Pitch=0,Yaw=-34,Roll=0",
+            "Scale": "X=1,Y=1,Z=1"
+          },
+          {
+            "Location": "X=520366,Y=-672669,Z=8910",
+            "Rotation": "Pitch=1,Yaw=-36,Roll=0",
+            "Scale": "X=1,Y=1,Z=1"
+          },
+          {
+            "Location": "X=528910,Y=-678970,Z=9070",
+            "Rotation": "Pitch=1,Yaw=-37,Roll=0",
+            "Scale": "X=1,Y=1,Z=1"
+          },
+          {
+            "Location": "X=536911,Y=-685079,Z=9162",
+            "Rotation": "Pitch=-1,Yaw=1,Roll=0",
+            "Scale": "X=1,Y=1,Z=1"
+          },
+          {
+            "Location": "X=538750,Y=-685060,Z=9130",
+            "Rotation": "Pitch=-10,Yaw=56,Roll=0",
+            "Scale": "X=1,Y=1,Z=1"
+          },
+          {
+            "Location": "X=544910,Y=-675790,Z=7150",
+            "Rotation": "Pitch=0,Yaw=52,Roll=0",
+            "Scale": "X=1,Y=1,Z=1"
+          },
+          {
+            "Location": "X=547850,Y=-672060,Z=7150",
+            "Rotation": "Pitch=0,Yaw=65,Roll=0",
+            "Scale": "X=1,Y=1,Z=1"
+          },
+          {
+            "Location": "X=550480,Y=-666430,Z=7150",
+            "Rotation": "Pitch=-34,Yaw=28,Roll=0",
+            "Scale": "X=1,Y=1,Z=1"
+          },
+          {
+            "Location": "X=554110,Y=-664540,Z=4370",
+            "Rotation": "Pitch=-90,Yaw=0,Roll=0",
+            "Scale": "X=1,Y=1,Z=1"
+          },
+          {
+            "Location": "X=554110,Y=-664540,Z=2040",
+            "Rotation": "Pitch=0,Yaw=0,Roll=0",
+            "Scale": "X=1,Y=1,Z=1"
+          }
+        ]
+      }
+    }
+  },
+  {
+    cName: 'initUavBlock',
+    remark: '初始化无人机飞行路线--有碰撞路线',
+    content: {
+      "ModuleName": "BluePrint",
+      "ActionName": "",
+      "Params": {
+        "ModuleName": "Uav",
+        "ActionName": "InitUav",
+        "Id": "UavId",
+        "Speed": 2000.0,
+        "SplinePoints": [
+          {
+            "Location": "X=443230,Y=-736980,Z=5250",
+            "Rotation": "Pitch=89,Yaw=71,Roll=0",
+            "Scale": "X=1,Y=1,Z=1"
+          },
+          {
+            "Location": "X=443240,Y=-736952,Z=6610",
+            "Rotation": "Pitch=26,Yaw=84,Roll=0",
+            "Scale": "X=1,Y=1,Z=1"
+          },
+          {
+            "Location": "X=443890,Y=-730610,Z=9720",
+            "Rotation": "Pitch=-3,Yaw=68,Roll=0",
+            "Scale": "X=1,Y=1,Z=1"
+          },
+          {
+            "Location": "X=451260,Y=-711980,Z=8820",
+            "Rotation": "Pitch=-1,Yaw=70,Roll=0",
+            "Scale": "X=1,Y=1,Z=1"
+          },
+          {
+            "Location": "X=457420,Y=-695130,Z=8650",
+            "Rotation": "Pitch=0,Yaw=69,Roll=0",
+            "Scale": "X=1,Y=1,Z=1"
+          },
+          {
+            "Location": "X=469110,Y=-664600,Z=8830",
+            "Rotation": "Pitch=-1,Yaw=69,Roll=0",
+            "Scale": "X=1,Y=1,Z=1"
+          },
+          {
+            "Location": "X=471450,Y=-658450,Z=8710",
+            "Rotation": "Pitch=1,Yaw=33,Roll=0",
+            "Scale": "X=1,Y=1,Z=1"
+          },
+          {
+            "Location": "X=474820,Y=-656240,Z=8780",
+            "Rotation": "Pitch=3,Yaw=-14,Roll=0",
+            "Scale": "X=1,Y=1,Z=1"
+          },
+          {
+            "Location": "X=480660,Y=-657700,Z=9130",
+            "Rotation": "Pitch=0,Yaw=-21,Roll=0",
+            "Scale": "X=1,Y=1,Z=1"
+          },
+          {
+            "Location": "X=489870,Y=-661280,Z=9200",
+            "Rotation": "Pitch=-2,Yaw=-22,Roll=0",
+            "Scale": "X=1,Y=1,Z=1"
+          },
+          {
+            "Location": "X=502730,Y=-666350,Z=8830",
+            "Rotation": "Pitch=0,Yaw=-17,Roll=0",
+            "Scale": "X=1,Y=1,Z=1"
+          },
+          {
+            "Location": "X=517900,Y=-671020,Z=8910",
+            "Rotation": "Pitch=0,Yaw=-34,Roll=0",
+            "Scale": "X=1,Y=1,Z=1"
+          },
+          {
+            "Location": "X=520366,Y=-672669,Z=8910",
+            "Rotation": "Pitch=1,Yaw=-36,Roll=0",
+            "Scale": "X=1,Y=1,Z=1"
+          },
+          {
+            "Location": "X=528910,Y=-678970,Z=9070",
+            "Rotation": "Pitch=1,Yaw=-37,Roll=0",
+            "Scale": "X=1,Y=1,Z=1"
+          },
+          {
+            "Location": "X=536911,Y=-685079,Z=9162",
+            "Rotation": "Pitch=-1,Yaw=1,Roll=0",
+            "Scale": "X=1,Y=1,Z=1"
+          },
+          {
+            "Location": "X=538750,Y=-685060,Z=9130",
+            "Rotation": "Pitch=-10,Yaw=56,Roll=0",
+            "Scale": "X=1,Y=1,Z=1"
+          },
+          {
+            "Location": "X=544910,Y=-675790,Z=7150",
+            "Rotation": "Pitch=0,Yaw=52,Roll=0",
+            "Scale": "X=1,Y=1,Z=1"
+          },
+          {
+            "Location": "X=547850,Y=-672060,Z=7150",
+            "Rotation": "Pitch=0,Yaw=65,Roll=0",
+            "Scale": "X=1,Y=1,Z=1"
+          },
+          {
+            "Location": "X=550480,Y=-666430,Z=7150",
+            "Rotation": "Pitch=-34,Yaw=28,Roll=0",
+            "Scale": "X=1,Y=1,Z=1"
+          },
+          {
+            "Location": "X=554110,Y=-664540,Z=4370",
+            "Rotation": "Pitch=-90,Yaw=0,Roll=0",
+            "Scale": "X=1,Y=1,Z=1"
+          },
+          {
+            "Location": "X=554110,Y=-664540,Z=2040",
+            "Rotation": "Pitch=0,Yaw=0,Roll=0",
+            "Scale": "X=1,Y=1,Z=1"
+          }
+        ]
+      }
+    }
+  },
+  {
+    cName: 'initUavBack',
+    remark: '初始化无人机飞行路线--返航路线',
+    content: {
+      "ModuleName": "BluePrint",
+      "ActionName": "",
+      "Params": {
+        "ModuleName": "Uav",
+        "ActionName": "InitUav",
+        "Id": "UavId",
+        "Speed": 2000.0,
+        "SplinePoints": [
+          {
+            "Location": "X=554110,Y=-664540,Z=2040",
+            "Rotation": "Pitch=0,Yaw=0,Roll=0",
+            "Scale": "X=1,Y=1,Z=1"
+          },
+          {
+            "Location": "X=554110,Y=-664540,Z=4370",
+            "Rotation": "Pitch=-90,Yaw=0,Roll=0",
+            "Scale": "X=1,Y=1,Z=1"
+          },
+          {
+            "Location": "X=550480,Y=-666430,Z=7150",
+            "Rotation": "Pitch=-34,Yaw=28,Roll=0",
+            "Scale": "X=1,Y=1,Z=1"
+          },
+          {
+            "Location": "X=547850,Y=-672060,Z=9640",
+            "Rotation": "Pitch=-22,Yaw=65,Roll=0",
+            "Scale": "X=1,Y=1,Z=1"
+          },
+          {
+            "Location": "X=544910,Y=-675790,Z=9890",
+            "Rotation": "Pitch=-3,Yaw=52,Roll=0",
+            "Scale": "X=1,Y=1,Z=1"
+          },
+          {
+            "Location": "X=538750,Y=-685060,Z=10990",
+            "Rotation": "Pitch=-6,Yaw=56,Roll=0",
+            "Scale": "X=1,Y=1,Z=1"
+          },
+          {
+            "Location": "X=536911,Y=-685079,Z=10982",
+            "Rotation": "Pitch=0,Yaw=1,Roll=0",
+            "Scale": "X=1,Y=1,Z=1"
+          },
+          {
+            "Location": "X=528910,Y=-678970,Z=11490",
+            "Rotation": "Pitch=-3,Yaw=-37,Roll=0",
+            "Scale": "X=1,Y=1,Z=1"
+          },
+          {
+            "Location": "X=520366,Y=-672669,Z=11310",
+            "Rotation": "Pitch=1,Yaw=-36,Roll=0",
+            "Scale": "X=1,Y=1,Z=1"
+          },
+          {
+            "Location": "X=517900,Y=-671020,Z=11180",
+            "Rotation": "Pitch=3,Yaw=-34,Roll=0",
+            "Scale": "X=1,Y=1,Z=1"
+          },
+          {
+            "Location": "X=502730,Y=-666350,Z=10840",
+            "Rotation": "Pitch=1,Yaw=-17,Roll=0",
+            "Scale": "X=1,Y=1,Z=1"
+          },
+          {
+            "Location": "X=489870,Y=-661280,Z=10980",
+            "Rotation": "Pitch=-1,Yaw=-22,Roll=0",
+            "Scale": "X=1,Y=1,Z=1"
+          },
+          {
+            "Location": "X=480660,Y=-657700,Z=11030",
+            "Rotation": "Pitch=0,Yaw=-21,Roll=0",
+            "Scale": "X=1,Y=1,Z=1"
+          },
+          {
+            "Location": "X=474820,Y=-656240,Z=11090",
+            "Rotation": "Pitch=-1,Yaw=-14,Roll=0",
+            "Scale": "X=1,Y=1,Z=1"
+          },
+          {
+            "Location": "X=471450,Y=-658450,Z=10900",
+            "Rotation": "Pitch=3,Yaw=33,Roll=0",
+            "Scale": "X=1,Y=1,Z=1"
+          },
+          {
+            "Location": "X=467700,Y=-664600,Z=10880",
+            "Rotation": "Pitch=0,Yaw=59,Roll=0",
+            "Scale": "X=1,Y=1,Z=1"
+          },
+          {
+            "Location": "X=456270,Y=-695130,Z=11360",
+            "Rotation": "Pitch=-1,Yaw=69,Roll=0",
+            "Scale": "X=1,Y=1,Z=1"
+          },
+          {
+            "Location": "X=449660,Y=-711980,Z=11570",
+            "Rotation": "Pitch=-1,Yaw=69,Roll=0",
+            "Scale": "X=1,Y=1,Z=1"
+          },
+          {
+            "Location": "X=442020,Y=-730610,Z=11930",
+            "Rotation": "Pitch=-1,Yaw=68,Roll=0",
+            "Scale": "X=1,Y=1,Z=1"
+          },
+          {
+            "Location": "X=443240,Y=-736952,Z=6610",
+            "Rotation": "Pitch=39,Yaw=101,Roll=0",
+            "Scale": "X=1,Y=1,Z=1"
+          },
+          {
+            "Location": "X=443230,Y=-736980,Z=5250",
+            "Rotation": "Pitch=89,Yaw=71,Roll=0",
+            "Scale": "X=1,Y=1,Z=1"
+          }
+        ]
+      }
+    }
+  },
+  {
+    cName: 'initUavFall',
+    remark: '初始化无人机飞行路线--坠落路线',
+    content: {
+      "ModuleName": "BluePrint",
+      "ActionName": "",
+      "Params": {
+        "ModuleName": "Uav",
+        "ActionName": "InitUav",
+        "Id": "UavId",
+        "Speed": 2000.0,
+        "SplinePoints": [
+          {
+            "Location": "X=443230,Y=-736980,Z=5250",
+            "Rotation": "Pitch=89,Yaw=71,Roll=0",
+            "Scale": "X=1,Y=1,Z=1"
+          },
+          {
+            "Location": "X=443240,Y=-736952,Z=6610",
+            "Rotation": "Pitch=39,Yaw=101,Roll=0",
+            "Scale": "X=1,Y=1,Z=1"
+          },
+          {
+            "Location": "X=442020,Y=-730610,Z=11930",
+            "Rotation": "Pitch=-1,Yaw=68,Roll=0",
+            "Scale": "X=1,Y=1,Z=1"
+          },
+          {
+            "Location": "X=449660,Y=-711980,Z=11570",
+            "Rotation": "Pitch=-1,Yaw=69,Roll=0",
+            "Scale": "X=1,Y=1,Z=1"
+          },
+          {
+            "Location": "X=456270,Y=-695130,Z=11360",
+            "Rotation": "Pitch=-86,Yaw=90,Roll=0",
+            "Scale": "X=1,Y=1,Z=1"
+          },
+          {
+            "Location": "X=456270,Y=-694860,Z=7390",
+            "Rotation": "Pitch=-83,Yaw=90,Roll=0",
+            "Scale": "X=1,Y=1,Z=1"
+          },
+          {
+            "Location": "X=456270,Y=-694270,Z=2290",
+            "Rotation": "Pitch=0,Yaw=0,Roll=0",
+            "Scale": "X=1,Y=1,Z=1"
+          }
+        ]
+      }
+    }
+  },
+  {
+    cName: 'startUav',
+    remark: '无人机开始飞行',
+    content: {
+      "ModuleName": "BluePrint",
+      "ActionName": "",
+      "Params": {
+        "ModuleName": "Uav",
+        "ActionName": "StartUav",
+        "Id": "UavId"
+      }
+    }
+  },
+  {
+    cName: 'pauseUav',
+    remark: '无人机暂停飞行',
+    content: {
+      "ModuleName": "BluePrint",
+      "ActionName": "",
+      "Params": {
+        "ModuleName": "Uav",
+        "ActionName": "PauseUav",
+        "Id": "UavId"
+      }
+    }
+  },
+  {
+    cName: 'stopUav',
+    remark: '无人机停止飞行',
+    content: {
+      "ModuleName": "BluePrint",
+      "ActionName": "",
+      "Params": {
+        "ModuleName": "Uav",
+        "ActionName": "StopUav",
+        "Id": "UavId"
+      }
+    }
+  },
+  {
+    cName: 'startFollowUav',
+    remark: '无人机视角跟随-开始',
+    content: {
+      "ModuleName": "BluePrint",
+      "ActionName": "",
+      "Params": {
+        "ModuleName": "Uav",
+        "ActionName": "StartPawnFollowUav",
+        "Id": "UavId"
+      }
+    }
+  },
+  {
+    cName: 'endFollowUav',
+    remark: '无人机视角跟随-结束',
+    content: {
+      "ModuleName": "BluePrint",
+      "ActionName": "",
+      "Params": {
+        "ModuleName": "Uav",
+        "ActionName": "EndPawnFollowUav"
+      }
+    }
+  },
+  {
+    cName: 'setGoodsVisible',
+    remark: '无人机设置货物显隐',
+    content: {
+      "ModuleName": "BluePrint",
+      "ActionName": "",
+      "Params": {
+        "ModuleName": "Uav",
+        "ActionName": "SetGoodsBox",
+        "Id": "UavId",
+        "Visible": false
+      }
+    }
+  },
+  {
+    cName: 'destroyUav',
+    remark: '无人机销毁',
+    content: {
+      "ModuleName": "BluePrint",
+      "ActionName": "",
+      "Params": {
+        "ModuleName": "Uav",
+        "ActionName": "DestroyUav",
+        "Id": "UavId"
+      }
+    }
+  },
+  {
+    cName: 'toggleSignalTower',
+    remark: '信号塔显示/隐藏',
+    content: {
+      "ModuleName": "BluePrint",
+      "ActionName": "",
+      "Params": {
+        "ModuleName": "SignalTower",
+        "ActionName": "PoiShowHidden",
+        "Visible": true
+      }
+    }
+  },
+  {
+    cName: 'locateTakeoff',
+    remark: '起飞点定位',
+    content: {
+      "ModuleName": "Roam",
+      "ActionName": "Goto",
+      "Params": {
+        "X": 441357.803266,
+        "Y": -738441.043188,
+        "Z": 6880.074496,
+        "Pitch": -37.385139,
+        "Yaw": 36.402233,
+        "Roll": -0.000219,
+        "Duration": 0.1
+      }
+    }
+  },
+  {
+    cName: 'locateLandfall',
+    remark: '降落点定位',
+    content: {
+      "ModuleName": "Roam",
+      "ActionName": "Goto",
+      "Params": {
+        "X": 552993.01402,
+        "Y": -663972.824815,
+        "Z": 2846.776697,
+        "Pitch": -32.906322,
+        "Yaw": -30.157532,
+        "Roll": -1.833872,
+        "Duration": 0.1
+      }
+    }
+  },
+  {
+    cName: 'resetCamera',
+    remark: '回到初始视角',
+    content: {
+      "ModuleName": "Roam",
+      "ActionName": "Goto",
+      "Params": {
+        "X": 424678.164214,
+        "Y": -750393.023912,
+        "Z": 26005.213753,
+        "Pitch": -21.6,
+        "Yaw": 42.600002,
+        "Roll": 0.0,
+        "Duration": 0.1
+      }
+    }
+  },
+  {
+    cName: 'setRotateSpeed',
+    remark: '调整操控灵敏度',
+    content: {
+      "ModuleName": "Roam",
+      "ActionName": "Update",
+      "Params": {
+        "MovementSpeedMultiplier": 0.03,
+        "RotationSpeedMultiplier": 0.5,
+        "ScaleSpeedMultiplier": 0.1,
+        "MinMovementLimit": {
+          "X": 0,
+          "Y": 0,
+          "Z": 0
+        },
+        "MaxMovementLimit": {
+          "X": 0,
+          "Y": 0,
+          "Z": 0
+        }
+      }
+    }
+  }
+]
+
+// 旧版功能
 const nameFunctions = {
+  // '正常航线': "rodOkShow", ✅
+  // '异常航线': "rodNoShow", ✅
+  // '隐藏航线': "rodHidden", ✅
+  // '网格展示': "netShow", ✅
+  // '网格隐藏': "netHidden", ✅
+  // '适飞区': "netGreen", ✅
+  // '禁飞区': "netRed", ✅
+  // '跟随飞行(自动来回)': "flay", ✅ (暂无法实现自动来回)
+  // '清除跟随无人机': "clearFlowFlay", ✅
+  // '起飞点定位': "upLocation", ✅
+  // '降落点定位': "downLocation", ✅
+  // '航路显示': "rowShow", ✅
+  // '航路隐藏': "rowHidden", ✅
+  // '信号塔显示': "signShow", ✅
+  // '信号塔隐藏': "signHidden", ✅
+  // '坠落飞行': "downFly", ✅
+  // '风险区': "netYellow", ❌
+  // '空间网格整体查看': "netLookUp", ❌
+  // '查看航路视角移动': "lookFlyRow", ❌
+  // '飞行全景查看': "LookflayAll", ❌
+  // '场景概览': "lookAll", ❌
+  // '预警视角移动': "yujingFly" ❌
+
+
   '区域展示': "areaInfoShow",
   '区域隐藏': "areaInfoHidden",
-  // '起飞点定位': "upLocation",
-  // '降落点定位': "downLocation",
-  // '网格展示': "netShow",
-  // '网格隐藏': "netHidden",
-  // '适飞区': "netGreen",
-  // '风险区': "netYellow",
-  // '禁飞区': "netRed",
-  // '正常航线': "rodOkShow",
-  // '异常航线': "rodNoShow",
-  // '隐藏航线': "rodHidden",
   '清除有问题航线并查看没问题航线': "autoRow",
   '自动生成对面无人机': "autoMakeFlay",
-  // '跟随飞行(自动来回)': "flay",
-  '清除跟随无人机': "clearFlowFlay",
-  '清除自动生成无人机'   : "closeAutoFlay",
-  // '空间网格整体查看': "netLookUp",
-  // '查看航路视角移动': "lookFlyRow",
-  // '飞行全景查看': "LookflayAll",
-  // '坠落飞行': "downFly",
-  // '场景概览': "lookAll",
-  // '航路显示': "rowShow",
-  // '航路隐藏': "rowHidden",
-  // '信号塔显示': "signShow",
-  // '信号塔隐藏': "signHidden",
-
-  // '预警视角移动': "yujingFly"
+  '清除自动生成无人机': "closeAutoFlay",
+}
 
-}
+export default getFn

+ 924 - 0
src/utils/peer-stream.js

@@ -0,0 +1,924 @@
+'5.1.3';
+// Must be kept in sync with JavaScriptKeyCodeToFKey C++ array.
+// special keycodes different from KeyboardEvent.keyCode
+const SpecialKeyCodes = {
+  Backspace: 8,
+  ShiftLeft: 16,
+  ControlLeft: 17,
+  AltLeft: 18,
+  ShiftRight: 253,
+  ControlRight: 254,
+  AltRight: 255,
+};
+// https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/button
+const MouseButton = {
+  MainButton: 0, // Left button.
+  AuxiliaryButton: 1, // Wheel button.
+  SecondaryButton: 2, // Right button.
+  FourthButton: 3, // Browser Back button.
+  FifthButton: 4, // Browser Forward button.
+};
+// https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/buttons#value
+const MouseButtonsMask = {
+  1: 0,
+  2: 2,
+  4: 1,
+  8: 3,
+  16: 4,
+};
+// Must be kept in sync with PixelStreamingProtocol::EToClientMsg C++ enum.
+const RECEIVE = {
+  QualityControlOwnership: 0,
+  Response: 1,
+  Command: 2,
+  FreezeFrame: 3,
+  UnfreezeFrame: 4,
+  VideoEncoderAvgQP: 5,
+  LatencyTest: 6,
+  InitialSettings: 7,
+  FileExtension: 8,
+  FileMimeType: 9,
+  FileContents: 10,
+  InputControlOwnership: 12,
+  CompositionStart: 64,
+  Protocol: 255,
+};
+// Must be kept in sync with PixelStreamingProtocol::EToUE4Msg C++ enum.
+const SEND = {
+  /*
+   * Control Messages. Range = 0..49.
+   */
+  IFrameRequest: 0,
+  RequestQualityControl: 1,
+  FpsRequest: 2,
+  AverageBitrateRequest: 3,
+  StartStreaming: 4,
+  StopStreaming: 5,
+  LatencyTest: 6,
+  RequestInitialSettings: 7,
+  /*
+   * Input Messages. Range = 50..89.
+   */
+  // Generic Input Messages. Range = 50..59.
+  UIInteraction: 50,
+  Command: 51,
+  // Keyboard Input Message. Range = 60..69.
+  KeyDown: 60,
+  KeyUp: 61,
+  KeyPress: 62,
+  FindFocus: 63,
+  CompositionEnd: 64,
+  // Mouse Input Messages. Range = 70..79.
+  MouseEnter: 70,
+  MouseLeave: 71,
+  MouseDown: 72,
+  MouseUp: 73,
+  MouseMove: 74,
+  MouseWheel: 75,
+  // Touch Input Messages. Range = 80..89.
+  TouchStart: 80,
+  TouchEnd: 81,
+  TouchMove: 82,
+  // Gamepad Input Messages. Range = 90..99
+  GamepadButtonPressed: 90,
+  GamepadButtonReleased: 91,
+  GamepadAnalog: 92,
+};
+let iceServers = undefined;
+
+class PeerStream extends HTMLVideoElement {
+  constructor() {
+    super();
+    window.ps = this;
+    this.ws = {
+      send() {},
+      close() {},
+    }; // WebSocket
+    this.pc = {
+      close() {},
+    }; // RTCPeerConnection
+    this.setupVideo();
+    this.registerKeyboardEvents();
+    this.registerMouseHoverEvents();
+    this.registerFakeMouseEvents();
+    
+    // 获取自定义属性
+    this.sceneId = this.dataset.sceneId;
+    this.viewMode = this.dataset.viewMode;
+    this.token = this.dataset.token;
+    // 初始化事件处理器
+    this._onClose = null;
+    this._onOpen = null;
+    this._onMessage = null;
+
+    document.addEventListener(
+      'pointerlockchange',
+      () => {
+        if (document.pointerLockElement === this) {
+          this.registerPointerLockEvents();
+        } else {
+          this.registerMouseHoverEvents();
+        }
+      },
+      false,
+    );
+    this.addEventListener('loadeddata', (e) => {
+      this.style['aspect-ratio'] = this.videoWidth / this.videoHeight;
+    });
+    // this.setupPeerConnection();
+  }
+// 用户设置事件处理函数
+    set onclose(callback) {
+        if (typeof callback === 'function') {
+            this._onClose = callback;
+        }
+    }
+
+    set onopen(callback) {
+        if (typeof callback === 'function') {
+            this._onOpen = callback;
+        }
+    }
+    set onmessage(callback) {
+      if (typeof callback === 'function') {
+          this._onMessage = callback;
+      }
+  }
+  checkWebRTCSupport() {
+    // Step 2: Check for RTCPeerConnection
+    const RTCPeerConnection =
+      window.RTCPeerConnection ||
+      window.webkitRTCPeerConnection ||
+      window.mozRTCPeerConnection;
+    if (!RTCPeerConnection) {
+      console.warn('checkWebRTCSupport RTCPeerConnection not supported');
+      return false;
+    }
+    // Step 3: Check for DataChannel
+    let dataChannelSupported = false;
+    let pc = null;
+    if (RTCPeerConnection) {
+      try {
+        pc = new RTCPeerConnection();
+        const dc = pc.createDataChannel('test');
+        dataChannelSupported = !!dc;
+        dc.close(); // Close the DataChannel when done
+        pc.close();
+      } catch (e) {
+        console.error(e);
+        console.warn('checkWebRTCSupport dataChannelSupported not supported');
+        return false;
+      }
+      if (!dataChannelSupported) {
+        console.warn('checkWebRTCSupport DataChannel not supported');
+        return false;
+      }
+    }
+    return true;
+  }
+
+  // setupWebsocket
+  async connectedCallback() {
+    if (false == this.checkWebRTCSupport()) {
+      const overlayDiv = document.createElement('div');
+      overlayDiv.innerHTML =
+        '你的浏览器版本过低!<br>推荐使用谷歌100以上版本浏览器!!';
+      overlayDiv.style.position = 'absolute';
+      overlayDiv.style.top = '50%';
+      overlayDiv.style.left = '50%';
+      overlayDiv.style.transform = 'translate(-50%, -50%)';
+      overlayDiv.style.background = 'rgba(255, 255, 255, 0.8)';
+      overlayDiv.style.padding = '10px';
+      overlayDiv.style.borderRadius = '5px';
+      overlayDiv.style.display = 'block'; // Initially hidden
+      this.parentNode.appendChild(overlayDiv);
+    }
+    // This will happen each time the node is moved, and may happen before the element"s contents have been fully parsed. may be called once your element is no longer connected
+    if (!this.isConnected) return;
+    if (
+      this.pc.connectionState === 'connected' &&
+      this.dc.readyState === 'open' &&
+      this.ws.readyState === 1
+    ) {
+      // this.pc.restartIce();
+      this.play();
+      return;
+    }
+    // await new Promise((res) => setTimeout(res, 1000));
+    this.ws.onclose = null;
+    this.ws.close(1000);
+    this.sceneId = this.dataset.sceneId;
+    this.viewMode = this.dataset.viewMode;
+    this.token = this.dataset.token;
+    const wsUrl =
+      'ws:10.1.161.53:3000' + 
+      `?sceneId=${this.sceneId}&token=${this.token}&&view_mode=${this.viewMode}`;
+    this.ws = new WebSocket(wsUrl, 'peer-stream');
+    this.ws.onerror;
+    this.ws.onopen = () => {
+      console.info('✅', this.ws);
+    };
+    this.ws.onmessage = (e) => {
+      this.onWebSocketMessage(e.data);
+    };
+    this.ws.onclose = (e) => {
+      console.warn(e);
+      this.dispatchEvent(new CustomEvent('playerdisconnected', {}));
+      this.dispatchEvent(new CustomEvent('disConnected', {}));
+      // clearTimeout(this.reconnect);
+      // this.reconnect = setTimeout(() => this.connectedCallback(), 3000);
+    };
+  }
+  // 当属性发生变化时,会调用这个回调
+  static get observedAttributes() {
+    return ['sceneId', 'token','viewMode']; // 监听的属性
+  }
+  disconnectedCallback() {
+    if(this._onClose)this._onClose();
+    // lifecycle binding
+    setTimeout(() => {
+      if (this.isConnected) return;
+      this.ws.close(1000);
+      this.pc.close();
+      console.log('❌ peer connection closing');
+      if(this.dc)this.dc.close();
+    }, 100);
+  }
+
+  adoptedCallback() {}
+
+  attributeChangedCallback(name, oldValue, newValue) {
+    if (!this.isConnected) return;
+    // fired before connectedCallback when startup
+    this.ws.close(1000);
+    //
+    if (name === 'viewMode') {
+      this.viewMode = newValue;
+    } else if (name === 'sceneId') {
+      this.sceneId = newValue ; // controls 属性只接受 'true' 或 'false'
+    } else if(name === 'token') {
+      this.token = newValue;
+    }
+  }
+
+  async onWebSocketMessage(msg) {
+    try {
+      msg = JSON.parse(msg);
+    } catch {
+      console.debug('↓↓', msg);
+      return;
+    }
+    console.error(msg.type);
+    if (msg.type === 'offer') {
+      this.setupPeerConnection();
+      const offer = new RTCSessionDescription(msg);
+      console.log('↓↓ offer', offer);
+      await this.pc.setRemoteDescription(offer);
+      // Setup a transceiver for getting UE video
+      this.pc.addTransceiver('video', { direction: 'recvonly' });
+      const answer = await this.pc.createAnswer();
+      await this.pc.setLocalDescription(answer);
+      console.log('↑↑ answer', answer);
+      this.ws.send(JSON.stringify(answer));
+      for (let receiver of this.pc.getReceivers()) {
+        receiver.playoutDelayHint = 0;
+      }
+    } else if (msg.type === 'iceCandidate') {
+      const candidate = new RTCIceCandidate(msg.candidate);
+      console.log('↓↓ candidate:', candidate);
+      await this.pc.addIceCandidate(candidate);
+    } else if (msg.type === 'answer') {
+      const answer = new RTCSessionDescription(msg);
+      await this.pc.setRemoteDescription(answer);
+      console.log('↓↓ answer:', answer);
+      for (const receiver of this.pc.getReceivers()) {
+        receiver.playoutDelayHint = 0;
+      }
+    } else if (msg.type === 'playerqueue') {
+      this.dispatchEvent(new CustomEvent('playerqueue', { detail: msg }));
+      console.log('↓↓ playerqueue:', msg);
+    } else if (msg.type === 'setIceServers') {
+      iceServers = msg.iceServers;
+      console.log('↓↓ setIceServers:', msg);
+    } else if (msg.type === 'playerConnected') {
+      console.log('↓↓ playerConnected:', msg);
+      this.setupPeerConnection_ue4();
+      this.setupDataChannel_ue4();
+    } else if (msg.type === 'ping') {
+      console.log('↓↓ ping:', msg);
+      msg.type = 'pong';
+      this.ws.send(JSON.stringify(msg));
+      if (this.mouseReleaseTime) {
+        let now = new Date();
+        if (now - this.lastmouseTime > this.mouseReleaseTime * 1000) {
+          msg.type = 'mouseRelease';
+          this.ws.send(JSON.stringify(msg));
+        }
+      }
+    } else if (msg.type === 'ueDisConnected') {
+      this.dispatchEvent(new CustomEvent('ueDisConnected', { detail: msg }));
+      console.log('↓↓ ueDisConnected:', msg);
+    } else if (msg.type === 'setmouseReleaseTime') {
+      this.mouseReleaseTime = msg.mouseReleaseTime;
+      this.lastmouseTime = new Date();
+      console.log('↓↓ setmouseReleaseTime:', msg);
+    } else if (msg.type === 'getStatus') {
+      console.log('↓↓ getStatus:', msg);
+      this.handleGetStatus(msg);
+    } else {
+      console.warn('↓↓', msg);
+    }
+  }
+
+  handleGetStatus(msg) {
+    if (false == this.pc instanceof RTCPeerConnection) {
+      msg.videoencoderqp = null;
+      msg.netrate = null;
+      this.ws.send(JSON.stringify(msg));
+      console.log('↑↑ handleGetStatus:', msg);
+      return;
+    }
+    let initialBytesReceived = 0;
+    // 获取初始统计信息
+    this.pc.getStats(null).then((stats) => {
+      stats.forEach((report) => {
+        if (report.type === 'transport') {
+          initialBytesReceived = report.bytesReceived;
+        }
+      });
+    });
+    // 等待指定的时间间隔后再次获取统计信息
+    let durationInSeconds = 0.2;
+    setTimeout(() => {
+      this.pc.getStats(null).then((stats) => {
+        stats.forEach((report) => {
+          if (report.type === 'transport') {
+            const finalBytesReceived = report.bytesReceived;
+            const bytesReceived = finalBytesReceived - initialBytesReceived;
+            // 计算平均带宽(单位:字节/秒)
+            const averageReceiveBandwidth =
+              ((bytesReceived / durationInSeconds) * 8) / 1000 / 1000;
+            msg.videoencoderqp = this.VideoEncoderQP;
+            msg.netrate = averageReceiveBandwidth.toFixed(2);
+            this.ws.send(JSON.stringify(msg));
+            console.log('↑↑ handleGetStatus:', msg);
+          }
+        });
+      });
+    }, durationInSeconds * 1000);
+  }
+
+  onDataChannelMessage(data) {
+    data = new Uint8Array(data);
+    const utf16 = new TextDecoder('utf-16');
+    switch (data[0]) {
+      case RECEIVE.VideoEncoderAvgQP: {
+        this.VideoEncoderQP = +utf16.decode(data.slice(1));
+        // console.debug("↓↓ QP:", this.VideoEncoderQP);
+        break;
+      }
+      case RECEIVE.Response: {
+        // user custom message
+        const detail = utf16.decode(data.slice(1));
+        this.dispatchEvent(new CustomEvent('message', { detail }));
+        console.info(detail);
+        break;
+      }
+      case RECEIVE.Command: {
+        const command = JSON.parse(utf16.decode(data.slice(1)));
+        console.info('↓↓ command:', command);
+        if (command.command === 'onScreenKeyboard') {
+          console.info('You should setup a on-screen keyboard');
+          if (command.showOnScreenKeyboard) {
+            if (this.enableChinese) {
+              let input = document.createElement('input');
+              input.style.position = 'fixed';
+              input.style.zIndex = -1;
+              input.autofocus = true;
+              document.body.append(input);
+              input.focus();
+              input.addEventListener('compositionend', (e) => {
+                console.log(e.data);
+                this.emitMessage(e.data, SEND.CompositionEnd);
+              });
+              input.addEventListener('blue', (e) => {
+                input.remove();
+              });
+              input.addEventListener('keydown', (e) => {
+                this.onkeydown(e);
+              });
+              input.addEventListener('keyup', (e) => {
+                this.onkeyup(e);
+              });
+              input.addEventListener('keypress', (e) => {
+                this.onkeypress(e);
+              });
+            }
+          }
+        }
+        break;
+      }
+      case RECEIVE.FreezeFrame: {
+        const size = new DataView(data.slice(1, 5).buffer).getInt32(0, true);
+        const jpeg = data.slice(1 + 4);
+        console.info('↓↓ freezed frame:', jpeg);
+        break;
+      }
+      case RECEIVE.UnfreezeFrame: {
+        console.info('↓↓ 【unfreeze frame】');
+        break;
+      }
+      case RECEIVE.LatencyTest: {
+        const latencyTimings = JSON.parse(utf16.decode(data.slice(1)));
+        console.info('↓↓ latency timings:', latencyTimings);
+        break;
+      }
+      case RECEIVE.QualityControlOwnership: {
+        this.QualityControlOwnership = data[1] !== 0;
+        console.info(
+          '↓↓ Quality Control Ownership:',
+          this.QualityControlOwnership,
+        );
+        break;
+      }
+      case RECEIVE.InitialSettings: {
+        this.InitialSettings = JSON.parse(utf16.decode(data.slice(1)));
+        console.log('↓↓ initial setting:', this.InitialSettings);
+        break;
+      }
+      case RECEIVE.InputControlOwnership: {
+        this.InputControlOwnership = data[1] !== 0;
+        console.log('↓↓ input control ownership:', this.InputControlOwnership);
+        break;
+      }
+      case RECEIVE.Protocol: {
+        let protocol = JSON.parse(utf16.decode(data.slice(1)));
+        console.log(protocol);
+        if (protocol.Direction === 0) {
+          for (let key in protocol) {
+            SEND[key] = protocol[key].id;
+          }
+        } else if (protocol.Direction === 1) {
+          for (let key in protocol) {
+            RECEIVE[key] = protocol[key].id;
+          }
+        }
+        this.dc.send(new Uint8Array([SEND.RequestInitialSettings]));
+        this.dc.send(new Uint8Array([SEND.RequestQualityControl]));
+        break;
+      }
+      default: {
+        console.error('↓↓ invalid data:', data);
+      }
+    }
+  }
+
+  setupVideo() {
+    this.tabIndex = 0; // easy to focus..
+    // this.autofocus = true;
+    this.playsInline = true;
+    this.disablepictureinpicture = true;
+    // Recently many browsers can only autoplay the videos with sound off
+    this.muted = true;
+    this.autoplay = true;
+    // this.onsuspend
+    // this.onresize
+    // this.requestPointerLock();
+    this.style['pointer-events'] = 'none';
+    this.style['object-fit'] = 'fill';
+  }
+
+  setupDataChannel(e) {
+    // See https://www.w3.org/TR/webrtc/#dom-rtcdatachannelinit for values (this is needed for Firefox to be consistent with Chrome.)
+    // this.dc = this.pc.createDataChannel(label, { ordered: true });
+    this.dc = e.channel;
+    // Inform browser we would like binary data as an ArrayBuffer (FF chooses Blob by default!)
+    this.dc.binaryType = 'arraybuffer';
+    this.dc.onopen = (e) => {
+      console.log('✅', this.dc);
+      this.style.pointerEvents = 'auto';
+      // setTimeout(() => {
+      // 	this.dc.send(new Uint8Array([SEND.RequestInitialSettings]));
+      // 	this.dc.send(new Uint8Array([SEND.RequestQualityControl]));
+      // }, 500);
+    };
+    this.dc.onclose = (e) => {
+      console.info('❌ data channel closed');
+      this.style.pointerEvents = 'none';
+      this.blur();
+      // this.dispatchEvent(new CustomEvent('ueDisConnected', {  }));
+    };
+    this.dc.onerror;
+    this.dc.onmessage = (e) => {
+      this.onDataChannelMessage(e.data);
+    };
+  }
+
+  setupDataChannel_ue4(label = 'hello') {
+    // See https://www.w3.org/TR/webrtc/#dom-rtcdatachannelinit for values (this is needed for Firefox to be consistent with Chrome.)
+    this.dc = this.pc.createDataChannel(label, { ordered: true });
+    // Inform browser we would like binary data as an ArrayBuffer (FF chooses Blob by default!)
+    this.dc.binaryType = 'arraybuffer';
+    this.dc.onopen = (e) => {
+      console.log('✅ data channel connected:', label);
+      this.style.pointerEvents = 'auto';
+      this.dc.send(new Uint8Array([SEND.RequestInitialSettings]));
+      this.dc.send(new Uint8Array([SEND.RequestQualityControl]));
+    };
+    this.dc.onclose = (e) => {
+      console.info('❌ data channel closed:', label);
+      this.style.pointerEvents = 'none';
+    };
+    this.dc.onmessage = (e) => {
+      this.onDataChannelMessage(e.data);
+    };
+  }
+
+  setupPeerConnection() {
+    this.pc.close();
+    this.pc = new RTCPeerConnection({
+      sdpSemantics: 'unified-plan',
+      bundlePolicy: 'balanced',
+      iceServers: iceServers,
+    });
+    this.pc.ontrack = (e) => {
+      console.log(`↓↓ ${e.track.kind} track:`, e);
+      if (e.track.kind === 'video') {
+        this.srcObject = e.streams[0];
+      } else if (e.track.kind === 'audio') {
+        this.audio = document.createElement('audio');
+        this.audio.autoplay = true;
+        this.audio.srcObject = e.streams[0];
+      }
+    };
+    this.pc.onicecandidate = (e) => {
+      // firefox
+      if (e.candidate?.candidate) {
+        console.log('↑↑ candidate:', e.candidate);
+        this.ws.send(
+          JSON.stringify({ type: 'iceCandidate', candidate: e.candidate }),
+        );
+      } else {
+        // Notice that the end of negotiation is detected here when the event"s candidate property is null.
+      }
+    };
+    this.pc.ondatachannel = (e) => {
+      this.setupDataChannel(e);
+    };
+  }
+
+  setupPeerConnection_ue4() {
+    this.pc.close();
+    this.pc = new RTCPeerConnection({
+      sdpSemantics: 'unified-plan',
+      bundlePolicy: 'balanced',
+      iceServers: iceServers,
+    });
+    this.pc.ontrack = (e) => {
+      console.log(`↓↓ ${e.track.kind} track:`, e);
+      if (e.track.kind === 'video') {
+        this.srcObject = e.streams[0];
+      } else if (e.track.kind === 'audio') {
+        this.audio = document.createElement('audio');
+        this.audio.autoplay = true;
+        this.audio.srcObject = e.streams[0];
+      }
+    };
+    this.pc.onicecandidate = (e) => {
+      // firefox
+      if (e.candidate?.candidate) {
+        console.log('↑↑ candidate:', e.candidate);
+        this.ws.send(
+          JSON.stringify({ type: 'iceCandidate', candidate: e.candidate }),
+        );
+      } else {
+        // Notice that the end of negotiation is detected here when the event"s candidate property is null.
+      }
+    };
+    this.pc.onnegotiationneeded = (e) => {
+      this.setupOffer();
+    };
+  }
+
+  async setupOffer() {
+    // this.pc.addTransceiver("video", { direction: "recvonly" });
+    const offer = await this.pc.createOffer({
+      offerToReceiveAudio: +this.hasAttribute('audio'),
+      offerToReceiveVideo: 1,
+      voiceActivityDetection: false,
+    });
+    // this indicate we support stereo (Chrome needs this)
+    offer.sdp = offer.sdp.replace(
+      'useinbandfec=1',
+      'useinbandfec=1;stereo=1;sprop-maxcapturerate=48000',
+    );
+    this.pc.setLocalDescription(offer);
+    this.ws.send(JSON.stringify(offer));
+    console.log('↓↓ sending offer:', offer);
+  }
+
+  keysDown = new Set();
+
+  registerKeyboardEvents() {
+    this.onkeydown = (e) => {
+      const keyCode = SpecialKeyCodes[e.code] || e.keyCode;
+      this.dc.send(new Uint8Array([SEND.KeyDown, keyCode, e.repeat]));
+      this.keysDown.add(keyCode);
+      // Backspace is not considered a keypress in JavaScript but we need it
+      // to be so characters may be deleted in a UE text entry field.
+      if (e.keyCode === SpecialKeyCodes.Backspace) {
+        this.onkeypress({
+          keyCode: SpecialKeyCodes.Backspace,
+        });
+      }
+      // whether to prevent browser"s default behavior when keyboard/mouse have inputs, like F1~F12 and Tab
+      // e.preventDefault();
+    };
+    this.onkeyup = (e) => {
+      const keyCode = SpecialKeyCodes[e.code] || e.keyCode;
+      this.dc.send(new Uint8Array([SEND.KeyUp, keyCode]));
+      this.keysDown.delete(keyCode);
+    };
+    this.onkeypress = (e) => {
+      const data = new DataView(new ArrayBuffer(3));
+      data.setUint8(0, SEND.KeyPress);
+      data.setUint16(1, SpecialKeyCodes[e.code] || e.keyCode, true);
+      this.dc.send(data);
+    };
+    this.onblur = (e) => {
+      this.keysDown.forEach((keyCode) => {
+        this.dc.send(new Uint8Array([SEND.KeyUp, keyCode]));
+      });
+      this.keysDown.clear();
+    };
+  }
+
+  registerTouchEvents() {
+    // We need to assign a unique identifier to each finger.
+    // We do this by mapping each Touch object to the identifier.
+    const fingers = [9, 8, 7, 6, 5, 4, 3, 2, 1, 0];
+    const fingerIds = {};
+    this.ontouchstart = (e) => {
+      // Assign a unique identifier to each touch.
+      for (const touch of e.changedTouches) {
+        // remember touch
+        const finger = fingers.pop();
+        if (finger === undefined) {
+          console.info('exhausted touch indentifiers');
+        }
+        fingerIds[touch.identifier] = finger;
+      }
+      this.emitTouchData(SEND.TouchStart, e.changedTouches, fingerIds);
+      e.preventDefault();
+    };
+    this.ontouchend = (e) => {
+      this.emitTouchData(SEND.TouchEnd, e.changedTouches, fingerIds);
+      // Re-cycle unique identifiers previously assigned to each touch.
+      for (const touch of e.changedTouches) {
+        // forget touch
+        fingers.push(fingerIds[touch.identifier]);
+        delete fingerIds[touch.identifier];
+      }
+      e.preventDefault();
+    };
+    this.ontouchmove = (e) => {
+      this.emitTouchData(SEND.TouchMove, e.touches, fingerIds);
+      e.preventDefault();
+    };
+  }
+
+  // touch as mouse
+  registerFakeMouseEvents() {
+    let finger = undefined;
+    const { left, top } = this.getBoundingClientRect();
+    this.ontouchstart = (e) => {
+      if (finger === undefined) {
+        const firstTouch = e.changedTouches[0];
+        finger = {
+          id: firstTouch.identifier,
+          x: firstTouch.clientX - left,
+          y: firstTouch.clientY - top,
+        };
+        // Hack: Mouse events require an enter and leave so we just enter and leave manually with each touch as this event is not fired with a touch device.
+        this.onmouseenter(e);
+        this.emitMouseDown(MouseButton.MainButton, finger.x, finger.y);
+      }
+      e.preventDefault();
+    };
+    this.ontouchend = (e) => {
+      // filtering multi finger touch events temporarily
+      if (finger) {
+        for (const touch of e.changedTouches) {
+          if (touch.identifier === finger.id) {
+            const x = touch.clientX - left;
+            const y = touch.clientY - top;
+            this.emitMouseUp(MouseButton.MainButton, x, y);
+            // Hack: Manual mouse leave event.
+            this.onmouseleave(e);
+            finger = undefined;
+            break;
+          }
+        }
+      }
+      e.preventDefault();
+    };
+    this.ontouchmove = (e) => {
+      // filtering multi finger touch events temporarily
+      if (finger) {
+        for (const touch of e.touches) {
+          if (touch.identifier === finger.id) {
+            const x = touch.clientX - left;
+            const y = touch.clientY - top;
+            this.emitMouseMove(x, y, x - finger.x, y - finger.y);
+            finger.x = x;
+            finger.y = y;
+            break;
+          }
+        }
+      }
+      e.preventDefault();
+    };
+  }
+
+  registerMouseHoverEvents() {
+    this.registerMouseEnterAndLeaveEvents();
+    this.onmousemove = (e) => {
+      this.emitMouseMove(e.offsetX, e.offsetY, e.movementX, e.movementY);
+      e.preventDefault();
+    };
+    this.onmousedown = (e) => {
+      this.emitMouseDown(e.button, e.offsetX, e.offsetY);
+      // e.preventDefault();
+    };
+    this.onmouseup = (e) => {
+      this.emitMouseUp(e.button, e.offsetX, e.offsetY);
+      // e.preventDefault();
+    };
+    // When the context menu is shown then it is safest to release the button which was pressed when the event happened. This will guarantee we will get at least one mouse up corresponding to a mouse down event. Otherwise the mouse can get stuck.
+    // https://github.com/facebook/react/issues/5531
+    this.oncontextmenu = (e) => {
+      this.emitMouseUp(e.button, e.offsetX, e.offsetY);
+      e.preventDefault();
+    };
+    this.onwheel = (e) => {
+      this.emitMouseWheel(e.wheelDelta, e.offsetX, e.offsetY);
+      e.preventDefault();
+    };
+  }
+
+  registerPointerLockEvents() {
+    this.registerMouseEnterAndLeaveEvents();
+    console.info('mouse locked in, ESC to exit');
+    const { clientWidth, clientHeight } = this;
+    let x = clientWidth / 2;
+    let y = clientHeight / 2;
+    this.onmousemove = (e) => {
+      x += e.movementX;
+      y += e.movementY;
+      x = (x + clientWidth) % clientWidth;
+      y = (y + clientHeight) % clientHeight;
+      this.emitMouseMove(x, y, e.movementX, e.movementY);
+    };
+    this.onmousedown = (e) => {
+      this.emitMouseDown(e.button, x, y);
+    };
+    this.onmouseup = (e) => {
+      this.emitMouseUp(e.button, x, y);
+    };
+    this.onwheel = (e) => {
+      this.emitMouseWheel(e.wheelDelta, x, y);
+    };
+  }
+
+  registerMouseEnterAndLeaveEvents() {
+    this.onmouseenter = (e) => {
+      this.dc.send(new Uint8Array([SEND.MouseEnter]));
+    };
+    this.onmouseleave = (e) => {
+      if (this.dc.readyState === 'open')
+        this.dc.send(new Uint8Array([SEND.MouseLeave]));
+      // 释放掉
+      for (let i = 1; i <= 16; i *= 2) {
+        if (e.buttons & i) {
+          this.emitMouseUp(MouseButtonsMask[i], 0, 0);
+        }
+      }
+    };
+  }
+
+  emitMouseMove(x, y, deltaX, deltaY) {
+    const coord = this.normalize(x, y);
+    deltaX = (deltaX * 65536) / this.clientWidth;
+    deltaY = (deltaY * 65536) / this.clientHeight;
+    const data = new DataView(new ArrayBuffer(9));
+    data.setUint8(0, SEND.MouseMove);
+    data.setUint16(1, coord.x, true);
+    data.setUint16(3, coord.y, true);
+    data.setInt16(5, deltaX, true);
+    data.setInt16(7, deltaY, true);
+    this.dc.send(data);
+    this.lastmouseTime = new Date();
+  }
+
+  emitMouseDown(button, x, y) {
+    const coord = this.normalize(x, y);
+    const data = new DataView(new ArrayBuffer(6));
+    data.setUint8(0, SEND.MouseDown);
+    data.setUint8(1, button);
+    data.setUint16(2, coord.x, true);
+    data.setUint16(4, coord.y, true);
+    this.dc.send(data);
+    if (this.enableChinese) {
+      this.dc.send(new Uint8Array([SEND.FindFocus]));
+    }
+  }
+
+  emitMouseUp(button, x, y) {
+    const coord = this.normalize(x, y);
+    const data = new DataView(new ArrayBuffer(6));
+    data.setUint8(0, SEND.MouseUp);
+    data.setUint8(1, button);
+    data.setUint16(2, coord.x, true);
+    data.setUint16(4, coord.y, true);
+    this.dc.send(data);
+  }
+
+  emitMouseWheel(delta, x, y) {
+    const coord = this.normalize(x, y);
+    const data = new DataView(new ArrayBuffer(7));
+    data.setUint8(0, SEND.MouseWheel);
+    data.setInt16(1, delta, true);
+    data.setUint16(3, coord.x, true);
+    data.setUint16(5, coord.y, true);
+    this.dc.send(data);
+  }
+
+  emitTouchData(type, touches, fingerIds) {
+    const data = new DataView(new ArrayBuffer(2 + 6 * touches.length));
+    data.setUint8(0, type);
+    data.setUint8(1, touches.length);
+    let byte = 2;
+    for (const touch of touches) {
+      const x = touch.clientX - this.offsetLeft;
+      const y = touch.clientY - this.offsetTop;
+      const coord = this.normalize(x, y);
+      data.setUint16(byte, coord.x, true);
+      byte += 2;
+      data.setUint16(byte, coord.y, true);
+      byte += 2;
+      data.setUint8(byte, fingerIds[touch.identifier], true);
+      byte += 1;
+      data.setUint8(byte, 255 * touch.force, true); // force is between 0.0 and 1.0 so quantize into byte.
+      byte += 1;
+    }
+    this.dc.send(data);
+  }
+
+  // emit string
+  emitMessage(msg, messageType = SEND.UIInteraction) {
+    if (typeof msg !== 'string') msg = JSON.stringify(msg);
+    // Add the UTF-16 JSON string to the array byte buffer, going two bytes at a time.
+    const data = new DataView(new ArrayBuffer(1 + 2 + 2 * msg.length));
+    let byteIdx = 0;
+    data.setUint8(byteIdx, messageType);
+    byteIdx++;
+    data.setUint16(byteIdx, msg.length, true);
+    byteIdx += 2;
+    for (let i = 0; i < msg.length; i++) {
+      // charCodeAt() is UTF-16, codePointAt() is Unicode.
+      data.setUint16(byteIdx, msg.charCodeAt(i), true);
+      byteIdx += 2;
+    }
+    this.dc.send(data);
+    return new Promise((resolve) =>
+      this.addEventListener('message', (e) => resolve(e.detail), {
+        once: true,
+      }),
+    );
+  }
+
+  normalize(x, y) {
+    const normalizedX = x / this.clientWidth;
+    const normalizedY = y / this.clientHeight;
+    if (
+      normalizedX < 0.0 ||
+      normalizedX > 1.0 ||
+      normalizedY < 0.0 ||
+      normalizedY > 1.0
+    ) {
+      return {
+        inRange: false,
+        x: 65535,
+        y: 65535,
+      };
+    } else {
+      return {
+        inRange: true,
+        x: normalizedX * 65536,
+        y: normalizedY * 65536,
+      };
+    }
+  }
+}
+
+customElements.define('peer-stream', PeerStream, { extends: 'video' });

+ 236 - 92
src/views/Home.vue

@@ -1,7 +1,7 @@
 <template>
-  <div class="home-wrapper" :class="{'loading': pageLoading}">
+  <div class="home-wrapper" :class="{ 'loading': pageLoading }">
     <div v-if="pageLoading" class="my-loader"></div>
-    
+
     <div class="top">
       <h2 class="t-title">杨浦低空经济运营管理平台</h2>
     </div>
@@ -10,7 +10,7 @@
       <button class="bottom-collapse" @click="toggleAside()"></button>
       <ul class="tools">
         <li v-for="item in tools" @click="handleClickTool(item)">
-          <span>{{ item.label }}</span>
+          <span :class="{ 'active': item.isOn }">{{ item.label }}</span>
         </li>
       </ul>
     </div>
@@ -28,14 +28,14 @@
       </div>
       <button class="aside-collapse" @click="toggleAside('left')"></button>
       <div class="aside-deco"></div>
-      <div class="aside-shadow" :class="{'is-hide': panelHide.left}"></div>
-      <div class="aside-main left-main" :class="{'is-hide': panelHide.left}">
-        <div class="title-block"><span>区域信息</span></div>
+      <div class="aside-shadow" :class="{ 'is-hide': panelHide.left }"></div>
+      <div class="aside-main left-main" :class="{ 'is-hide': panelHide.left }">
+        <div class="title-block" @dblclick="handleResetCamera"><span>区域信息</span></div>
         <ul class="block-qyxx">
           <li v-for="(item, index) in panelData.qyxx" :key="item.title" @click="handleClickQyxx(item)">
             <img :src="item.icon" alt="">
             <div>
-              <span :class="{'active': qyxxActive===item.title}">{{ item.title }}</span>
+              <span :class="{ 'active': qyxxActive === item.title }">{{ item.title }}</span>
               <div>
                 <span>{{ item.count }}</span>
                 <span>个</span>
@@ -47,7 +47,7 @@
         <div class="dialog dialog-jld" v-if="dialogShow.jldList">
           <div class="header">
             <span>降落点</span>
-            <i @click="dialogShow.jldList=false"></i>
+            <i @click="dialogShow.jldList = false"></i>
           </div>
           <div class="main">
             <table class="table-default">
@@ -59,8 +59,8 @@
                 </tr>
               </thead>
               <tbody>
-                <tr v-for="(item,index) in dialogData.jldList">
-                  <td style="padding-left: 20px;">{{ index+1 }}</td>
+                <tr v-for="(item, index) in dialogData.jldList">
+                  <td style="padding-left: 20px;">{{ index + 1 }}</td>
                   <td>{{ item.name }}</td>
                   <td>
                     <button class="my-button" @click="handleClickJld(item)">定位</button>
@@ -84,7 +84,8 @@
           <div class="header"><span>网格规划</span></div>
           <ul class="main">
             <li v-for="item in dialogData.legend">
-              <i :style="{backgroundColor: item.color}"></i>
+              <input checked type="checkbox" class="form-check-input mt-0" @change="(e)=>handleToggleLayer(e, item)">
+              <i :style="{ backgroundColor: item.color }"></i>
               <span>{{ item.label }}</span>
             </li>
           </ul>
@@ -104,8 +105,10 @@
                 <td style="padding-left: 10px;">{{ item.from }}</td>
                 <td>{{ item.to }}</td>
                 <td>
-                  <button class="my-button" :class="{'orange': item.hasChecked}" @click="handleClickCkhl(item)">查看航路</button>
-                  <button v-if="item.hasPlaned>0" class="my-button orange" @click="handleLCickCxgh(item)">重新规划</button>
+                  <button class="my-button" :class="{ 'orange': item.hasChecked }"
+                    @click="handleClickCkhl(item)">查看航路</button>
+                  <button v-if="item.hasPlaned > 0" class="my-button orange"
+                    @click="handleLCickCxgh(item)">重新规划</button>
                   <button v-else class="my-button" @click="handleClickHxgh(item)">航线规划</button>
                 </td>
               </tr>
@@ -135,7 +138,7 @@
 
       </div>
     </div>
-    
+
     <div class="right">
       <div class="date-time">
         <span class="now-date">{{ date }}</span>
@@ -143,8 +146,8 @@
       </div>
       <button class="aside-collapse" @click="toggleAside('right')"></button>
       <div class="aside-deco"></div>
-      <div class="aside-shadow" :class="{'is-hide': panelHide.right}"></div>
-      <div class="aside-main right-main" :class="{'is-hide': panelHide.right}">
+      <div class="aside-shadow" :class="{ 'is-hide': panelHide.right }"></div>
+      <div class="aside-main right-main" :class="{ 'is-hide': panelHide.right }">
         <div class="title-block"><span>实时飞行</span></div>
         <ul class="block-ssfx">
           <li v-for="item in panelData.ssfx" @click="handleClickSsfx(item)">
@@ -160,7 +163,7 @@
         <div class="dialog dialog-uav" v-if="dialogShow.uavList">
           <div class="header">
             <span>无人机</span>
-            <i @click="dialogShow.uavList=false"></i>
+            <i @click="dialogShow.uavList = false"></i>
           </div>
           <div class="main">
             <table class="table-default">
@@ -175,8 +178,8 @@
                 </tr>
               </thead>
               <tbody>
-                <tr v-for="(item,index) in dialogData.uavList" @click="ueCallByName('flay')">
-                  <td style="padding-left: 10px;">{{ index+1 }}</td>
+                <tr v-for="(item, index) in dialogData.uavList" @click="handlePickUav(item)" :class="{'active': item.isOn}">
+                  <td style="padding-left: 10px;">{{ index + 1 }}</td>
                   <td>{{ item.seriesNo }}</td>
                   <td>{{ item.owner }}</td>
                   <td>{{ item.class }}</td>
@@ -204,7 +207,7 @@
               <tr v-for="item in panelData.fxsp">
                 <td style="padding-left: 20px;">{{ item.time }}</td>
                 <td>{{ item.applyBy }}</td>
-                <td v-if="item.status==='已审批'" style="color: #A6FFD2;">{{ item.status }}</td>
+                <td v-if="item.status === '已审批'" style="color: #A6FFD2;">{{ item.status }}</td>
                 <td v-else style="color: #FF4000;">{{ item.status }}</td>
               </tr>
             </tbody>
@@ -226,20 +229,22 @@
     </div>
 
     <UeVideo class="ue" />
-    
+
   </div>
 </template>
 
 <script setup>
-import { ref, onBeforeMount, reactive, onMounted } from 'vue'
+import { ref, onBeforeMount, reactive, onMounted, getCurrentInstance } from 'vue'
 import { getAssetsFile } from '@/utils/require'
 import { setSdtj } from '@/echarts/options.js'
 import { useNow, useDateFormat } from '@vueuse/core'
-import { ueCallByName } from '@/utils/UIInteractions'
+import getUeFn from '@/utils/UIInteractions'
 import UeVideo from '@/components/UeVideo.vue'
 
 const pageLoading = ref(true)
 
+const { proxy } = getCurrentInstance()
+
 const panelHide = reactive({
   left: false,
   right: false
@@ -247,7 +252,7 @@ const panelHide = reactive({
 
 function toggleAside(target) {
   Object.keys(dialogShow).forEach(key => dialogShow[key] = false)
-  if(target) {
+  if (target) {
     panelHide[target] = !panelHide[target]
   } else {
     panelHide.left = panelHide.right = !(panelHide.left && panelHide.right)
@@ -255,17 +260,61 @@ function toggleAside(target) {
 }
 
 const tools = ref([
-  { label: '场景游览', isOn: false, onAction: 'lookAll' },
-  { label: '网格游览', isOn: false, onAction: 'netLookUp' },
-  { label: '预警航线', isOn: false, onAction: 'yujingFly' },
-  { label: '航线游览', isOn: false, onAction: 'lookFlyRow' },
-  { label: '飞行全景', isOn: false, onAction: 'LookflayAll' },
-  { label: '坠落模拟', isOn: false, onAction: 'downFly' },
-  { label: '停止跟随', isOn: false, onAction: 'clearFlowFlay' },
+  // { label: '场景游览', isOn: false },
+  // { label: '网格游览', isOn: false },
+  // { label: '预警航线', isOn: false },
+  // { label: '航线游览', isOn: false },
+  // { label: '飞行全景', isOn: false },
+  { label: '坠落模拟', isOn: false },
+  { label: '停止跟随', isOn: false, onAction: getUeFn('endFollowUav') },
 ])
 
+// 开始无人机模拟
+function startUavDemo(routeName) {
+  proxy.$bus.emit('callUE', getUeFn(routeName))
+  setTimeout(() => {
+    proxy.$bus.emit('callUE', getUeFn('startUav'))
+  }, 1000);
+  setTimeout(() => {
+    proxy.$bus.emit('callUE', getUeFn('startFollowUav'))
+  }, 1500);
+}
+
+// 结束无人机模拟
+function endUavDemo(reset = true) {
+  proxy.$bus.emit('callUE', getUeFn('endFollowUav'))
+  setTimeout(() => {
+    proxy.$bus.emit('callUE', getUeFn('destroyUav'))
+  }, 1000);
+  if (reset) {
+    setTimeout(() => {
+      proxy.$bus.emit('callUE', getUeFn('resetCamera'))
+    }, 1500);
+  }
+}
+
 function handleClickTool(tool) {
-  ueCallByName(tool.onAction)
+  tool.isOn = !(tool.isOn)
+  if (tool.isOn && tool.onAction) {
+    proxy.$bus.emit('callUE', tool.onAction)
+    if (!(tool.offAction)) {
+      setTimeout(() => {
+        tool.isOn = false
+      }, 300);
+    }
+  } else if (!(tool.isOn) && tool.offAction) {
+    proxy.$bus.emit('callUE', tool.offAction)
+  } else {
+    switch (tool.label) {
+      case '坠落模拟':
+        if (tool.isOn) {
+          startUavDemo('initUavFall')
+        } else {
+          endUavDemo()
+        }
+        break;
+    }
+  }
 }
 
 const time = useDateFormat(useNow(), 'HH:mm:ss')
@@ -280,13 +329,17 @@ const weather = ref({
 
 const panelData = reactive({
   qyxx: [
-    { title: '信号塔', count: 3, icon: getAssetsFile('icon-xinht.png'), onAction: 'signShow', offAction: 'signHidden' },
-    { title: '起飞点', count: 1, icon: getAssetsFile('icon-qifd.png'), onAction: 'upLocation' },
+    {
+      title: '信号塔', count: 3, icon: getAssetsFile('icon-xinht.png'),
+      onAction: getUeFn('toggleSignalTower', { Visible: true }),
+      offAction: getUeFn('toggleSignalTower', { Visible: false })
+    },
+    { title: '起飞点', count: 1, icon: getAssetsFile('icon-qifd.png'), onAction: getUeFn('locateTakeoff') },
     { title: '降落点', count: 7, icon: getAssetsFile('icon-jiangld.png') },
   ],
   kygh: [
-    { title: '空域网格', icon: getAssetsFile('icon-kywg.png'), isOn: false, onAction: ['netShow'], offAction: 'netHidden' },
-    { title: '网格规划', icon: getAssetsFile('icon-wggh.png'), isOn: false, onAction: ['netGreen', 'netYellow', 'netRed'], offAction: 'netHidden' },
+    { title: '空域网格', icon: getAssetsFile('icon-kywg.png'), isOn: false },
+    { title: '网格规划', icon: getAssetsFile('icon-wggh.png'), isOn: false },
   ],
   hxgh: [
     { from: '合生汇', to: '黄兴公园', hasPlaned: 0, hasChecked: false },
@@ -320,44 +373,40 @@ const panelData = reactive({
 
 function handleClickHxgh(item) {
   item.hasPlaned = 1
-  ueCallByName('rodNoShow')
+  proxy.$bus.emit('callUE', getUeFn('toggleLine', { Visible: true, Id: 'Block' }))
 }
 
 function handleLCickCxgh(item) {
-  if(item.hasPlaned===1) {
-    ueCallByName('rodOkShow')
+  if (item.hasPlaned === 1) {
+    proxy.$bus.emit('callUE', getUeFn('toggleLine', { Visible: false, Id: 'Block' }))
+    proxy.$bus.emit('callUE', getUeFn('toggleLine', { Visible: true, Id: 'Normal' }))
     item.hasPlaned++
-  } else if(item.hasPlaned===2) {
-    ueCallByName('rodHidden')
+  } else if (item.hasPlaned === 2) {
+    proxy.$bus.emit('callUE', getUeFn('toggleLine', { Visible: false, Id: 'Normal' }))
     item.hasPlaned = 0
   }
 }
 
 function handleClickCkhl(item) {
-  if(item.hasChecked) {
-    ueCallByName('rowHidden')
-    item.hasChecked = false
-  } else {
-    item.hasChecked = true
-    ueCallByName('rowShow')
-  }
+  item.hasChecked = !(item.hasChecked)
+  proxy.$bus.emit('callUE', getUeFn('toggleRoute', { Visible: item.hasChecked }))
 }
 
 const qyxxActive = ref('')
 
 function handleClickQyxx(item) {
-  if(item.title==='降落点') {
+  if (item.title === '降落点') {
     dialogShow.jldList = true
     dialogShow.uavList = false
     return
   }
-  qyxxActive.value = item.title===qyxxActive.value? '': item.title
-  if(qyxxActive.value) {
-    ueCallByName(item.onAction)
-  } else if(item.offAction) {
-    ueCallByName(item.offAction)
+  qyxxActive.value = item.title === qyxxActive.value ? '' : item.title
+  if (qyxxActive.value && item.onAction) {
+    proxy.$bus.emit('callUE', item.onAction)
+  } else if (item.offAction) {
+    proxy.$bus.emit('callUE', item.offAction)
   }
-  if(!item.offAction) {
+  if (!item.offAction) {
     setTimeout(() => {
       qyxxActive.value = ''
     }, 300);
@@ -365,16 +414,14 @@ function handleClickQyxx(item) {
 }
 
 function handleClickWggh(item) {
-  if(item.isOn) {
-    ueCallByName(item.offAction)
-    item.isOn = false
-  } else {
-    item.onAction.forEach(action => {
-      ueCallByName(action)
-    })
-    item.isOn = true
+  item.isOn = !(item.isOn)
+  if (item.title === '空域网格') {
+    proxy.$bus.emit('callUE', getUeFn('toggleGrid', { Id: 'Green', Visible: item.isOn }))
+    proxy.$bus.emit('callUE', getUeFn('toggleGrid', { Id: 'Red', Visible: false }))
   }
-  if(item.title==='网格规划') {
+  if (item.title === '网格规划') {
+    proxy.$bus.emit('callUE', getUeFn('toggleGrid', { Id: 'Green', Visible: item.isOn }))
+    proxy.$bus.emit('callUE', getUeFn('toggleGrid', { Id: 'Red', Visible: item.isOn }))
     dialogShow.legend = item.isOn
   }
 }
@@ -388,9 +435,9 @@ const dialogShow = reactive({
 const dialogData = reactive({
   uavList: [],
   legend: [
-    { label: '适飞区', color: 'green' },
-    { label: '风险区', color: 'yellow' },
-    { label: '禁飞区', color: 'red' },
+    { label: '适飞区', color: 'green', id: 'Green', isOn: true },
+    { label: '风险区', color: 'yellow', isOn: true },
+    { label: '禁飞区', color: 'red', id: 'Red', isOn: true },
   ],
   jldList: [
     { name: '黄兴公园' },
@@ -403,14 +450,29 @@ const dialogData = reactive({
   ]
 })
 
+function handleToggleLayer(e, item) {
+  item.isOn = e.target.checked
+  if(!(item.id)) return
+  proxy.$bus.emit('callUE', getUeFn('toggleGrid', { Id: item.id, Visible: item.isOn }))
+}
+
+function handlePickUav(item) {
+  item.isOn = !(item.isOn)
+  if(item.isOn) {
+    startUavDemo('initUavNormal')
+  } else {
+    endUavDemo()
+  }
+}
+
 function handleClickJld(item) {
-  if(item.name==='黄兴公园') {
-    ueCallByName('downLocation')
+  if (item.name === '黄兴公园') {
+    proxy.$bus.emit('callUE', getUeFn('locateLandfall'))
   }
 }
 
 function handleClickSsfx(item) {
-  if(item.title==='在飞航空器') {
+  if (item.title === '在飞航空器') {
     dialogShow.uavList = true
     dialogShow.jldList = false
   }
@@ -418,15 +480,16 @@ function handleClickSsfx(item) {
 
 function getDialogData() {
   let uavTemp = []
-  const classArr = ['小型','中型','大型']
-  const ownerArr = ['美团','饿了么']
-  for(let i=0; i<panelData.ssfx[0].count; i++) {
+  const classArr = ['小型', '中型', '大型']
+  const ownerArr = ['美团', '饿了么']
+  for (let i = 0; i < panelData.ssfx[0].count; i++) {
     uavTemp.push({
       seriesNo: '1581F0254DSA65324',
       owner: ownerArr.at(Math.floor(Math.random() * ownerArr.length)),
       class: classArr.at(Math.floor(Math.random() * classArr.length)),
       route: '合生汇-黄兴公园',
-      status: '正常'
+      status: '正常',
+      isOn: false
     })
   }
   dialogData.uavList = uavTemp
@@ -434,22 +497,39 @@ function getDialogData() {
 
 onBeforeMount(() => {
   getDialogData()
-  setTimeout(() => {
-    pageLoading.value = false
-  }, 2000);
 })
 
 function initCharts() {
   setSdtj(document.getElementById('chart-sdtj'), null)
 }
 
+function handleResetCamera() {
+  proxy.$bus.emit('callUE', getUeFn('resetCamera'))
+}
+
 onMounted(() => {
+  // 加载echarts
   initCharts()
+  // 启用全局事件监听
+  proxy.$bus.on('toggleLoading', (val) => {
+    pageLoading.value = val
+  })
+  proxy.$bus.on('ueConnected', () => {
+    // 重置视角
+    /* setTimeout(() => {
+      handleResetCamera()
+    }, 1000); */
+    proxy.$bus.emit('callUE', getUeFn('setRotateSpeed'))
+  })
+  // 禁用右键菜单
+  document.addEventListener('contextmenu',function(e){
+    e.preventDefault();
+  })
 })
+
 </script>
 
 <style lang="scss" scoped>
-
 $header_height: 106px;
 $bottom_height: 63px;
 $panel_width: 450px;
@@ -506,6 +586,7 @@ $panel_width: 450px;
     background: url('../assets/images/bg-bottom.png') no-repeat;
     background-size: cover;
     z-index: 2;
+
     .bottom-collapse {
       position: absolute;
       top: 0;
@@ -523,6 +604,7 @@ $panel_width: 450px;
       transform: translateX(-50%);
       z-index: 6;
       display: flex;
+
       li {
         cursor: pointer;
         width: 113px;
@@ -531,6 +613,7 @@ $panel_width: 450px;
         background-size: cover;
         margin: 0 5px;
         text-align: center;
+
         &:hover {
           filter: brightness(1.1);
         }
@@ -541,13 +624,18 @@ $panel_width: 450px;
           font-style: italic;
           font-size: 18px;
           color: #eee;
+
+          &.active {
+            color: #ffea01;
+          }
         }
 
       }
     }
   }
 
-  .left, .right {
+  .left,
+  .right {
     position: absolute;
     height: 100%;
     z-index: 3;
@@ -594,30 +682,37 @@ $panel_width: 450px;
       z-index: 4;
     }
   }
-  
+
   .left {
     left: 0;
+
     .aside-shadow {
       left: 0;
       background: url('../assets/images/shadow-panel.png') no-repeat;
       background-size: cover;
+
       &.is-hide {
         left: -$panel_width;
       }
     }
+
     .aside-deco {
       left: 0;
     }
+
     .aside-collapse {
       left: 15px;
     }
+
     .aside-main {
       left: 50px;
       padding-right: 50px;
+
       &.is-hide {
         left: calc(-50px - $panel_width);
       }
     }
+
     .weather {
       position: absolute;
       left: 30px;
@@ -633,23 +728,28 @@ $panel_width: 450px;
         width: 65px;
         height: 65px;
       }
+
       &>div {
         opacity: 0.71;
         width: 150px;
+
         &>div {
           display: flex;
           align-items: flex-start;
           margin-bottom: 3px;
+
           .w-type {
             font-size: 14px;
             line-height: 14px;
             color: #99CCFF;
           }
+
           .w-rh {
             font-size: 10px;
             margin-left: 4px;
           }
         }
+
         .w-temp {
           font-family: PFlight;
           font-size: 22px;
@@ -658,28 +758,34 @@ $panel_width: 450px;
       }
     }
   }
-  
+
   .right {
     right: 0;
+
     .aside-shadow {
       right: 0;
       background: url('../assets/images/shadow-panel.png') no-repeat;
       background-size: cover;
       transform: rotateY(180deg);
+
       &.is-hide {
         right: -$panel_width;
       }
     }
+
     .aside-deco {
       right: 0;
       transform: rotateY(180deg);
     }
+
     .aside-collapse {
       right: 15px;
     }
+
     .aside-main {
       right: 50px;
       padding-left: 50px;
+
       &.is-hide {
         right: calc(-50px - $panel_width);
       }
@@ -693,14 +799,17 @@ $panel_width: 450px;
       font-family: Barlow;
       font-weight: 500;
       z-index: 6;
+
       span {
         display: block;
         width: 150px;
         text-align: right;
       }
+
       .now-date {
         font-size: 14px;
       }
+
       .now-time {
         font-size: 22px;
       }
@@ -734,29 +843,34 @@ $panel_width: 450px;
   .block-qyxx {
     display: flex;
     justify-content: space-between;
+
     li {
       display: flex;
       align-items: center;
       cursor: pointer;
+
       img {
         width: 55px;
         height: 55px;
       }
+
       &>div {
         &>span {
           display: block;
           font-size: 16px;
           color: #FFFFFF;
           line-height: 29px;
-          text-shadow: 1px 2px 0px rgba(17,20,22,0.22);
+          text-shadow: 1px 2px 0px rgba(17, 20, 22, 0.22);
           font-style: italic;
           background: url('../assets/images/textbg-1-1.png') no-repeat;
           background-size: 100% 15px !important;
           background-position-y: 12px !important;
+
           &.active {
             color: #ffea01;
           }
         }
+
         &>div {
           margin-top: 5px;
           background: url('../assets/images/textbg-1-2.png') no-repeat;
@@ -764,15 +878,17 @@ $panel_width: 450px;
           display: flex;
           align-items: end;
           justify-content: center;
+
           span:first-child {
             margin-right: 5px;
             font-family: BarlowBold;
             font-size: 26px;
             line-height: 26px;
             color: transparent;
-            background: linear-gradient(0deg, rgba(29,128,224,0.8) 0%, rgba(255,255,255,0.8) 100%);
+            background: linear-gradient(0deg, rgba(29, 128, 224, 0.8) 0%, rgba(255, 255, 255, 0.8) 100%);
             -webkit-background-clip: text !important;
           }
+
           span:last-child {
             font-size: 14px;
             color: #FFFFFF;
@@ -786,10 +902,12 @@ $panel_width: 450px;
           &>span {
             background: url('../assets/images/textbg-2-1.png') no-repeat;
           }
+
           &>div {
             background: url('../assets/images/textbg-2-2.png') no-repeat;
+
             &>span:first-child {
-              background: linear-gradient(0deg, rgba(224,224,29,0.8) 0%, rgba(255,255,255,0.8) 100%);
+              background: linear-gradient(0deg, rgba(224, 224, 29, 0.8) 0%, rgba(255, 255, 255, 0.8) 100%);
             }
           }
         }
@@ -800,10 +918,12 @@ $panel_width: 450px;
           &>span {
             background: url('../assets/images/textbg-3-1.png') no-repeat;
           }
+
           &>div {
             background: url('../assets/images/textbg-3-2.png') no-repeat;
+
             &>span:first-child {
-              background: linear-gradient(0deg, rgba(29,224,224,0.64) 0%, rgba(255,255,255,0.64) 100%);
+              background: linear-gradient(0deg, rgba(29, 224, 224, 0.64) 0%, rgba(255, 255, 255, 0.64) 100%);
             }
           }
         }
@@ -815,13 +935,16 @@ $panel_width: 450px;
     display: flex;
     justify-content: space-around;
     margin-right: 10px;
+
     li {
       text-align: center;
       cursor: pointer;
+
       img {
         width: 110px;
         height: 115px;
       }
+
       div {
         display: block;
         margin-top: 10px;
@@ -830,7 +953,7 @@ $panel_width: 450px;
         font-size: 18px;
         color: #FFFFFF;
         line-height: 20px;
-        text-shadow: 1px 2px 0px rgba(17,20,22,0.22);
+        text-shadow: 1px 2px 0px rgba(17, 20, 22, 0.22);
         background: url('../assets/images/textbg-kygh.png') no-repeat;
         background-size: cover;
       }
@@ -850,16 +973,23 @@ $panel_width: 450px;
     ul.main {
       margin: 22px 15px 0;
       overflow: hidden;
+
       li {
         height: 50px;
         display: flex;
         align-items: center;
+
+        input {
+          margin-right: 10px;
+        }
+
         i {
           display: block;
           width: 11px;
           height: 11px;
           margin-right: 13px;
         }
+
         span {
           font-size: 18px;
           color: #d9cfc5;
@@ -892,6 +1022,7 @@ $panel_width: 450px;
     background-size: 100% 100%;
     display: flex;
     flex-wrap: wrap;
+
     img {
       position: absolute;
       top: calc(50% - 30px);
@@ -899,12 +1030,15 @@ $panel_width: 450px;
       width: 60px;
       height: 60px;
     }
+
     &>div {
       width: 50%;
       margin-top: 5px;
       text-align: left;
+
       &:nth-child(2n-1) {
         text-align: right;
+
         span:last-child {
           transform: translateX(-45px);
         }
@@ -912,17 +1046,19 @@ $panel_width: 450px;
 
       span {
         display: block;
+
         &:first-child {
           font-weight: 400;
           font-size: 14px;
           color: #839ECB;
         }
+
         &:last-child {
           transform: translateX(45px);
           font-family: BarlowBold;
           font-size: 18px;
           color: transparent;
-          background: linear-gradient(0deg, rgb(132, 163, 214, 1) 0%, rgba(255,255,255,0.7) 100%);
+          background: linear-gradient(0deg, rgb(132, 163, 214, 1) 0%, rgba(255, 255, 255, 0.7) 100%);
           -webkit-background-clip: text;
         }
       }
@@ -932,20 +1068,24 @@ $panel_width: 450px;
   .block-ssfx {
     display: flex;
     justify-content: space-around;
+
     li {
       display: flex;
       flex-direction: column;
       align-items: center;
       cursor: pointer;
+
       &>span {
         font-size: 16px;
         color: #99CCFF;
         font-style: italic;
       }
+
       &>div {
         display: flex;
         align-items: flex-end;
         margin: 10px 0 4px;
+
         span:first-child {
           padding-right: 8px;
           font-family: BarlowBold;
@@ -956,6 +1096,7 @@ $panel_width: 450px;
           background: linear-gradient(0deg, rgba(255, 195, 45, 1) 0%, rgba(255, 219, 77, 1) 100%);
           -webkit-background-clip: text !important;
         }
+
         span:last-child {
           font-size: 14px;
           color: #B4C1D8;
@@ -980,7 +1121,11 @@ $panel_width: 450px;
     padding: 30px;
     background: url('../assets/images/bg-list.png') no-repeat;
     background-size: 100% 100%;
-    
+
+    tr.active td {
+      color: #ffea01;
+    }
+
     .state-label {
       display: block;
       width: 64px;
@@ -989,6 +1134,7 @@ $panel_width: 450px;
       background: url('../assets/images/label-green.png') no-repeat;
       background-size: cover;
       text-align: center;
+      color: #eee;
     }
   }
 
@@ -998,9 +1144,7 @@ $panel_width: 450px;
   }
 }
 
-.right-main {
-  
+ul {
+  padding-left: 0;
 }
-
-
 </style>

+ 0 - 303
src/webRtcPlayer.js

@@ -1,303 +0,0 @@
-// Copyright Epic Games, Inc. All Rights Reserved.
-// universal module definition - read https://www.davidbcalhoun.com/2014/what-is-amd-commonjs-and-umd/
-
-(function (root, factory) {
-    if (typeof define === 'function' && define.amd) {
-        console.log("11111111111111111")
-        // AMD. Register as an anonymous module.
-        define(["./adapter"], factory);
-    } else if (typeof exports === 'object') {
-        console.log("22222222222222222")
-        // Node. Does not work with strict CommonJS, but
-        // only CommonJS-like environments that support module.exports,
-        // like Node.
-        module.exports = factory(require("./adapter"));
-    } else {
-        console.log("333333333333333")
-        // Browser globals (root is window)
-        // root.webRtcPlayer = factory(root.adapter);
-        window.webRtcPlayer = factory(window.adapter);
-    }
-}(this, function (adapter) {
-
-    function webRtcPlayer(parOptions) {
-    	parOptions = parOptions || {};
-    	
-        var self = this;
-
-        //**********************
-        //Config setup
-        //**********************;
-		this.cfg = parOptions.peerConnectionOptions || {};
-        this.cfg.sdpSemantics = 'unified-plan';
-        //If this is true in Chrome 89+ SDP is sent that is incompatible with UE WebRTC and breaks.
-        this.cfg.offerExtmapAllowMixed = false;
-        this.pcClient = null;
-        this.dcClient = null;
-        this.tnClient = null;
-
-        this.sdpConstraints = {
-          offerToReceiveAudio: 1,
-          offerToReceiveVideo: 1
-        };
-
-        // See https://www.w3.org/TR/webrtc/#dom-rtcdatachannelinit for values
-        this.dataChannelOptions = {ordered: true};
-
-        //**********************
-        //Functions
-        //**********************
-
-        //Create Video element and expose that as a parameter
-        function createWebRtcVideo () {
-            var video = document.createElement('video');
-            // video.style.width = '100vw';
-            // video.style.height = '100vh';
-            video.id = "streamingVideo";
-            video.playsInline = true;
-            video.muted = 'muted'
-            video.addEventListener('loadedmetadata', function(e){
-                if(self.onVideoInitialised){
-                    self.onVideoInitialised();
-                }
-            }, true);
-            return video;
-        }
-
-        this.video = createWebRtcVideo();
-
-        function onsignalingstatechange (state) {
-            //console.info('signaling state change:', state)
-        };
-
-        function oniceconnectionstatechange (state) {
-            //console.info('ice connection state change:', state)
-        };
-
-        function onicegatheringstatechange (state) {
-            //console.info('ice gathering state change:', state)
-        };
-
-        function handleOnTrack (e) {
-            //console.log('handleOnTrack', e.streams);
-            if (self.video.srcObject !== e.streams[0]) {
-                //console.log('setting video stream from ontrack');
-                self.video.srcObject = e.streams[0];
-            }
-        };
-
-        function setupDataChannel (pc, label, options) {
-            try {
-                console.log("setupDataChannel",pc,label,options)
-                var datachannel = pc.createDataChannel(label, options)
-                //console.log(`Created datachannel (${label})`)
-                
-                datachannel.onopen = function (e) {
-                  //console.log(`data channel (${label}) connect`)
-                  if(self.onDataChannelConnected){
-                    self.onDataChannelConnected();
-                  }
-                }
-
-                datachannel.onclose = function (e) {
-                  //console.log(`data channel (${label}) closed`)
-                }
-
-                datachannel.onmessage = function (e) {
-                //   //console.log(`Got message (${label})`, e.data)
-                  if (self.onDataChannelMessage)
-                    self.onDataChannelMessage(e.data);
-                }
-
-                return datachannel;
-            } catch (e) { 
-                console.warn('No data channel', e);
-                return null;
-            }
-        }
-
-        function onicecandidate (e) {
-			//console.log('ICE candidate', e)
-			if (e.candidate && e.candidate.candidate) {
-                self.onWebRtcCandidate(e.candidate);
-            }
-        };
-
-        function handleCreateOffer (pc) {
-            pc.createOffer(self.sdpConstraints).then(function (offer) {
-                offer.sdp = offer.sdp.replace("useinbandfec=1", "useinbandfec=1;stereo=1;maxaveragebitrate=128000");
-            	pc.setLocalDescription(offer);
-            	if (self.onWebRtcOffer) {
-            		// (andriy): increase start bitrate from 300 kbps to 20 mbps and max bitrate from 2.5 mbps to 100 mbps
-                    // (100 mbps means we don't restrict encoder at all)
-                    // after we `setLocalDescription` because other browsers are not c happy to see google-specific config
-            		offer.sdp = offer.sdp.replace(/(a=fmtp:\d+ .*level-asymmetry-allowed=.*)\r\n/gm, "$1;x-google-start-bitrate=10000;x-google-max-bitrate=20000\r\n");
-            		self.onWebRtcOffer(offer);
-                }
-            },
-            function () { console.warn("Couldn't create offer") });
-        }
-        
-        function setupPeerConnection (pc) {
-        	if (pc.SetBitrate)
-        		//console.log("Hurray! there's RTCPeerConnection.SetBitrate function");
-
-            //Setup peerConnection events
-            pc.onsignalingstatechange = onsignalingstatechange;
-            pc.oniceconnectionstatechange = oniceconnectionstatechange;
-            pc.onicegatheringstatechange = onicegatheringstatechange;
-
-            pc.ontrack = handleOnTrack;
-            pc.onicecandidate = onicecandidate;
-        };
-
-        function generateAggregatedStatsFunction (){
-            if(!self.aggregatedStats)
-                self.aggregatedStats = {};
-
-            return function(stats){
-                ////console.log('Printing Stats');
-
-                let newStat = {};
-                // //console.log('----------------------------- Stats start -----------------------------');
-                stats.forEach(stat => {
-//                    //console.log(JSON.stringify(stat, undefined, 4));
-                    if (stat.type == 'inbound-rtp' 
-                        && !stat.isRemote 
-                        && (stat.mediaType == 'video' || stat.id.toLowerCase().includes('video'))) {
-
-                        newStat.timestamp = stat.timestamp;
-                        newStat.bytesReceived = stat.bytesReceived;
-                        newStat.framesDecoded = stat.framesDecoded;
-                        newStat.packetsLost = stat.packetsLost;
-                        newStat.bytesReceivedStart = self.aggregatedStats && self.aggregatedStats.bytesReceivedStart ? self.aggregatedStats.bytesReceivedStart : stat.bytesReceived;
-                        newStat.framesDecodedStart = self.aggregatedStats && self.aggregatedStats.framesDecodedStart ? self.aggregatedStats.framesDecodedStart : stat.framesDecoded;
-                        newStat.timestampStart = self.aggregatedStats && self.aggregatedStats.timestampStart ? self.aggregatedStats.timestampStart : stat.timestamp;
-
-                        if(self.aggregatedStats && self.aggregatedStats.timestamp){
-                            if(self.aggregatedStats.bytesReceived){
-                                // bitrate = bits received since last time / number of ms since last time
-                                //This is automatically in kbits (where k=1000) since time is in ms and stat we want is in seconds (so a '* 1000' then a '/ 1000' would negate each other)
-                                newStat.bitrate = 8 * (newStat.bytesReceived - self.aggregatedStats.bytesReceived) / (newStat.timestamp - self.aggregatedStats.timestamp);
-                                newStat.bitrate = Math.floor(newStat.bitrate);
-                                newStat.lowBitrate = self.aggregatedStats.lowBitrate && self.aggregatedStats.lowBitrate < newStat.bitrate ? self.aggregatedStats.lowBitrate : newStat.bitrate
-                                newStat.highBitrate = self.aggregatedStats.highBitrate && self.aggregatedStats.highBitrate > newStat.bitrate ? self.aggregatedStats.highBitrate : newStat.bitrate
-                            }
-
-                            if(self.aggregatedStats.bytesReceivedStart){
-                                newStat.avgBitrate = 8 * (newStat.bytesReceived - self.aggregatedStats.bytesReceivedStart) / (newStat.timestamp - self.aggregatedStats.timestampStart);
-                                newStat.avgBitrate = Math.floor(newStat.avgBitrate);
-                            }
-
-                            if(self.aggregatedStats.framesDecoded){
-                                // framerate = frames decoded since last time / number of seconds since last time
-                                newStat.framerate = (newStat.framesDecoded - self.aggregatedStats.framesDecoded) / ((newStat.timestamp - self.aggregatedStats.timestamp) / 1000);
-                                newStat.framerate = Math.floor(newStat.framerate);
-                                newStat.lowFramerate = self.aggregatedStats.lowFramerate && self.aggregatedStats.lowFramerate < newStat.framerate ? self.aggregatedStats.lowFramerate : newStat.framerate
-                                newStat.highFramerate = self.aggregatedStats.highFramerate && self.aggregatedStats.highFramerate > newStat.framerate ? self.aggregatedStats.highFramerate : newStat.framerate
-                            }
-
-                            if(self.aggregatedStats.framesDecodedStart){
-                                newStat.avgframerate = (newStat.framesDecoded - self.aggregatedStats.framesDecodedStart) / ((newStat.timestamp - self.aggregatedStats.timestampStart) / 1000);
-                                newStat.avgframerate = Math.floor(newStat.avgframerate);
-                            }
-                        }
-                    }
-
-                    //Read video track stats
-                    if(stat.type == 'track' && (stat.trackIdentifier == 'video_label' || stat.kind == 'video')) {
-                        newStat.framesDropped = stat.framesDropped;
-                        newStat.framesReceived = stat.framesReceived;
-                        newStat.framesDroppedPercentage = stat.framesDropped / stat.framesReceived * 100;
-                        newStat.frameHeight = stat.frameHeight;
-                        newStat.frameWidth = stat.frameWidth;
-                        newStat.frameHeightStart = self.aggregatedStats && self.aggregatedStats.frameHeightStart ? self.aggregatedStats.frameHeightStart : stat.frameHeight;
-                        newStat.frameWidthStart = self.aggregatedStats && self.aggregatedStats.frameWidthStart ? self.aggregatedStats.frameWidthStart : stat.frameWidth;
-                    }
-
-                    if(stat.type =='candidate-pair' && stat.hasOwnProperty('currentRoundTripTime') && stat.currentRoundTripTime != 0){
-                        newStat.currentRoundTripTime = stat.currentRoundTripTime;
-                    }
-                });
-
-                ////console.log(JSON.stringify(newStat));
-                self.aggregatedStats = newStat;
-
-                if(self.onAggregatedStats)
-                    self.onAggregatedStats(newStat)
-            }
-        };
-
-        //**********************
-        //Public functions
-        //**********************
-
-        //This is called when revceiving new ice candidates individually instead of part of the offer
-        //This is currently not used but would be called externally from this class
-        this.handleCandidateFromServer = function(iceCandidate) {
-            console.log("ICE candidate: ", iceCandidate);
-            let candidate = new RTCIceCandidate(iceCandidate);
-            self.pcClient.addIceCandidate(candidate).then(_=>{
-                //console.log('ICE candidate successfully added');
-            });
-        };
-
-        //Called externaly to create an offer for the server
-        this.createOffer = function() {
-            if(self.pcClient){
-                //console.log("Closing existing PeerConnection")
-                self.pcClient.close();
-                self.pcClient = null;
-            }
-            console.log("cfg",self.cfg,self.dataChannelOptions)
-            self.pcClient = new RTCPeerConnection(self.cfg);
-            setupPeerConnection(self.pcClient);
-            self.dcClient = setupDataChannel(self.pcClient, 'cirrus', self.dataChannelOptions);
-            handleCreateOffer(self.pcClient);
-        };
-
-        //Called externaly when an answer is received from the server
-        this.receiveAnswer = function(answer) {
-            //console.log(`Received answer:\n${answer}`);
-            var answerDesc = new RTCSessionDescription(answer);
-            self.pcClient.setRemoteDescription(answerDesc);
-        };
-
-        this.close = function(){
-            if(self.pcClient){
-                //console.log("Closing existing peerClient")
-                self.pcClient.close();
-                self.pcClient = null;
-            }
-            if(self.aggregateStatsIntervalId)
-                clearInterval(self.aggregateStatsIntervalId);
-        }
-
-        //Sends data across the datachannel
-        this.send = function(data){
-            if(self.dcClient && self.dcClient.readyState == 'open'){
-                ////console.log('Sending data on dataconnection', self.dcClient)
-                self.dcClient.send(data);
-            }
-        };
-
-        this.getStats = function(onStats){
-            if(self.pcClient && onStats){
-                self.pcClient.getStats(null).then((stats) => { 
-                    onStats(stats); 
-                });
-            }
-        }
-
-        this.aggregateStats = function(checkInterval){
-            let calcAggregatedStats = generateAggregatedStatsFunction();
-            let printAggregatedStats = () => { self.getStats(calcAggregatedStats); }
-            self.aggregateStatsIntervalId = setInterval(printAggregatedStats, checkInterval);
-        }
-    };
-
-    return webRtcPlayer;
-  
-}));
-
-export default webRtcPlayer

File diff suppressed because it is too large
+ 0 - 1756
src/webRtcVideo.js