Browse Source

接入方向盘数据,优化逻辑

caner 2 years ago
parent
commit
ddd8bcc81b

+ 1 - 1
.eslintrc.json

@@ -23,7 +23,7 @@
     "max-len": 0, // 强制一行的最大长度
     "max-len": 0, // 强制一行的最大长度
     "semi": [ 2, "never" ], // 禁止使用分号
     "semi": [ 2, "never" ], // 禁止使用分号
     "no-unused-vars": 0,
     "no-unused-vars": 0,
-    "no-unneeded-ternary": 2, // 禁止不必要的嵌套 var isYes = answer === 1 ? true : false;
+    "no-unneeded-ternary": 0, // 禁止不必要的嵌套 var isYes = answer === 1 ? true : false;
     "no-unreachable": 2, // 不能有无法执行的代码
     "no-unreachable": 2, // 不能有无法执行的代码
     "no-unused-expressions": 1, // 禁止无用的表达式
     "no-unused-expressions": 1, // 禁止无用的表达式
     "linebreak-style": [ 0, "error", "windows" ],
     "linebreak-style": [ 0, "error", "windows" ],

BIN
electron/icon/playGame.ico


BIN
electron/icon/playGame.png


BIN
electron/icon/playGame@2x.png


+ 2 - 2
electron/logiControl.js

@@ -1,11 +1,11 @@
 const HID = require('node-hid');
 const HID = require('node-hid');
 try {
 try {
     const devices = HID.devices();
     const devices = HID.devices();
-    const logitech = devices.filter(el => el.manufacturer == 'Logitech');
+    const logitech = devices.filter(el => el.manufacturer === 'Logitech' && el.product.includes('G923'));
     const data = new HID.HID(logitech[0].vendorId, logitech[0].productId);
     const data = new HID.HID(logitech[0].vendorId, logitech[0].productId);
     data.on('data', (db) => {
     data.on('data', (db) => {
         process.send(db)
         process.send(db)
     })
     })
 } catch (error) {
 } catch (error) {
-    process.send({ type: 'err'})
+    process.send({ type: 'err' })
 }
 }

+ 66 - 53
electron/main.js

@@ -2,26 +2,35 @@ const { app, BrowserWindow, Menu, ipcMain, globalShortcut, dialog, screen, Tray
 const { join } = require('path');
 const { join } = require('path');
 const { fork } = require('child_process');
 const { fork } = require('child_process');
 const { platform } = require('process');
 const { platform } = require('process');
-
+const sleep = (ms) => new Promise(res => setTimeout(res, ms));
 class MainSerivce {
 class MainSerivce {
   constructor() {
   constructor() {
     Menu.setApplicationMenu(null) // 去掉菜单栏
     Menu.setApplicationMenu(null) // 去掉菜单栏
     app.commandLine.appendSwitch('wm-window-animations-disabled') // 拖动闪屏
     app.commandLine.appendSwitch('wm-window-animations-disabled') // 拖动闪屏
+    app.commandLine.appendSwitch('autoplay-policy', 'no-user-gesture-required');
     this.loadingWin = null
     this.loadingWin = null
     this.mainWin = null
     this.mainWin = null
     this.icon = join(__dirname, './icon/playGame.png')
     this.icon = join(__dirname, './icon/playGame.png')
+    this.contrlEvent = null
     app.on('ready', this.onRead.bind(this))
     app.on('ready', this.onRead.bind(this))
-    app.on('activate', this.createWindow.bind(this))
-    app.on('window-all-closed', app.quit)
+    app.on('activate', (e, isVisible) => {
+      if (!isVisible && this.mainWin) {
+        // 兼容Mac dock 栏点击
+        this.mainWin.show()
+      }
+    })
+    app.on('window-all-closed', () => app.quit())
   }
   }
 
 
   createLoading() {
   createLoading() {
+    const { size: { width, height } } = screen.getPrimaryDisplay()
     this.loadingWin = new BrowserWindow({
     this.loadingWin = new BrowserWindow({
       frame: false, // 无边框(窗口、工具栏等),只包含网页内容
       frame: false, // 无边框(窗口、工具栏等),只包含网页内容
-      width: 200,
-      height: 200,
+      width,
+      height,
       resizable: false,
       resizable: false,
       center: true,
       center: true,
+      icon: this.icon,
       alwaysOnTop: true,
       alwaysOnTop: true,
       transparent: true // 窗口是否支持透明,如果想做高级效果最好为true
       transparent: true // 窗口是否支持透明,如果想做高级效果最好为true
     })
     })
@@ -32,35 +41,33 @@ class MainSerivce {
   }
   }
 
 
   createWindow() {
   createWindow() {
-    if (!this.mainWin) {
-      this.mainWin = new BrowserWindow({
-        minWidth: 1300,
-        minHeight: 760,
-        width: 1300,
-        height: 760,
-        frame: false,
-        transparent: true,
-        icon: this.icon,
-        webPreferences: {
-          contextIsolation: true,
-          nodeIntegration: true,
-          webSecurity: false, // 去掉跨越
-          nodeIntegrationInWorker: true,
-          preload: join(__dirname, './preload.js')
-        },
-        show: false
-      })// 创建一个窗口
-
-      // 不同环境加载不同文件
-      if (app.isPackaged) {
-        this.mainWin.loadFile('dist/index.html')
-      } else {
-        this.mainWin.loadURL('http://localhost:6547/')
-      }
+    this.mainWin = new BrowserWindow({
+      minWidth: 1300,
+      minHeight: 760,
+      width: 1300,
+      height: 760,
+      frame: false,
+      transparent: true,
+      icon: this.icon,
+      webPreferences: {
+        contextIsolation: true,
+        nodeIntegration: true,
+        webSecurity: false, // 去掉跨越
+        nodeIntegrationInWorker: true,
+        preload: join(__dirname, './preload.js')
+      },
+      show: false
+    })// 创建一个窗口
 
 
-      // 事件监听
-      this.mainWin.on('close', () => { this.mainWin = null })
+    // 不同环境加载不同文件
+    if (app.isPackaged) {
+      this.mainWin.loadFile('dist/index.html')
+    } else {
+      this.mainWin.loadURL('http://localhost:6547/')
     }
     }
+
+    // 事件监听
+    this.mainWin.on('close', () => { this.mainWin = null })
   }
   }
 
 
   onRead() {
   onRead() {
@@ -78,30 +85,35 @@ class MainSerivce {
         }
         }
       }
       }
     ])
     ])
