From 3d3aa884febdfaf6a930bc00109cef437cdf6fbd Mon Sep 17 00:00:00 2001 From: Daniel Bulant Date: Wed, 27 Jul 2022 19:32:01 +0200 Subject: [PATCH] connection + chat --- client/src/lib/Websocket.ts | 233 +++++++++------- client/src/lib/view/components/button.svelte | 21 ++ client/src/lib/view/game/game.svelte | 5 + client/src/lib/view/game/waiting.svelte | 168 ++++++++++++ client/src/lib/view/menu/list.svelte | 35 +-- client/src/lib/view/menu/nameChoose.svelte | 19 +- server/src/index.js | 274 ++++++++++++++----- 7 files changed, 554 insertions(+), 201 deletions(-) create mode 100644 client/src/lib/view/components/button.svelte create mode 100644 client/src/lib/view/game/waiting.svelte diff --git a/client/src/lib/Websocket.ts b/client/src/lib/Websocket.ts index 2f930c9..66bcae3 100644 --- a/client/src/lib/Websocket.ts +++ b/client/src/lib/Websocket.ts @@ -8,59 +8,19 @@ class FastEvent extends Event { } } -const hosts: { urls: string, credential?: string, username?: string }[] = ("stun.ipfire.org:3478\n" + - "stun.rolmail.net:3478\n" + - "stun.steinbeis-smi.de:3478\n" + - "stun.marcelproust.it:3478\n" + - "stun3.3cx.com:3478\n" + - "stun.voipraider.com:3478\n" + - "stun.kore.com:3478\n" + - "stun.voipstunt.com:3478\n" + - "stun.fairytel.at:3478\n" + - "stun.h4v.eu:3478\n" + - "stun.peethultra.be:3478\n" + - "stun.ortopediacoam.it:3478\n" + - "stun.infra.net:3478\n" + - "stun.vavadating.com:3478\n" + - "stun.mixvoip.com:3478\n" + - "stun.tele2.net:3478\n" + - "stun2.3cx.com:3478\n" + - "stun.myhowto.org:3478\n" + - "stun.cellmail.com:3478\n" + - "stun.poetamatusel.org:3478\n" + - "stun.textz.com:3478\n" + - "stun.romancecompass.com:3478\n" + - "stun.ixc.ua:3478\n" + - "stun.actionvoip.com:3478\n" + - "stun.bethesda.net:3478\n" + - "stun.parcodeinebrodi.it:3478\n" + - "stun.jay.net:3478\n" + - "stun.demos.ru:3478\n" + - "stun.cloopen.com:3478\n" + - "stun.crimeastar.net:3478\n" + - "stun.vivox.com:3478\n" + - "stun.openjobs.hu:3478\n" + - "stun.kaznpu.kz:3478\n" + - "stun.linphone.org:3478\n" + - "stun.l.google.com:19302\n" + - "stun.sonetel.net:3478").split("\n").map(t => ({ urls: "stun:" + t })); - -hosts.push({ - urls: 'turn:relay.backups.cz', - credential: 'webrtc', - username: 'webrtc' -}, +const hosts: { urls: string, credential?: string, username?: string }[] = [ { - urls: 'turn:relay.backups.cz?transport=tcp', - credential: 'webrtc', - username: 'webrtc' - }); + urls: "stun:openrelay.metered.ca:80", + } +] class ConnectedClient extends EventTarget { conn: RTCPeerConnection; sendChannel: RTCDataChannel; candidates: any[] = []; state: RTCDataChannelState | null = null; + readyState: number = 0; + pings: number[] = []; constructor(public ws: WebsocketConnection, public name: string) { super(); @@ -72,41 +32,62 @@ class ConnectedClient extends EventTarget { } initializeConnection() { + this.pings = []; console.log("Initializing connection"); this.conn = new RTCPeerConnection({ iceServers: hosts }); this.conn.onicecandidate = e => { - console.log(e); + console.log("candidate", e, e.candidate); if (!e.candidate) return; this.candidates.push(e.candidate); this.ws.send(JSON.stringify({ t: "cand", target: this.name, d: e.candidate })); }; this.conn.onicecandidateerror = (e) => console.error(e); - this.conn.ondatachannel = e => { - this.sendChannel = e.channel; - let timer: any; - this.sendChannel.onclose = (e) => { - clearInterval(timer); - this.statusChanged(); - } - this.sendChannel.onopen = (e) => { - timer = setInterval(() => { - this.send({ t: "p", d: Date.now() }); - }, 300); - this.statusChanged(); - } + this.conn.ondatachannel = e => this.onDataChannel(e.channel); + } + + onDataChannel(channel: RTCDataChannel) { + console.log("on data channel"); + this.sendChannel = channel; + let timer: any; + this.sendChannel.onclose = (e) => { + clearInterval(timer); this.statusChanged(); - this.sendChannel.onmessage = (e) => { - const msg = JSON.parse(e.data); - switch (msg.t) { - default: - console.log("MSG", msg); - this.dispatchEvent(new FastEvent(msg.t, msg.d)); - } - }; } + this.sendChannel.onopen = (e) => { + timer = setInterval(() => { + this.send({ t: "p", d: Date.now() }); + }, 100); + this.statusChanged(); + } + this.statusChanged(); + this.sendChannel.onmessage = (e) => { + const msg = JSON.parse(e.data); + switch (msg.t) { + case "p": + this.send({ + t: "pr", + d: msg.d, + y: Date.now() + }) + break; + case "pr": + this.pings.push(Date.now() - msg.d); + if(this.pings.length > 15) this.pings = this.pings.slice(-15); + players.update(t => t); + break; + case "msg": + console.log("message", msg.d); + this.dispatchEvent(new FastEvent("message", msg.d)); + messages.update(t => { t.push({ author: this.name, content: msg.d });return t}) + break; + default: + console.log("MSG", msg); + this.dispatchEvent(new FastEvent(msg.t, msg.d)); + } + }; } send(data: any) { @@ -120,7 +101,10 @@ class ConnectedClient extends EventTarget { this.initializeConnection(); } this.state = this.sendChannel.readyState; + if (this.state === "open") this.readyState = 3; + if (["closing", "closed"].includes(this.state)) this.readyState = 4; console.log("state", this.state); + players.update(t => t); } } } @@ -130,13 +114,14 @@ export class WebsocketConnection extends EventTarget { ws: WebSocket; fast: Map = new Map(); roomName: string | null = null; - roomId: string | null = null; + roomHost: string | null = null; constructor(public name: string) { super(); // @ts-ignore Initialized in the next function call this.ws = null; this.connect(); + players.set(this.fast); } connect() { @@ -149,11 +134,15 @@ export class WebsocketConnection extends EventTarget { console.log("WS closed"); lastError.set(e.reason || "Connection closed"); connection.set(null); + room.set(null); + list.set(null); }); this.ws.addEventListener("error", (e) => { console.error("WS error"); lastError.set("Connection error"); connection.set(null); + room.set(null); + list.set(null); }); this.ws.addEventListener("message", (e) => { const msg = JSON.parse(e.data); @@ -161,58 +150,79 @@ export class WebsocketConnection extends EventTarget { switch (msg.t) { case "cand": { const fast = this.fast.get(msg.source); - if (!fast) return; + if (!fast) return console.log("No fast connection"); + if (fast.readyState < 1) fast.readyState == 1; + players.set(this.fast); + console.log("Received candidates"); if (fast.state === "open") return console.log("Already open"); - for (const candidate of msg.d) { - fast.conn.addIceCandidate(candidate).then(); - } + fast.conn.addIceCandidate(msg.d).then(); break; } case "desc": { const fast = this.fast.get(msg.source); - if (!fast) return; + if (!fast) return console.log("No fast connection"); + if (fast.readyState < 2) fast.readyState == 2; + players.set(this.fast); if (fast.state === "open") return console.log("Already open"); - fast.conn.setRemoteDescription(msg.d) - .then(() => fast.conn.createAnswer()) - .then(answer => fast.conn.setLocalDescription(answer)) - .then(() => - this.ws.send(JSON.stringify({ t: "desc", target: fast.name, d: fast.conn.localDescription })) - ) + if (msg.d.type === "answer") { + fast.conn.setRemoteDescription(msg.d); + } else if (msg.d.type === "offer") { + fast.conn.setRemoteDescription(msg.d) + .then(() => fast.conn.createAnswer()) + .then(answer => fast.conn.setLocalDescription(answer)) + .then(() => + this.ws.send(JSON.stringify({ t: "desc", target: fast.name, d: fast.conn.localDescription })) + ) + } break; } case "join": { - const fast = new ConnectedClient(this, msg.name); - this.fast.set(msg.name, fast); - if (fast.conn.localDescription) { - this.ws.send(JSON.stringify({ t: "desc", target: msg.name, d: fast.conn.localDescription })) - } - if (fast.candidates) { - this.ws.send(JSON.stringify({ t: "cand", target: msg.name, d: fast.candidates })); + const fast = new ConnectedClient(this, msg.client); + players.set(this.fast); + this.fast.set(msg.client, fast); + if (fast.candidates && fast.candidates.length) { + for (const candidate of fast.candidates) { + this.ws.send(JSON.stringify({ t: "cand", target: msg.client, d: candidate })); + } } + messages.update(t => { t.push({ author: " SYS ", content: `${msg.client} joined`});return t}) break; } case "joined": { const clients = msg.clients; this.fast = new Map(); for (const client of clients) { + if (client === this.name) continue; const fast = new ConnectedClient(this, client); - if (fast.conn.localDescription) { - this.ws.send(JSON.stringify({ t: "desc", target: msg.name, d: fast.conn.localDescription })) - } - if (fast.candidates) { - this.ws.send(JSON.stringify({ t: "cand", target: msg.name, d: fast.candidates })); - } + fast.conn.createOffer() + .then(offer => fast.conn.setLocalDescription(offer)) + .then(() => + this.ws.send(JSON.stringify({ t: "desc", target: client, d: fast.conn.localDescription })) + ); + fast.sendChannel = fast.conn.createDataChannel("sendChannel"); + fast.onDataChannel(fast.sendChannel); this.fast.set(client, fast); } - // missing break on purpose + players.set(this.fast); + messages.set([{ + author: " SYS ", content: `${msg.client} joined` + }]); + this.roomName = msg.name; + this.roomHost = msg.host; + room.set({ + name: msg.name, + host: msg.host + }); + break; } case "create": { this.roomName = msg.name; - this.roomId = msg.id; + this.roomHost = this.name; room.set({ name: msg.name, - id: msg.id + host: this.name }); + messages.update(t => { t.push({ author: " SYS ", content: `${msg.name} created the room`});return t}) break; } case "leave": { @@ -220,15 +230,24 @@ export class WebsocketConnection extends EventTarget { if (!fast) return; fast.conn.close(); this.fast.delete(msg.name); + players.set(this.fast); + messages.update(t => { t.push({ author: " SYS ", content: `${msg.client} left`});return t}) + break; + } + case "host": { + this.roomHost = msg.host; + room.update(t => { t!.host = this.roomHost!; return t }); + messages.update(t => { t.push({ author: " SYS ", content: `${msg.host} is now host`});return t}) break; } case "left": { console.log("Left room successfully"); this.roomName = null; - this.roomId = null; room.set(null); this.fast.forEach(connection => connection.conn.close()); this.fast = new Map(); + players.set(this.fast); + messages.set([]); break; } case "list": { @@ -245,10 +264,22 @@ export class WebsocketConnection extends EventTarget { }); } + sendMessage(msg: string) { + if (!this.roomName) return console.log("Not in a room"); + for(const [, client] of this.fast) { + client.send({ t: "msg", d: msg }); + } + messages.update(t => { t.push({ author: this.name, content: msg }); return t }); + } + createGame(name: string) { this.ws.send(JSON.stringify({ t: "create", name: name })); } + join(name: string) { + this.ws.send(JSON.stringify({ t: "join", name: name })); + } + refreshList() { this.ws.send(JSON.stringify({ t: "list" })); listLoading.set(true); @@ -259,8 +290,10 @@ export class WebsocketConnection extends EventTarget { } } -export const connection: Writable = writable(null); -export const list: Writable<{ id: string, name: string, count: number }[]|null> = writable(null); +export const connection: Writable = writable(null); +export const list: Writable<{ name: string, count: number }[] | null> = writable(null); export const listLoading = writable(true); export const lastError: Writable = writable(""); -export const room: Writable<{ name: string, id: string }|null> = writable(null); \ No newline at end of file +export const room: Writable<{ name: string, host: string } | null> = writable(null); +export const players: Writable> = writable(new Map); +export const messages: Writable<{ author: string, content: string }[]> = writable([]); \ No newline at end of file diff --git a/client/src/lib/view/components/button.svelte b/client/src/lib/view/components/button.svelte new file mode 100644 index 0000000..0d60859 --- /dev/null +++ b/client/src/lib/view/components/button.svelte @@ -0,0 +1,21 @@ + + + + \ No newline at end of file diff --git a/client/src/lib/view/game/game.svelte b/client/src/lib/view/game/game.svelte index e69de29..8d1286b 100644 --- a/client/src/lib/view/game/game.svelte +++ b/client/src/lib/view/game/game.svelte @@ -0,0 +1,5 @@ + + + \ No newline at end of file diff --git a/client/src/lib/view/game/waiting.svelte b/client/src/lib/view/game/waiting.svelte new file mode 100644 index 0000000..a6fcb44 --- /dev/null +++ b/client/src/lib/view/game/waiting.svelte @@ -0,0 +1,168 @@ + + +
+
+
    +
  • + {$connection?.name} + {#if $room?.host === $connection?.name} + Host + {/if} + YOU +
  • + {#each [...$players.values()] as player (player.name)} +
  • + {player.name} + {#if $room?.host === player.name} + Host + {/if} + {['joined', 'connecting', 'connecting...', 'ready', 'reconnecting'][ + player.readyState + ]} + {#if player.pings && player.pings.length > 0} + {Math.floor(player.pings.reduce((a, b) => a + b, 0) / player.pings.length * 10) / 10}ms + {/if} +
  • + {/each} +
+
+
+
+ {#each $messages as message} +
+ {#if message.author !== " SYS "} + {message.author}: + {/if} + {message.content} +
+ {/each} +
+
+
+ +
+ + {#if $room?.host === $connection?.name} + + {:else} + + {/if} +
+
+
+ + diff --git a/client/src/lib/view/menu/list.svelte b/client/src/lib/view/menu/list.svelte index 7dcb56d..4039c3e 100644 --- a/client/src/lib/view/menu/list.svelte +++ b/client/src/lib/view/menu/list.svelte @@ -1,6 +1,7 @@ {#if creatingGame} @@ -44,8 +49,8 @@ {error}
- - + +
{/if} @@ -54,20 +59,20 @@

Games - {$connection?.name}

- +
{#if $listLoading} Loading... {/if}
    {#if $list} - {#each $list as $game} -
  • + {#each $list as game} +
  • connect(game)}> - {$game.name} + {game.name} - {$game.count} + {game.count}
  • {/each} @@ -133,19 +138,7 @@ font-size: 1.5rem; cursor: pointer; } - button { - width: 10em; - height: 3em; - border: 1px solid #bd5ce6; - border-radius: 0; - padding: 0.5em; - font-size: 1.5rem; - border: 10px solid #bd5ce6; - background-color: #85E65C; - cursor: pointer; - } - button:active { - background-color: #bd5ce6; - color: #85E65C; + input { + font-family: inherit; } \ No newline at end of file diff --git a/client/src/lib/view/menu/nameChoose.svelte b/client/src/lib/view/menu/nameChoose.svelte index 1357ac1..8ad1b3f 100644 --- a/client/src/lib/view/menu/nameChoose.svelte +++ b/client/src/lib/view/menu/nameChoose.svelte @@ -1,5 +1,6 @@