# Example: Video Chat Room Using the Pluggable Architecture This example shows how to create a simple video chat room application using the reusable Room/WebRTC infrastructure. ## Step 1: Define Your Metadata Types ```typescript // chat-app/types.ts import type { Session, Room } from '../server/routes/room/types'; /** * Chat-specific session metadata * Extends base Session with chat-specific data */ export interface ChatSessionMetadata { status: 'online' | 'away' | 'busy'; customStatus?: string; joinedAt: number; messageCount: number; } /** * Chat-specific room metadata * Extends base Room with chat-specific data */ export interface ChatRoomMetadata { topic: string; messages: ChatMessage[]; pinnedMessages: string[]; createdBy: string; maxParticipants: number; } export interface ChatMessage { id: string; senderId: string; senderName: string; text: string; timestamp: number; } // Type aliases for convenience export type ChatSession = Session; export type ChatRoom = Room; // Extended participant with chat-specific fields export interface ChatParticipant { session_id: string; name: string | null; live: boolean; has_media: boolean; status: 'online' | 'away' | 'busy'; customStatus?: string; messageCount: number; } ``` ## Step 2: Create Room Helpers ```typescript // chat-app/helpers.ts import { getParticipants as getBaseParticipants } from '../server/routes/room/helpers'; import type { ChatRoom, ChatParticipant } from './types'; /** * Get participants with chat-specific data */ export function getChatParticipants(room: ChatRoom): ChatParticipant[] { // Get base participant data from reusable helper const baseParticipants = getBaseParticipants(room.sessions); // Extend with chat-specific metadata return baseParticipants.map(p => { const session = room.sessions[p.session_id]; const metadata = session.metadata; return { ...p, status: metadata?.status || 'online', customStatus: metadata?.customStatus, messageCount: metadata?.messageCount || 0, }; }); } /** * Create a new chat room */ export function createChatRoom(roomId: string, roomName: string, creatorId: string): ChatRoom { return { id: roomId, name: roomName, sessions: {}, state: 'active', created: Date.now(), lastActivity: Date.now(), metadata: { topic: 'General Chat', messages: [], pinnedMessages: [], createdBy: creatorId, maxParticipants: 50, }, }; } /** * Add a message to the chat room */ export function addMessage(room: ChatRoom, senderId: string, text: string): void { const session = room.sessions[senderId]; if (!session) return; const message = { id: `${Date.now()}-${senderId}`, senderId, senderName: session.name || 'Anonymous', text, timestamp: Date.now(), }; room.metadata.messages.push(message); // Update sender's message count if (session.metadata) { session.metadata.messageCount++; } } ``` ## Step 3: Server WebSocket Handler ```typescript // chat-app/server.ts import express from 'express'; import expressWs from 'express-ws'; import type { ChatRoom, ChatSession } from './types'; import { createBaseSession } from '../server/routes/room/helpers'; import { getChatParticipants, createChatRoom, addMessage } from './helpers'; const app = expressWs(express()).app; const rooms: Record = {}; // WebSocket endpoint for chat room app.ws('/chat/:roomId', async (ws, req) => { const { roomId } = req.params; const sessionId = req.cookies?.session || generateSessionId(); // Get or create room let room = rooms[roomId]; if (!room) { room = createChatRoom(roomId, `Chat Room ${roomId}`, sessionId); rooms[roomId] = room; } // Create or get session let session: ChatSession = room.sessions[sessionId]; if (!session) { session = { ...createBaseSession(sessionId), metadata: { status: 'online', joinedAt: Date.now(), messageCount: 0, }, }; room.sessions[sessionId] = session; } // Attach WebSocket session.ws = ws; session.live = true; session.connected = true; // Notify all participants broadcastUpdate(room, { type: 'participants', participants: getChatParticipants(room), }); // Handle incoming messages ws.on('message', (msg: string) => { const data = JSON.parse(msg); switch (data.type) { case 'set-name': session.name = data.name; broadcastUpdate(room, { type: 'participants', participants: getChatParticipants(room), }); break; case 'set-status': if (session.metadata) { session.metadata.status = data.status; session.metadata.customStatus = data.customStatus; } broadcastUpdate(room, { type: 'participants', participants: getChatParticipants(room), }); break; case 'send-message': addMessage(room, sessionId, data.text); broadcastUpdate(room, { type: 'new-message', messages: room.metadata.messages.slice(-50), // Last 50 messages }); break; case 'get-messages': ws.send(JSON.stringify({ type: 'messages', messages: room.metadata.messages.slice(-50), })); break; case 'get-participants': ws.send(JSON.stringify({ type: 'participants', participants: getChatParticipants(room), })); break; // WebRTC signaling messages (handled by reusable code) case 'join': case 'part': case 'relayICECandidate': case 'relaySessionDescription': // Use the same WebRTC handlers as the game // (This code is application-agnostic) handleWebRTCMessage(room, session, data); break; } }); ws.on('close', () => { session.live = false; session.connected = false; // Clean up after timeout setTimeout(() => { if (!session.live) { delete room.sessions[sessionId]; broadcastUpdate(room, { type: 'participants', participants: getChatParticipants(room), }); } }, 60000); // 1 minute grace period }); }); function broadcastUpdate(room: ChatRoom, update: any) { const message = JSON.stringify(update); Object.values(room.sessions).forEach(session => { if (session.ws && session.connected) { session.ws.send(message); } }); } // WebRTC handlers (reusable from game implementation) function handleWebRTCMessage(room: ChatRoom, session: ChatSession, data: any) { // Same join/part/ICE/SDP handling as in games.ts // This code doesn't care about chat vs. game - it's pure WebRTC } ``` ## Step 4: Client Component ```tsx // chat-app/client/ChatRoom.tsx import React, { useState, useEffect } from 'react'; import { MediaAgent, MediaControl, Peer } from './MediaControl'; import useWebSocket from 'react-use-websocket'; import type { ChatParticipant, ChatMessage } from './types'; interface ChatRoomProps { roomId: string; session: { id: string; name: string | null; has_media: boolean }; } export function ChatRoom({ roomId, session }: ChatRoomProps) { const [participants, setParticipants] = useState([]); const [messages, setMessages] = useState([]); const [messageText, setMessageText] = useState(''); const [peers, setPeers] = useState>({}); const socketUrl = `ws://localhost:3000/chat/${roomId}`; const { sendJsonMessage, lastJsonMessage, readyState } = useWebSocket(socketUrl); // Handle WebSocket messages useEffect(() => { if (!lastJsonMessage) return; const data: any = lastJsonMessage; switch (data.type) { case 'participants': setParticipants(data.participants); break; case 'messages': case 'new-message': setMessages(data.messages); break; } }, [lastJsonMessage]); // Send message const handleSendMessage = () => { if (!messageText.trim()) return; sendJsonMessage({ type: 'send-message', text: messageText, }); setMessageText(''); }; return (
{/* MediaAgent handles WebRTC (reusable component) */}
{/* Participant list with video feeds */} {/* Chat messages */}
{messages.map(msg => (
{msg.senderName} {new Date(msg.timestamp).toLocaleTimeString()}

{msg.text}

))}
setMessageText(e.target.value)} onKeyPress={e => e.key === 'Enter' && handleSendMessage()} placeholder="Type a message..." />
); } ``` ## What's Reused (No Changes Needed) ✅ **MediaControl.tsx** - Entire component works as-is ✅ **MediaAgent** - All WebRTC signaling logic ✅ **Room helpers** - Session management, participant lists ✅ **WebSocket infrastructure** - Connection handling, reconnection ✅ **Type definitions** - Base Session, Room, Participant types ## What's Application-Specific (Your Code) 🎯 **ChatSessionMetadata** - Your session data (status, message count) 🎯 **ChatRoomMetadata** - Your room data (messages, topic) 🎯 **getChatParticipants()** - Extends base participants with chat data 🎯 **Message handlers** - Your business logic 🎯 **UI** - Your specific interface design ## Benefits - **~90% code reuse** from the game infrastructure - **Video chat works immediately** with zero WebRTC code - **Type-safe metadata** for your application - **Well-tested infrastructure** from the game - **Clean separation** between framework and app ## Summary By separating infrastructure from application logic, the entire Room/WebRTC system becomes a **reusable framework** that provides: - WebSocket room management - Session/participant tracking - WebRTC video/audio signaling - Peer connection management - UI components (MediaControl) All you need to do is: 1. Define your metadata types 2. Extend participant helper with your data 3. Handle your application messages 4. Use the provided components **Result**: Professional video chat functionality in ~200 lines of your code!