199 lines
5.6 KiB
TypeScript
199 lines
5.6 KiB
TypeScript
import WebSocket from "@tauri-apps/plugin-websocket";
|
|
import { PeerInfo } from './ts/bindings/PeerInfo';
|
|
import { ClientWsMessage } from './ts/bindings/ClientWsMessage';
|
|
import { ServerWsMessage } from './ts/bindings/ServerWsMessage';
|
|
|
|
type RemoveListener = (() => void) | null;
|
|
|
|
export class LocalSignallingClient {
|
|
private socket: WebSocket | null = null;
|
|
private removeListener: RemoveListener = null;
|
|
private myPeerId: string | null = null;
|
|
private peers: PeerInfo[] = [];
|
|
|
|
constructor(
|
|
private readonly displayName: string,
|
|
private readonly onLog: (message: string) => void,
|
|
private readonly onPeers: (peers: PeerInfo[]) => void,
|
|
private readonly onChat: (line: string) => void,
|
|
) {}
|
|
|
|
public async connect(): Promise<void> {
|
|
if (this.socket !== null) {
|
|
this.onLog("signalling already connected");
|
|
return;
|
|
}
|
|
|
|
const url = "ws://127.0.0.1:3012";
|
|
|
|
this.onLog(`connecting to ${url}`);
|
|
|
|
let socket: WebSocket;
|
|
try {
|
|
socket = await WebSocket.connect(url);
|
|
} catch (error) {
|
|
throw new Error(`websocket connection failed: ${String(error)}`);
|
|
}
|
|
|
|
this.socket = socket;
|
|
|
|
const removeListener = socket.addListener((message) => {
|
|
if (message.type === "Text") {
|
|
this.handleServerMessage(message.data);
|
|
return;
|
|
}
|
|
|
|
if (message.type === "Close") {
|
|
this.onLog("signalling disconnected");
|
|
this.cleanupDisconnectedState();
|
|
return;
|
|
}
|
|
|
|
if (message.type === "Binary") {
|
|
this.onLog("unexpected binary websocket message");
|
|
return;
|
|
}
|
|
});
|
|
|
|
this.removeListener = removeListener;
|
|
|
|
this.onLog("signalling connected");
|
|
|
|
await this.send({
|
|
type: "hello",
|
|
display_name: this.displayName,
|
|
});
|
|
}
|
|
|
|
public async disconnect(): Promise<void> {
|
|
if (this.socket === null) {
|
|
return;
|
|
}
|
|
|
|
const socket = this.socket;
|
|
|
|
if (this.removeListener !== null) {
|
|
this.removeListener();
|
|
this.removeListener = null;
|
|
}
|
|
|
|
this.socket = null;
|
|
|
|
try {
|
|
await socket.disconnect();
|
|
} catch (error) {
|
|
this.onLog(`disconnect error: ${String(error)}`);
|
|
}
|
|
|
|
this.cleanupDisconnectedState();
|
|
}
|
|
|
|
public async sendChat(text: string): Promise<void> {
|
|
await this.send({
|
|
type: "chat_send",
|
|
text,
|
|
});
|
|
}
|
|
|
|
public getMyPeerId(): string | null {
|
|
return this.myPeerId;
|
|
}
|
|
|
|
public getPeers(): PeerInfo[] {
|
|
return [...this.peers];
|
|
}
|
|
|
|
private async send(message: ClientWsMessage): Promise<void> {
|
|
if (this.socket === null) {
|
|
this.onLog("cannot send: signalling not connected");
|
|
return;
|
|
}
|
|
|
|
const payload = JSON.stringify(message);
|
|
|
|
try {
|
|
await this.socket.send(payload);
|
|
} catch (error) {
|
|
this.onLog(`send failed: ${String(error)}`);
|
|
}
|
|
}
|
|
|
|
private cleanupDisconnectedState(): void {
|
|
this.socket = null;
|
|
this.myPeerId = null;
|
|
this.peers = [];
|
|
this.onPeers([]);
|
|
}
|
|
|
|
private handleServerMessage(raw: string): void {
|
|
let message: ServerWsMessage;
|
|
|
|
try {
|
|
message = JSON.parse(raw) as ServerWsMessage;
|
|
} catch (error) {
|
|
this.onLog(`invalid server message: ${String(error)}`);
|
|
return;
|
|
}
|
|
|
|
switch (message.type) {
|
|
case "welcome":
|
|
this.myPeerId = message.peer_id;
|
|
this.onLog(`welcome peer_id=${message.peer_id}`);
|
|
break;
|
|
|
|
case "peer_list":
|
|
this.peers = message.peers;
|
|
this.onPeers([...this.peers]);
|
|
this.onLog(`peer_list received (${message.peers.length})`);
|
|
break;
|
|
|
|
case "peer_joined":
|
|
this.peers = mergePeer(this.peers, message.peer);
|
|
this.onPeers([...this.peers]);
|
|
this.onLog(`peer joined: ${message.peer.display_name}`);
|
|
break;
|
|
|
|
case "peer_left":
|
|
this.peers = this.peers.filter((peer) => peer.peer_id !== message.peer_id);
|
|
this.onPeers([...this.peers]);
|
|
this.onLog(`peer left: ${message.peer_id}`);
|
|
break;
|
|
|
|
case "chat_receive":
|
|
this.onChat(`${message.from_display_name}: ${message.text}`);
|
|
break;
|
|
|
|
case "offer":
|
|
this.onLog(`offer received from ${message.from_peer_id}`);
|
|
break;
|
|
|
|
case "answer":
|
|
this.onLog(`answer received from ${message.from_peer_id}`);
|
|
break;
|
|
|
|
case "ice_candidate":
|
|
this.onLog(`ice candidate received from ${message.from_peer_id}`);
|
|
break;
|
|
|
|
case "pong":
|
|
this.onLog("pong");
|
|
break;
|
|
|
|
case "error":
|
|
this.onLog(`server error: ${message.message}`);
|
|
break;
|
|
|
|
default:
|
|
this.onLog(`unhandled server message: ${raw}`);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
function mergePeer(peers: PeerInfo[], incoming: PeerInfo): PeerInfo[] {
|
|
const next = peers.filter((peer) => peer.peer_id !== incoming.peer_id);
|
|
next.push(incoming);
|
|
next.sort((a, b) => a.display_name.localeCompare(b.display_name));
|
|
return next;
|
|
}
|