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.
This commit is contained in:
173
videochat/rooms.go
Normal file
173
videochat/rooms.go
Normal file
@@ -0,0 +1,173 @@
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user