|
@@ -1,11 +1,424 @@
|
|
|
<template>
|
|
<template>
|
|
|
- <div>test</div>
|
|
|
|
|
|
|
+ <div
|
|
|
|
|
+ v-if="isLogin"
|
|
|
|
|
+ class="box"
|
|
|
|
|
+ >
|
|
|
|
|
+ <video
|
|
|
|
|
+ id="v2"
|
|
|
|
|
+ autoplay
|
|
|
|
|
+ playsinline
|
|
|
|
|
+ muted
|
|
|
|
|
+ />
|
|
|
|
|
+ <div class="maker">
|
|
|
|
|
+ <!-- 信号 -->
|
|
|
|
|
+ <Signal :signal="signal" />
|
|
|
|
|
+ <!-- 手柄 -->
|
|
|
|
|
+ <div class="contrl">
|
|
|
|
|
+ 
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <!-- 音频 -->
|
|
|
|
|
+ <div class="audio">
|
|
|
|
|
+ 
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <!-- 喇叭 -->
|
|
|
|
|
+ <div class="arcode">
|
|
|
|
|
+ 
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <!-- 电量 -->
|
|
|
|
|
+ <Battery :quantity="60" />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="gauge">
|
|
|
|
|
+ <Gauge
|
|
|
|
|
+ :value="gauge.value"
|
|
|
|
|
+ :gears="gauge.gears"
|
|
|
|
|
+ />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <Loading v-if="showLoading" />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <Login
|
|
|
|
|
+ v-else
|
|
|
|
|
+ :err="err"
|
|
|
|
|
+ @loginBack="login"
|
|
|
|
|
+ />
|
|
|
</template>
|
|
</template>
|
|
|
|
|
|
|
|
<script setup lang='ts'>
|
|
<script setup lang='ts'>
|
|
|
|
|
+import {
|
|
|
|
|
+ onMounted,
|
|
|
|
|
+ onUnmounted, provide, reactive, ref, watch
|
|
|
|
|
+} from 'vue'
|
|
|
|
|
+import Login from '@/components/login.vue'
|
|
|
|
|
+import Loading from '@/components/loading.vue'
|
|
|
|
|
+import Gauge from '@/components/gauge.vue'
|
|
|
|
|
+import Signal from '@/components/signal.vue'
|
|
|
|
|
+import Battery from '@/components/battery.vue'
|
|
|
|
|
|
|
|
|
|
+const isLogin = ref(true)
|
|
|
|
|
+const err = ref('')
|
|
|
|
|
+const showLoading = ref(false)
|
|
|
|
|
+const gauge = reactive({
|
|
|
|
|
+ value: 40,
|
|
|
|
|
+ gears: 1 // 1低速档 | 2 高速档
|
|
|
|
|
+})
|
|
|
|
|
+const signal = ref(2)
|
|
|
|
|
+const audio = reactive({
|
|
|
|
|
+ state: false,
|
|
|
|
|
+ muted: false,
|
|
|
|
|
+ warn: false,
|
|
|
|
|
+ Recorder: null as any,
|
|
|
|
|
+ chunks: [] as any
|
|
|
|
|
+})
|
|
|
|
|
+const webRtc: any = {
|
|
|
|
|
+ Peer: null,
|
|
|
|
|
+ remoteVideo: null,
|
|
|
|
|
+ remoteAudioTrak: null,
|
|
|
|
|
+ iceServers: [
|
|
|
|
|
+ {
|
|
|
|
|
+ urls: [ 'stun:caner.top:3478' ]
|
|
|
|
|
+ },
|
|
|
|
|
+ {
|
|
|
|
|
+ urls: 'turn:caner.top:3478',
|
|
|
|
|
+ username: 'admin',
|
|
|
|
|
+ credential: '123456'
|
|
|
|
|
+ }
|
|
|
|
|
+ ]
|
|
|
|
|
+}
|
|
|
|
|
+const conctrl = {
|
|
|
|
|
+ state: false,
|
|
|
|
|
+ gear: 1,
|
|
|
|
|
+ conNumber: 0
|
|
|
|
|
+}
|
|
|
|
|
+const HOST = 'ws://127.0.0.1:49800'
|
|
|
|
|
+let socket = null as any
|
|
|
|
|
+const sleep = (ms: number) => new Promise((res) => setTimeout(res, ms))
|
|
|
|
|
+let num = 0
|
|
|
|
|
+
|
|
|
|
|
+// 挡位
|
|
|
|
|
+const Gear = (gear: number, speed: number) => {
|
|
|
|
|
+ // 低速档
|
|
|
|
|
+ if (gear === 1) {
|
|
|
|
|
+ if (speed < 116) {
|
|
|
|
|
+ // 前
|
|
|
|
|
+ speed = 120
|
|
|
|
|
+ } else if (speed >= 120 && speed <= 131) {
|
|
|
|
|
+ speed = 128
|
|
|
|
|
+ } else if (speed > 140) {
|
|
|
|
|
+ // 后
|
|
|
|
|
+ speed = 140
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ // 高速档
|
|
|
|
|
+ if (gear === 2) {
|
|
|
|
|
+ if (speed < 96) {
|
|
|
|
|
+ speed = 96
|
|
|
|
|
+ } else if (speed >= 120 && speed <= 131) {
|
|
|
|
|
+ speed = 128
|
|
|
|
|
+ } else if (speed > 160) {
|
|
|
|
|
+ speed = 160
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ return speed
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 手柄数据
|
|
|
|
|
+const ControlData = () => {
|
|
|
|
|
+ const data = navigator.getGamepads()
|
|
|
|
|
+ const db = data[0]
|
|
|
|
|
+ if (!db) return
|
|
|
|
|
+ // 挡位选择AB
|
|
|
|
|
+ if (db.buttons[1].touched) conctrl.gear = 2
|
|
|
|
|
+ if (db.buttons[0].touched) conctrl.gear = 1
|
|
|
|
|
+ // 语音按键R2
|
|
|
|
|
+ audio.state = db.buttons[7].touched
|
|
|
|
|
+ // 静音X3
|
|
|
|
|
+ audio.muted = db.buttons[3].touched
|
|
|
|
|
+ // 播放警笛Y2
|
|
|
|
|
+ audio.warn = 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(conctrl.gear, Math.floor(db.axes[3] * 128 + 128))
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (socket) socket.send({ type: 'conctrl', conctrl: params })
|
|
|
|
|
+ conctrl.conNumber = requestAnimationFrame(ControlData)
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 手柄连接
|
|
|
|
|
+const conControl = () => {
|
|
|
|
|
+ conctrl.state = true
|
|
|
|
|
+ ControlData()
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 手柄断开连接
|
|
|
|
|
+const disControl = () => {
|
|
|
|
|
+ conctrl.state = false
|
|
|
|
|
+ cancelAnimationFrame(conctrl.conNumber)
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 关闭
|
|
|
|
|
+const close = (error: string) => {
|
|
|
|
|
+ if (webRtc.Peer) webRtc.Peer.close()
|
|
|
|
|
+ if (webRtc.remoteVideo) webRtc.remoteVideo.srcObject = null
|
|
|
|
|
+ socket.close()
|
|
|
|
|
+ isLogin.value = false
|
|
|
|
|
+ showLoading.value = false
|
|
|
|
|
+ err.value = error || ''
|
|
|
|
|
+ socket = null
|
|
|
|
|
+ webRtc.Peer = null
|
|
|
|
|
+ window.removeEventListener('gamepadconnected', conControl)
|
|
|
|
|
+ window.removeEventListener('gamepaddisconnected', disControl)
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// socket连接
|
|
|
|
|
+const onOpen = () => {
|
|
|
|
|
+ console.log('连接成功!')
|
|
|
|
|
+ try {
|
|
|
|
|
+ isLogin.value = true
|
|
|
|
|
+ // init webrtc
|
|
|
|
|
+ webRtc.Peer = new RTCPeerConnection({
|
|
|
|
|
+ iceServers: webRtc.iceServers,
|
|
|
|
|
+ bundlePolicy: 'max-bundle'
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ // listen state
|
|
|
|
|
+ webRtc.Peer.onicegatheringstatechange = () => {
|
|
|
|
|
+ console.log('GatheringState: ', webRtc.Peer.iceGatheringState)
|
|
|
|
|
+ if (webRtc.Peer.iceGatheringState === 'complete') {
|
|
|
|
|
+ const answer = webRtc.Peer.localDescription
|
|
|
|
|
+ socket.send(answer)
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ // listen track
|
|
|
|
|
+ webRtc.Peer.ontrack = async (evt: any) => {
|
|
|
|
|
+ console.log('track', evt)
|
|
|
|
|
+ webRtc.remoteVideo = document.getElementById('v2')
|
|
|
|
|
+ webRtc.remoteVideo.srcObject = { ...evt.streams[0] }
|
|
|
|
|
+ if (evt.track.kind === 'audio') webRtc.remoteAudioTrak = { ...evt.streams[0] }
|
|
|
|
|
+ }
|
|
|
|
|
+ // listen changestate
|
|
|
|
|
+ webRtc.Peer.oniceconnectionstatechange = async () => {
|
|
|
|
|
+ const state = webRtc.Peer.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)
|
|
|
|
|
+ await sleep(3000)
|
|
|
|
|
+ showLoading.value = false
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ socket.close()
|
|
|
|
|
+ isLogin.value = false
|
|
|
|
|
+ err.value = 'webrtc初始化错误'
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// socket信息
|
|
|
|
|
+const onMsg = (event: { data: string }) => {
|
|
|
|
|
+ console.log(`收到消息啦:${event.data}`)
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// socket 连接错误
|
|
|
|
|
+const onErr = async () => {
|
|
|
|
|
+ isLogin.value = false
|
|
|
|
|
+ err.value = '连接错误!'
|
|
|
|
|
+ await sleep(3000)
|
|
|
|
|
+ if (!socket.userInfo) return
|
|
|
|
|
+ err.value = '重连中...'
|
|
|
|
|
+ const { roomID, name } = socket.userInfo
|
|
|
|
|
+ socket = new WebSocket(`${HOST}/${roomID}/${name}`)
|
|
|
|
|
+ socket.onmessage = onMsg
|
|
|
|
|
+ socket.onopen = onOpen
|
|
|
|
|
+ socket.userInfo = { roomID, name }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 登录
|
|
|
|
|
+const login = (params: { name: string; roomID: string }) => {
|
|
|
|
|
+ err.value = '连接中...'
|
|
|
|
|
+ if (socket) { socket.close(); socket = null; return }
|
|
|
|
|
+ socket = new WebSocket(`${HOST}/${params.roomID}/${params.name}`)
|
|
|
|
|
+ socket.onmessage = onMsg
|
|
|
|
|
+ socket.onopen = onOpen
|
|
|
|
|
+ socket.onerror = onErr
|
|
|
|
|
+ socket.userInfo = params
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 发送音频
|
|
|
|
|
+const sendAudio = (blob: Blob) => {
|
|
|
|
|
+ console.log('发送', blob)
|
|
|
|
|
+ if (!socket && !socket.connected) return
|
|
|
|
|
+ socket.send({
|
|
|
|
|
+ type: 'Meadia',
|
|
|
|
|
+ Meadia: blob
|
|
|
|
|
+ })
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 初始化声音
|
|
|
|
|
+const initRecorder = async () => {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const stream = await navigator.mediaDevices.getUserMedia({
|
|
|
|
|
+ audio: true
|
|
|
|
|
+ })
|
|
|
|
|
+ audio.Recorder = new MediaRecorder(stream)
|
|
|
|
|
+
|
|
|
|
|
+ // 事件监听
|
|
|
|
|
+ audio.Recorder.ondataavailable = (e: { data: any }) => {
|
|
|
|
|
+ audio.chunks.push(e.data)
|
|
|
|
|
+ }
|
|
|
|
|
+ audio.Recorder.onstart = () => {
|
|
|
|
|
+ audio.chunks = []
|
|
|
|
|
+ }
|
|
|
|
|
+ audio.Recorder.onstop = () => {
|
|
|
|
|
+ const blob = new Blob(audio.chunks, {
|
|
|
|
|
+ type: 'audio/webm;codecs=opus'
|
|
|
|
|
+ })
|
|
|
|
|
+ sendAudio(blob)
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ err.value = '不支持webrtc音频'
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+provide('err', err)
|
|
|
|
|
+
|
|
|
|
|
+// 静音
|
|
|
|
|
+watch(() => audio.muted, (v) => {
|
|
|
|
|
+ if (v) {
|
|
|
|
|
+ num++
|
|
|
|
|
+ const state = !(num % 2)
|
|
|
|
|
+ audio.state = state
|
|
|
|
|
+ if (socket && socket.connected) socket.send(JSON.stringify({ type: 'contrlAudio', contrlAudio: state }))
|
|
|
|
|
+ }
|
|
|
|
|
+})
|
|
|
|
|
+
|
|
|
|
|
+// 鸣笛
|
|
|
|
|
+watch(() => audio.warn, (v) => {
|
|
|
|
|
+ if (v && socket && socket.connected) socket.send(JSON.stringify({ type: 'warnAudio', warnAudio: v }))
|
|
|
|
|
+})
|
|
|
|
|
+
|
|
|
|
|
+// 发送音频
|
|
|
|
|
+watch(() => audio.state, (v) => {
|
|
|
|
|
+ if (!audio.Recorder) return
|
|
|
|
|
+ if (v) {
|
|
|
|
|
+ audio.Recorder.start()
|
|
|
|
|
+ } else {
|
|
|
|
|
+ audio.Recorder.stop()
|
|
|
|
|
+ }
|
|
|
|
|
+})
|
|
|
|
|
+
|
|
|
|
|
+onMounted(() => {
|
|
|
|
|
+ initRecorder()
|
|
|
|
|
+})
|
|
|
|
|
+
|
|
|
|
|
+onUnmounted(() => {
|
|
|
|
|
+ if (socket) socket.close()
|
|
|
|
|
+ if (audio.Recorder) audio.Recorder = null
|
|
|
|
|
+ audio.chunks = []
|
|
|
|
|
+ socket = null
|
|
|
|
|
+})
|
|
|
</script>
|
|
</script>
|
|
|
|
|
+<style lang="less" scoped>
|
|
|
|
|
+.box {
|
|
|
|
|
+ video {
|
|
|
|
|
+ width: 100%;
|
|
|
|
|
+ height: 100%;
|
|
|
|
|
+ background: none;
|
|
|
|
|
+ object-fit: fill;
|
|
|
|
|
+ font-size: 0;
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
|
|
+ .maker {
|
|
|
|
|
+ position: fixed;
|
|
|
|
|
+ top: 0;
|
|
|
|
|
+ left: 50%;
|
|
|
|
|
+ width: 555px;
|
|
|
|
|
+ height: 30px;
|
|
|
|
|
+ transform: translate(-50%, 0);
|
|
|
|
|
+ z-index: 1;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: center;
|
|
|
|
|
+
|
|
|
|
|
+ &::before {
|
|
|
|
|
+ position: absolute;
|
|
|
|
|
+ z-index: 0;
|
|
|
|
|
+ content: "";
|
|
|
|
|
+ width: 100%;
|
|
|
|
|
+ height: 0;
|
|
|
|
|
+ border-top: 30px solid rgba(0, 0, 0, 0.25);
|
|
|
|
|
+ border-left: 15px solid transparent;
|
|
|
|
|
+ border-right: 15px solid transparent;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ &>div {
|
|
|
|
|
+ margin: 0 3px;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .audio,
|
|
|
|
|
+ .arcode {
|
|
|
|
|
+ font-size: 24px;
|
|
|
|
|
+ color: rgba(0, 0, 0, 0.3);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .contrl {
|
|
|
|
|
+ font-size: 32px;
|
|
|
|
|
+ color: rgba(0, 0, 0, 0.3);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .gauge {
|
|
|
|
|
+ position: fixed;
|
|
|
|
|
+ bottom: 0;
|
|
|
|
|
+ left: 50%;
|
|
|
|
|
+ transform: translate(-50%, 0);
|
|
|
|
|
+ width: 500px;
|
|
|
|
|
+ height: 185px;
|
|
|
|
|
+ z-index: 9;
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+</style>
|
|
|
<style>
|
|
<style>
|
|
|
|
|
+html,
|
|
|
|
|
+body,
|
|
|
|
|
+#app {
|
|
|
|
|
+ margin: 0;
|
|
|
|
|
+ padding: 0;
|
|
|
|
|
+ width: 100%;
|
|
|
|
|
+ height: 100%;
|
|
|
|
|
+ user-select: none;
|
|
|
|
|
+ min-width: 1000px;
|
|
|
|
|
+ min-height: 900px;
|
|
|
|
|
+ /* background: black; */
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+#app {
|
|
|
|
|
+ font-family: "fonts";
|
|
|
|
|
+ font-style: normal;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* 隐藏滚动条 */
|
|
|
|
|
+::-webkit-scrollbar {
|
|
|
|
|
+ width: 0 !important;
|
|
|
|
|
+ display: none;
|
|
|
|
|
+}
|
|
|
|
|
|
|
|
|
|
+@font-face {
|
|
|
|
|
+ font-family: "fonts";
|
|
|
|
|
+ src: url("./assets/iconfont.woff2") format("woff2"),
|
|
|
|
|
+ url("./assets/iconfont.woff") format("woff"),
|
|
|
|
|
+ url("./assets/iconfont.ttf") format("truetype");
|
|
|
|
|
+}
|
|
|
</style>
|
|
</style>
|