-    tray.setContextMenu(contextMenu)
     tray.setToolTip('控制端')
     tray.setToolTip('控制端')
-    tray.on('click', () => { this.mainWin.show() })
-
+    tray.on('click', () => this.mainWin.show())
     // 注册调试模式
     // 注册调试模式
     globalShortcut.register('Control+F12', () => {
     globalShortcut.register('Control+F12', () => {
       this.mainWin.webContents.toggleDevTools()
       this.mainWin.webContents.toggleDevTools()
     })
     })
 
 
-    // 禁用右键
-    if(platform === 'win32') this.mainWin.hookWindowMessage(278, () => {
-      this.mainWin.setEnabled(false);//窗口禁用
-      setTimeout(() => {
-        this.mainWin.setEnabled(true);
-      }, 100) //延时太快会立刻启动,太慢会妨碍窗口其他操作,可自行测试最佳时间
-      return true
-    })
+    // 系统环境
+    if (platform === 'win32') {
+      // 右键
+      tray.setContextMenu(contextMenu)
+      // 禁用右键
+      this.mainWin.hookWindowMessage(278, () => {
+        this.mainWin.setEnabled(false);//窗口禁用
+        setTimeout(() => {
+          this.mainWin.setEnabled(true);
+        }, 100) //延时太快会立刻启动,太慢会妨碍窗口其他操作,可自行测试最佳时间
+        return true
+      })
+    } else if (platform === 'darwin') {
+      app.dock.setIcon(join(__dirname, './icon/playGame@2x.png'))
+    }
 
 
     // 通信
     // 通信
     ipcMain.on('signal', (_, evt, data) => {
     ipcMain.on('signal', (_, evt, data) => {
       if (evt === 'close-loading') {
       if (evt === 'close-loading') {
         if (this.loadingWin) this.loadingWin.close()
         if (this.loadingWin) this.loadingWin.close()
         this.mainWin.show()
         this.mainWin.show()
-        if(this.mainWin.isVisible()) this.connectLogi()
+        if (this.mainWin.isVisible()) this.connectLogi()
       } else if (evt === 'minWin') {
       } else if (evt === 'minWin') {
         this.mainWin.minimize()
         this.mainWin.minimize()
       } else if (evt === 'closeWin') {
       } else if (evt === 'closeWin') {
@@ -117,18 +129,19 @@ class MainSerivce {
   }
   }
 
 
   connectLogi() {
   connectLogi() {
-    const signal = fork(join(__dirname, './logiControl.js'))
-    signal.on('message',msg=>{
-      if(msg.type === 'err'){
-        dialog.showMessageBox(this.mainWin, { message: '连接方向盘失败', type: 'error', title: '连接错误', detail: '请检查方向盘是否连接' }).then(({ response }) => {
+    this.contrlEvent = fork(join(__dirname, './logiControl.js'));
+    this.contrlEvent.on('message', msg => {
+      if (msg.type === 'err') {
+        this.contrlEvent.kill('SIGHUP')
+        this.contrlEvent = null
+        dialog.showMessageBox(this.mainWin, { message: '请尝试旋转方向或重新插入USB!', type: 'error', title: '连接错误' }).then(async ({ response }) => {
           if (!response) {
           if (!response) {
-            this.mainWin.close()
-            app.quit()
+            await sleep(2000)
+            this.connectLogi()
           }
           }
         })
         })
-      }else{
-        console.log(msg.data);
-        if (this.mainWin && !this.mainWin.isDestroyed()) this.mainWin?.webContents.send('contrlData', msg.data)
+      } else {
+        if (this.mainWin && !this.mainWin.isDestroyed() && this.contrlEvent) this.mainWin?.webContents.send('contrlData', msg.data)
       }
       }
     })
     })
   }
   }

+ 1 - 0
index.html

@@ -19,6 +19,7 @@
       overflow: hidden;
       overflow: hidden;
       border-radius: 20px;
       border-radius: 20px;
       background: none;
       background: none;
+      position: relative;
     }
     }
   </style>
   </style>
 </head>
 </head>

+ 38 - 12
loading.html

@@ -1,32 +1,58 @@
 <!DOCTYPE html>
 <!DOCTYPE html>
 <html lang="en">
 <html lang="en">
+
 <head>
 <head>
     <meta charset="UTF-8">
     <meta charset="UTF-8">
     <meta http-equiv="X-UA-Compatible" content="IE=edge">
     <meta http-equiv="X-UA-Compatible" content="IE=edge">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
     <style>
     <style>
+        html,body{
+            margin: 0;
+            padding: 0;
+            width: 100vw;
+            height: 100vh;
+            display: flex;
+            justify-content: center;
+            align-items: center;
+            box-sizing: border-box;
+            overflow: hidden;
+            margin-bottom: 10%;
+        }
         .loader {
         .loader {
-        border: 5px solid #f3f3f3;
-        border-radius: 50%;
-        border-top: 5px solid #3498db;
-        width: 30px;
-        height: 30px;
-        -webkit-animation: spin 2s linear infinite;
-        animation: spin 2s linear infinite;
+            border: 5px solid #f3f3f3;
+            border-radius: 50%;
+            border-top: 5px solid #3498db;
+            width: 30px;
+            height: 30px;
+            -webkit-animation: spin 2s linear infinite;
+            animation: spin 2s linear infinite;
+            margin: auto auto;
         }
         }
 
 
         @-webkit-keyframes spin {
         @-webkit-keyframes spin {
-        0% { -webkit-transform: rotate(0deg); }
-        100% { -webkit-transform: rotate(360deg); }
+            0% {
+                -webkit-transform: rotate(0deg);
+            }
+
+            100% {
+                -webkit-transform: rotate(360deg);
+            }
         }
         }
 
 
         @keyframes spin {
         @keyframes spin {
-        0% { transform: rotate(0deg); }
-        100% { transform: rotate(360deg); }
-        }        
+            0% {
+                transform: rotate(0deg);
+            }
+
+            100% {
+                transform: rotate(360deg);
+            }
+        }
     </style>
     </style>
 </head>
 </head>
+
 <body>
 <body>
     <div class="loader"></div>
     <div class="loader"></div>
 </body>
 </body>
+
 </html>
 </html>

+ 4 - 3
package.json

@@ -30,7 +30,6 @@
     "vite-plugin-eslint": "^1.8.1",
     "vite-plugin-eslint": "^1.8.1",
     "vite-plugin-svg-icons": "^2.0.1",
     "vite-plugin-svg-icons": "^2.0.1",
     "vue": "^3.3.2",
     "vue": "^3.3.2",
-    "vue-router": "^4.2.0",
     "vue-tsc": "^1.6.5"
     "vue-tsc": "^1.6.5"
   },
   },
   "build": {
   "build": {
@@ -41,8 +40,10 @@
       "dist/js",
       "dist/js",
       "dist/assets",
       "dist/assets",
       "dist/index.html",
       "dist/index.html",
-      "electron"
+      "electron",
+      "dist-electron"
     ],
     ],
-    "asar": false
+    "asar": false,
+    "icon": "./icon/"
   }
   }
 }
 }

