FOUNDRY
C8 Platform
← 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