@memberjunction/livekit-room-server
v5.48.0
Published
MemberJunction: Server-side LiveKit room support — mints scoped client/bot access tokens (livekit-server-sdk) and provides the session-start coordinator that opens a realtime model session and bridges it into a LiveKit room via AIBridgeEngine. The MJ real
Maintainers
Keywords
Readme
@memberjunction/livekit-room-server
Server-side LiveKit support for MemberJunction: scoped access-token minting, the agent-room session-start coordinator, and recording (egress) control. This is the seam between the MJ realtime bridge and the browser LiveKit UI.
browser ── GraphQL ──► RealtimeBridgeResolver (MJServer)
│
▼
@memberjunction/livekit-room-server ← you are here
├─ LiveKitTokenService mint client + bot tokens (livekit-server-sdk)
├─ LiveKitAgentRoomCoordinator open realtime session → AIBridgeEngine.StartBridgeSession
└─ LiveKitEgressService start/stop room recordingWhy it exists
The realtime bridge already joins the agent bot to a LiveKit room with an MJ-minted token. Two gaps
remained: (1) a human browser participant needs its own scoped token for the same room, and (2) nothing
outside the bridge package opened a realtime session and called StartBridgeSession. This package closes
both.
Configuration
Credentials resolve from explicit config or environment variables (never inline secrets):
LIVEKIT_URL=wss://livekit.myorg.com
LIVEKIT_API_KEY=...
LIVEKIT_API_SECRET=...Where the LiveKit server comes from — see
docker/livekit/README.md(canonical setup guide):
- Local dev: spin up a throwaway LiveKit server with matching dev creds — Docker (
docker compose up -d) for browser-only testing, or nativelivekit-server --config docker/livekit/livekit.yamlfor the agent-bot path on macOS. ThenLIVEKIT_URL=ws://localhost:7880/LIVEKIT_API_KEY=devkey/LIVEKIT_API_SECRET=mj-local-dev-livekit-secret-0123456789.- Production / real-world: use LiveKit Cloud — a hosted
wss://…livekit.cloudURL + API key + secret to drop into.env, no server to run (or a properly-networked self-host).
Token minting (fully functional)
import { LiveKitTokenService } from '@memberjunction/livekit-room-server';
const tokens = new LiveKitTokenService(); // reads env, or pass { ServerUrl, ApiKey, ApiSecret }
const human = await tokens.MintClientToken('support-42', 'user-abc', 'Amith');
// → { ServerUrl, Token, Identity, RoomName } — hand Token to <mj-livekit-room>Agent-room session-start harness
LiveKitAgentRoomCoordinator is a BaseSingleton. It mints the bot token, opens the realtime model
session via an injectable factory seam, and bridges it into the room:
import { LiveKitAgentRoomCoordinator } from '@memberjunction/livekit-room-server';
// At deployment/startup, bind the realtime-session factory (resolves a BaseRealtimeModel → IRealtimeSession):
LiveKitAgentRoomCoordinator.Instance.SetSessionFactory(async (ctx) => openRealtimeSession(ctx));
// Then, per request:
const session = await LiveKitAgentRoomCoordinator.Instance.StartAgentRoomSession({
AgentSessionID, RoomName: 'support-42', AgentName: 'Sage', TurnMode: 'Passive', ContextUser,
});The factory is a seam (like LiveKitBridge.SetSdkFactory) so this package carries no heavy agent-runtime
coupling and unit-tests with a stub session. The actual native room media client (@livekit/rtc-node) is
bound on the bridge driver at deployment, per the realtime-bridge buildout plan.
Recording (egress)
import { LiveKitEgressService } from '@memberjunction/livekit-room-server';
const egress = new LiveKitEgressService();
const rec = await egress.StartRoomRecording({ RoomName: 'support-42' }); // → { EgressID, Status }
const done = await egress.StopRecording(rec.EgressID);
// On stop/complete, RecordingInfo also surfaces the produced file:
// done.OutputLocation – the file's path/key in the egress sink
// done.OutputSizeBytes – byte count
// done.OutputDurationMs – duration in ms (normalized from the SDK's nanoseconds)
// These are what the server registers into MJStorage on the Meeting-Room Conversation
// (Conversation.RecordingFileID + EgressID). While recording is in progress they are undefined.Egress output storage (S3 / GCS / Azure / LiveKit Cloud) is configured on the LiveKit project.
Meeting-recording registration (egress → MJStorage → Conversation)
When the browser stops a room recording, MJServer's RealtimeBridgeResolver.StopLiveKitRecording calls
registerMeetingRecordingFile (packages/MJServer/src/resolvers/meetingRecordingRegistration.ts), which:
- Resolves the room's Meeting-Room Conversation (by
EgressID, then room name, else creates one). - Creates an
MJ: Filesrow for the egress MP4 (ProviderKey=OutputLocation,ContentType = video/mp4,Status = Uploaded). v1 points the Files row directly at the egress output — no byte copy — so playback streams straight from the sink. - Stamps
Conversation.RecordingFileID(+EgressID) and returns the new file id on the result.
This requires the egress sink and the MJStorage account to target the same bucket/container, configured via the provider:
MJ_MEETING_RECORDING_STORAGE_PROVIDER=<MJ: File Storage Providers ID>(ormeetingRecordingStorageProviderIDinmj.config.cjs).
Optional copy-to-canonical ("copy into Box") — set a different canonical provider via
MJ_MEETING_RECORDING_CANONICAL_STORAGE_PROVIDER (or meetingRecordingCanonicalStorageProviderID) to read
the bytes out of the sink and re-upload them into a separate provider, pointing the Files row there. OFF by
default.
Registration is best-effort: a missing storage provider or any failure leaves RecordingFileID unset
and logs — the stop-recording mutation still succeeds. See
REALTIME_SESSION_CAPTURE_GUIDE.md
for the full loop incl. Meet-app playback.
GraphQL surface
These are exposed to the browser via MJServer's RealtimeBridgeResolver:
MintLiveKitClientToken, StartLiveKitAgentRoomSession, StartLiveKitRecording, StopLiveKitRecording
(returns RecordingFileID once registered). Call them from the browser with GraphQLLiveKitClient in
@memberjunction/graphql-dataprovider.
License
ISC © MemberJunction.com
