| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366 |
- <script setup lang="ts">
- import { 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'
- import Signal from '@/components/signal.vue'
- const sleep = (ms: number) => new Promise((res) => setTimeout(res, ms))
- const iceServers = [
- {
- urls: [ 'stun:caner.top:3478' ]
- },
- {
- urls: 'turn:caner.top:3478',
- username: 'admin',
- credential: '123456'
- }
- ]
- const Peer = ref(null as null | RTCPeerConnection | undefined)
- const isLogin = ref(false)
- const remoteVideo = ref()
- const showLoading = ref(true)
- const winMaxOrMin = 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: 128, v1: 128, v2: 128, v3: 128
- })
- const conctrlAnimation = ref(0)
- const conctrlGrears = ref(false)
- const conctrlNum = ref(0)
- const SpeedValue = ref(0)
- const signalValue = ref(0)
- // 发送控制数据
- function sendContrlData() {
- if (showLoading.value) return
- window.$electron.send('sendMqtt', { type: 'conctrl', conctrl: { ...conctrlData.value } })
- conctrlAnimation.value = requestAnimationFrame(sendContrlData)
- }
- // 关闭
- function close(err?: string) {
- if (Peer.value) Peer.value?.close()
- if (remoteVideo.value) remoteVideo.value.srcObject = null
- isLogin.value = false
- showLoading.value = false
- error.value = err || ''
- Peer.value = null
- audioStateNum.value = 0
- quantity.value = 0
- winMaxOrMin.value = false
- cancelAnimationFrame(conctrlAnimation.value)
- window.$electron.send('closeMqtt')
- }
- // 初始化rtc
- function initRTC() {
- try {
- console.log('start RTC')
- 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
- console.log('send answer', answer)
- window.$electron.send('sendMqtt', answer?.toJSON())
- }
- }
- // 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('StateChange', state)
- if (
- state === 'failed'
- || state === 'disconnected'
- || state === 'closed'
- ) {
- close('P2P通信失败')
- }
- // ICE连接成功
- if (state === 'connected') {
- await sleep(3000)
- showLoading.value = false
- sendContrlData()
- }
- }
- console.log('RTC SUCCESS')
- } catch (error) {
- close('RTC 初始化失败')
- }
- }
- // 登录
- function login(data: { name: string, room: string, url: string }) {
- window.$electron.send('loginMqtt', { room: data.room, name: data.name, url: data.url })
- }
- // 档位计算
- function countContrlData(v: number) {
- // 转10进制
- const num = parseInt(v.toString(), 10)
- if (conctrlNum.value % 2) {
- // 倒档
- return Math.floor(num / 2)
- }
- // 前进
- return Math.floor(((128 - (num / 2)) + 128))
- }
- // 发送语音
- function sendAudio(blob: Blob) {
- window.$electron.send('sendMqtt', { type: 'Meadia', Meadia: blob })
- }
- // 窗口事件
- function titleEvent(type: string) {
- if (type === 'maxWin') winMaxOrMin.value = !winMaxOrMin.value
- if (type === 'closeWin') {
- if (winMaxOrMin.value) window.$electron?.send('maxWin', false)
- close()
- console.log('MQTT and RTC close')
- } else {
- window.$electron?.send(type, winMaxOrMin.value)
- }
- }
- // mqtt
- window.$electron.on('message', async (msg: any) => {
- switch (msg.type) {
- case 'connect':
- console.log('mqtt connected')
- isLogin.value = true
- showLoading.value = true
- initRTC()
- break
- case 'disconnect':
- close('服务器连接失败')
- break
- case 'join':
- console.log('join mqtt channel', msg)
- window.$electron.send('sendMqtt', { type: 'startRTC' })
- break
- case 'leave':
- close('对方断开连接')
- break
- case 'offer':
- {
- console.log('get offer?', msg)
- await Peer.value?.setRemoteDescription(msg)
- const answerd = await Peer.value?.createAnswer()
- await Peer.value?.setLocalDescription(answerd)
- }
- break
- case 'power':
- quantity.value = msg.power
- console.log('电量', msg.power)
- break
- case 'speed':
- SpeedValue.value = Math.floor(msg.speed)
- console.log('速度')
- break
- case 'signal':
- signalValue.value = Math.floor(msg.signal)
- console.log('信号')
- break
- case 'contrl':
- {
- const db = msg.content
- // 倒档 | 前进 =>右拨片
- conctrlGrears.value = db[6] === 2
- // 鸣笛 =>回车
- warnAudio.value = !!db[54]
- // 录音 =>左拨片
- micState.value = db[6] === 1
- // 静音 =>L3
- audioState.value = db[6] === 64
- // 控制
- conctrlData.value = {
- v0: 128,
- v1: 128,
- v2: parseInt(db[44].toString(), 10), // 方向盘
- v3: countContrlData(db[46]) // 油门
- }
- }
- console.log('遥控数据', conctrlData.value)
- break
- default:
- break
- }
- })
- // 关闭loadingwin
- window.$electron.send('close-loading')
- // 监听按钮状态
- watch([ audioState, warnAudio, conctrlGrears ], () => {
- if (showLoading.value) return
- if (audioState.value) {
- audioStateNum.value++
- window.$electron.send('sendMqtt', { type: 'contrlAudio', contrlAudio: !!(audioStateNum.value % 2) })
- }
- if (conctrlGrears.value) {
- conctrlNum.value++
- }
- if (warnAudio.value) window.$electron.send('sendMqtt', { type: 'warnAudio', contrlAudio: !!(audioStateNum.value % 2) })
- })
- onUnmounted(() => close())
- </script>
- <template>
- <template v-if="isLogin">
- <video
- ref="remoteVideo"
- autoplay
- playsinline
- />
- <div class="marke">
- <div class="marke-left">
- <!-- 音频状态 -->
- <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" />
- <Signal :signal-value="signalValue" />
- </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('closeWin')"
- />
- </div>
- </div>
- <!-- 码数 -->
- <div class="gauge">
- <Gauge
- :value="SpeedValue"
- :gears="conctrlNum % 2 ? '倒档' : '前进'"
- />
- </div>
- <Loading v-if="showLoading" />
- </template>
- <Login
- v-else
- v-model="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: 35px;
- z-index: 1;
- background: #666666;
- display: flex;
- align-items: center;
- justify-content: flex-end;
- overflow: hidden;
- border-top-left-radius: 13px;
- border-top-right-radius: 13px;
- &>div {
- display: flex;
- align-items: center;
- }
- &-left {
- justify-content: center;
- flex: 1;
- -webkit-app-region: drag;
- padding-left: 110px;
- height: 100%;
- &>* {
- margin: 0 18px;
- }
- }
- &-right {
- display: flex;
- align-items: center;
- margin-right: 15px;
- &>* {
- cursor: pointer;
- &:not(:first-child) {
- margin-left: 15px;
- }
- }
- }
- }
- .gauge {
- position: fixed;
- bottom: 0;
- left: 50%;
- transform: translate(-50%, 0);
- width: 30vw;
- height: 15vw;
- max-width: 460px;
- max-height: 230px;
- z-index: 9;
- }
- /* 隐藏滚动条 */
- ::-webkit-scrollbar {
- width: 0 !important;
- display: none;
- }
- </style>
|