App.vue 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328
  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. <svg
  8. viewBox="0 -50 1024 1024"
  9. version="1.1"
  10. xmlns="http://www.w3.org/2000/svg"
  11. width="30"
  12. height="30"
  13. >
  14. <path
  15. 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"
  16. :fill="contrlState ? '#00CED1' : 'rgba(0, 0, 0, 0.3)'"
  17. ></path>
  18. <path
  19. 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"
  20. :fill="contrlState ? '#00CED1' : 'rgba(0, 0, 0, 0.3)'"
  21. ></path>
  22. <path
  23. 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"
  24. :fill="contrlState ? '#00CED1' : 'rgba(0, 0, 0, 0.3)'"
  25. ></path>
  26. <path
  27. 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"
  28. :fill="contrlState ? '#00CED1' : 'rgba(0, 0, 0, 0.3)'"
  29. ></path>
  30. </svg>
  31. <!-- 音频 -->
  32. <Record />
  33. <!-- 信号 -->
  34. <Signal :signalValue="4" />
  35. <!-- 电量 -->
  36. <Battery :quantity="60" />
  37. </div>
  38. <div class="gauge">
  39. <Gauge :value="50" :gears="speed" />
  40. </div>
  41. <Loading v-if="showLoading" />
  42. </template>
  43. <Login v-else :err="error" @loginBack="login" />
  44. </div>
  45. </template>
  46. <script>
  47. const { io } = require("socket.io-client");
  48. import Login from "@/components/login";
  49. import Loading from "@/components/loading";
  50. import Record from "@/components/record";
  51. import Signal from "@/components/signal";
  52. import Battery from "@/components/battery";
  53. import Gauge from "@/components/gauge";
  54. import { Message } from "view-design";
  55. const sleep = (ms) => new Promise((res) => setTimeout(res, ms));
  56. export default {
  57. components: { Login, Loading, Record, Signal, Battery, Gauge },
  58. data() {
  59. return {
  60. socket: null,
  61. HOST: "wss://car.caner.top",
  62. Peer: null,
  63. isLogin: false,
  64. error: "",
  65. remoteVideo: null,
  66. showLoading: true,
  67. contrlState: false,
  68. speed: 1, //1低速档 | 2 高速档
  69. iceServers: [
  70. {
  71. urls: ["stun:caner.top:3478"],
  72. },
  73. {
  74. urls: "turn:caner.top:3478",
  75. username: "admin",
  76. credential: "123456",
  77. },
  78. ],
  79. };
  80. },
  81. methods: {
  82. // 网络连接
  83. intSoketRtc(host) {
  84. // int socket
  85. this.socket = io(host, {
  86. autoConnect: false,
  87. transports: ["websocket"],
  88. });
  89. // socket
  90. this.socket.on("connect", () => {
  91. try {
  92. this.isLogin = true;
  93. // init webrtc
  94. this.Peer = new RTCPeerConnection({
  95. iceServers: this.iceServers,
  96. bundlePolicy: "max-bundle",
  97. });
  98. // listen state
  99. this.Peer.onicegatheringstatechange = () => {
  100. console.log("GatheringState: ", this.Peer.iceGatheringState);
  101. if (this.Peer.iceGatheringState === "complete") {
  102. const answer = this.Peer.localDescription;
  103. this.socket.emit("msg", answer);
  104. }
  105. };
  106. // listen track
  107. this.Peer.ontrack = async (evt) => {
  108. console.log("track", evt);
  109. this.remoteVideo = document.getElementById("v2");
  110. this.remoteVideo.srcObject = evt.streams[0];
  111. };
  112. // listen changestate
  113. this.Peer.oniceconnectionstatechange = async () => {
  114. const state = this.Peer.iceConnectionState;
  115. console.log("ICE状态", state);
  116. if (
  117. state === "failed" ||
  118. state === "disconnected" ||
  119. state === "closed"
  120. ) {
  121. this.close("P2P通信失败");
  122. }
  123. // ICE连接成功|初始化摇杆
  124. if (state === "connected") {
  125. // init Control
  126. window.addEventListener("gamepadconnected", this.conControl);
  127. window.addEventListener("gamepaddisconnected", this.disControl);
  128. await sleep(3000);
  129. this.showLoading = false;
  130. }
  131. };
  132. } catch (error) {
  133. this.socket.disconnect();
  134. this.isLogin = false;
  135. Message.error({
  136. content: "webrtc初始化错误" + error,
  137. duration: 3,
  138. });
  139. }
  140. });
  141. this.socket.on("msg", async (data) => {
  142. if (data.type === "offer") {
  143. console.log(data.sdp);
  144. await this.Peer.setRemoteDescription(data);
  145. const answer = await this.Peer.createAnswer();
  146. await this.Peer.setLocalDescription(answer);
  147. } else if (data.type === "power") {
  148. console.log("电量", data);
  149. } else if (data.type === "signal") {
  150. console.log("4G信号");
  151. }
  152. });
  153. this.socket.on("joined", async () =>
  154. this.socket.emit("msg", { type: "startRTC" })
  155. );
  156. this.socket.on("leaved", () => this.close("车端断开"));
  157. this.socket.on("connect_error", (err) => this.close(err));
  158. },
  159. // 手柄数据
  160. ControlData() {
  161. const data = navigator.getGamepads();
  162. const db = data[0];
  163. if (!db) return;
  164. // 挡位选择AB
  165. if (db.buttons[1].touched) this.speed = 2;
  166. if (db.buttons[0].touched) this.speed = 1;
  167. // 语音按键
  168. if (db.buttons[7].touched) console.log("R2");
  169. const params = {
  170. v0: Math.floor(db.axes[0] * 128 + 128),
  171. v1: Math.floor(db.axes[1] * 128 + 128),
  172. v2: Math.floor(db.axes[2] * 128 + 128),
  173. v3: this.Gear(this.speed, Math.floor(db.axes[3] * 128 + 128)),
  174. };
  175. if (this.socket.connected) {
  176. this.socket.emit("msg", { type: "conctrl", conctrl: params });
  177. }
  178. requestAnimationFrame(this.ControlData);
  179. },
  180. // 挡位
  181. Gear(speed, num) {
  182. // 低速档
  183. if (speed === 1) {
  184. if (num < 116) {
  185. num = 116;
  186. } else if (num >= 120 && num <= 131) {
  187. num = 128;
  188. } else if (num > 140) {
  189. num = 140;
  190. }
  191. }
  192. // 高速档
  193. if (speed === 2) {
  194. if (num < 96) {
  195. num = 96;
  196. } else if (num >= 120 && num <= 131) {
  197. num = 128;
  198. } else if (num > 160) {
  199. num = 160;
  200. }
  201. }
  202. return num;
  203. },
  204. // 登录
  205. login(data) {
  206. if (this.socket) {
  207. this.socket.auth = {
  208. roomID: data.roomID,
  209. name: data.name,
  210. };
  211. this.socket.connect();
  212. } else {
  213. this.error = "服务器连接失败";
  214. }
  215. },
  216. // 关闭
  217. close(err) {
  218. if (this.Peer) this.Peer.close();
  219. if (this.remoteVideo) this.remoteVideo.srcObject = null;
  220. this.socket.disconnect();
  221. this.isLogin = false;
  222. this.showLoading = false;
  223. this.error = err || "";
  224. this.socket = null;
  225. cancelAnimationFrame(this.ControlData);
  226. window.removeEventListener("gamepadconnected", this.conControl);
  227. window.removeEventListener("gamepaddisconnected", this.disControl);
  228. },
  229. // 手柄连接
  230. conControl() {
  231. this.contrlState = true;
  232. this.ControlData();
  233. },
  234. // 手柄断开连接
  235. disControl() {
  236. this.contrlState = false;
  237. cancelAnimationFrame(this.ControlData);
  238. },
  239. },
  240. mounted() {
  241. window.addEventListener("gamepadconnected", this.conControl);
  242. window.addEventListener("gamepaddisconnected", this.disControl);
  243. this.intSoketRtc(this.HOST);
  244. },
  245. destroyed() {
  246. this.close();
  247. },
  248. };
  249. </script>
  250. <style>
  251. video,
  252. #app,
  253. html,
  254. body {
  255. margin: 0;
  256. padding: 0;
  257. user-select: none;
  258. width: 100%;
  259. height: 100%;
  260. overflow: hidden;
  261. min-width: 1000px;
  262. min-height: 900px;
  263. }
  264. video {
  265. background: none;
  266. object-fit: fill;
  267. font-size: 0;
  268. }
  269. .marke {
  270. position: fixed;
  271. top: 0;
  272. left: 50%;
  273. width: 555px;
  274. height: 30px;
  275. transform: translate(-50%, 0);
  276. z-index: 1;
  277. display: flex;
  278. align-items: center;
  279. justify-content: center;
  280. }
  281. .marke::before {
  282. position: absolute;
  283. z-index: 0;
  284. content: "";
  285. width: 100%;
  286. height: 0;
  287. border-top: 30px solid rgba(0, 0, 0, 0.25);
  288. border-left: 15px solid transparent;
  289. border-right: 15px solid transparent;
  290. }
  291. .marke > div {
  292. margin: 0 3px;
  293. }
  294. .gauge {
  295. position: fixed;
  296. bottom: 0;
  297. left: 50%;
  298. transform: translate(-50%, 0);
  299. width: 500px;
  300. height: 185px;
  301. z-index: 9;
  302. }
  303. /* 隐藏滚动条 */
  304. ::-webkit-scrollbar {
  305. width: 0 !important;
  306. display: none;
  307. }
  308. </style>