Files
tauri-video03/frontend/chat.ts
2026-04-01 11:50:32 +02:00

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;
}