godot-webrtc-mplayer-testing/server.ts

484 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
2022-05-25 10:00:06 -05:00
const SERVER_VERSION = "1.0.0";
2021-11-17 13:57:45 -06:00
// TODO: version comparison
2021-11-15 14:26:39 -06:00
2021-12-10 12:31:48 -06:00
type ID = number;
2021-12-11 00:08:28 -06:00
type UUID = string;
2021-11-15 14:26:39 -06:00
2021-11-17 13:57:45 -06:00
// app state
2021-12-10 12:31:48 -06:00
const allLobbies = new Map<UUID, Lobby>();
const allClients = new Map<UUID, Client>();
// 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<
2021-12-08 23:39:56 -06:00
"peerId" | string,
2021-12-07 16:09:50 -06:00
string | number | symbol | null | boolean
>;
2021-12-10 12:31:48 -06:00
type ServerData =
| ServerDataObject[]
| ServerDataObject
| string
| boolean
| number;
2021-11-17 13:57:45 -06:00
type Message = string | DataMessage;
const buildMessage = (
type: string,
data: ServerData | ServerData[],
2021-12-08 23:39:56 -06:00
) => {
if (Array.isArray(data)) return { type, data };
else if (typeof data === "object") return { type, ...data };
return `${type}:${data}`;
};
2021-11-17 13:57:45 -06:00
class Client {
2021-12-10 12:31:48 -06:00
id: number | null;
uuid: string;
2021-11-17 08:18:12 -06:00
name: string;
2021-11-17 13:57:45 -06:00
socket: WebSocket;
lobby: Lobby | null;
constructor(socket: WebSocket) {
2021-12-10 12:31:48 -06:00
this.id = null;
this.uuid = crypto.randomUUID();
2021-11-17 13:57:45 -06:00
this.socket = socket;
this.name = "Client";
this.lobby = null;
2021-12-10 12:31:48 -06:00
allClients.set(this.uuid, this);
2021-11-17 13:57:45 -06:00
}
isConnected() {
return this.socket.readyState == WebSocket.OPEN;
}
remove() {
2021-12-11 00:08:28 -06:00
console.debug(`Removing Client ${this.uuid}`);
2021-11-17 13:57:45 -06:00
this.lobbyLeave();
if (this.isConnected()) {
this.socket.close();
}
2021-12-10 12:31:48 -06:00
allClients.delete(this.uuid);
2021-11-17 13:57:45 -06:00
}
send(message: Message) {
try {
2021-11-17 14:01:35 -06:00
if (this.isConnected()) {
this.socket.send(
2022-05-25 10:00:06 -05:00
typeof message === "object" ? (JSON.stringify(message)) : message,
2021-11-17 14:01:35 -06:00
);
}
2021-12-11 00:08:28 -06:00
// TODO: maybe log when we try to send and we're not connected?
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-12-08 23:39:56 -06:00
if (!this.lobby) {
this.send(
buildMessage(
"info",
"you cannot request a list of peers unless you are in a lobby",
),
);
return;
}
2021-12-07 16:09:50 -06:00
const peers: {
id: ID;
name: string;
}[] = [];
2021-12-10 12:31:48 -06:00
this.lobby.clients.forEach(({ id, name }) => {
if (typeof id === "number") peers.push({ id, name });
});
2021-12-11 00:08:28 -06:00
console.debug(`Sending peer list with ${peers.length} peers...`);
2021-12-08 23:39:56 -06:00
this.send(buildMessage("peer_data", peers));
2021-11-17 13:57:45 -06:00
// TODO: chunk async?
}
lobbyList() {
2021-12-07 16:09:50 -06:00
const netLobbies: {
2021-12-10 12:31:48 -06:00
uuid: UUID;
2021-12-07 16:09:50 -06:00
name: string;
maxPlayers: number;
locked: boolean;
currentPlayers: number;
}[] = [];
2021-12-08 23:39:56 -06:00
allLobbies.forEach((lobby) => netLobbies.push(lobby.toData()));
this.send(buildMessage("lobby_data", netLobbies));
2021-11-17 13:57:45 -06:00
}
2021-12-07 16:09:50 -06:00
lobbyNew(lobby: Lobby) {
2021-12-08 23:39:56 -06:00
// if the client is already in a lobby, do not send them lobby updates
if (this.lobby) return;
2021-12-07 16:09:50 -06:00
this.send(
2021-12-08 23:39:56 -06:00
buildMessage("lobby_data", [lobby.toData()]),
2021-12-07 16:09:50 -06:00
);
2021-11-17 13:57:45 -06:00
}
2021-12-10 12:31:48 -06:00
lobbyDelete({ uuid }: Lobby) {
2021-11-17 13:57:45 -06:00
if (this.lobby) return;
2021-12-10 12:31:48 -06:00
this.send(buildMessage("lobby_delete", { uuid }));
2021-11-17 13:57:45 -06:00
}
2021-12-07 16:09:50 -06:00
lobbyCreate(opts?: Partial<Lobby>) {
2021-11-17 13:57:45 -06:00
if (this.lobby) {
this.send(
2021-12-08 23:39:56 -06:00
buildMessage(
"info",
2021-12-10 12:31:48 -06:00
`cannot create lobby: already in lobby ${this.lobby.uuid}`,
2021-12-08 23:39:56 -06:00
),
2021-11-17 13:57:45 -06:00
);
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) {
2021-12-08 23:39:56 -06:00
this.send(
buildMessage(
`info`,
2021-12-10 12:31:48 -06:00
`cannot join lobby: already in lobby ${this.lobby.uuid}`,
2021-12-08 23:39:56 -06:00
),
);
2021-11-17 13:57:45 -06:00
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", {
2021-12-08 23:39:56 -06:00
...this.lobby.toData(),
2021-12-10 12:31:48 -06:00
id: this.id,
2021-12-07 09:32:20 -06:00
}),
2021-11-17 16:24:00 -06:00
);
2021-11-17 13:57:45 -06:00
}
lobbyLeave() {
const leavingLobby = this.lobby;
if (!leavingLobby) {
this.send(`[info] cannot leave lobby (not in a lobby)`);
return;
}
2021-12-11 00:08:28 -06:00
console.debug(`Client ${this.uuid} leaving Lobby ${this.lobby?.uuid}`);
leavingLobby.removeClient(this);
2021-12-10 12:31:48 -06:00
this.id = null;
2021-11-17 13:57:45 -06:00
this.lobby = null;
2021-12-11 00:08:28 -06:00
this.send(buildMessage("lobby_left", { uuid: leavingLobby.uuid }));
2021-11-17 13:57:45 -06:00
}
2021-11-15 14:26:39 -06:00
}
2021-11-17 13:57:45 -06:00
class Lobby {
2021-12-10 12:31:48 -06:00
uuid: UUID;
2021-11-17 13:57:45 -06:00
name: string;
clients: Map<ID, Client>;
2021-12-10 12:31:48 -06:00
hostClientUuid: UUID;
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-12-10 12:31:48 -06:00
this.uuid = crypto.randomUUID();
this.hostClientUuid = host.uuid;
2021-11-17 13:57:45 -06:00
this.clients = new Map<ID, Client>();
2021-12-10 12:31:48 -06:00
this.name = name || this.uuid;
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-12-11 00:08:28 -06:00
console.debug(
`Lobby ${this.uuid} created by Client ${this.hostClientUuid}`,
);
2021-12-10 12:31:48 -06:00
allLobbies.set(this.uuid, this);
host.id = 1;
2021-11-17 13:57:45 -06:00
host.lobbyJoin(this);
2021-12-07 16:09:50 -06:00
this.notify();
}
2021-12-08 23:39:56 -06:00
toData() {
return {
2021-12-10 12:31:48 -06:00
uuid: this.uuid,
2021-12-08 23:39:56 -06:00
name: this.name,
maxPlayers: this.maxPlayers,
locked: this.locked,
currentPlayers: this.clients.size,
};
}
2021-12-07 16:09:50 -06:00
update(
requestor: Client,
newValues: { name?: string; maxPlayers?: number; locked?: boolean },
) {
2021-12-11 00:08:28 -06:00
if (requestor.uuid === this.hostClientUuid) {
2021-12-07 16:09:50 -06:00
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-11 00:08:28 -06:00
console.debug(`Removing Lobby ${this.uuid}`);
2021-12-10 12:31:48 -06:00
allLobbies.delete(this.uuid);
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-12-10 12:31:48 -06:00
if (!client.id) {
2021-11-17 16:24:00 -06:00
const arr = new Int32Array(1);
crypto.getRandomValues(arr);
2021-12-10 12:31:48 -06:00
client.id = Math.abs(arr[0]);
2021-11-17 16:24:00 -06:00
}
2021-12-11 00:08:28 -06:00
console.debug(
`Adding Client ${client.uuid} Lobby ${this.uuid} as peer ${client.id}`,
);
2021-12-10 12:31:48 -06:00
client.send(buildMessage("init_peer", client.id));
2021-11-17 13:57:45 -06:00
this.broadcast(
2021-12-08 23:39:56 -06:00
buildMessage("peer_data", [{
2021-11-17 16:24:00 -06:00
id: client.id,
name: client.name,
2021-12-06 16:27:16 -06:00
}]),
2021-11-17 13:57:45 -06:00
);
client.send(
buildMessage(
2021-12-08 23:39:56 -06:00
"peer_data",
Array.from(this.clients.values()).map(
2021-12-10 12:31:48 -06:00
({ id, name }) => ({ id, name }),
),
),
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
}
2021-12-10 12:31:48 -06:00
removeClient({ id, uuid }: Client) {
if (typeof id !== "number") return;
2021-11-17 13:57:45 -06:00
this.clients.delete(id);
2021-12-11 00:08:28 -06:00
this.broadcast(buildMessage("peer_left", { id }));
console.debug(`Removing Client ${this.uuid} from Lobby ${this.uuid}`);
2021-12-10 12:31:48 -06:00
if (uuid === this.hostClientUuid) {
2021-12-11 00:08:28 -06:00
console.warn(`Host left, removing Lobby ${this.uuid}`);
2021-11-17 13:57:45 -06:00
this.remove();
}
2021-12-07 16:09:50 -06:00
this.notify();
2021-11-17 13:57:45 -06:00
}
2021-12-08 23:39:56 -06:00
2021-12-10 12:31:48 -06:00
getPeer(id: number): Client | null {
2021-12-08 23:39:56 -06:00
for (const [_id, client] of this.clients) {
2021-12-10 12:31:48 -06:00
if (client.id == id) return client;
2021-12-08 23:39:56 -06:00
}
return null;
}
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-12-08 23:39:56 -06:00
function parseMessage(message: string): { type: string; data?: ServerData } {
const trimmedMessage = message.trim();
2022-05-25 10:00:06 -05:00
if (trimmedMessage.startsWith("{")) {
const { type, ...data } = JSON.parse(trimmedMessage);
2021-12-08 23:39:56 -06:00
return { type, data };
} else {
const splitAt = trimmedMessage.indexOf(":");
if (splitAt > 0) {
return {
2022-05-25 10:00:06 -05:00
type: trimmedMessage.substring(0, splitAt),
data: trimmedMessage.substring(splitAt + 1),
2021-12-08 23:39:56 -06:00
};
} else {
return { type: trimmedMessage };
}
}
}
2021-11-17 13:57:45 -06:00
// events
function onMessage(client: Client, ev: MessageEvent) {
// TODO: log who from? IPs etc.?
2021-12-08 23:39:56 -06:00
const msg = parseMessage(ev.data);
if (msg.type === "pong") return;
2021-12-11 00:08:28 -06:00
console.debug("Client Message Received", msg);
2021-12-08 23:39:56 -06:00
switch (msg.type) {
case "init":
if (msg.data && (msg.data as { name: string })["name"]) {
client.name = (msg.data as { name: string }).name.toString();
}
client.send(buildMessage("init", {
2021-12-07 16:09:50 -06:00
id: client.id,
name: client.name,
serverVersion: SERVER_VERSION,
2021-12-08 23:39:56 -06:00
}));
break;
case "lobby_create":
client.lobbyCreate(msg.data as Partial<Lobby>);
break;
case "lobby_leave":
client.lobbyLeave();
break;
case "request_lobby_list":
client.lobbyList();
break;
case "request_peer_list":
2021-12-06 16:27:16 -06:00
client.peerList();
2021-12-08 23:39:56 -06:00
break;
case "update_display_name":
if (msg.data) client.name = msg.data.toString();
break;
case "lobby_join":
if (msg.data) {
const lobby = allLobbies.get(msg.data.toString());
if (lobby) {
client.lobbyJoin(lobby);
} else {
client.send(buildMessage("info", `count not find lobby ${msg.data}`));
}
2021-12-07 09:32:20 -06:00
}
2021-12-08 23:39:56 -06:00
break;
case "update_lobby":
2021-12-07 16:09:50 -06:00
if (client.lobby) {
2021-12-08 23:39:56 -06:00
if (typeof (msg.data) === "object") {
client.lobby.update(client, msg.data as Partial<Lobby>);
}
2021-12-07 16:09:50 -06:00
} else {
2021-12-08 23:39:56 -06:00
client.send(
buildMessage(
"info",
"failed to update lobby info: you are not in a lobby",
),
);
2021-12-07 09:32:20 -06:00
}
2021-12-08 23:39:56 -06:00
break;
case "candidate":
/* falls through */
case "answer":
/* falls through */
case "offer": {
if (!client.lobby) return;
if (typeof msg.data !== "object") return;
2021-12-10 12:31:48 -06:00
const webrtcMessage = (msg.data as { id?: number });
if (!webrtcMessage.id || typeof webrtcMessage.id !== "number") {
2021-12-08 23:39:56 -06:00
return;
2021-11-17 16:24:00 -06:00
}
2021-12-09 21:10:17 -06:00
console.log(
2021-12-10 12:31:48 -06:00
`Received WebRTC Negotiation Message (type: ${msg.type}, from: ${client.id}, to: ${webrtcMessage.id})...`,
2021-12-09 21:10:17 -06:00
);
2021-12-10 12:31:48 -06:00
const destClient = client.lobby.getPeer(webrtcMessage.id);
if (!destClient || !destClient.id) return;
webrtcMessage.id = client.id as number;
2021-12-08 23:39:56 -06:00
destClient.send(buildMessage(msg.type, webrtcMessage));
break;
2021-11-17 16:24:00 -06:00
}
2021-12-08 23:39:56 -06:00
default:
console.debug("Unknown message: ", msg);
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) {
2021-12-11 00:08:28 -06:00
console.log("New Client", client.uuid);
2021-11-17 13:57:45 -06:00
}
function onClientLeave(client: Client) {
client.remove();
}
function onSocketClose(client: Client, _ev: Event) {
2021-12-09 21:10:17 -06:00
console.log("Client Socket Close");
2021-11-17 13:57:45 -06:00
onClientLeave(client);
}
function onSocketError(client: Client, _ev: Event) {
2021-12-09 21:10:17 -06:00
console.log("Client Socket Error");
2021-11-17 13:57:45 -06:00
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 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) {
try {
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
}
})();
}