+ 338 - 4
src/App.vue

@@ -1,6 +1,340 @@
+<script setup lang="ts">
+import { Socket, io } from 'socket.io-client'
+import {
+  onMounted, onUnmounted, ref, watch
+} from 'vue'
+import Login from '@/components/login.vue'
+import Gauge from '@/components/gauge.vue'
+import Record from '@/components/record.vue'
+import Battery from '@/components/battery.vue'
+import Loading from '@/components/loading.vue'
+
+const sleep = (ms: number) => new Promise((res) => setTimeout(res, ms))
+const HOST = 'wss://car.caner.top'
+const iceServers = [
+  {
+    urls: [ 'stun:caner.top:3478' ]
+  },
+  {
+    urls: 'turn:caner.top:3478',
+    username: 'admin',
+    credential: '123456'
+  }
+]
+const socket = ref(null as null | Socket)
+const Peer = ref(null as null | RTCPeerConnection | undefined)
+const isLogin = ref(false)
+const remoteVideo = ref()
+const showLoading = ref(true)
+const winMaxOrMin = ref(false)
+const contrlState = ref(false)
+const micState = ref(false)
+const audioState = ref(false)
+const audioStateNum = ref(0)
+const warnAudio = ref(false)
+const quantity = ref(0)
+const error = ref('')
+const conctrlData = ref({
+  v0: 0, v1: 0, v2: 0, v3: 0
+})
+
+const speed = ref(1) // 1低速档 | 2 高速档
+const SpeedValue = ref(0)
+
+// 关闭
+function close(err?: string) {
+  if (Peer.value) Peer.value?.close()
+  if (remoteVideo.value) remoteVideo.value.srcObject = null
+  if (socket.value) socket.value.disconnect()
+  isLogin.value = false
+  showLoading.value = false
+  error.value = err || ''
+  socket.value = null
+  Peer.value = null
+  audioStateNum.value = 0
+  quantity.value = 0
+}
+
+// init socket
+function intSoketRtc(host: string) {
+  showLoading.value = true
+  // int socket
+  socket.value = io(host, {
+    autoConnect: false,
+    transports: [ 'websocket' ]
+  })
+
+  // socket
+  socket.value.on('connect', () => {
+    try {
+      isLogin.value = true
+      // init webrtc
+      Peer.value = new RTCPeerConnection({
+        iceServers,
+        bundlePolicy: 'max-bundle'
+      })
+
+      // listen state
+      Peer.value.onicegatheringstatechange = () => {
+        console.log('GatheringState: ', Peer.value?.iceGatheringState)
+        if (Peer.value?.iceGatheringState === 'complete') {
+          const answer = Peer.value.localDescription
+          socket.value?.emit('msg', answer)
+        }
+      }
+
+      // listen track
+      Peer.value.ontrack = async (evt) => {
+        console.log('track', evt)
+        remoteVideo.value.srcObject = evt.streams[0]
+      }
+
+      // listen changestate·
+      Peer.value.oniceconnectionstatechange = async () => {
+        const state = Peer.value?.iceConnectionState
+        console.log('ICE状态', state)
+        if (
+          state === 'failed'
+          || state === 'disconnected'
+          || state === 'closed'
+        ) {
+          close('P2P通信失败')
+        }
+
+        // ICE连接成功
+        if (state === 'connected') {
+          await sleep(3000)
+          showLoading.value = false
+        }
+      }
+    } catch (error) {
+      close('webrtc初始化失败')
+    }
+  })
+
+  socket.value.on('msg', async (data) => {
+    const key = data.type
+    if (key === 'offer') {
+      await Peer.value?.setRemoteDescription(data)
+      const answer = await Peer.value?.createAnswer()
+      await Peer.value?.setLocalDescription(answer)
+    } else if (key === 'power') {
+      quantity.value = data.power
+    } else if (key === 'speed') {
+      const d = Math.floor(data.data)
+      SpeedValue.value = d
+    }
+  })
+
+  socket.value.on('joined', async () => socket.value?.emit('msg', { type: 'startRTC' }))
+
+  socket.value.on('leaved', () => close('车端断开'))
+
+  socket.value.on('connect_error', () => close('服务器连接失败'))
+}
+
+// 遥控数据
+window.$electron?.on('contrlData', (db: Uint8Array) => {
+  console.log(1)
+
+  // 鸣笛
+  warnAudio.value = !!db[54]
+  // 录音
+  micState.value = db[6] === 1
+  // 静音
+  audioState.value = db[6] === 64
+  // 控制
+  conctrlData.value = {
+    v0: 0,
+    v1: 0,
+    v2: parseInt(db[44].toString(), 10), // 转10进制
+    v3: 0
+  }
+})
+
+// 窗口事件
+function titleEvent(type: string) {
+  if (type === 'maxWin') winMaxOrMin.value = !winMaxOrMin.value
+  if (type === 'loginOut') { close(); return }
+  window.$electron?.send(type, winMaxOrMin.value)
+}
+
+// 发送语音
+function sendAudio(blob: Blob) {
+  socket.value?.emit('msg', {
+    type: 'Meadia',
+    Meadia: blob
+  })
+}
+
+// 登录
+async function login(data: { name: string, roomID: string }) {
+  if (!socket.value) intSoketRtc(HOST)
+  socket.value!.auth = {
+    roomID: data.roomID,
+    name: data.name
+  }
+  socket.value?.connect()
+}
+
+// 监听按钮状态
+watch([ audioState, warnAudio ], () => {
+  if (showLoading.value) return
+  if (audioState.value) {
+    audioStateNum.value++
+    socket.value?.emit('msg', { type: 'contrlAudio', contrlAudio: !!(audioStateNum.value % 2) })
+  }
+  if (warnAudio.value) socket.value?.emit('msg', { type: 'warnAudio' })
+})
+
+// 监听方向盘
+watch(conctrlData, () => {
+  if (showLoading.value) return
+  socket.value?.emit('msg', { type: 'conctrl', conctrl: { ...conctrlData.value } })
+})
+
+// 初始化
+onMounted(() => intSoketRtc(HOST))
+onUnmounted(() => close())
+
+// 关闭loadingwin
+window.$electron.send('close-loading')
+</script>·
 <template>
 <template>
