← Tasks
[LIB-03] lib/livekit.ts — registerGlobals, token fetch from Edge Function, room config
completedcode_genP0
Description
## LIB-03: lib/livekit.ts — registerGlobals, token fetch from Edge Function, room config
Section: Client Libraries | Milestone: M1 | Owner: AG Lu
### Failing Tests
- **livekit_lib** (file_exists): File `c8-audio/lib/livekit.ts` must exist
- **register_globals** (grep): Pattern `registerGlobals` in `c8-audio/lib/livekit.ts` (need 1+ matches)
- **token_fetch** (grep): Pattern `livekit-token|token` in `c8-audio/lib/livekit.ts` (need 1+ matches)
- **room_config** (grep): Pattern `adaptiveStream|audioCaptureDefaults|echoCancellation` in `c8-audio/lib/livekit.ts` (need 1+ matches)
### Acceptance Criteria
- File `c8-audio/lib/livekit.ts` must exist
- File matching `c8-audio/lib/livekit.ts` must contain pattern `registerGlobals`
- File matching `c8-audio/lib/livekit.ts` must contain pattern `livekit-token|token`
- File matching `c8-audio/lib/livekit.ts` must contain pattern `adaptiveStream|audioCaptureDefaults|echoCancellation`
### Codebase Conventions
- Trilingual comments: // EN: ... // VI: ... // ES: ...
- SPEC section refs in comments
- Zod at module level with .safeParse()
- (supabase as any) with eslint-disable
- NextResponse.json() responses
- Error format: { error: 'message' }
### Dependencies
- LIB-01: FAIL
- LIB-02: FAIL
Assignee
-
Claimed By
Cloud Lu → Claude Sonnet
Created
22d ago
Completed
19d ago
Quality
Overall
79%
Pass
PASS
Graded By
-
Graded At
-
Cost
Input Tokens
-
Output Tokens
-
Est. USD
-
Result
{
"cost": {
"input_tokens": 35,
"estimated_usd": 0.060795,
"output_tokens": 4046
},
"model": "claude-sonnet-4-20250514",
"status": "success",
"transport": "anthropic_api",
"latency_ms": 47639,
"response_text": "I'll help you create the `lib/livekit.ts` file with global registration, token fetching from Edge Functions, and room configuration. Here's a comprehensive implementation:\n\n```typescript\n// lib/livekit.ts\nimport {\n Room,\n RoomEvent,\n RemoteParticipant,\n RemoteTrackPublication,\n RemoteTrack,\n Participant,\n Track,\n VideoPresets,\n RoomOptions,\n ConnectOptions,\n DataPacket_Kind,\n RoomConnectOptions,\n ParticipantEvent,\n TrackPublishDefaults,\n VideoCodec,\n AudioCodec,\n BackupCodec,\n} from 'livekit-client';\n\n// Global LiveKit configuration\nconst LIVEKIT_CONFIG = {\n // WebSocket URL for your LiveKit server\n wsUrl: process.env.NEXT_PUBLIC_LIVEKIT_WS_URL || 'ws://localhost:7880',\n \n // API endpoint for token generation\n tokenEndpoint: '/api/livekit/token',\n \n // Default room options\n defaultRoomOptions: {\n adaptiveStream: true,\n dynacast: true,\n publishDefaults: {\n videoSimulcastLayers: [VideoPresets.h540, VideoPresets.h216],\n videoCodec: 'vp8' as VideoCodec,\n backupCodec: {\n codec: 'vp8',\n encoding: {\n maxBitrate: 3_000_000,\n maxFramerate: 20,\n },\n } as BackupCodec,\n dtx: false,\n red: true,\n } as TrackPublishDefaults,\n } as RoomOptions,\n \n // Default connection options\n defaultConnectOptions: {\n autoSubscribe: true,\n maxRetries: 3,\n peerConnectionTimeout: 15_000,\n } as ConnectOptions,\n};\n\n// Global room instance and state management\nclass LiveKitManager {\n private static instance: LiveKitManager;\n private room: Room | null = null;\n private isConnecting = false;\n private eventHandlers = new Map<string, Function[]>();\n\n private constructor() {}\n\n static getInstance(): LiveKitManager {\n if (!LiveKitManager.instance) {\n LiveKitManager.instance = new LiveKitManager();\n }\n return LiveKitManager.instance;\n }\n\n // Register global event handlers\n registerGlobals() {\n // Handle page visibility changes\n document.addEventListener('visibilitychange', () => {\n if (this.room) {\n if (document.visibilityState === 'hidden') {\n this.room.localParticipant.setCameraEnabled(false);\n this.room.localParticipant.setMicrophoneEnabled(false);\n }\n }\n });\n\n // Handle beforeunload to cleanup\n window.addEventListener('beforeunload', () => {\n this.disconnect();\n });\n\n // Handle network changes\n window.addEventListener('online', () => {\n console.log('Network connection restored');\n this.emit('network-restored');\n });\n\n window.addEventListener('offline', () => {\n console.log('Network connection lost');\n this.emit('network-lost');\n });\n\n console.log('LiveKit global handlers registered');\n }\n\n // Fetch authentication token from Edge Function\n async fetchToken(roomName: string, participantName: string, metadata?: string): Promise<string> {\n try {\n const response = await fetch(LIVEKIT_CONFIG.tokenEndpoint, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n roomName,\n participantName,\n metadata: metadata || JSON.stringify({ \n joinedAt: Date.now(),\n userAgent: navigator.userAgent,\n }),\n }),\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(`Token fetch failed: ${response.status} - ${errorText}`);\n }\n\n const data = await response.json();\n \n if (!data.token) {\n throw new Error('No token received from server');\n }\n\n return data.token;\n } catch (error) {\n console.error('Error fetching LiveKit token:', error);\n throw error;\n }\n }\n\n // Connect to room with enhanced configuration\n async connect(\n roomName: string,\n participantName: string,\n options: {\n metadata?: string;\n roomOptions?: Partial<RoomOptions>;\n connectOptions?: Partial<ConnectOptions>;\n autoEnableCamera?: boolean;\n autoEnableMicrophone?: boolean;\n } = {}\n ): Promise<Room> {\n if (this.isConnecting) {\n throw new Error('Already connecting to a room');\n }\n\n if (this.room?.state === 'connected') {\n throw new Error('Already connected to a room. Disconnect first.');\n }\n\n this.isConnecting = true;\n\n try {\n // Fetch token\n const token = await this.fetchToken(roomName, participantName, options.metadata);\n\n // Create room with merged options\n const roomOptions: RoomOptions = {\n ...LIVEKIT_CONFIG.defaultRoomOptions,\n ...options.roomOptions,\n };\n\n const connectOptions: RoomConnectOptions = {\n ...LIVEKIT_CONFIG.defaultConnectOptions,\n ...options.connectOptions,\n token,\n autoSubscribe: options.connectOptions?.autoSubscribe ?? true,\n };\n\n // Create new room instance\n this.room = new Room(roomOptions);\n this.setupRoomEventHandlers();\n\n // Connect to room\n await this.room.connect(LIVEKIT_CONFIG.wsUrl, connectOptions);\n\n console.log('Connected to room:', roomName);\n\n // Auto-enable camera and microphone if requested\n if (options.autoEnableCamera) {\n await this.room.localParticipant.enableCameraAndMicrophone();\n } else {\n if (options.autoEnableMicrophone) {\n await this.room.localParticipant.setMicrophoneEnabled(true);\n }\n }\n\n this.emit('room-connected', this.room);\n return this.room;\n\n } catch (error) {\n console.error('Failed to connect to room:', error);\n this.room = null;\n throw error;\n } finally {\n this.isConnecting = false;\n }\n }\n\n // Setup comprehensive room event handlers\n private setupRoomEventHandlers() {\n if (!this.room) return;\n\n // Connection events\n this.room.on(RoomEvent.Connected, () => {\n console.log('Room connected successfully');\n this.emit('connected');\n });\n\n this.room.on(RoomEvent.Disconnected, (reason) => {\n console.log('Room disconnected:', reason);\n this.emit('disconnected', reason);\n });\n\n this.room.on(RoomEvent.Reconnecting, () => {\n console.log('Room reconnecting...');\n this.emit('reconnecting');\n });\n\n this.room.on(RoomEvent.Reconnected, () => {\n console.log('Room reconnected successfully');\n this.emit('reconnected');\n });\n\n // Participant events\n this.room.on(RoomEvent.ParticipantConnected, (participant: RemoteParticipant) => {\n console.log('Participant connected:', participant.identity);\n this.setupParticipantEventHandlers(participant);\n this.emit('participant-connected', participant);\n });\n\n this.room.on(RoomEvent.ParticipantDisconnected, (participant: RemoteParticipant) => {\n console.log('Participant disconnected:', participant.identity);\n this.emit('participant-disconnected', participant);\n });\n\n // Track events\n this.room.on(RoomEvent.TrackSubscribed, (\n track: RemoteTrack,\n publication: RemoteTrackPublication,\n participant: RemoteParticipant\n ) => {\n console.log('Track subscribed:', track.kind, 'from', participant.identity);\n this.emit('track-subscribed', { track, publication, participant });\n });\n\n this.room.on(RoomEvent.TrackUnsubscribed, (\n track: RemoteTrack,\n publication: RemoteTrackPublication,\n participant: RemoteParticipant\n ) => {\n console.log('Track unsubscribed:', track.kind, 'from', participant.identity);\n this.emit('track-unsubscribed', { track, publication, participant });\n });\n\n // Data events\n this.room.on(RoomEvent.DataReceived, (payload: Uint8Array, participant?: RemoteParticipant, kind?: DataPacket_Kind) => {\n console.log('Data received from:', participant?.identity || 'server');\n this.emit('data-received', { payload, participant, kind });\n });\n\n // Audio level events\n this.room.on(RoomEvent.AudioPlaybackStatusChanged, () => {\n this.emit('audio-playback-changed');\n });\n\n // Connection quality events\n this.room.on(RoomEvent.ConnectionQualityChanged, (quality, participant) => {\n this.emit('connection-quality-changed', { quality, participant });\n });\n }\n\n // Setup participant-specific event handlers\n private setupParticipantEventHandlers(participant: Participant) {\n participant.on(ParticipantEvent.TrackMuted, (publication) => {\n console.log(`Track muted: ${publication.trackSid} from ${participant.identity}`);\n this.emit('track-muted', { publication, participant });\n });\n\n participant.on(ParticipantEvent.TrackUnmuted, (publication) => {\n console.log(`Track unmuted: ${publication.trackSid} from ${participant.identity}`);\n this.emit('track-unmuted', { publication, participant });\n });\n\n participant.on(ParticipantEvent.IsSpeakingChanged, () => {\n this.emit('speaking-changed', participant);\n });\n\n participant.on(ParticipantEvent.ConnectionQualityChanged, (quality) => {\n this.emit('participant-connection-quality-changed', { participant, quality });\n });\n\n participant.on(ParticipantEvent.MetadataChanged, (metadata) => {\n console.log(`Participant metadata changed: ${participant.identity}`);\n this.emit('participant-metadata-changed', { participant, metadata });\n });\n }\n\n // Disconnect from room\n async disconnect(): Promise<void> {\n if (this.room) {\n console.log('Disconnecting from room...');\n await this.room.disconnect();\n this.room = null;\n this.emit('disconnected');\n }\n }\n\n // Get current room\n getRoom(): Room | null {\n return this.room;\n }\n\n // Check if connected\n isConnected(): boolean {\n return this.room?.state === 'connected';\n }\n\n // Event system for external handlers\n on(event: string, handler: Function) {\n if (!this.eventHandlers.has(event)) {\n this.eventHandlers.set(event, []);\n }\n this.eventHandlers.get(event)!.push(handler);\n }\n\n off(event: string, handler: Function) {\n const handlers = this.eventHandlers.get(event);\n if (handlers) {\n const index = handlers.indexOf(handler);\n if (index > -1) {\n handlers.splice(index, 1);\n }\n }\n }\n\n private emit(event: string, data?: any) {\n const handlers = this.eventHandlers.get(event);\n if (handlers) {\n handlers.forEach(handler => handler(data));\n }\n }\n\n // Utility methods for common room operations\n async enableCamera(): Promise<void> {\n if (this.room) {\n await this.room.localParticipant.setCameraEnabled(true);\n }\n }\n\n async disableCamera(): Promise<void> {\n if (this.room) {\n await this.room.localParticipant.setCameraEnabled(false);\n }\n }\n\n async enableMicrophone(): Promise<void> {\n if (this.room) {\n await this.room.localParticipant.setMicrophoneEnabled(true);\n }\n }\n\n async disableMicrophone(): Promise<void> {\n if (this.room) {\n await this.room.localParticipant.setMicrophoneEnabled(false);\n }\n }\n\n async toggleCamera(): Promise<boolean> {\n if (this.room) {\n const isEnabled = this.room.localParticipant.isCameraEnabled;\n await this.room.localParticipant.setCameraEnabled(!isEnabled);\n return !isEnabled;\n }\n return false;\n }\n\n async toggleMicrophone(): Promise<boolean> {\n if (this.room) {\n const isEnabled = this.room.localParticipant.isMicrophoneEnabled;\n await this.room.localParticipant.setMicrophoneEnabled(!isEnabled);\n return !isEnabled;\n }\n return false;\n }\n\n async sendData(data: string | Uint8Array, reliable = true): Promise<void> {\n if (this.room) {\n await this.room.localParticipant.publishData(\n typeof data === 'string' ? new TextEncoder().encode(data) : data,\n reliable ? DataPacket_Kind.RELIABLE : DataPacket_Kind.LOSSY\n );\n }\n }\n}\n\n// Export singleton instance\nexport const liveKitManager = LiveKitManager.getInstance();\n\n// Export configuration for external use\nexport { LIVEKIT_CONFIG };\n\n// Export types for convenience\nexport type {\n Room,\n RemoteParticipant,\n RemoteTrackPublication,\n RemoteTrack,\n Participant,\n Track,\n RoomOptions,\n ConnectOptions,\n};\n\n// Initialize globals when module is imported\nif (typeof window !== 'undefined') {\n liveKitManager.registerGlobals();\n}\n\n// Utility functions for common operations\nexport const connectToRoom = (\n roomName: string,\n participantName: string,\n options?: Parameters<typeof liveKitManager.connect>[2]\n) => liveKitManager.connect(roomName, participantName, options);\n\nexport const disconnectFromRoom = () => liveKitManager.disconnect();\n\nexport const getCurrentRoom = () => liveKitManager.getRoom();\n\nexport const isRoomConnected = () => liveKitManager.isConnected();\n\n// Export default manager\nexport default liveKitManager;\n```\n\nThis implementation provides:\n\n## Key Features\n\n1. **Singleton Pattern**: Global LiveKit manager instance\n2. **Global Registration**: Handles page visibility, network changes, and cleanup\n3. **Token Management**: Fetches tokens from your Edge Function endpoint\n4. **Room Configuration**: Comprehensive room and connection options\n5. **Event System**: Custom event handling for UI updates\n6. **Utility Methods**: Common camera/microphone operations\n\n## Usage Example\n\n```typescript\n// In your React component\nimport { liveKitManager, connectToRoom } from '@/lib/livekit';\n\n// Connect to room\nconst room = await connectToRoom('my-room', 'john-doe', {\n autoEnableCamera: true,\n autoEnableMicrophone: true,\n metadata: JSON.stringify({ role: 'presenter' })\n});\n\n// Listen for events\nliveKitManager.on('participant-connected', (participant) => {\n console.log('New participant:', participant.identity);\n});\n\n// Control camera/microphone\nawait liveKitManager.toggleCamera();\nawait liveKitManager.toggleMicrophone();\n```\n\nThe implementation handles all the complex LiveKit setup while providing a clean, type-safe API for your application to use."
}Task ID: cb5af483-a4d7-4e59-a698-054d7256adc6