Files
encore-test/videochat/rooms.go
fabio 0fe1bc2292 feat(videochat): implement LiveKit integration for video chat functionality
- Add LiveKit client setup and token generation for room access.
- Implement room management functions including CreateRoom, OpenRoom, ListRooms, and DeleteRoom.
- Introduce participant management with ListParticipants and RemoveParticipant functions.
- Create tests for room and participant management, ensuring proper authentication and authorization.
- Add error handling for LiveKit API interactions.
- Define data structures for room metadata and token responses.
- Implement status endpoint for service availability check.
2026-09-29 18:45:21 +02:00

174 lines
5.3 KiB
Go

package videochat
import (
"context"
"encoding/json"
"encore.dev/beta/auth"
"encore.dev/beta/errs"
"encore.dev/storage/sqldb"
"encore.dev/types/uuid"
"github.com/livekit/protocol/livekit"
)
var profilesDB = sqldb.Named("profiles")
type CreateRoomParams struct {
Name string `json:"name"`
MaxParticipants uint32 `json:"max_participants"`
EmptyTimeout uint32 `json:"empty_timeout"`
}
type Room struct {
Active bool `json:"active"`
OwnerID string `json:"owner_id"`
DisplayName string `json:"display_name"`
Mode string `json:"mode"`
SID string `json:"sid"`
Name string `json:"name"`
NumParticipants uint32 `json:"num_participants"`
MaxParticipants uint32 `json:"max_participants"`
CreationTime int64 `json:"creation_time"`
}
type ListRoomsResponse struct {
Rooms []*Room `json:"rooms"`
}
func roomResponse(room *livekit.Room) *Room {
metadata, _ := readBroadcast(room)
return &Room{Active: true, OwnerID: metadata.OwnerID, DisplayName: metadata.DisplayName, Mode: metadata.Mode, SID: room.Sid, Name: room.Name, NumParticipants: room.NumParticipants,
MaxParticipants: room.MaxParticipants, CreationTime: room.CreationTime}
}
// CreateRoom creates a LiveKit room. Requires an administrator.
//
//encore:api auth method=POST path=/videochat/rooms
func CreateRoom(ctx context.Context, p *CreateRoomParams) (*Room, error) {
if err := requireAdmin(); err != nil {
return nil, err
}
if err := validateRoomName(p.Name); err != nil {
return nil, err
}
client, _, err := liveKitClient()
if err != nil {
return nil, err
}
ctx, cancel := context.WithTimeout(ctx, apiTimeout)
defer cancel()
uid, _ := auth.UserID()
id, err := uuid.NewV4()
if err != nil {
return nil, errs.WrapCode(err, errs.Internal, "failed to create room identifier")
}
metadata, err := json.Marshal(broadcastMetadata{Mode: "broadcast", OwnerID: string(uid), DisplayName: p.Name})
if err != nil {
return nil, errs.WrapCode(err, errs.Internal, "failed to encode room metadata")
}
room, err := client.CreateRoom(ctx, &livekit.CreateRoomRequest{
Name: "broadcast-" + id.String(), Metadata: string(metadata), MaxParticipants: p.MaxParticipants, EmptyTimeout: p.EmptyTimeout,
})
if err != nil {
return nil, liveKitError(err)
}
return roomResponse(room), nil
}
// ListRooms lists live broadcasts and enabled personal setups for authenticated viewers.
// Configured rooms absent from LiveKit are returned with active=false.
//
//encore:api auth method=GET path=/videochat/rooms
func ListRooms(ctx context.Context) (*ListRoomsResponse, error) {
if _, ok := auth.UserID(); !ok {
return nil, &errs.Error{Code: errs.Unauthenticated, Message: "must be logged in"}
}
client, _, err := liveKitClient()
if err != nil {
return nil, err
}
ctx, cancel := context.WithTimeout(ctx, apiTimeout)
defer cancel()
result, err := client.ListRooms(ctx, &livekit.ListRoomsRequest{})
if err != nil {
return nil, liveKitError(err)
}
response := &ListRoomsResponse{Rooms: make([]*Room, 0, len(result.Rooms))}
for _, room := range result.Rooms {
if _, ok := readBroadcast(room); ok {
response.Rooms = append(response.Rooms, roomResponse(room))
}
}
configured, err := configuredBroadcastRooms(ctx)
if err != nil {
return nil, err
}
response.Rooms = mergeBroadcastRooms(response.Rooms, configured)
return response, nil
}
func configuredBroadcastRooms(ctx context.Context) ([]*Room, error) {
rows, err := profilesDB.Query(ctx, `
SELECT user_id::text, room_name
FROM personal_setup
WHERE can_broadcast = TRUE AND room_name <> ''
ORDER BY room_name, user_id
`)
if err != nil {
return nil, errs.WrapCode(err, errs.Internal, "failed to list configured broadcast rooms")
}
defer rows.Close()
rooms := make([]*Room, 0)
for rows.Next() {
room := &Room{Mode: "broadcast"}
if err := rows.Scan(&room.OwnerID, &room.Name); err != nil {
return nil, errs.WrapCode(err, errs.Internal, "failed to read configured broadcast room")
}
room.DisplayName = room.Name
rooms = append(rooms, room)
}
if err := rows.Err(); err != nil {
return nil, errs.WrapCode(err, errs.Internal, "failed to list configured broadcast rooms")
}
return rooms, nil
}
func mergeBroadcastRooms(active, configured []*Room) []*Room {
type ownerTitle struct{ owner, title string }
names := make(map[string]bool, len(active))
titles := make(map[ownerTitle]bool, len(active))
for _, room := range active {
names[room.Name] = true
titles[ownerTitle{room.OwnerID, room.DisplayName}] = true
}
for _, room := range configured {
if !names[room.Name] && !titles[ownerTitle{room.OwnerID, room.Name}] {
active = append(active, room)
}
}
return active
}
// DeleteRoom closes a room and disconnects its participants. Requires the room owner.
//
//encore:api auth method=DELETE path=/videochat/rooms/:roomName
func DeleteRoom(ctx context.Context, roomName string) error {
if _, ok := auth.UserID(); !ok {
return &errs.Error{Code: errs.Unauthenticated, Message: "must be logged in"}
}
if err := validateRoomName(roomName); err != nil {
return err
}
client, _, err := liveKitClient()
if err != nil {
return err
}
ctx, cancel := context.WithTimeout(ctx, apiTimeout)
defer cancel()
if err := requireRoomOwner(ctx, client, roomName); err != nil {
return err
}
_, err = client.DeleteRoom(ctx, &livekit.DeleteRoomRequest{Room: roomName})
return liveKitError(err)
}