-  <router-view />
+  <template v-if="isLogin">
+    <video
+      ref="remoteVideo"
+      autoplay
+      playsinline
+    />
+    <div class="marke">
+      <div class="marke-left">
+        <!-- 手柄状态 -->
+        <Icon
+          name="gemePad"
+          :size="30"
+          :color="contrlState ? '#00CED1' : 'rgba(0, 0, 0, 0.3)'"
+        />
+        <!-- 音频状态 -->
+        <Record
+          class="marke-audio"
+          :size="25"
+          :audio-state="micState"
+          @callBack="sendAudio"
+        />
+        <!-- 喇叭状态 -->
+        <Icon
+          name="audio"
+          :size="25"
+          :color="(!!(audioStateNum % 2)) ? '#00CED1' : 'rgba(0, 0, 0, 0.3)'"
+        />
+        <!-- 电量状态 -->
+        <Battery :quantity="quantity" />
+      </div>
+      <div class="marke-right">
+        <Icon
+          name="min"
+          :size="20"
+          color="#fff"
+          @click="titleEvent('minWin')"
+        />
+        <Icon
+          :name="winMaxOrMin ? 'max' : 'maxMin'"
+          :size="20"
+          color="#fff"
+          @click="titleEvent('maxWin')"
+        />
+        <Icon
+          name="close"
+          :size="20"
+          color="#fff"
+          @click="titleEvent('loginOut')"
+        />
+      </div>
+    </div>
+    <!-- 码数 -->
+    <div class="gauge">
+      <Gauge
+        :value="SpeedValue"
+        :gears="speed"
+      />
+    </div>
+    <Loading v-if="showLoading" />
+  </template>
+  <Login
+    v-else
+    v-model="error"
+    @loginBack="login"
+  />
 </template>
 </template>
