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:
fabio
2026-09-29 18:45:21 +02:00
parent a051eef991
commit 0fe1bc2292
21 changed files with 1904 additions and 12 deletions

99
videochat/rooms_test.go Normal file
View File

@@ -0,0 +1,99 @@
package videochat
import (
"context"
"net/http"
"net/http/httptest"
"testing"
authsvc "encore.app/auth"
"encore.dev/et"
"github.com/livekit/protocol/livekit"
"google.golang.org/protobuf/proto"
)
func useTestProfilesDB(t *testing.T) {
t.Helper()
testDB, err := et.NewTestDatabase(context.Background(), "profiles")
if err != nil {
t.Fatal(err)
}
original := profilesDB
profilesDB = testDB
t.Cleanup(func() { profilesDB = original })
}
func TestListRoomsIncludesInactiveSetups(t *testing.T) {
useTestProfilesDB(t)
ctx := context.Background()
const owner = "00000000-0000-4000-8000-000000000001"
const offline = "00000000-0000-4000-8000-000000000002"
const disabled = "00000000-0000-4000-8000-000000000003"
const sameTitle = "00000000-0000-4000-8000-000000000004"
for _, id := range []string{owner, offline, disabled, sameTitle} {
if _, err := profilesDB.Exec(ctx, `INSERT INTO user_profiles (user_id, display_name, email) VALUES ($1, 'Test', $2)`, id, id+"@example.invalid"); err != nil {
t.Fatal(err)
}
}
for _, setup := range []struct {
id, name string
enabled bool
}{
{owner, "Live show", true}, {offline, "Offline show", true},
{disabled, "", false}, {sameTitle, "Live show", true},
} {
if _, err := profilesDB.Exec(ctx, `INSERT INTO personal_setup (user_id, can_broadcast, room_name) VALUES ($1, $2, $3)`, setup.id, setup.enabled, setup.name); err != nil {
t.Fatal(err)
}
}
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/twirp/livekit.RoomService/ListRooms" {
t.Errorf("unexpected operation: %s", r.URL.Path)
}
payload, err := proto.Marshal(&livekit.ListRoomsResponse{Rooms: []*livekit.Room{
{Sid: "sid-live", Name: "broadcast-generated", NumParticipants: 3,
Metadata: `{"mode":"broadcast","owner_id":"` + owner + `","display_name":"Live show"}`},
{Name: "unmanaged"},
}})
if err != nil {
t.Error(err)
}
w.Header().Set("Content-Type", "application/protobuf")
_, _ = w.Write(payload)
}))
defer server.Close()
original := secrets
defer func() { secrets = original; et.OverrideAuthInfo("", nil) }()
secrets.LiveKitURL, secrets.LiveKitAPIKey, secrets.LiveKitAPISecret = server.URL, "test-key", "test-secret"
et.OverrideAuthInfo("viewer", &authsvc.AuthData{Role: "user"})
result, err := ListRooms(ctx)
if err != nil {
t.Fatal(err)
}
if len(result.Rooms) != 3 {
t.Fatalf("expected one live and two inactive rooms: %+v", result.Rooms)
}
byOwner := make(map[string]*Room)
for _, room := range result.Rooms {
byOwner[room.OwnerID] = room
}
if room := byOwner[owner]; room == nil || !room.Active || room.Name != "broadcast-generated" || room.SID != "sid-live" || room.NumParticipants != 3 {
t.Fatalf("live room must retain its data without duplication: %+v", room)
}
for _, id := range []string{offline, sameTitle} {
if room := byOwner[id]; room == nil || room.Active || room.SID != "" || room.Mode != "broadcast" || room.Name != room.DisplayName {
t.Fatalf("expected inactive configured room: %+v", room)
}
}
}
func TestMergeBroadcastRoomsMatchesTechnicalName(t *testing.T) {
live := &Room{Active: true, Name: "broadcast-123", OwnerID: "owner", DisplayName: "Title"}
result := mergeBroadcastRooms([]*Room{live}, []*Room{{Name: "broadcast-123", OwnerID: "owner"}})
if len(result) != 1 || result[0] != live {
t.Fatal("technical room name must not produce a duplicate")
}
if got := mergeBroadcastRooms([]*Room{}, nil); got == nil || len(got) != 0 {
t.Fatal("empty list must stay an empty JSON array")
}
}