| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328 |
- <template>
- <div id="app">
- <template v-if="isLogin">
- <video id="v2" autoplay playsinline muted></video>
- <div class="marke">
- <!-- 手柄状态 -->
- <svg
- viewBox="0 -50 1024 1024"
- version="1.1"
- xmlns="http://www.w3.org/2000/svg"
- width="30"
- height="30"
- >
- <path
- 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"
- :fill="contrlState ? '#00CED1' : 'rgba(0, 0, 0, 0.3)'"
- ></path>
- <path
- 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"
- :fill="contrlState ? '#00CED1' : 'rgba(0, 0, 0, 0.3)'"
- ></path>
- <path
- 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"
- :fill="contrlState ? '#00CED1' : 'rgba(0, 0, 0, 0.3)'"
- ></path>
- <path
- 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"
- :fill="contrlState ? '#00CED1' : 'rgba(0, 0, 0, 0.3)'"
- ></path>
- </svg>
- <!-- 音频 -->
- <Record />
- <!-- 信号 -->
- <Signal :signalValue="4" />
- <!-- 电量 -->
- <Battery :quantity="60" />
- </div>
- <div class="gauge">
- <Gauge :value="50" :gears="speed" />
- </div>
- <Loading v-if="showLoading" />
- </template>
- <Login v-else :err="error" @loginBack="login" />
- </div>
- </template>
- <script>
- const { io } = require("socket.io-client");
- import Login from "@/components/login";
- import Loading from "@/components/loading";
- import Record from "@/components/record";
- import Signal from "@/components/signal";
- import Battery from "@/components/battery";
- import Gauge from "@/components/gauge";
- import { Message } from "view-design";
- const sleep = (ms) => new Promise((res) => setTimeout(res, ms));
- export default {
- components: { Login, Loading, Record, Signal, Battery, Gauge },
- data() {
- return {
- socket: null,
- HOST: "wss://car.caner.top",
- Peer: null,
- isLogin: false,
- error: "",
- remoteVideo: null,
- showLoading: true,
- contrlState: false,
- speed: 1, //1低速档 | 2 高速档
- iceServers: [
- {
- urls: ["stun:caner.top:3478"],
- },
- {
- urls: "turn:caner.top:3478",
- username: "admin",
- credential: "123456",
- },
- ],
- };
- },
- methods: {
- // 网络连接
- intSoketRtc(host) {
- // int socket
- this.socket = io(host, {
- autoConnect: false,
- transports: ["websocket"],
- });
- // socket
- this.socket.on("connect", () => {
- try {
- this.isLogin = true;
- // init webrtc
- this.Peer = new RTCPeerConnection({
- iceServers: this.iceServers,
- bundlePolicy: "max-bundle",
- });
- // listen state
- this.Peer.onicegatheringstatechange = () => {
- console.log("GatheringState: ", this.Peer.iceGatheringState);
- if (this.Peer.iceGatheringState === "complete") {
- const answer = this.Peer.localDescription;
- this.socket.emit("msg", answer);
- }
- };
- // listen track
- this.Peer.ontrack = async (evt) => {
- console.log("track", evt);
- this.remoteVideo = document.getElementById("v2");
- this.remoteVideo.srcObject = evt.streams[0];
- };
- // listen changestate
- this.Peer.oniceconnectionstatechange = async () => {
- const state = this.Peer.iceConnectionState;
- console.log("ICE状态", state);
- if (
- state === "failed" ||
- state === "disconnected" ||
- state === "closed"
- ) {
- this.close("P2P通信失败");
- }
- // ICE连接成功|初始化摇杆
- if (state === "connected") {
- // init Control
- window.addEventListener("gamepadconnected", this.conControl);
- window.addEventListener("gamepaddisconnected", this.disControl);
- await sleep(3000);
- this.showLoading = false;
- }
- };
- } catch (error) {
- this.socket.disconnect();
- this.isLogin = false;
- Message.error({
- content: "webrtc初始化错误" + error,
- duration: 3,
- });
- }
- });
- this.socket.on("msg", async (data) => {
- if (data.type === "offer") {
- console.log(data.sdp);
- await this.Peer.setRemoteDescription(data);
- const answer = await this.Peer.createAnswer();
- await this.Peer.setLocalDescription(answer);
- } else if (data.type === "power") {
- console.log("电量", data);
- } else if (data.type === "signal") {
- console.log("4G信号");
- }
- });
- this.socket.on("joined", async () =>
- this.socket.emit("msg", { type: "startRTC" })
- );
- this.socket.on("leaved", () => this.close("车端断开"));
- this.socket.on("connect_error", (err) => this.close(err));
- },
- // 手柄数据
- ControlData() {
- const data = navigator.getGamepads();
- const db = data[0];
- if (!db) return;
- // 挡位选择AB
- if (db.buttons[1].touched) this.speed = 2;
- if (db.buttons[0].touched) this.speed = 1;
- // 语音按键
- if (db.buttons[7].touched) console.log("R2");
- 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: this.Gear(this.speed, Math.floor(db.axes[3] * 128 + 128)),
- };
- if (this.socket.connected) {
- this.socket.emit("msg", { type: "conctrl", conctrl: params });
- }
- requestAnimationFrame(this.ControlData);
- },
- // 挡位
- Gear(speed, num) {
- // 低速档
- if (speed === 1) {
- if (num < 116) {
- num = 116;
- } 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;
- },
- // 登录
- login(data) {
- if (this.socket) {
- this.socket.auth = {
- roomID: data.roomID,
- name: data.name,
- };
- this.socket.connect();
- } else {
- this.error = "服务器连接失败";
- }
- },
- // 关闭
- close(err) {
- if (this.Peer) this.Peer.close();
- if (this.remoteVideo) this.remoteVideo.srcObject = null;
- this.socket.disconnect();
- this.isLogin = false;
- this.showLoading = false;
- this.error = err || "";
- this.socket = null;
- cancelAnimationFrame(this.ControlData);
- window.removeEventListener("gamepadconnected", this.conControl);
- window.removeEventListener("gamepaddisconnected", this.disControl);
- },
- // 手柄连接
- conControl() {
- this.contrlState = true;
- this.ControlData();
- },
- // 手柄断开连接
- disControl() {
- this.contrlState = false;
- cancelAnimationFrame(this.ControlData);
- },
- },
- mounted() {
- window.addEventListener("gamepadconnected", this.conControl);
- window.addEventListener("gamepaddisconnected", this.disControl);
- this.intSoketRtc(this.HOST);
- },
- destroyed() {
- this.close();
- },
- };
- </script>
- <style>
- video,
- #app,
- html,
- body {
- margin: 0;
- padding: 0;
- user-select: none;
- width: 100%;
- height: 100%;
- overflow: hidden;
- min-width: 1000px;
- min-height: 900px;
- }
- video {
- background: none;
- object-fit: fill;
- font-size: 0;
- }
- .marke {
- 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;
- }
- .marke::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;
- }
- .marke > div {
- margin: 0 3px;
- }
- .gauge {
- position: fixed;
- bottom: 0;
- left: 50%;
- transform: translate(-50%, 0);
- width: 500px;
- height: 185px;
- z-index: 9;
- }
- /* 隐藏滚动条 */
- ::-webkit-scrollbar {
- width: 0 !important;
- display: none;
- }
- </style>
|