godot-webrtc-mplayer-testing/server.ts

457 lines
12 KiB
TypeScript
Raw Normal View History

2021-12-02 15:13:12 -06:00
// import { randomInt } from "./deps.ts";
2021-11-15 15:43:33 -06:00
2021-12-07 16:09:50 -06:00
const SERVER_VERSION = "0.2.0";
2021-11-17 13:57:45 -06:00
// TODO: version comparison
2021-11-15 14:26:39 -06:00
2021-11-17 13:57:45 -06:00
type ID = string;
2021-11-15 14:26:39 -06:00
2021-11-17 13:57:45 -06:00
// app state
const allLobbies = new Map<ID, Lobby>();
const allClients = new Map<ID, Client>();
2021-12-02 16:30:24 -06:00
// TODO: client index by id
2021-11-17 13:57:45 -06:00
interface DataMessage {
type: string;
2021-11-17 08:18:12 -06:00
}
2021-11-15 14:26:39 -06:00
2021-12-07 16:09:50 -06:00
type ServerDataObject = Record<
string,
string | number | symbol | null | boolean
>;
type ServerData = ServerDataObject[] | ServerDataObject | string | boolean;
2021-11-17 13:57:45 -06:00
type Message = string | DataMessage;
const buildMessage = (
type: string,
data: ServerData | ServerData[],
) =>
Object.assign(
{ type },
Array.isArray(data)
? { data }
: (typeof data === "object" ? data : { data }),
);
class Client {
id: ID;
2021-11-17 16:24:00 -06:00
peerId: number | null;
2021-11-17 08:18:12 -06:00
name: string;
2021-11-17 13:57:45 -06:00
socket: WebSocket;
lobby: Lobby | null;
2021-12-07 16:09:50 -06:00
ready: boolean;
2021-11-17 13:57:45 -06:00
constructor(socket: WebSocket) {
this.id = crypto.randomUUID();
this.socket = socket;
2021-11-17 16:24:00 -06:00
this.peerId = null;
2021-11-17 13:57:45 -06:00
this.name = "Client";
this.lobby = null;
2021-12-07 16:09:50 -06:00
this.ready = false;
2021-11-17 13:57:45 -06:00
allClients.set(this.id, this);
}
isConnected() {
return this.socket.readyState == WebSocket.OPEN;
}
2021-12-07 16:09:50 -06:00
setReady(ready: boolean) {
this.ready = ready;
this.lobby?.broadcast(
buildMessage("ready_change", { id: this.id, ready: this.ready }),
);
}
2021-11-17 13:57:45 -06:00
remove() {
this.lobbyLeave();
if (this.isConnected()) {
this.socket.close();
}
allClients.delete(this.id);
}
send(message: Message) {
try {
2021-11-17 14:01:35 -06:00
if (this.isConnected()) {
this.socket.send(
typeof message === "object"
? ("json:" + JSON.stringify(message))
: message,
);
}
2021-11-17 13:57:45 -06:00
} catch (e) {
console.error(
2021-12-07 12:33:09 -06:00
`Failed to send on socket ${this.socket} to client ${this.id}: ${
JSON.stringify(e)
}`,
2021-11-17 13:57:45 -06:00
);
2021-12-07 12:33:09 -06:00
this.remove();
2021-11-17 13:57:45 -06:00
}
}
2021-12-07 12:33:09 -06:00
ping() {
this.send(buildMessage("ping", {}));
}
2021-12-06 16:27:16 -06:00
peerList() {
2021-11-17 13:57:45 -06:00
if (!this.lobby) return;
2021-12-07 16:09:50 -06:00
const peers: {
id: ID;
name: string;
peerId: number | null;
ready: boolean;
}[] = [];
this.lobby.clients.forEach(({ id, name, peerId, ready }) =>
peers.push({ id, name, peerId, ready })
2021-11-17 16:24:00 -06:00
);
2021-12-06 16:27:16 -06:00
this.send(buildMessage("peer_joined", peers));
2021-11-17 13:57:45 -06:00
// TODO: chunk async?
}
lobbyList() {
2021-12-07 16:09:50 -06:00
const netLobbies: {
id: ID;
name: string;
maxPlayers: number;
locked: boolean;
currentPlayers: number;
}[] = [];
allLobbies.forEach((lobby) => {
const { id, name, maxPlayers, locked } = lobby;
netLobbies.push({
id,
name,
maxPlayers,
locked,
currentPlayers: lobby.clients.size,
});
});
2021-11-17 13:57:45 -06:00
// TODO: chunk async?
this.send(buildMessage("lobby_list", netLobbies));
}
2021-12-07 16:09:50 -06:00
lobbyNew(lobby: Lobby) {
2021-12-08 16:35:37 -06:00
// if the client is already in a lobby, only send messages about their lobby
if (this.lobby && this.lobby != lobby) return;
2021-12-07 16:09:50 -06:00
const { id, name, maxPlayers, locked } = lobby;
this.send(
buildMessage("lobby_new", {
id,
name,
maxPlayers,
locked,
currentPlayers: lobby.clients.size,
}),
);
2021-11-17 13:57:45 -06:00
}
lobbyDelete({ id }: Lobby) {
if (this.lobby) return;
this.send(buildMessage("lobby_delete", { id }));
}
2021-12-07 16:09:50 -06:00
lobbyCreate(opts?: Partial<Lobby>) {
2021-12-08 16:35:37 -06:00
this.ready = true;
2021-11-17 13:57:45 -06:00
if (this.lobby) {
this.send(
`[info] cannot create lobby (already in lobby ${this.lobby.id})`,
);
return;
}
2021-12-07 16:09:50 -06:00
new Lobby(this, opts?.name || `${this.name}'s lobby`, opts?.maxPlayers);
2021-11-17 13:57:45 -06:00
}
lobbyJoin(lobby: Lobby) {
if (this.lobby) {
this.send(`[info] cannot join lobby (already in lobby ${this.lobby.id})`);
return;
}
this.lobby = lobby;
lobby.addClient(this);
2021-11-17 16:24:00 -06:00
this.send(
2021-12-07 09:32:20 -06:00
buildMessage("lobby_joined", {
id: lobby.id,
name: lobby.name,
peerId: this.peerId,
}),
2021-11-17 16:24:00 -06:00
);
2021-11-17 13:57:45 -06:00
}
lobbyLeave() {
2021-12-08 16:35:37 -06:00
this.ready = false;
2021-11-17 13:57:45 -06:00
const leavingLobby = this.lobby;
if (!leavingLobby) {
this.send(`[info] cannot leave lobby (not in a lobby)`);
return;
}
2021-11-17 16:24:00 -06:00
this.peerId = null;
2021-11-17 13:57:45 -06:00
this.lobby = null;
if (this.isConnected()) {
this.send(buildMessage("lobby_left", { id: leavingLobby.id }));
}
leavingLobby.removeClient(this);
}
2021-11-15 14:26:39 -06:00
}
2021-11-17 13:57:45 -06:00
class Lobby {
id: ID;
name: string;
clients: Map<ID, Client>;
hostClientId: ID;
2021-12-07 16:09:50 -06:00
maxPlayers: number;
locked: boolean;
2021-11-17 13:57:45 -06:00
2021-12-08 16:35:37 -06:00
_shouldNotify: boolean;
2021-12-07 16:09:50 -06:00
constructor(host: Client, name?: string, maxPlayers = 20) {
2021-11-17 13:57:45 -06:00
this.id = crypto.randomUUID();
this.hostClientId = host.id;
this.clients = new Map<ID, Client>();
this.name = name || this.id;
2021-12-07 16:09:50 -06:00
this.maxPlayers = maxPlayers;
this.locked = false;
2021-11-17 13:57:45 -06:00
2021-12-08 16:35:37 -06:00
this._shouldNotify = true;
2021-11-17 13:57:45 -06:00
allLobbies.set(this.id, this);
2021-11-17 16:24:00 -06:00
host.peerId = 1;
2021-11-17 13:57:45 -06:00
host.lobbyJoin(this);
2021-12-07 16:09:50 -06:00
this.notify();
}
update(
requestor: Client,
newValues: { name?: string; maxPlayers?: number; locked?: boolean },
) {
if (requestor.peerId === 1) {
for (const k in newValues) {
switch (k) {
case "name":
if (newValues?.name) this.name = newValues.name;
break;
case "maxPlayers":
if (
newValues?.maxPlayers && typeof newValues.maxPlayers === "number"
) {
this.maxPlayers = newValues.maxPlayers;
}
break;
case "locked":
if (newValues?.locked) this.locked = newValues.locked;
break;
}
}
this.notify();
} else {
requestor.send(
"[info] you cannot update this lobby (you are not the host!)",
);
}
}
notify() {
2021-12-08 16:35:37 -06:00
if (!this._shouldNotify) return;
2021-11-17 13:57:45 -06:00
allClients.forEach((client) => client.lobbyNew(this));
}
remove() {
2021-12-08 16:35:37 -06:00
this._shouldNotify = false;
2021-12-07 12:33:09 -06:00
allLobbies.delete(this.id);
2021-11-17 13:57:45 -06:00
allClients.forEach((client) => client.lobbyDelete(this));
this.clients.forEach((client) => {
client.lobbyLeave();
});
}
broadcast(message: Message) {
this.clients.forEach((client) => client.send(message));
}
addClient(client: Client) {
2021-11-17 16:24:00 -06:00
if (!client.peerId) {
const arr = new Int32Array(1);
crypto.getRandomValues(arr);
client.peerId = Math.abs(arr[0]);
}
2021-12-02 15:13:12 -06:00
client.send(buildMessage("your_peer_id", client.peerId.toString()));
2021-11-17 13:57:45 -06:00
this.broadcast(
2021-12-06 16:27:16 -06:00
buildMessage("peer_joined", [{
2021-11-17 16:24:00 -06:00
id: client.id,
name: client.name,
peerId: client.peerId,
2021-12-06 16:27:16 -06:00
}]),
2021-11-17 13:57:45 -06:00
);
2021-12-06 16:17:36 -06:00
console.log("Sending peer_joined...");
client.send(
buildMessage(
"peer_joined",
Array.from(this.clients.values()).map(
({ id, name, peerId }) => ({ id, name, peerId }),
),
),
2021-12-02 15:13:12 -06:00
);
2021-11-17 16:24:00 -06:00
this.clients.set(client.id, client);
2021-12-07 16:09:50 -06:00
this.notify();
2021-11-17 13:57:45 -06:00
}
removeClient({ id }: Client) {
this.clients.delete(id);
2021-12-06 16:27:16 -06:00
this.broadcast(buildMessage("peer_left", [{ id }]));
2021-11-17 13:57:45 -06:00
if (id === this.hostClientId) {
console.warn("Host left!");
this.remove();
}
2021-12-07 16:09:50 -06:00
this.notify();
2021-11-17 13:57:45 -06:00
}
}
2021-11-15 14:26:39 -06:00
2021-11-17 16:24:00 -06:00
interface ClientMessage {
2021-12-07 16:09:50 -06:00
type:
| "candidate"
| "offer"
| "answer"
| "init"
| "lobby_create"
| "update_lobby";
2021-11-17 16:24:00 -06:00
data: ServerDataObject;
}
2021-11-17 13:57:45 -06:00
// events
function onMessage(client: Client, ev: MessageEvent) {
// TODO: log who from?
2021-12-03 17:05:31 -06:00
const msg = ev.data.trim();
2021-12-07 12:41:01 -06:00
if (msg === "pong") return;
2021-12-03 17:05:31 -06:00
console.log("Client Message Received", msg);
2021-12-06 21:56:37 -06:00
if (msg === "init") {
2021-12-07 16:09:50 -06:00
client.send(
buildMessage("init", {
id: client.id,
name: client.name,
serverVersion: SERVER_VERSION,
}),
);
2021-12-06 21:56:37 -06:00
}
2021-12-03 17:05:31 -06:00
if (msg === "lobby_create") client.lobbyCreate();
2021-12-07 16:09:50 -06:00
if (msg.startsWith("set_ready:")) {
client.setReady(JSON.parse(msg.split(":", 2)[1]));
}
2021-12-03 17:05:31 -06:00
if (msg === "lobby_leave") client.lobbyLeave();
if (msg === "request_lobby_list") client.lobbyList();
2021-12-06 16:17:36 -06:00
if (msg === "request_peer_list") {
if (client.lobby == null) {
client.send(`[info] not in a lobby`);
} else {
2021-12-06 16:27:16 -06:00
client.peerList();
2021-12-06 16:17:36 -06:00
}
}
2021-12-07 09:32:20 -06:00
if (msg.startsWith("update_display_name:")) {
const displayName = msg.substr("update_display_name:".indexOf(":") + 1);
client.name = displayName;
}
2021-12-03 17:05:31 -06:00
if (msg.startsWith("lobby_join:")) {
const id = msg.substr(11);
2021-11-17 13:57:45 -06:00
const lobby = allLobbies.get(id);
2021-12-02 15:13:12 -06:00
if (lobby) {
client.lobbyJoin(lobby);
2021-12-06 16:27:16 -06:00
// client.peerList();
2021-12-02 15:13:12 -06:00
} else client.send(`[info] could not find lobby ${id}`);
2021-11-17 13:57:45 -06:00
}
2021-12-03 17:05:31 -06:00
if (msg.startsWith("json:")) {
const data: ClientMessage = JSON.parse(msg.substr(5));
2021-12-07 09:32:20 -06:00
if (data.type === "init") {
if (data.data.name) {
client.name = data.data.name.toString();
}
client.send(buildMessage("init", { id: client.id, name: client.name }));
2021-12-07 16:09:50 -06:00
} else if (data.type === "update_lobby") {
if (client.lobby) {
client.lobby.update(client, data["data"]);
} else {
client.send("[info] cannot update lobby (you're not even in one!)");
}
2021-12-07 09:32:20 -06:00
} else if (data.type === "lobby_create") {
2021-12-07 16:09:50 -06:00
client.lobbyCreate({ name: data.data?.name?.toString() });
2021-12-07 09:32:20 -06:00
if (data.data.name) {
client.name = data.data.name.toString();
}
client.send(buildMessage("init", { id: client.id, name: client.name }));
} else if (["candidate", "answer", "offer"].includes(data.type)) {
2021-12-02 15:13:12 -06:00
console.log("Received WebRTC Negotiation Message...");
2021-11-17 16:24:00 -06:00
if (typeof data.data === "object") {
const subdata = data.data;
if (typeof subdata["peerId"] === "number") {
2021-12-02 16:09:43 -06:00
const destPeerId: number = subdata["peerId"];
2021-12-02 15:13:12 -06:00
for (const iClient of client.lobby?.clients.values() || []) {
2021-12-02 16:09:43 -06:00
if (iClient.peerId == destPeerId) {
const payload = Object.assign({}, data);
const srcPeerId = client.peerId;
payload.data.peerId = srcPeerId;
2021-12-02 15:13:12 -06:00
console.log(
2021-12-02 16:09:43 -06:00
`Forwarding WebRTC Negotiation Message from peer ${srcPeerId} to peer ${destPeerId}...`,
2021-12-02 15:13:12 -06:00
);
2021-12-02 16:09:43 -06:00
iClient.send(payload);
2021-12-02 15:13:12 -06:00
break;
}
2021-11-17 16:24:00 -06:00
}
}
}
}
}
2021-11-17 13:57:45 -06:00
}
2021-12-02 15:13:12 -06:00
function onSocketOpen(client: Client, _ev: Event) {
console.log("New Client", client.id);
2021-11-17 13:57:45 -06:00
}
function onClientLeave(client: Client) {
client.remove();
}
function onSocketClose(client: Client, _ev: Event) {
console.log("Client Close");
onClientLeave(client);
}
function onSocketError(client: Client, _ev: Event) {
console.log("Client Error");
onClientLeave(client);
}
2021-12-07 12:33:09 -06:00
setInterval(() => {
allLobbies.forEach((lobby) => {
if (lobby.clients.size <= 0) lobby.remove();
});
allClients.forEach((client) => client.ping());
}, 5000);
2021-11-17 13:57:45 -06:00
const PORT = parseInt(Deno.env.get("PORT") || "80");
2021-11-17 08:18:12 -06:00
console.log("Listening on port", PORT);
2021-11-17 08:52:25 -06:00
const listener = Deno.listen({ port: PORT });
for await (const conn of listener) {
2021-11-17 13:57:45 -06:00
// console.debug("Connection received:", conn);
2021-11-17 08:18:12 -06:00
(async () => {
2021-11-17 08:52:25 -06:00
const server = Deno.serveHttp(conn);
for await (const { respondWith, request } of server) {
2021-11-17 13:57:45 -06:00
// console.debug("HTTP Request Received", request);
2021-11-17 08:52:25 -06:00
try {
2021-12-02 15:13:12 -06:00
// console.warn(JSON.stringify([allClients, allLobbies]));
2021-11-17 08:52:25 -06:00
const { socket, response } = Deno.upgradeWebSocket(request);
2021-11-17 13:57:45 -06:00
const client = new Client(socket);
socket.onmessage = (ev) => onMessage(client, ev);
socket.onopen = (ev) => onSocketOpen(client, ev);
socket.onclose = (ev) => onSocketClose(client, ev);
socket.onerror = (ev) => onSocketError(client, ev);
2021-11-17 08:52:25 -06:00
respondWith(response);
} catch (e) {
2021-11-17 13:57:45 -06:00
console.log("Could not add client for unhandled reason:", e);
2021-11-17 08:52:25 -06:00
respondWith(
2021-11-17 13:57:45 -06:00
new Response("400 Bad Request", {
2021-11-17 08:52:25 -06:00
status: 400,
headers: { "content-type": "text/html" },
}),
);
}
2021-11-17 08:18:12 -06:00
}
})();
}