App.vue 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391
  1. <template>
  2. <div id="app">
  3. <template v-if="isLogin">
  4. <video id="v2" autoplay playsinline muted></video>
  5. <div class="marke">
  6. <!-- 信号 -->
  7. <Signal :signalValue="signalValue" />
  8. <!-- 手柄状态 -->
  9. <svg viewBox="0 -50 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" width="30" height="30">
  10. <path
  11. d="M817.68 803.17a130.23 130.23 0 0 1-125.6-96.37l-14-52.19a54.08 54.08 0 0 0-52.16-40H398.07a54.08 54.08 0 0 0-52.16 40l-14 52.19c-15.54 58-68.21 96.37-125.6 96.37A130 130 0 0 1 80.78 639.51l66.72-249a181.66 181.66 0 0 1 63.19-97.15A177.79 177.79 0 0 1 322 254.58h380a177.79 177.79 0 0 1 111.31 38.79 181.66 181.66 0 0 1 63.19 97.15l66.72 249a130 130 0 0 1-125.54 163.65zM322 274.58A160 160 0 0 0 166.87 395.5v0.13l-66.73 249a110 110 0 0 0 212.5 56.94l14-52.19a74.11 74.11 0 0 1 71.48-54.85h227.81a74.11 74.11 0 0 1 71.48 54.85l14 52.19a110 110 0 0 0 212.5-56.94L857.13 395.5A160 160 0 0 0 702 274.58z"
  12. :fill="contrlState ? '#00CED1' : 'rgba(0, 0, 0, 0.3)'"></path>
  13. <path
  14. d="M580 213.86a12 12 0 0 1 12 12v28H432v-28a12 12 0 0 1 12-12h136m0-20H444a32 32 0 0 0-32 32v48h200v-48a32 32 0 0 0-32-32z"
  15. :fill="contrlState ? '#00CED1' : 'rgba(0, 0, 0, 0.3)'"></path>
  16. <path
  17. d="M512 213.86a10 10 0 0 1-10-10v-63a60.07 60.07 0 0 1 60-60 10 10 0 0 1 0 20 40 40 0 0 0-40 40v63a10 10 0 0 1-10 10zM330 344.86a90 90 0 1 1-90 90 90.1 90.1 0 0 1 90-90m0-20a110 110 0 1 0 110 110 110 110 0 0 0-110-110z"
  18. :fill="contrlState ? '#00CED1' : 'rgba(0, 0, 0, 0.3)'"></path>
  19. <path
  20. d="M330 384.86a50 50 0 1 1-50 50 50.06 50.06 0 0 1 50-50m0-20a70 70 0 1 0 70 70 70 70 0 0 0-70-70zM697 344.86a14 14 0 1 1-14 14 14 14 0 0 1 14-14m0-20a34 34 0 1 0 34 34 34 34 0 0 0-34-34zM697 496.86a14 14 0 1 1-14 14 14 14 0 0 1 14-14m0-20a34 34 0 1 0 34 34 34 34 0 0 0-34-34zM773 420.86a14 14 0 1 1-14 14 14 14 0 0 1 14-14m0-20a34 34 0 1 0 34 34 34 34 0 0 0-34-34zM621 420.86a14 14 0 1 1-14 14 14 14 0 0 1 14-14m0-20a34 34 0 1 0 34 34 34 34 0 0 0-34-34z"
  21. :fill="contrlState ? '#00CED1' : 'rgba(0, 0, 0, 0.3)'"></path>
  22. </svg>
  23. <!-- 音频 -->
  24. <Record @callBack="sendAudio" :audioState="audioState" />
  25. <!-- 喇叭 -->
  26. <svg viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" width="30" height="30">
  27. <path :fill="muted ? '#00CED1' : 'rgba(0, 0, 0, 0.3)'"
  28. d="M393.707231 292.571429L343.13933 487.619048l46.955908 234.779541-97.523809-45.149912-66.82187-108.359788-21.671958-86.687831 66.82187-113.777778z">
  29. </path>
  30. <path :fill="muted ? '#00CED1' : 'rgba(0, 0, 0, 0.3)'"
  31. d="M451.499118 509.291005a104.747795 61.40388 90 1 0 122.80776 0 104.747795 61.40388 90 1 0-122.80776 0Z">
  32. </path>
  33. <path :fill="muted ? '#00CED1' : 'rgba(0, 0, 0, 0.3)'"
  34. d="M426.215168 781.996473c-3.611993 0-7.223986-1.805996-10.835979-3.611993l-117.389771-86.687831c-36.119929-23.477954-65.015873-57.791887-83.075838-99.329806-10.835979-25.283951-16.253968-52.373898-16.253968-81.269841s5.417989-55.985891 16.253968-81.269842c16.253968-41.537919 45.149912-74.045855 83.075838-97.523809l110.165785-77.657848c9.029982-5.417989 19.865961-3.611993 25.28395 3.611993 5.417989 9.029982 3.611993 19.865961-3.611993 25.28395L319.661376 361.199295c-32.507937 19.865961-55.985891 48.761905-70.433863 81.269841-9.029982 21.671958-14.447972 45.149912-14.447972 68.627866 0 23.477954 3.611993 45.149912 12.641976 66.821869 14.447972 34.313933 37.925926 63.209877 68.627866 83.075838l117.38977 86.687831c7.223986 5.417989 9.029982 18.059965 3.611993 25.28395 0 5.417989-5.417989 9.029982-10.835978 9.029983z">
  35. </path>
  36. <path :fill="muted ? '#00CED1' : 'rgba(0, 0, 0, 0.3)'"
  37. d="M523.738977 830.758377c-108.359788 0-193.241623-140.867725-193.241623-317.855379S415.379189 193.241623 523.738977 193.241623c25.283951 0 50.567901 7.223986 74.045855 23.477954 9.029982 5.417989 10.835979 16.253968 5.41799 25.28395s-16.253968 10.835979-25.283951 5.41799c-18.059965-10.835979-34.313933-18.059965-54.179894-18.059965-84.881834 0-157.121693 130.031746-157.121693 281.73545S438.857143 794.638448 523.738977 794.638448s157.121693-130.031746 157.121693-281.73545c0-74.045855-16.253968-146.285714-46.955908-198.659612-5.417989-9.029982-1.805996-19.865961 7.223986-25.28395 9.029982-5.417989 19.865961-1.805996 25.28395 7.223986 32.507937 59.597884 50.567901 135.449735 50.567902 216.719576C718.786596 689.890653 633.904762 830.758377 523.738977 830.758377z">
  38. </path>
  39. <path :fill="muted ? '#00CED1' : 'rgba(0, 0, 0, 0.3)'"
  40. d="M523.738977 646.546737c-48.761905 0-86.687831-59.597884-86.687831-133.643739S474.977072 379.259259 523.738977 379.259259s86.687831 59.597884 86.687831 133.643739-37.925926 133.643739-86.687831 133.643739z m0-232.973545c-23.477954 0-50.567901 39.731922-50.567901 97.52381s27.089947 97.52381 50.567901 97.523809 50.567901-39.731922 50.567901-97.523809-27.089947-97.52381-50.567901-97.52381z">
  41. </path>
  42. <path :fill="muted ? '#00CED1' : 'rgba(0, 0, 0, 0.3)'"
  43. d="M523.738977 413.573192h-1.805996l-92.105821-10.835979c-9.029982-1.805996-16.253968-10.835979-16.253968-19.865961 1.805996-9.029982 10.835979-16.253968 19.865961-16.253968l92.105821 10.835979c9.029982 1.805996 16.253968 10.835979 16.253968 19.865961 0 9.029982-9.029982 16.253968-18.059965 16.253968zM372.035273 662.800705c-9.029982 0-16.253968-7.223986-18.059964-16.253968-1.805996-9.029982 5.417989-18.059965 16.253968-19.865961l153.5097-16.253968c9.029982-1.805996 18.059965 5.417989 19.865961 16.253968 1.805996 9.029982-5.417989 18.059965-16.253968 19.865961l-153.5097 16.253968h-1.805997z">
  44. </path>
  45. <path :fill="muted ? '#00CED1' : 'rgba(0, 0, 0, 0.3)'"
  46. d="M763.936508 364.811287c-5.417989 0-9.029982-1.805996-12.641975-5.417989-7.223986-7.223986-7.223986-18.059965 0-25.283951l25.28395-25.28395c7.223986-7.223986 18.059965-7.223986 25.283951 0s7.223986 18.059965 0 25.28395L776.578483 359.393298c-3.611993 3.611993-9.029982 5.417989-12.641975 5.417989zM771.160494 720.592593c-5.417989 0-9.029982-1.805996-12.641975-5.41799l-25.283951-25.28395c-7.223986-7.223986-7.223986-18.059965 0-25.283951s18.059965-7.223986 25.283951 0l25.28395 25.283951c7.223986 7.223986 7.223986 18.059965 0 25.28395-3.611993 3.611993-7.223986 5.417989-12.641975 5.41799zM819.922399 529.156966h-54.179895c-10.835979 0-18.059965-7.223986-18.059964-18.059964s7.223986-18.059965 18.059964-18.059965h54.179895c10.835979 0 18.059965 7.223986 18.059964 18.059965s-9.029982 18.059965-18.059964 18.059964z">
  47. </path>
  48. </svg>
  49. <!-- 电量 -->
  50. <Battery :quantity="60" />
  51. </div>
  52. <!-- 码数 -->
  53. <div class="gauge">
  54. <Gauge :value="SpeedValue" :gears="speed" />
  55. </div>
  56. <Loading v-if="showLoading" />
  57. </template>
  58. <Login v-else :err="error" @loginBack="login" />
  59. </div>
  60. </template>
  61. <script>
  62. const { io } = require("socket.io-client");
  63. import Login from "@/components/login";
  64. import Loading from "@/components/loading";
  65. import Record from "@/components/record";
  66. import Signal from "@/components/signal";
  67. import Battery from "@/components/battery";
  68. import Gauge from "@/components/gauge";
  69. import { Message } from "view-design";
  70. const sleep = (ms) => new Promise((res) => setTimeout(res, ms));
  71. export default {
  72. components: { Login, Loading, Record, Signal, Battery, Gauge },
  73. data() {
  74. return {
  75. socket: null,
  76. // HOST: "wss://car.caner.top",
  77. HOST:'ws://127.0.0.1:10086',
  78. Peer: null,
  79. isLogin: false,
  80. error: "",
  81. remoteVideo: null,
  82. showLoading: true,
  83. contrlState: false,
  84. audioState: false,
  85. mutedState: true,
  86. warnAudio: false,// 鸣笛
  87. speed: 1, //1低速档 | 2 高速档
  88. muted: true,// 是否静音
  89. signalValue: 0,
  90. SpeedValue: 0,
  91. iceServers: [
  92. {
  93. urls: ["stun:caner.top:3478"],
  94. },
  95. {
  96. urls: "turn:caner.top:3478",
  97. username: "admin",
  98. credential: "123456",
  99. },
  100. ],
  101. num: 0
  102. };
  103. },
  104. methods: {
  105. // 网络连接
  106. intSoketRtc(host) {
  107. // int socket
  108. this.socket = io(host, {
  109. autoConnect: false,
  110. transports: ["websocket"],
  111. });
  112. // socket
  113. this.socket.on("connect", () => {
  114. try {
  115. this.isLogin = true;
  116. // init webrtc
  117. this.Peer = new RTCPeerConnection({
  118. iceServers: this.iceServers,
  119. bundlePolicy: "max-bundle",
  120. });
  121. // listen state
  122. this.Peer.onicegatheringstatechange = () => {
  123. console.log("GatheringState: ", this.Peer.iceGatheringState);
  124. if (this.Peer.iceGatheringState === "complete") {
  125. const answer = this.Peer.localDescription;
  126. this.socket.emit("msg", answer);
  127. }
  128. };
  129. // listen track
  130. this.Peer.ontrack = async (evt) => {
  131. console.log("track", evt);
  132. this.remoteVideo = document.getElementById("v2");
  133. this.remoteVideo.srcObject = evt.streams[0];
  134. if (evt.track.kind === 'audio') this.remoteAudioTrak = evt.streams[0]
  135. };
  136. // listen changestate
  137. this.Peer.oniceconnectionstatechange = async () => {
  138. const state = this.Peer.iceConnectionState;
  139. console.log("ICE状态", state);
  140. if (
  141. state === "failed" ||
  142. state === "disconnected" ||
  143. state === "closed"
  144. ) {
  145. this.close("P2P通信失败");
  146. }
  147. // ICE连接成功|初始化摇杆
  148. if (state === "connected") {
  149. // init Control
  150. window.addEventListener("gamepadconnected", this.conControl);
  151. window.addEventListener("gamepaddisconnected", this.disControl);
  152. await sleep(3000);
  153. this.showLoading = false;
  154. }
  155. };
  156. } catch (error) {
  157. this.socket.disconnect();
  158. this.isLogin = false;
  159. Message.error({
  160. content: "webrtc初始化错误" + error,
  161. duration: 3,
  162. });
  163. }
  164. });
  165. this.socket.on("msg", async (data) => {
  166. if (data.type === "offer") {
  167. console.log(data.sdp);
  168. await this.Peer.setRemoteDescription(data);
  169. const answer = await this.Peer.createAnswer();
  170. await this.Peer.setLocalDescription(answer);
  171. } else if (data.type === "power") {
  172. console.log("电量", data);
  173. } else if (data.type === "signal") {
  174. const v = data.data <= 20 ? Math.floor(data.data / 4) : 5
  175. this.signalValue = v || 1
  176. } else if (data.type === 'speed') {
  177. const v = Math.floor(data.data)
  178. this.SpeedValue = v
  179. }
  180. });
  181. this.socket.on("joined", async () =>
  182. this.socket.emit("msg", { type: "startRTC" })
  183. );
  184. this.socket.on("leaved", () => this.close("车端断开"));
  185. this.socket.on("connect_error", (err) => this.close(err));
  186. },
  187. // 手柄数据
  188. ControlData() {
  189. const data = navigator.getGamepads();
  190. const db = data[0];
  191. if (!db) return;
  192. // 挡位选择AB
  193. if (db.buttons[1].touched) this.speed = 2;
  194. if (db.buttons[0].touched) this.speed = 1;
  195. // 语音按键R2
  196. this.audioState = db.buttons[7].touched
  197. // 静音X3
  198. this.mutedState = db.buttons[3].touched
  199. // 播放警笛Y2
  200. this.warnAudio = db.buttons[2].touched
  201. // console.log(db.buttons);
  202. const params = {
  203. v0: Math.floor(db.axes[0] * 128 + 128),
  204. v1: Math.floor(db.axes[1] * 128 + 128),
  205. v2: Math.floor(db.axes[2] * 128 + 128),
  206. v3: this.Gear(this.speed, Math.floor(db.axes[3] * 128 + 128)),
  207. };
  208. if (this.socket && this.socket.connected) this.socket.emit("msg", { type: "conctrl", conctrl: params });
  209. requestAnimationFrame(this.ControlData);
  210. },
  211. // 发送语音
  212. sendAudio(blob) {
  213. if (!this.socket && !this.socket.connected) return;
  214. this.socket.emit("msg", {
  215. type: "Meadia",
  216. Meadia: blob,
  217. });
  218. },
  219. // 挡位
  220. Gear(speed, num) {
  221. // 低速档
  222. if (speed === 1) {
  223. if (num < 116) {
  224. // 前
  225. num = 120;
  226. } else if (num >= 120 && num <= 131) {
  227. num = 128;
  228. } else if (num > 140) {
  229. // 后
  230. num = 140;
  231. }
  232. }
  233. // 高速档
  234. if (speed === 2) {
  235. if (num < 96) {
  236. num = 96;
  237. } else if (num >= 120 && num <= 131) {
  238. num = 128;
  239. } else if (num > 160) {
  240. num = 160;
  241. }
  242. }
  243. return num;
  244. },
  245. // 登录
  246. login(data) {
  247. // if (this.socket) {
  248. // this.socket.auth = {
  249. // roomID: data.roomID,
  250. // name: data.name,
  251. // };
  252. // this.socket.connect();
  253. // } else {
  254. // this.error = "服务器连接失败";
  255. // }
  256. const obj = {
  257. roomID: data.roomID,
  258. name: data.name
  259. }
  260. const socket = new WebSocket(this.HOST+'?test=123');
  261. console.log(666,socket);
  262. },
  263. // 关闭
  264. close(err) {
  265. if (this.Peer) this.Peer.close();
  266. if (this.remoteVideo) this.remoteVideo.srcObject = null;
  267. this.socket.disconnect();
  268. this.isLogin = false;
  269. this.showLoading = false;
  270. this.error = err || "";
  271. this.socket = null;
  272. this.Peer = null
  273. this.num = 0
  274. cancelAnimationFrame(this.ControlData);
  275. window.removeEventListener("gamepadconnected", this.conControl);
  276. window.removeEventListener("gamepaddisconnected", this.disControl);
  277. },
  278. // 手柄连接
  279. conControl() {
  280. this.contrlState = true;
  281. this.ControlData();
  282. },
  283. // 手柄断开连接
  284. disControl() {
  285. this.contrlState = false;
  286. cancelAnimationFrame(this.ControlData);
  287. },
  288. },
  289. mounted() {
  290. // this.intSoketRtc(this.HOST);
  291. },
  292. watch: {
  293. // 静音
  294. mutedState(v) {
  295. if (v) {
  296. this.num++
  297. const state = this.num % 2 ? false : true
  298. this.muted = state
  299. if (this.socket && this.socket.connected) this.socket.emit("msg", { type: "contrlAudio", contrlAudio: state });
  300. }
  301. },
  302. // 鸣笛
  303. warnAudio(v) {
  304. if (v && this.socket && this.socket.connected) this.socket.emit("msg", { type: "warnAudio", warnAudio: v });
  305. }
  306. }
  307. };
  308. </script>
  309. <style>
  310. video,
  311. #app,
  312. html,
  313. body {
  314. margin: 0;
  315. padding: 0;
  316. user-select: none;
  317. width: 100%;
  318. height: 100%;
  319. overflow: hidden;
  320. min-width: 1000px;
  321. min-height: 900px;
  322. }
  323. video {
  324. background: none;
  325. object-fit: fill;
  326. font-size: 0;
  327. }
  328. .marke {
  329. position: fixed;
  330. top: 0;
  331. left: 50%;
  332. width: 555px;
  333. height: 30px;
  334. transform: translate(-50%, 0);
  335. z-index: 1;
  336. display: flex;
  337. align-items: center;
  338. justify-content: center;
  339. }
  340. .marke::before {
  341. position: absolute;
  342. z-index: 0;
  343. content: "";
  344. width: 100%;
  345. height: 0;
  346. border-top: 30px solid rgba(0, 0, 0, 0.25);
  347. border-left: 15px solid transparent;
  348. border-right: 15px solid transparent;
  349. }
  350. .marke>div {
  351. margin: 0 3px;
  352. }
  353. .gauge {
  354. position: fixed;
  355. bottom: 0;
  356. left: 50%;
  357. transform: translate(-50%, 0);
  358. width: 500px;
  359. height: 185px;
  360. z-index: 9;
  361. }
  362. /* 隐藏滚动条 */
  363. ::-webkit-scrollbar {
  364. width: 0 !important;
  365. display: none;
  366. }
  367. </style>