Conference¶
Multi-party conferences on an external SFU: RoomKit orchestrates and joins as a bot participant; the SFU owns the media plane (RFC §12.10). See the Conference guide for concepts and wiring.
Channel¶
ConferenceChannel ¶
ConferenceChannel(channel_id, *, backend, stt=None, tts=None, realtime=None, pipeline=None, interruption=None, recording=None, recorder=None, bot_identity='roomkit', bot_grants=None, default_grants=None, e2ee=False, close_room_on_detach=False, speak_text_events=False, close_providers=True, max_queued_frames=100, identity_address_keys=None, identity_trusts_unasserted_metadata=False)
Bases: ConferenceAttachmentMixin, ConferenceSessionMixin, ConferenceAccessMixin, ConferenceSubscriptionMixin, ConferencePlugMixin, ConferenceLanesMixin, FrameworkAwareChannel, Channel
Multi-party conference channel backed by an external SFU.
Example::
channel = ConferenceChannel("conf", backend=backend, stt=stt, tts=tts)
kit.register_channel(channel)
await kit.attach_channel("room-1", "conf")
access = await channel.mint_access("room-1", "p-alice")
sender_is_participant
class-attribute
instance-attribute
¶
A conference utterance is attributed to a participant, not to an address.
What a lane puts on sender_id is the identity the track was published
under, which is the Room Participant.id this channel keeps its roster on
(RFC §12.10.2 rule 2). Resolving it would be resolving on the backend's
opaque identity, which rule 3 rules out for exactly the reason it fails
here: no resolver can match it, so every utterance comes back UNKNOWN, and a
hook written to refuse unknown senders makes the transcripts of a
participant the framework identified on arrival disappear.
A conference resolves once, when a participant arrives and its provider's
address is still there to resolve (:mod:roomkit.channels._conference_identity).
Speaking again asks nothing new.
capabilities ¶
Audio, and only audio — see the module docstring for why.
The binding copies this at attachment and it is what routing and transcoding read, so it is the one place an integrator can find out what a conference carries without waiting for frames that never come.
info ¶
What the bot is, where it is, and what it is doing with the media.
Disclosure rules for a transcribing bot differ by jurisdiction, so the RFC mandates no announcement and instead requires that an integrator be able to ask (§17.7): the bot's identity and hidden status, and whether speech recognition, vision or recording is running — at any time, not only when the channel was configured.
Which is why the answer is per conference, under rooms. A channel
serving three rooms is configured once and behaves differently in each:
the bot may be in one and not another, and a binding closed to
Access.NONE stops collection in that room alone. "Is this meeting
being transcribed" is the question a disclosure obligation asks, and a
channel-wide flag cannot answer it — stt_configured says only what
the channel was built with.
set_framework ¶
Wire the channel to the framework.
Attaching does not come through here: on_room_attached and
on_room_detached are the Channel contract and the framework
awaits them itself — see :mod:roomkit.channels._conference_attachment.
may_interrupt ¶
Whether a participant is allowed to interrupt the bot.
deliver
async
¶
Speak an event into the conference on the single bot track.
What a conference is willing to read aloud is the channel's decision, and it is the whole of what happens here: a meeting is not a place to recite orchestration metadata, nor every message arriving from another channel. Synthesis, publication, and the barge-in that can stop it belong to ConferenceVoice.
close
async
¶
Run the channel's one shutdown, or join the one already running.
There is one logical shutdown per channel (RFC 12.10.4): concurrent
callers await the same shielded task, a caller cancelled mid-wait
abandons only its own wait, and once the shutdown reaches its terminal
result every later call replays that result — an immediate return
after a success, the same ConferenceCloseError after a failure —
rather than running the steps again.
Backend¶
ConferenceBackend ¶
Bases: ABC
Abstract base class for SFU conference backends.
The backend does three things: it administers conference rooms, it mints the credentials human clients use to join the SFU directly, and it gives the framework one bot connection through which to subscribe to tracks and publish the AI's voice.
What it deliberately does not do is carry human-to-human media. Clients connect to the SFU themselves; the framework never proxies their signalling or their packets.
Example::
backend = MyConferenceBackend()
backend.on_track_audio(handle_audio)
await backend.ensure_room("room-1")
access = await backend.mint_access("room-1", "p-alice", ConferenceGrants())
bot = await backend.join_as_bot("room-1", "roomkit", ConferenceGrants.observer())
capabilities
abstractmethod
property
¶
What this backend supports.
The framework refuses configurations the backend cannot honour rather
than discovering it at runtime — egress recording without
EGRESS_RECORDING, remote unmute without REMOTE_UNMUTE, bot
video without VIDEO_PUBLISH.
ensure_room
abstractmethod
async
¶
Create the conference room if it does not exist.
Idempotent: called whenever a channel attaches, including for a room that is already conferring.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
room_id
|
str
|
Room this conference belongs to, one-to-one. |
required |
metadata
|
dict[str, Any] | None
|
Provider-specific room configuration. |
None
|
e2ee
|
bool
|
Request end-to-end encryption. Must raise a configuration
error when the backend lacks |
False
|
close_room
abstractmethod
async
¶
Tear down the conference room and disconnect its participants.
mint_access
abstractmethod
async
¶
Mint credentials for a participant to join the SFU directly.
The framework passes its own Participant.id. A backend whose SFU
cannot carry a caller-supplied identity must keep the mapping itself
and translate at this boundary, because every attribution guarantee
downstream depends on participant_id meaning the same thing on both
sides.
display_name is presentation, never identity: a backend that can
carry it should put it in the credential so the SFU's own clients
render the participant as the room named them, and report it back on
ConferenceParticipant.display_name — which is what returns names
to a roster rebuilt from the join's catch-up after a restart (RFC
12.10.3). One that cannot simply ignores it; attribution never
depends on it.
The returned credential is opaque to the framework: the integrator hands it to its client application, and the provider's SDK consumes it.
list_participants
abstractmethod
async
¶
Return the participants currently connected to the conference.
remove_participant
abstractmethod
async
¶
Disconnect a participant from the conference.
mute_track
abstractmethod
async
¶
Mute a published track as a moderator.
Always available, unlike unmuting.
unmute_track
abstractmethod
async
¶
Unmute a published track as a moderator.
Requires ConferenceCapability.REMOTE_UNMUTE, and must raise a
configuration error without it rather than failing silently or
appearing to succeed. Unmuting someone else's microphone is a privacy
decision rather than a technical one, and SFUs commonly refuse it
unless explicitly enabled server-side.
join_as_bot
abstractmethod
async
¶
Connect the framework to the conference as a participant.
Grants are applied to the bot session rather than assumed: a speaking
bot needs publish_audio, while an observer is subscribe-only and
hidden.
The backend must not auto-subscribe the bot to anything. The
framework's subscription set is authoritative, and it is expressed
through subscribe_track() alone.
update_bot_grants
async
¶
Replace a connected session's grants in place. Requires BOT_GRANT_UPDATE.
Same session, same connection, subscriptions and callbacks undisturbed —
the SFU changes what the session may do, which is what lets a
hot-plugged voice speak without a re-join (RFC 12.10.4). grants is
the session's whole grant set, not a delta.
Concrete rather than abstract so that a backend written before this
call existed keeps working: the channel only calls it on a backend
whose capabilities declare BOT_GRANT_UPDATE, and falls back to a
re-join otherwise. This default is the contract's required refusal —
a configuration error rather than silence or apparent success (RFC
12.10.3).
subscribe_track
abstractmethod
async
¶
Start delivering a track's frames to the bot.
The only way frames begin arriving. A backend must not deliver
on_track_audio or on_track_video for a track that was never
passed here — selective subscription is what keeps unconsumed video out
of the framework process entirely.
unsubscribe_track
abstractmethod
async
¶
Stop delivering a track's frames to the bot.
publish_audio
abstractmethod
async
¶
Publish decoded PCM audio on the bot's track.
AudioChunk is the outbound stream type: it names its own encoding in
format and marks the end of an utterance with is_final.
Implementations must reject a chunk that is not PCM rather than
forwarding it, because encoding belongs to the backend — a caller
choosing the wire format would defeat this interface.
One bot track, heard by every participant. Targeted per-participant audio is not supported: the AI is synthesized once and published once.
What that single track guarantees a backend, in return: an utterance
arrives contiguously — the framework never interleaves two — and ends on
a chunk whose is_final is set, so is_final is a boundary an
implementation can rely on rather than a hint. An utterance a
participant cut short ends the same way, and that closing chunk may
carry no audio at all (data=b""): there is nothing left to play,
only an end to declare. It is a boundary, not a flush — an
implementation must not discard queued audio on it, or a synthesizer
whose real ending is queued behind an empty final chunk would lose
it. What silences the room on a barge-in is :meth:stop_playback.
With one exception, which is the end of the session rather than the end
of an utterance. An utterance the channel abandons because it has left
the conference is not closed: the session the terminal chunk would
name is on its way out, so publishing into it would race the
:meth:leave behind it. Nothing in band can carry that boundary — a
process that crashed or a connection that dropped announces nothing
either — so an implementation must take :meth:leave, and a session
disconnecting by any other means, as ending whatever utterance was in
flight on it (RFC section 12.10.4).
stop_playback
abstractmethod
async
¶
Discard the bot's queued, unplayed audio: the barge-in gesture.
The one call that says the room asked for silence now, rather than
at the end of whatever the transport has buffered. An implementation
must immediately discard the audio it accepted through
:meth:publish_audio but has not yet delivered for playout; what has
already left it is beyond recall, and the smaller its local queue the
smaller that residue.
This does not end the utterance. is_final remains the only
boundary: the closing chunk still follows, and must be accepted after
the stop. The track stays usable — the next utterance publishes
normally.
A stop for a session this backend no longer holds is a no-op, not an
error: the silence the call asks for is already true of a session
that is gone. Deliberately unlike :meth:publish_audio, which
refuses media for a session that is out — a refusal there protects a
track from writes, and there is no track left to protect (RFC
section 12.10.3).
publish_video
abstractmethod
async
¶
Publish a raw video frame on the bot's track.
Requires ConferenceCapability.VIDEO_PUBLISH.
The frame must be raw (frame.is_raw). The backend owns encoding,
symmetrically with the decoding it owns inbound: requiring an encoded
frame would mean the framework choosing a codec the SFU accepts, which
is exactly what this interface exists to avoid.
on_participant_joined ¶
Register a callback for participants joining the media session.
on_participant_left ¶
Register a callback for participants leaving the media session.
on_track_unpublished ¶
Register a callback for tracks being unpublished.
on_track_muted ¶
Register a callback for a publisher muting their track.
The track record's muted flag is already updated when the callback
runs. Presence, not media: a muted VIDEO track is how most clients
express "camera off" (RFC 12.10.3).
on_track_unmuted ¶
Register a callback for a publisher unmuting their track.
on_track_audio ¶
Register a callback for decoded audio from subscribed tracks.
on_active_speaker_changed ¶
Register a callback for dominant-speaker changes.
on_connection_quality ¶
Register a callback for per-participant quality reports.
on_bot_session_ended ¶
Register a callback for the bot's session ending without a leave().
Reported when the backend observes the end — a dropped connection, an eviction, the room deleted underneath the bot. A backend that cannot observe the loss reports nothing, and knowingly inherits the failure mode: a dropped bot its channel goes on reporting present.
LiveKit¶
Install with: pip install roomkit[livekit]
LiveKitConferenceBackend ¶
Bases: ConferenceBackend
ConferenceBackend backed by a LiveKit SFU.
Example::
backend = LiveKitConferenceBackend(LiveKitConfig(url="ws://127.0.0.1:7880"))
await backend.ensure_room("room-1")
access = await backend.mint_access("room-1", "p-alice", ConferenceGrants())
bot = await backend.join_as_bot("room-1", "roomkit", ConferenceGrants.for_bot())
capabilities
property
¶
What this backend has wired, which is not everything LiveKit sells.
Declared: separate screen-share tracks, dominant-speaker events and
per-participant quality reports — each one an event this backend
translates and forwards. Plus, when the deployment says so,
REMOTE_UNMUTE and SIP_GATEWAY, because both depend on server
configuration this backend cannot see.
Absent, and each for its own reason. EGRESS_RECORDING: LiveKit can
do it, nothing here asks it to, and framework-side recording already
works through the lanes. VIDEO_PUBLISH: the bot has nothing to show
until an avatar gives it something, so the source is not built and the
capability is not claimed. E2EE: admitting the bot to a conference's
key exchange is a contract ConferenceBackend does not have, so declaring
it would promise a bot that can hear an encrypted room when it cannot.
ensure_room
async
¶
Create the LiveKit room if it is not there.
create_room is idempotent on the name, which is what the interface
asks for: a channel attaching to a room that is already conferring
re-issues this call and nothing changes.
mint_access
async
¶
Mint a join credential for a participant the framework named.
participant_id becomes LiveKit's participant identity verbatim, so
the value comes back on every participant and track LiveKit reports.
That is rule 2 of RFC section 12.10.2 satisfied by the SFU itself rather
than by a mapping table this backend would have to keep.
display_name rides the token as LiveKit's participant name —
what LiveKit's own clients render — and comes back on every
ParticipantInfo the server reports, which is how a roster rebuilt
after a restart gets its names back (RFC 12.10.3).
list_participants
async
¶
Who is connected, as the server sees it.
Read through the server API rather than off the bot's own room, so the answer is available before a bot has joined and does not depend on one being connected. The names LiveKit's two protocols use differ, so they are brought to the realtime dialect here — provenance is decided on those names, and a control-plane spelling would leave a dial-in unresolvable.
join_as_bot
async
¶
Connect the framework to the conference as a participant.
The bot's token carries the grants it was given and nothing more, and it joins with auto-subscription off: the framework's subscription set is the authoritative one, and a bot the SDK subscribed on its own behalf would deliver media nobody asked for.
update_bot_grants
async
¶
Replace the bot session's permissions on the server, in place.
UpdateParticipant re-permissions the connected session — same
connection, subscriptions and callbacks undisturbed — which is what
lets a hot-plugged voice speak without a re-join (RFC 12.10.4). The
session is looked up first so a session this backend no longer holds
is refused here rather than turned into a server call about a
participant that may have left.
leave
async
¶
Take the bot out, and forget the session only once it is out.
Popping first was how a failed disconnect became invisible: the registry had already forgotten the session, so a retry found nothing to leave and the channel's books called the bot gone while it may still have been in the meeting. The session stays registered until the disconnect returns, and the failure propagates for the channel's leaving ledger to record (RFC 12.10.4).
stop_playback
async
¶
Drop the bot's queued, unplayed audio — the barge-in gesture.
A session this backend no longer holds is a no-op rather than an
error, unlike :meth:publish_audio: the SFU may have dropped the bot
in the same breath as the barge-in, and the silence the call asks for
is already true of a session that is gone (RFC section 12.10.3).
publish_video
async
¶
Refused: this backend does not publish the bot's video.
LiveKit can carry it. Nothing here builds the source that would, because the bot has nothing to show until an avatar gives it something — so the capability is not declared and this is what not declaring it means (RFC section 12.10.3).
close
async
¶
Release the sessions and the API client. Idempotent.
A session whose disconnect fails stays registered and is raised, together, once every session has been attempted and the client is released — a close that only logged them reported bots possibly still in their meetings as a clean shutdown, which is the one answer the channel's books must never get (RFC 12.10.4). The channel records the failure against its own close and keeps naming the sessions.
LiveKitConfig
dataclass
¶
LiveKitConfig(url=None, api_key=None, api_secret=None, access_ttl=timedelta(minutes=15), audio_sample_rate=48000, audio_channels=1, publish_queue_ms=300, remote_unmute=False, sip_gateway=False, room_metadata_key='roomkit')
Connection and behaviour settings for :class:LiveKitConferenceBackend.
url, api_key and api_secret fall back to LiveKit's own
environment variables — LIVEKIT_URL, LIVEKIT_API_KEY,
LIVEKIT_API_SECRET — so a deployment that already sets them needs no
RoomKit-specific configuration. The URL is the signalling one (ws:// or
wss://); it is what clients are handed, and the server API derives its
own endpoint from it.
api_key
class-attribute
instance-attribute
¶
API key. Defaults to LIVEKIT_API_KEY.
api_secret
class-attribute
instance-attribute
¶
API secret. Defaults to LIVEKIT_API_SECRET. Kept out of repr().
access_ttl
class-attribute
instance-attribute
¶
How long a minted credential stays valid.
Short by default: the credential leaves the process, and RFC section 12.10.2 recommends short-lived ones. Long enough that a client which fetches its token then asks the user for microphone permission still gets in.
audio_sample_rate
class-attribute
instance-attribute
¶
Rate to ask LiveKit's decoder for on subscribed audio tracks.
48 kHz is Opus's own rate and LiveKit's default, so it is the closest thing to "what the publisher sent". Normalising it to what a recognizer wants is the lane's job (RFC section 12.10.4), not this backend's — every frame declares the format it arrives in.
audio_channels
class-attribute
instance-attribute
¶
Channel count to ask for on subscribed audio tracks.
Mono because that is what the pipeline works in and what LiveKit defaults to. Set it to 2 to have the SFU hand over a stereo publisher's audio unmixed, and the lane's downmix do the work instead.
publish_queue_ms
class-attribute
instance-attribute
¶
How much of the AI's voice LiveKit may buffer ahead of playout.
The buffer is what keeps the bot's speech gap-free when synthesis
stutters. It is not what a participant who interrupts must sit through: a
barge-in discards it (stop_playback, RFC section 12.10.3), and the
size bounds only what keeps playing when that gesture fails or a chunk
already on its way lands behind it. 300 ms keeps even that residue within
the range a person reads as responsive.
remote_unmute
class-attribute
instance-attribute
¶
Whether the server allows unmuting someone else's track.
Off by default because LiveKit's own default is off: unmuting a remote
microphone needs room.enable_remote_unmute in the server configuration.
Setting this without that is how a moderation UI comes to offer a button the
server refuses, so the capability is declared only when an integrator says
the server was configured for it.
sip_gateway
class-attribute
instance-attribute
¶
Whether PSTN participants can dial into this deployment's conferences.
Off by default for the same reason: LiveKit's SIP service needs a trunk and
a dispatch rule before a phone can reach a room. When it is on, dial-ins
arrive as ordinary participants whose sip. attributes this backend
asserts, and identity resolution can reach them.
room_metadata_key
class-attribute
instance-attribute
¶
Key under which ensure_room metadata is stored in LiveKit's room.
LiveKit's room metadata is one opaque string. Nesting under a key rather than writing the mapping at the top level leaves room for whatever else a deployment keeps there.
Mock¶
The test double, with fault injection — see Testing Patterns.
MockConferenceBackend ¶
MockConferenceBackend(*, capabilities=NONE)
Bases: ConferenceBackend
Conference backend that scripts SFU events for tests.
Example::
backend = MockConferenceBackend()
bot = await backend.join_as_bot("room-1", "roomkit", ConferenceGrants())
track = await backend.simulate_track_published("room-1", "p-alice")
await backend.subscribe_track(bot, track.id)
await backend.simulate_audio(track, AudioFrame(data=b"..."))
Made to misbehave::
backend.fail("join_as_bot", TimeoutError) # the SFU refuses the bot
backend.delay("track_audio", 0.05) # slow delivery
dial_in = MockTrackFormat(sample_rate=8_000, sample_width=1)
track = await backend.simulate_track_published(
"room-1", "p-bob", audio_format=dial_in
)
await backend.simulate_audio(track, backend.frame_for(track))
fail ¶
Make a backend call raise. See :meth:MockFaults.fail.
delay ¶
Make a backend call or a callback fanout take time.
See :meth:MockFaults.delay.
update_bot_grants
async
¶
Replace a connected session's grants, as a capable SFU would.
The whole grant set, not a delta (RFC 12.10.3). bot_grants keeps
the last set per session, so a test can assert what the SFU currently
believes the bot may do — after the join and after every update.
stop_playback
async
¶
Record the barge-in gesture. There is nothing here to discard.
The mock publishes synchronously — nothing queues, so a real flush would have no observable effect, and inventing one would have the mock outdo every real SFU. What a test asserts is that the gesture reached the backend at all, and for which bot. The open utterance is deliberately left open: a stop is not a boundary, and the closing chunk that follows is still owed (RFC section 12.10.3).
utterances_for ¶
What was published on one bot's track, in order.
A channel serving several rooms holds a bot session per room, and
utterances interleaves them the way the calls arrived. Asking per
bot is asking about one track.
simulate_participant_joined
async
¶
simulate_participant_joined(room_id, participant_id, *, display_name=None, metadata=None, client_metadata=None, asserts_provenance=True)
Announce a participant joining the media session.
metadata is what the SFU itself asserts — a dial-in's caller number
as the trunk reported it, which is what identity resolution consumes.
client_metadata is what the participant's own client supplied at
join: surfaced like any other attribute, never vouched for, and so
never an address (RFC §12.10.2). A key given in both is the SFU's.
asserts_provenance=False is the third kind of backend — one that
cannot tell the two apart and says so. Everything it surfaces becomes
unvouched, whichever argument it arrived in.
simulate_bot_disconnected
async
¶
End the bot's session the way an SFU does: without a leave().
The session is forgotten first — a dropped connection is not a
participant, and a later leave() for it finds nothing to do — and
then reported, which is the order the contract promises (RFC 12.10.3).
simulate_track_published
async
¶
simulate_track_published(room_id, participant_id, kind=AUDIO, *, track_id=None, audio_format=None)
Publish a track, optionally in a format of its own.
audio_format is what this publisher negotiated with the SFU.
Participants negotiate separately and nothing obliges them to agree, so
a conference of three can carry three formats — and a track that
declares one only accepts frames in it.
frame_for ¶
Build a frame in the format track was published in.
amplitude is a fraction of full scale: the default is loud enough
for an energy VAD to call speech, and 0.0 gives the silence that
ends an utterance.
simulate_audio
async
¶
Deliver an audio frame, if the bot subscribed to the track.
Returns whether it was delivered. An unsubscribed track produces no frame at all — a real SFU forwards nothing to a subscriber that did not ask, and a mock that delivered anyway would make selective subscription untestable.
simulate_video
async
¶
Deliver a video frame, if the bot subscribed to the track.
simulate_track_muted
async
¶
The publisher mutes their own track — a camera toggled off included.
simulate_bot_echo
async
¶
Report the bot back through its own callbacks, as some SFUs do.
Announces the bot as a participant and publishes a track in its name. Without self-exclusion the framework would then create a participant record for its own bot and transcribe the AI's own speech, so this is the scripted event sequence that proves the rule holds.
MockTrackFormat
dataclass
¶
The audio format a participant negotiated for one track.
Defaults to what the rest of the framework assumes downstream of format normalisation, so a track that declares nothing behaves as before.
Example::
dial_in = MockTrackFormat(sample_rate=8_000, channels=1, sample_width=1)
studio = MockTrackFormat(sample_rate=48_000, channels=2, sample_width=4)
MockUtterance
dataclass
¶
The chunks published for one utterance on one bot's track.
An utterance runs until a chunk marks itself final. Two utterances published concurrently on the same bot therefore land in the same record, which is the point: that is what interleaving looks like, and a flat list of chunks cannot show it.
Two bots never share a record. A bot is a track — one per conference room — so chunks alternating between two of them are two rooms talking at once, which is ordinary, while chunks alternating within one are a single track carrying two answers, which is not. A record that could not tell them apart would report the first as the second.
MockDelivery
dataclass
¶
MockFaults ¶
Per-operation failures and delays for a mock backend.
Operations are named: backend methods (join_as_bot, leave, ...) can
both fail and be slowed; callback emissions (track_audio,
participant_joined, ...) can only be slowed, since a backend's emission
loop swallows what its subscribers raise and a failure there would be
invisible by construction.
Example::
faults = MockFaults(methods={"leave"}, emissions={"track_audio"})
faults.fail("leave", TimeoutError, times=1) # first teardown only
faults.delay("track_audio", 0.05) # slow delivery
fail ¶
Make operation raise.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
operation
|
str
|
Backend method to fail. |
required |
error
|
ErrorSpec | None
|
Exception instance, class, or factory. Defaults to a
|
None
|
times
|
int | None
|
How many calls to fail. |
None
|
delay ¶
Make operation take seconds before doing its work.
Applies to backend methods and to callback emissions alike. A delayed emission is what a slow media path looks like from outside.
apply
async
¶
Sleep, then raise, as operation was configured to.
In that order: a call that fails slowly is what a timeout looks like, and a call that failed instantly would never let a test hold the window open around it.
Access and grants¶
ConferenceAccess
dataclass
¶
Credentials a client uses to join the conference directly.
Treated as opaque: the backend mints it, the integrator hands it to its client application, and the provider's client SDK consumes it. Framework code must not depend on its internal structure beyond these fields.
token
class-attribute
instance-attribute
¶
Provider-specific credential.
Excluded from repr() so that logging an access object, or letting one
surface in a traceback, cannot leak the credential.
expires_at
class-attribute
instance-attribute
¶
When the credential stops being valid, if it expires.
Short-lived credentials are recommended.
provider_data
class-attribute
instance-attribute
¶
Additional provider-specific fields.
Must not carry credentials: unlike token it appears in repr(), and
the framework has no way to know which provider keys are sensitive.
ConferenceGrants
dataclass
¶
ConferenceGrants(publish_audio=True, publish_video=True, publish_screen_share=True, subscribe=True, moderate=False, hidden=False)
Permissions encoded into a participant's conference access.
The defaults are deliberately permissive so the common case works unconfigured. Narrowing them is the integrator's call and is recommended wherever a role does not need to publish. This is a SHOULD, not a MUST: do not flip these defaults to deny-by-default without changing the specification first.
publish_audio
class-attribute
instance-attribute
¶
May publish a microphone track.
publish_screen_share
class-attribute
instance-attribute
¶
May publish a screen-share track.
subscribe
class-attribute
instance-attribute
¶
May receive other participants' tracks.
moderate
class-attribute
instance-attribute
¶
May mute or remove other participants.
hidden
class-attribute
instance-attribute
¶
Invisible to other participants (bots, monitors).
for_bot
classmethod
¶
Least privilege for the framework's own bot.
The permissive defaults above are for humans, whose needs the framework cannot know: an attendee may unmute, turn a camera on or share a screen at any point, and refusing that by default would break the common case. The bot is the opposite — the framework configured it, so it knows exactly what it will do, and asking the SFU for more than that is privilege nobody will use.
So: publish_audio only when a synthesizer is configured — without
one there is nothing to publish. subscribe only when something
consumes the tracks it would receive; a channel that only speaks
subscribes to none, and the grant would be permission to receive every
participant's media for nobody to read. publish_screen_share never:
the bot has no screen. publish_video stays off until the bot is
given something to show; an avatar would be what turns it on, and none
is configurable yet.
listens defaults to true because that is what a conference bot is
usually for, and because it is what makes :meth:observer mean what it
says.
hidden is deliberately not decided here. It is a disclosure choice,
not a privilege, and Section 17.7 leaves it to the integrator.
observer
classmethod
¶
Subscribe-only and hidden — the Observer participation pattern.
A silent bot that is also invisible, which is why it is expressed as
one: whether a silent transcribing bot may stay invisible to
participants is a legal question rather than a framework one, and the
framework exposes the bot's hidden status so integrators can meet
the disclosure rules that apply to them.
ConferenceCapability ¶
Bases: Flag
Capabilities a ConferenceBackend can support.
Backends declare these via their capabilities property so the framework
can refuse configurations the backend cannot honour, rather than failing at
runtime.
Example::
class MyBackend(ConferenceBackend):
@property
def capabilities(self) -> ConferenceCapability:
return (
ConferenceCapability.SCREEN_SHARE
| ConferenceCapability.ACTIVE_SPEAKER
)
SCREEN_SHARE
class-attribute
instance-attribute
¶
Separate screen-share tracks.
EGRESS_RECORDING
class-attribute
instance-attribute
¶
Server-side (SFU) recording and export.
SIP_GATEWAY
class-attribute
instance-attribute
¶
PSTN/SIP participants can dial into the conference.
ACTIVE_SPEAKER
class-attribute
instance-attribute
¶
Dominant-speaker change events.
CONNECTION_QUALITY
class-attribute
instance-attribute
¶
Per-participant connection quality reports.
VIDEO_PUBLISH
class-attribute
instance-attribute
¶
The bot can publish video tracks (avatar embodiment).
REMOTE_UNMUTE
class-attribute
instance-attribute
¶
A moderator can unmute another participant's track.
Separate from muting because unmuting someone else's microphone is a privacy decision, not a technical one: SFUs commonly refuse it by default and require an explicit server-side opt-in. Muting is always available.
BOT_GRANT_UPDATE
class-attribute
instance-attribute
¶
A connected bot session's grants can be changed in place.
A server-side participant update — the SFU changes what the session may do without reconnecting it. A capability because many SFUs can only set permissions at admission; against those, the one way to change a live bot's grants is to replace the session, and hot-plugging falls back to exactly that re-join (RFC 12.10.4). What this buys is continuity: a re-permission with the session, its subscriptions and the event bridge intact.
E2EE
class-attribute
instance-attribute
¶
End-to-end encryption between clients.
Constrains rather than extends what the framework can do: with E2EE active the bot receives ciphertext, so STT, vision and recording are unavailable unless the bot is admitted as a key holder.
Media models¶
ConferenceParticipant
dataclass
¶
ConferenceParticipant(participant_id, display_name=None, connected_at=(lambda: now(UTC))(), tracks=list(), metadata=dict(), asserted_metadata=None)
A participant's media presence in a conference.
participant_id
instance-attribute
¶
Identity of the participant.
For a participant the framework admitted, this is the RoomKit
Participant.id passed to mint_access() and echoed back by the
backend. For one it did not admit — a PSTN dial-in, or an out-of-band
admission — it is the backend's own stable identity.
display_name
class-attribute
instance-attribute
¶
Human-readable name, when the SFU carries one.
Presentation, never identity: attribution rides participant_id alone
(RFC 12.10.2), and this is what the SFU's own clients render. It usually
rode in on the credential mint_access() issued, which is what lets a
roster rebuilt from the join's catch-up get its names back after a
restart — the credential outlives the process that minted it.
connected_at
class-attribute
instance-attribute
¶
When the participant joined the media session.
tracks
class-attribute
instance-attribute
¶
Tracks this participant currently publishes.
metadata
class-attribute
instance-attribute
¶
Provider-supplied participant attributes.
Not decoration: for a participant the framework did not name, this is where the resolvable address lives — a PSTN dial-in carries its caller number here, and that number is what identity resolution consumes.
What it does not say is who put each attribute there, which is what
:attr:asserted_metadata is for.
asserted_metadata
class-attribute
instance-attribute
¶
The subset of :attr:metadata the SFU itself asserts.
One attribute map on most SFUs carries two very different things: facts the server established — the number a SIP trunk reported, a claim in a token it authenticated, an attribute set through a server-side API — and values a participant's own client supplied when it joined. Only the first kind can found an identity, and nothing in the map's shape tells them apart, so the backend says which is which here.
Three states, all meaningful (RFC §12.10.2):
- a mapping: these attributes the SFU asserts, and identity may be resolved on an address among them;
{}: this backend distinguishes, and the SFU asserts nothing here;None: this backend cannot distinguish. A statement, not an omission — the channel resolves nothing from it unless the integrator says otherwise.
A backend that fills this with everything it has is asserting a guess, and a guess is indistinguishable from a fact to whoever acts on it.
ConferenceTrack
dataclass
¶
A single media stream published by a conference participant.
room_id
instance-attribute
¶
Owning conference room.
Carried on the track because the frame callbacks receive only a track: a single backend instance serves many rooms, so without this the frames would not be routable.
participant_id
instance-attribute
¶
Publishing participant.
Track identity is what attributes speech to a speaker, which is why a conference needs no diarization.
muted
class-attribute
instance-attribute
¶
Whether the publisher has muted this track.
metadata
class-attribute
instance-attribute
¶
Provider-specific fields (sid, source, ...).
TrackKind ¶
Bases: StrEnum
Kind of media carried by a conference track.
BotSession
dataclass
¶
The framework's own connection to a conference.
One per conference. Every frame the framework receives and every frame it publishes passes through this single connection — it is the only crossing of the media-plane boundary.
identity
instance-attribute
¶
The bot's identity in the conference.
Used to recognise the bot's own participant and tracks when a backend reports them back, so they can be excluded from participant records, processing lanes and subscriptions.
joined_at
class-attribute
instance-attribute
¶
When the bot connected.
Defaulted to construction time, which is when a backend builds the session
it is returning from join_as_bot(). A backend with a more accurate
figure — one the SFU reports — sets it instead. What reads it is
conference_ended's duration_ms (RFC 8.2).
metadata
class-attribute
instance-attribute
¶
Provider-specific fields.
Pipeline payloads¶
ConferenceTranscription
dataclass
¶
What a lane produced, before it enters the room.
Carried to ON_TRANSCRIPTION so a hook can identify the track and the participant, block the text, or rewrite it.
ConferenceBargeIn
dataclass
¶
A participant spoke over the bot and was allowed to interrupt it.
Carried to ON_BARGE_IN. The interrupting participant is named, which is
what distinguishes a conference barge-in from a 1:1 one (RFC 12.10.5) —
BargeInEvent identifies a voice session, and a conference lane has
none.
Interruption¶
ConferenceInterruptionConfig
dataclass
¶
ConferenceInterruptionConfig(strategy=IMMEDIATE, scope=ANY, allowlist=list())
Multi-party interruption policy.
In a 1:1 voice session any user speech may interrupt playback. In a conference, who may interrupt is policy rather than mechanics.
strategy
class-attribute
instance-attribute
¶
How an interruption is confirmed once it is allowed.
scope
class-attribute
instance-attribute
¶
scope = ConferenceInterruptionScope.ANY
Which participants are allowed to interrupt at all.
allowlist
class-attribute
instance-attribute
¶
Participant identities allowed to interrupt when scope is ALLOWLIST.
ConferenceInterruptionScope ¶
Recording¶
ConferenceRecordingConfig
dataclass
¶
ConferenceRecordingConfig(mode=FRAMEWORK, storage='local', format='wav', metadata=dict())
Configuration for recording a conference.
mode
class-attribute
instance-attribute
¶
mode = ConferenceRecordingMode.FRAMEWORK
Who produces the recording.
storage
class-attribute
instance-attribute
¶
Integrator-defined storage identifier, resolved at runtime.
format
class-attribute
instance-attribute
¶
Output format. Composed video egress typically uses mp4.
metadata
class-attribute
instance-attribute
¶
Recording metadata (room_id, participant_id, ...).
ConferenceRecordingMode ¶
Bases: StrEnum
Where a conference recording is produced.
FRAMEWORK
class-attribute
instance-attribute
¶
Recorded by RoomKit from the tracks the bot subscribes to.
The path that always works: no backend capability, functions against the mock backend, and the file lands wherever the implementation writes it. Audio tracks are already subscribed for transcription, so recording them adds a file write and no additional media subscription.
EGRESS
class-attribute
instance-attribute
¶
Delegated to the SFU. Requires EGRESS_RECORDING.
Exists for one reason: a composed video recording — grid or active-speaker layout — cannot be produced by the framework without subscribing every video track, decoding all of them, compositing and re-encoding, which is the media-plane work RoomKit does not do. Carries no unified result contract: the SFU announces completion out of band, and the integrator collects the output through the provider's own mechanism.
ConferenceRecordingStarted
dataclass
¶
ConferenceRecordingStarted(room_id, track_id, participant_id, id, kind, sample_rate, channels=None, codec='')
A track's recording has opened.
Carried to ON_RECORDING_STARTED. Names the track and the participant publishing it, which is what a conference has instead of the session the voice path's event carries (RFC 12.10.8).
channels
class-attribute
instance-attribute
¶
Audio channel count of the track, as the recording was opened on it.
codec
class-attribute
instance-attribute
¶
Sample format of the track, e.g. pcm_s16le.
With :attr:sample_rate and :attr:channels it is the whole of what the
recording was opened on, and this event is the only place an integrator
learns it: two participants in one conference need not have negotiated the
same one.
ConferenceRecordingStopped
dataclass
¶
ConferenceRecordingStopped(room_id, track_id, participant_id, id, url, duration_seconds, size_bytes, format)
A track's recording has closed, and this is where it went.
Carried to ON_RECORDING_STOPPED. url is what the recorder reported —
a path, an object-store URL, whatever it writes to — and it is the whole
point of the event: without it the files exist and nothing says where.
Errors¶
ConferenceAlreadyAttachedError, ConferenceCapabilityError,
ConferenceCloseError and ParticipantNotAdmittedError are documented on the
Errors page.