Video Rooms
Planner includes video conferencing powered by LiveKit SFU (Selective Forwarding Unit).
Architecture
- LiveKit Server — Docker service (ports 7880 HTTP, 7881 WS, 3478 TURN, 50000-60000 UDP media)
- Backend —
livekit-server-sdkfor room management and token generation - Frontend —
livekit-client(WebRTC) for connecting to LiveKit SFU - WebSocket —
/videonamespace for signaling relay
Room Types
| Type | Description |
|---|---|
instant | Immediate call + participant notifications |
scheduled | Scheduled conference + calendar event + reminders |
API Endpoints
| Method | Path | Description |
|---|---|---|
GET | /api/video-rooms | List rooms (filter: active/scheduled/ended) |
POST | /api/video-rooms | Create room |
GET | /api/video-rooms/:id | Room details |
PATCH | /api/video-rooms/:id | Update room |
DELETE | /api/video-rooms/:id | End/delete room |
POST | /api/video-rooms/:id/join | Join (returns LiveKit token) |
POST | /api/video-rooms/:id/leave | Leave room |
GET | /api/video-rooms/:id/participants | List participants |
POST | /api/video-rooms/:id/token | Get LiveKit token |
POST | /api/video-rooms/:id/recording/start | Start recording |
POST | /api/video-rooms/:id/recording/stop | Stop recording |
GET | /api/video-rooms/:id/recording/status | Recording status |
WebSocket Events (/video)
Client → Server
| Event | Description |
|---|---|
join | Join room (room:<id>) |
leave | Leave room |
signal | WebRTC signaling relay |
Server → Client
| Event | Description |
|---|---|
video:participant_joined | Participant connected |
video:participant_left | Participant disconnected |
video:room_updated | Room updated |
video:call_started | Instant call started |
video:recording_started | Recording started |
video:recording_stopped | Recording stopped |
Frontend
Video Chat Store
typescript
const store = useVideoChatStore()
const { rooms, currentRoom, participants } = storeToRefs(store)
await store.fetchRooms()
await store.createRoom({ type, name, scheduledAt? })
await store.joinRoom(roomId) // gets LiveKit token
await store.leaveRoom(roomId)
await store.endRoom(roomId)Composables
typescript
const {
isConnected, remoteParticipants,
isMuted, isScreenSharing, isRecording, isPiP
} = useLiveKit()
await liveKit.connect(tokenData) // connect to LiveKit
liveKit.toggleMute('audio') // toggle microphone
liveKit.toggleMute('video') // toggle camera
liveKit.toggleScreenShare() // screen sharing
liveKit.startRecording(roomId) // start recording
liveKit.enterPiP() // Picture-in-PictureComponents
VideoRoomsView.vue(/video-rooms) — hub with tabs: Active, Scheduled, PastVideoRoomView.vue(/video-rooms/:id) — active room: VideoGrid + VideoControls + participants sidebar + invite link copyVideoGrid— participant video gridParticipantTile— single participant tileVideoControls— control panel (mute, camera, screen share, recording, leave)ScheduleVideoDialog— scheduled room creation dialogVideoRoomCard— room card in list
Incoming Call Dialog
When an instant video call is started, participants receive an IncomingCallDialog.vue popup:
- Displays caller name and room name
- Accept → joins the room and connects to LiveKit
- Decline → sends a decline notification
- Auto-dismisses after 30 seconds
Key Implementation Details
- Scheduled rooms create a
CalendarEventwith typevideo - Reminders are sent 15 minutes before start (cron every 5 minutes)
- Picture-in-Picture auto-enables on tab hide
- Orphaned LiveKit rooms are cleaned up on module init