-<script setup lang='ts'>
-window.$electron.send('close-loading')
-</script>
+<style scoped lang="scss">
+video {
+  background: black;
+  object-fit: fill;
+  font-size: 0;
+}
+
+.marke {
+  position: fixed;
+  top: 0;
+  left: 0;
+  width: available;
+  width: -webkit-fill-available;
+  height: 0.35rem;
+  z-index: 1;
+  background: #666666;
+  display: flex;
+  align-items: center;
+  justify-content: flex-end;
+  overflow: hidden;
+  border-top-left-radius: 20px;
+  border-top-right-radius: 20px;
+  padding-left: 1.5rem;
+
+  &>div {
+    display: flex;
+    align-items: center;
+  }
+
+  &-left {
+    justify-content: center;
+    flex: 1;
+    -webkit-app-region: drag;
+
+    &>* {
+      margin: 0 0.18rem;
+    }
+  }
+
+  &-right {
+    display: flex;
+    align-items: center;
+    margin-right: 0.15rem;
+
+    &>* {
+      cursor: pointer;
+
+      &:not(:first-child) {
+        margin-left: 0.15rem;
+
+      }
+    }
+  }
+}
+
+.gauge {
+  position: fixed;
+  bottom: 0;
+  left: 50%;
+  transform: translate(-50%, 0);
+  width: 3rem;
+  height: 1.65rem;
+  z-index: 9;
+}
+
+/* 隐藏滚动条 */
+::-webkit-scrollbar {
+  width: 0 !important;
+  display: none;
+}
+</style>

+ 2 - 2
src/components/battery.vue

@@ -10,7 +10,7 @@
       />
       />
     </div>
     </div>
     <div class="electric-berText">
     <div class="electric-berText">
-      50%
+      {{ quantity }}%
     </div>
     </div>
   </div>
   </div>
 </template>
 </template>
