mirror of
https://github.com/Monadical-SAS/reflector.git
synced 2026-04-02 12:16:47 +00:00
178 lines
5.7 KiB
Python
178 lines
5.7 KiB
Python
"""
|
|
LiveKit API client wrapping the official livekit-api Python SDK.
|
|
|
|
Handles room management, access tokens, and Track Egress for
|
|
per-participant audio recording to S3-compatible storage.
|
|
"""
|
|
|
|
from datetime import timedelta
|
|
|
|
from livekit.api import (
|
|
AccessToken,
|
|
CreateRoomRequest,
|
|
DeleteRoomRequest,
|
|
DirectFileOutput,
|
|
EgressInfo,
|
|
ListEgressRequest,
|
|
ListParticipantsRequest,
|
|
LiveKitAPI,
|
|
Room,
|
|
S3Upload,
|
|
StopEgressRequest,
|
|
TrackEgressRequest,
|
|
VideoGrants,
|
|
)
|
|
|
|
|
|
class LiveKitApiClient:
|
|
"""Thin wrapper around LiveKitAPI for Reflector's needs."""
|
|
|
|
def __init__(
|
|
self,
|
|
url: str,
|
|
api_key: str,
|
|
api_secret: str,
|
|
s3_bucket: str | None = None,
|
|
s3_region: str | None = None,
|
|
s3_access_key: str | None = None,
|
|
s3_secret_key: str | None = None,
|
|
s3_endpoint: str | None = None,
|
|
):
|
|
self._url = url
|
|
self._api_key = api_key
|
|
self._api_secret = api_secret
|
|
self._s3_bucket = s3_bucket
|
|
self._s3_region = s3_region or "us-east-1"
|
|
self._s3_access_key = s3_access_key
|
|
self._s3_secret_key = s3_secret_key
|
|
self._s3_endpoint = s3_endpoint
|
|
self._api = LiveKitAPI(url=url, api_key=api_key, api_secret=api_secret)
|
|
|
|
# ── Room management ──────────────────────────────────────────
|
|
|
|
async def create_room(
|
|
self,
|
|
name: str,
|
|
empty_timeout: int = 300,
|
|
max_participants: int = 0,
|
|
) -> Room:
|
|
"""Create a LiveKit room.
|
|
|
|
Args:
|
|
name: Room name (unique identifier).
|
|
empty_timeout: Seconds to keep room alive after last participant leaves.
|
|
max_participants: 0 = unlimited.
|
|
"""
|
|
req = CreateRoomRequest(
|
|
name=name,
|
|
empty_timeout=empty_timeout,
|
|
max_participants=max_participants,
|
|
)
|
|
return await self._api.room.create_room(req)
|
|
|
|
async def delete_room(self, room_name: str) -> None:
|
|
await self._api.room.delete_room(DeleteRoomRequest(room=room_name))
|
|
|
|
async def list_participants(self, room_name: str):
|
|
resp = await self._api.room.list_participants(
|
|
ListParticipantsRequest(room=room_name)
|
|
)
|
|
return resp.participants
|
|
|
|
# ── Access tokens ────────────────────────────────────────────
|
|
|
|
def create_access_token(
|
|
self,
|
|
room_name: str,
|
|
participant_identity: str,
|
|
participant_name: str | None = None,
|
|
can_publish: bool = True,
|
|
can_subscribe: bool = True,
|
|
room_admin: bool = False,
|
|
ttl_seconds: int = 86400,
|
|
) -> str:
|
|
"""Generate a JWT access token for a participant."""
|
|
token = AccessToken(
|
|
api_key=self._api_key,
|
|
api_secret=self._api_secret,
|
|
)
|
|
token.identity = participant_identity
|
|
token.name = participant_name or participant_identity
|
|
token.ttl = timedelta(seconds=ttl_seconds)
|
|
token.with_grants(
|
|
VideoGrants(
|
|
room_join=True,
|
|
room=room_name,
|
|
can_publish=can_publish,
|
|
can_subscribe=can_subscribe,
|
|
room_admin=room_admin,
|
|
)
|
|
)
|
|
return token.to_jwt()
|
|
|
|
# ── Track Egress (per-participant audio recording) ───────────
|
|
|
|
def _build_s3_upload(self) -> S3Upload:
|
|
"""Build S3Upload config for egress output."""
|
|
if not all([self._s3_bucket, self._s3_access_key, self._s3_secret_key]):
|
|
raise ValueError(
|
|
"S3 storage not configured for LiveKit egress. "
|
|
"Set LIVEKIT_STORAGE_AWS_* environment variables."
|
|
)
|
|
kwargs = {
|
|
"access_key": self._s3_access_key,
|
|
"secret": self._s3_secret_key,
|
|
"bucket": self._s3_bucket,
|
|
"region": self._s3_region,
|
|
"force_path_style": True, # Required for Garage/MinIO
|
|
}
|
|
if self._s3_endpoint:
|
|
kwargs["endpoint"] = self._s3_endpoint
|
|
return S3Upload(**kwargs)
|
|
|
|
async def start_track_egress(
|
|
self,
|
|
room_name: str,
|
|
track_sid: str,
|
|
s3_filepath: str,
|
|
) -> EgressInfo:
|
|
"""Start Track Egress for a single audio track (writes OGG/Opus to S3).
|
|
|
|
Args:
|
|
room_name: LiveKit room name.
|
|
track_sid: Track SID to record.
|
|
s3_filepath: S3 key path for the output file.
|
|
"""
|
|
req = TrackEgressRequest(
|
|
room_name=room_name,
|
|
track_id=track_sid,
|
|
file=DirectFileOutput(
|
|
filepath=s3_filepath,
|
|
s3=self._build_s3_upload(),
|
|
),
|
|
)
|
|
return await self._api.egress.start_track_egress(req)
|
|
|
|
async def list_egress(self, room_name: str | None = None) -> list[EgressInfo]:
|
|
req = ListEgressRequest()
|
|
if room_name:
|
|
req.room_name = room_name
|
|
resp = await self._api.egress.list_egress(req)
|
|
return list(resp.items)
|
|
|
|
async def stop_egress(self, egress_id: str) -> EgressInfo:
|
|
return await self._api.egress.stop_egress(
|
|
StopEgressRequest(egress_id=egress_id)
|
|
)
|
|
|
|
# ── Cleanup ──────────────────────────────────────────────────
|
|
|
|
async def close(self):
|
|
await self._api.aclose()
|
|
|
|
async def __aenter__(self):
|
|
return self
|
|
|
|
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
|
await self.close()
|