@@ -44,7 +44,7 @@ const bgClass = computed(() => {
   &-panel {
   &-panel {
     box-sizing: border-box;
     box-sizing: border-box;
     width: 0.25rem;
     width: 0.25rem;
-    height: 0.18rem;
+    height: 0.15rem;
     position: relative;
     position: relative;
     border: 0.02rem solid #ccc;
     border: 0.02rem solid #ccc;
     padding: 0.01rem;
     padding: 0.01rem;

+ 4 - 0
src/components/loading.vue

@@ -10,6 +10,10 @@
   height: 0.3rem;
   height: 0.3rem;
   -webkit-animation: spin 2s linear infinite;
   -webkit-animation: spin 2s linear infinite;
   animation: spin 2s linear infinite;
   animation: spin 2s linear infinite;
+  position: absolute;
+  left: 50%;
+  top: 50%;
+  z-index: 22;
 }
 }
 
 
 @-webkit-keyframes spin {
 @-webkit-keyframes spin {

+ 24 - 6
src/components/login.vue

@@ -1,10 +1,13 @@
 <script setup lang="ts">
 <script setup lang="ts">
-import { ref } from 'vue'
+import { computed, ref, watch } from 'vue'
 
 
 const name = ref('')
 const name = ref('')
 const roomID = ref('')
 const roomID = ref('')
-defineProps<{ err: string }>()
-const emit = defineEmits<{(evt: 'loginBack', value: { name: string, roomID: string }): void }>()
+const props = defineProps<{ modelValue: string }>()
+const err = computed(() => props.modelValue)
+const emit = defineEmits<{(evt: 'update:modelValue', value: string): void
+  (evt: 'loginBack', value: { name: string, roomID: string }): void
+}>()
 
 
 function login() {
 function login() {
   if (name.value && roomID.value) emit('loginBack', { name: name.value, roomID: roomID.value })
   if (name.value && roomID.value) emit('loginBack', { name: name.value, roomID: roomID.value })
@@ -13,6 +16,10 @@ function login() {
 function titleEvent(type: string) {
 function titleEvent(type: string) {
   window.$electron?.send(type)
   window.$electron?.send(type)
 }
 }
+
+watch([ name, roomID ], () => {
+  if (name.value && roomID.value) emit('update:modelValue', '')
+})
 </script>
 </script>
 <template>
 <template>
   <div class="login">
   <div class="login">
@@ -55,6 +62,7 @@ function titleEvent(type: string) {
             maxlength="20"
             maxlength="20"
           >
           >
         </div>
         </div>
+        <span>{{ err }}</span>
         <div>
         <div>
           <button
           <button
             :disabled="!name && !roomID"
             :disabled="!name && !roomID"
@@ -86,7 +94,7 @@ function titleEvent(type: string) {
     right: 0.15rem;
     right: 0.15rem;
     top: 0.12rem;
     top: 0.12rem;
     cursor: pointer;
     cursor: pointer;
-    -webkit-app-region:no-drag;
+    -webkit-app-region: no-drag;
   }
   }
 
 
   &-content {
   &-content {
@@ -105,9 +113,10 @@ function titleEvent(type: string) {
     }
     }
 
 
     &-right {
     &-right {
-      flex: 1;
+      width: 3rem;
       padding: 0 0.3rem;
       padding: 0 0.3rem;
       -webkit-app-region: no-drag;
       -webkit-app-region: no-drag;
+      overflow: hidden;
 
 
       &>p {
       &>p {
         font-size: 0.3rem;
         font-size: 0.3rem;
@@ -151,7 +160,7 @@ function titleEvent(type: string) {
           font-size: 0.16rem;
           font-size: 0.16rem;
           font-weight: 500;
           font-weight: 500;
           border-radius: 1rem;
           border-radius: 1rem;
-          margin-top: 0.5rem;
+          margin-top: 0.4rem;
           margin-left: auto;
           margin-left: auto;
           line-height: 0.35rem;
           line-height: 0.35rem;
           padding: 0 0.35rem;
           padding: 0 0.35rem;
@@ -159,6 +168,15 @@ function titleEvent(type: string) {
         }
         }
       }
       }
 
 
+      &>span {
+        height: 0.17rem;
+        color: red;
+        font-size: 0.12rem;
+        overflow: hidden;
+        display: block;
+        text-overflow: ellipsis;
+      }
+
       .resetStyle {
       .resetStyle {
         background: #2d8cf0;
         background: #2d8cf0;
         cursor: pointer;
         cursor: pointer;

+ 35 - 34
src/components/record.vue

@@ -6,50 +6,51 @@
   />
   />
 </template>
 </template>
 <script setup lang="ts">
 <script setup lang="ts">
-import { ref, watch } from 'vue'
+import { onMounted, ref, watch } from 'vue'
 
 
-// const chunks = ref([] as Blob[])
+const chunks = ref([] as Blob[])
 const mediaRecorder = ref(null as null | MediaRecorder)
 const mediaRecorder = ref(null as null | MediaRecorder)
 const show = ref(false)
 const show = ref(false)
 
 
 const props = defineProps<{
 const props = defineProps<{
   audioState: boolean,
   audioState: boolean,
-  size:number
+  size: number
 }>()
 }>()
-// const emit = defineEmits<{(evt: 'callBack', value: Blob): void;
-// }>()
+const emit = defineEmits<{(evt: 'callBack', value: Blob): void }>()
 
 
 // 初始化音频
 // 初始化音频
-// async function initRecorder() {
-//   try {
-//     const stream = await navigator.mediaDevices.getUserMedia({ audio: true })
-//     mediaRecorder.value = new MediaRecorder(stream)
+async function initRecorder() {
+  try {
+    const stream = await navigator.mediaDevices.getUserMedia({ audio: true })
+    mediaRecorder.value = new MediaRecorder(stream)
 
 
-//     // 事件监听
-//     mediaRecorder.value.ondataavailable = (e: { data: Blob }) => {
-//       chunks.value.push(e.data)
-//     }
-//     mediaRecorder.value.onstart = () => {
-//       chunks.value = []
-//     }
-//     mediaRecorder.value.onstop = () => {
-//       const blob = new Blob(chunks.value, {
-//         type: 'audio/webm;codecs=opus'
-//       })
-//       emit('callBack', blob)
-//     }
-//   } catch (error: any) {
-//     show.value = false
-//     mediaRecorder.value = null
-//     let txt = '不支持的音频'
-//     if (error.toString().includes('getUserMedia')) {
-//       txt = '不支持webrtc音频'
-//     } else {
-//       txt = '未获取到音频设备'
-//     }
-//     new window.Notification(txt)
-//   }
-// }
+    // 事件监听
+    mediaRecorder.value.ondataavailable = (e: { data: Blob }) => {
+      chunks.value.push(e.data)
+    }
+    mediaRecorder.value.onstart = () => {
+      chunks.value = []
+    }
+    mediaRecorder.value.onstop = () => {
+      const blob = new Blob(chunks.value, {
+        type: 'audio/webm;codecs=opus'
+      })
+      emit('callBack', blob)
+    }
+  } catch (error: any) {
+    show.value = false
+    mediaRecorder.value = null
+    let txt = '不支持的音频'
+    if (error.toString().includes('getUserMedia')) {
+      txt = '不支持webrtc音频'
+    } else {
+      txt = '未获取到音频设备'
+    }
+    new window.Notification(txt)
+  }
+}
+
+onMounted(() => initRecorder())
 
 
 watch(() => props.audioState, (v) => {
 watch(() => props.audioState, (v) => {
   if (!mediaRecorder.value) return
   if (!mediaRecorder.value) return

+ 1 - 15
src/main.ts

@@ -1,23 +1,9 @@
 import { createApp } from 'vue'
 import { createApp } from 'vue'
-import { createRouter, RouteRecordRaw, createWebHistory } from 'vue-router'
 import App from './App.vue'
 import App from './App.vue'
 import Icon from '@/components/icon.vue'
 import Icon from '@/components/icon.vue'
 import 'virtual:svg-icons-register'
 import 'virtual:svg-icons-register'
 import '@/services/rem'
 import '@/services/rem'
-// 动态路由
-const routes = Object.values(import.meta.glob('./views/*/route.ts', { eager: true, import: 'default' })) as unknown as RouteRecordRaw[]
-routes.push({ path: '/:path(.*)', redirect: '/' })
 
 
 const app = createApp(App)
 const app = createApp(App)
-const router = createRouter({
-  history: createWebHistory(),
-  routes
-})
 app.component('Icon', Icon)
 app.component('Icon', Icon)
-// 路由守卫
-// router.beforeEach((to, from, next) => {
-//   // do something
-//   next()
-// })
-
-app.use(router).mount('#app')
+app.mount('#app')

+ 0 - 401
src/views/index/index.vue

@@ -1,401 +0,0 @@
-<script setup lang="ts">
-import { Socket, io } from 'socket.io-client'
-import {
-  onMounted, onUnmounted, ref
-} from 'vue'
-import Login from '@/components/login.vue'
-import Gauge from '@/components/gauge.vue'
-import Record from '@/components/record.vue'
-import Battery from '@/components/battery.vue'
-import Loading from '@/components/loading.vue'
-
-const sleep = (ms: number) => new Promise((res) => setTimeout(res, ms))
-const socket = ref(null as null | Socket)
-const HOST = ref('wss://car.caner.top')
-const Peer = ref(null as null | RTCPeerConnection | undefined)
-const isLogin = ref(false)
-const error = ref('')
-const remoteVideo = ref()
-const showLoading = ref(true)
-const contrlState = ref(false)
-const audioState = ref(false)
-// const mutedState = ref(true)
-// const warnAudio = ref(false)// 鸣笛
-const speed = ref(1) // 1低速档 | 2 高速档
-// const muted = ref(true)// 是否静音
-const SpeedValue = ref(0)
-const iceServers = ref([
-  {
-    urls: [ 'stun:caner.top:3478' ]
-  },
-  {
-    urls: 'turn:caner.top:3478',
-    username: 'admin',
-    credential: '123456'
-  }
-])
-const num = ref(0)
-const winMaxOrMin = ref(false)
-// 发送语音
-function sendAudio(blob: Blob) {
-  if (!socket.value && !socket.value!.connected) return
-  socket.value?.emit('msg', {
-    type: 'Meadia',
-    Meadia: blob
-  })
-}
-
-// // 挡位
-// function Gear(speed: number, num: number) {
-//   // 低速档
-//   if (speed === 1) {
-//     if (num < 116) {
-//       // 前
-//       num = 120
-//     } else if (num >= 120 && num <= 131) {
-//       num = 128
-//     } else if (num > 140) {
-//       // 后
-//       num = 140
-//     }
-//   }
-//   // 高速档
-//   if (speed === 2) {
-//     if (num < 96) {
-//       num = 96
-//     } else if (num >= 120 && num <= 131) {
-//       num = 128
-//     } else if (num > 160) {
-//       num = 160
-//     }
-//   }
-//   return num
-// }
-
-// 登录
-function login(data: { name: string, roomID: string }) {
-  if (socket.value) {
-    socket.value.auth = {
-      roomID: data.roomID,
-      name: data.name
-    }
-    socket.value.connect()
-  } else {
-    error.value = '服务器连接失败'
-  }
-}
-
-// // 手柄数据
-// function ControlData() {
-//   const data = navigator.getGamepads()
-//   const db = data[0]
-//   if (!db) return
-//   // 挡位选择AB
-//   if (db.buttons[1].touched) speed.value = 2
-//   if (db.buttons[0].touched) speed.value = 1
-//   // 语音按键R2
-//   audioState.value = db.buttons[7].touched
-//   // 静音X3
-//   mutedState.value = db.buttons[3].touched
-//   // 播放警笛Y2
-//   warnAudio.value = db.buttons[2].touched
-//   // console.log(db.buttons);
-//   const params = {
-//     v0: Math.floor(db.axes[0] * 128 + 128),
-//     v1: Math.floor(db.axes[1] * 128 + 128),
-//     v2: Math.floor(db.axes[2] * 128 + 128),
-//     v3: Gear(speed.value, Math.floor(db.axes[3] * 128 + 128))
-//   }
-
-//   if (socket.value && socket.value.connected) socket.value.emit('msg', { type: 'conctrl', conctrl: params })
-//   requestAnimationFrame(ControlData)
-// }
-
-// // 手柄连接
-// function conControl() {
-//   contrlState.value = true
-//   ControlData()
-// }
-
-// // 手柄断开连接
-// function disControl() {
-//   contrlState.value = false
-//   cancelAnimationFrame(ControlData as any)
-// }
-
-// 关闭
-function close(err?: string) {
-  if (Peer.value) Peer.value?.close()
-  if (remoteVideo.value) remoteVideo.value.srcObject = null
-  if (socket.value) socket.value?.disconnect()
-  isLogin.value = false
-  showLoading.value = false
-  error.value = err || ''
-  socket.value = null
-  Peer.value = null
-  num.value = 0
-  // cancelAnimationFrame(ControlData as any)
-  // window.removeEventListener('gamepadconnected', conControl)
-  // window.removeEventListener('gamepaddisconnected', disControl)
-}
-
-/**
- * 网络连接
- * @param host
- */
-function intSoketRtc(host: string) {
-  // int socket
-  socket.value = io(host, {
-    autoConnect: false,
-    transports: [ 'websocket' ]
-  })
-
-  // socket
-  socket.value.on('connect', () => {
-    try {
-      isLogin.value = true
-      // init webrtc
-      Peer.value = new RTCPeerConnection({
-        iceServers: iceServers.value,
-        bundlePolicy: 'max-bundle'
-      })
-
-      // listen state
-      Peer.value.onicegatheringstatechange = () => {
-        console.log('GatheringState: ', Peer.value?.iceGatheringState)
-        if (Peer.value?.iceGatheringState === 'complete') {
-          const answer = Peer.value.localDescription
-          socket.value?.emit('msg', answer)
-        }
-      }
-
-      // listen track
-      Peer.value.ontrack = async (evt) => {
-        console.log('track', evt)
-        remoteVideo.value.srcObject = evt.streams[0]
-      }
-
-      // listen changestate·
-      Peer.value.oniceconnectionstatechange = async () => {
-        const state = Peer.value?.iceConnectionState
-        console.log('ICE状态', state)
-        if (
-          state === 'failed'
-          || state === 'disconnected'
-          || state === 'closed'
-        ) {
-          close('P2P通信失败')
-        }
-
-        // ICE连接成功|初始化摇杆
-        if (state === 'connected') {
-          // init Control
-          // window.addEventListener('gamepadconnected', conControl)
-          // window.addEventListener('gamepaddisconnected', disControl)
-          window.$electron.onContrl((db: ArrayBuffer) => {
-            console.log('123', db)
-          })
-          console.log('成功?')
-
-          await sleep(3000)
-          showLoading.value = false
-        }
-      }
-    } catch (error) {
-      close('webrtc初始化失败')
-    }
-  })
-
-  socket.value.on('msg', async (data) => {
-    const key = data.type
-    if (key === 'offer') {
-      await Peer.value?.setRemoteDescription(data)
-      const answer = await Peer.value?.createAnswer()
-      await Peer.value?.setLocalDescription(answer)
-    } else if (key === 'power') {
-      console.log('电量', data)
-    } else if (key === 'speed') {
-      const d = Math.floor(data.data)
-      SpeedValue.value = d
-    }
-  })
-
-  socket.value.on('joined', async () => socket.value?.emit('msg', { type: 'startRTC' }))
-
-  socket.value.on('leaved', () => close('车端断开'))
-
-  socket.value.on('connect_error', () => close('服务器连接失败'))
-}
-
-// watch(() => mutedState.value, (v) => {
-//   if (v) {
-//     const state = !!(num.value % 2)
-//     muted.value = state
-//     if (socket.value && socket.value.connected) socket.value.emit('msg', { type: 'contrlAudio', contrlAudio: state })
-//     num.value++
-//   }
-// })
-// watch(() => warnAudio.value, (v) => {
-//   if (v && socket.value && socket.value.connected) socket.value.emit('msg', { type: 'warnAudio', warnAudio: v })
-// })
-
-window.$electron?.on('contrlData', (db: Uint8Array) => {
-  // 拨片 2 是右拨片,1是左拨片,0是取消
-  const bp = db[6] === 2 ? '右拨片' : db[6] === 1 ? '左拨片' : ''
-  const fx = parseInt(db[44].toString(), 10) // 转10进制
-  const fxp = fx < 125 ? `左轮${fx}` : fx > 130 ? `右轮${fx}` : ''
-  const ym = parseInt(db[46].toString(), 10) // 油门
-  console.log(66, bp, fxp, 255 - ym)
-})
-
-function titleEvent(type: string) {
-  if (type === 'maxWin') winMaxOrMin.value = !winMaxOrMin.value
-  window.$electron?.send(type, winMaxOrMin.value)
-}
-
-onMounted(() => {
-  intSoketRtc(HOST.value)
-})
-onUnmounted(() => {
-  close()
-})
-</script>
-<template>
-  <template v-if="isLogin">
-    <video
-      ref="remoteVideo"
-      autoplay
-      playsinline
-      muted
-    />
-    <div class="marke">
-      <div class="marke-left">
-        <!-- 手柄状态 -->
-        <Icon
-          name="gemePad"
-          :size="40"
-          :color="contrlState ? '#00CED1' : 'rgba(0, 0, 0, 0.3)'"
-        />
-        <!-- 音频状态 -->
-        <Record
-          class="marke-audio"
-          :size="33"
-          :audio-state="audioState"
-          @callBack="sendAudio"
-        />
-        <!-- 喇叭状态 -->
-        <Icon
-          name="audio"
-          :size="30"
-          :color="contrlState ? '#00CED1' : 'rgba(0, 0, 0, 0.3)'"
-        />
-        <!-- 电量状态 -->
-        <Battery :quantity="33" />
-      </div>
-      <div class="marke-right">
-        <Icon
-          name="min"
-          :size="23"
-          color="#fff"
-          @click="titleEvent('minWin')"
-        />
-        <Icon
-          :name="winMaxOrMin ? 'max' : 'maxMin'"
-          :size="23"
-          color="#fff"
-          @click="titleEvent('maxWin')"
-        />
-        <Icon
-          name="close"
-          :size="23"
-          color="#fff"
-          @click="titleEvent('closeWin')"
-        />
-      </div>
-    </div>
-    <!-- 码数 -->
-    <div class="gauge">
-      <Gauge
-        :value="SpeedValue"
-        :gears="speed"
-      />
-    </div>
-    <Loading v-if="showLoading" />
-  </template>
-  <Login
-    v-else
-    :err="error"
-    @loginBack="login"
-  />
-</template>
-<style scoped lang="scss">
-video {
-  background: black;
-  object-fit: fill;
-  font-size: 0;
-}
-
-.marke {
-  position: fixed;
-    top: 0;
-    left: 0;
-    width: available;
-    width: -webkit-fill-available;
-    height: 0.5rem;
-    z-index: 1;
-    background: #666666;
-    display: flex;
-    align-items: center;
-    justify-content: flex-end;
-    overflow: hidden;
-    border-top-left-radius: 20px;
-    border-top-right-radius: 20px;
-    padding-left: 1.5rem;
-
-  &>div {
-    display: flex;
-    align-items: center;
-  }
-
-  &-left {
-    justify-content: center;
-    flex: 1;
-    -webkit-app-region: drag;
-
-    &>* {
-      margin: 0 0.18rem;
-    }
-  }
-
-  &-right {
-    display: flex;
-    align-items: center;
-    margin-right: 0.15rem;
-
-    &>* {
-      cursor: pointer;
-
-      &:not(:first-child) {
-        margin-left: 0.15rem;
-
-      }
-    }
-  }
-}
-
-.gauge {
-  position: fixed;
-  bottom: 0;
-  left: 50%;
-  transform: translate(-50%, 0);
-  width: 3rem;
-  height: 1.65rem;
-  z-index: 9;
-}
-
-/* 隐藏滚动条 */
-::-webkit-scrollbar {
-  width: 0 !important;
-  display: none;
-}
-</style>

+ 0 - 9
src/views/index/route.ts

@@ -1,9 +0,0 @@
-import home from './index.vue'
-
-export default {
-  path: '/',
-  meta: {
-    authorize: true
-  },
-  component: home
-}