6.1 KiB
iroh-live: Rooms — Multi-Party Coordination
Overview
The rooms module provides multi-party room coordination over iroh-gossip. Participants discover each other via a gossip topic, automatically connect and subscribe to each other's broadcasts, and receive RoomEvent notifications as peers join, publish, and leave.
Core Types
Room
The main room handle. Created via Room::new(live, ticket). Spawns an internal actor that manages all peer coordination.
// Create a room (generates a random topic)
let ticket = RoomTicket::generate();
let room = Room::new(&live, ticket.clone()).await?;
// Or join an existing room
let room = Room::new(&live, existing_ticket).await?;
Methods:
recv()— Wait for nextRoomEventtry_recv()— Non-blocking event checkticket()— Get a ticket that includes this peer as a bootstrap nodesplit()— Decompose into(RoomEvents, RoomHandle)for use in separate taskspublish(name, &LocalBroadcast)— Publish a broadcast to the roomset_chat_publisher(publisher)— Register a chat publishersend_chat(text)— Send a chat message
RoomHandle
Cloneable handle for publishing into a room. Obtained from Room::split(). Can be shared across tasks.
let (events, handle) = room.split();
// In one task: receive events
while let Some(event) = events.recv().await {
match event { ... }
}
// In another task: publish
handle.publish("camera", &broadcast).await?;
handle.send_chat("Hello!").await?;
handle.set_display_name("Alice").await?;
RoomTicket
pub struct RoomTicket {
pub bootstrap: Vec<EndpointId>, // Bootstrap peer IDs for gossip
pub topic_id: TopicId, // Gossip topic identifier
}
Serialized via iroh_tickets (binary format). Can be created from:
RoomTicket::generate()— Random topic, no bootstrapRoomTicket::new(topic_id, bootstrap)— Specific topic and peersRoomTicket::new_from_env()— FromIROH_LIVE_ROOMorIROH_LIVE_TOPICenv vars
RoomEvent
pub enum RoomEvent {
RemoteAnnounced {
remote: EndpointId,
broadcasts: Vec<String>,
},
BroadcastSubscribed {
session: Box<MoqSession>,
broadcast: Box<RemoteBroadcast>,
},
PeerJoined {
remote: EndpointId,
display_name: Option<String>,
},
PeerLeft {
remote: EndpointId,
},
ChatReceived {
remote: EndpointId,
message: ChatMessage,
},
}
Room Actor — Internal Architecture
The room actor is a spawned task that manages the gossip KV subscription and coordinates all peer connections.
State
struct Actor {
me: EndpointId,
_gossip: Gossip,
live: Live,
active_subscribe: HashSet<BroadcastId>, // (EndpointId, name) pairs
active_publish: HashSet<String>, // Locally published broadcast names
known_peers: HashMap<EndpointId, Option<String>>, // display names
connecting: ConnectingFutures, // In-flight subscribe attempts
subscribe_closed: FuturesUnordered, // Track subscription lifetimes
publish_closed: FuturesUnordered, // Track publish lifetimes
chat_messages: FuturesUnordered, // Active chat subscribers
chat_publisher: Option<ChatPublisher>,
display_name: Option<String>,
event_tx: mpsc::Sender<RoomEvent>,
kv: iroh_smol_kv::Client, // Distributed KV for peer state
kv_writer: WriteScope, // KV write access
}
Gossip KV for Peer Discovery
The room uses iroh-smol-kv over gossip for peer state coordination. Each peer writes their PeerState to key b"s":
struct PeerState {
broadcasts: Vec<String>,
display_name: Option<String>,
}
Serialized with postcard (binary format — no skip_serializing_if allowed since postcard is positional).
Event Loop
loop {
select! {
update = gossip_kv_stream.next() → handle_gossip_update
msg = inbox.recv() → handle_api_message
result = connecting.next() → subscribe succeeded/failed
broadcast_closed → remove from active, maybe emit PeerLeft
publish_closed → remove from active_publish, update KV
chat_message → emit ChatReceived
}
}
Peer Discovery Flow
- Peer A publishes a broadcast via
handle.publish("camera", &broadcast) - Actor publishes to MoQ AND updates gossip KV with
PeerState { broadcasts: ["camera"], display_name: ... } - Peer B's gossip KV stream receives the update
- Peer B's actor checks
known_peers— if new, emitsPeerJoined - Peer B's actor checks
active_subscribe— if new broadcast, initiateslive.subscribe(remote, name) - When subscription succeeds, Peer B emits
BroadcastSubscribed - If the broadcast has a chat track, a chat subscriber is spawned
Chat
Chat uses a dedicated MoQ track within each broadcast. Each message is a single MoQ group containing one frame of UTF-8 text. The sender identity comes from the broadcast context (peer ID), not the message payload.
Connection Lifecycle
- When a broadcast closes (
subscribe_closed), it's removed fromactive_subscribe - If this was the last broadcast from that peer,
PeerLeftis emitted - When a publish closes (
publish_closed), the KV is updated to remove that broadcast
RoomPublisherSync
A convenience wrapper for the common pattern of publishing camera+audio and optionally screen share into a room:
let publisher = RoomPublisherSync::new(room_handle, audio_backend);
publisher.set_state(&PublishOpts::default())?;
Automatically publishes a "camera" broadcast and manages a "screen" broadcast when screen sharing is toggled on.
API Messages
enum ApiMessage {
Publish { name: String, producer: BroadcastProducer },
SendChat { text: String },
SetChatPublisher { publisher: ChatPublisher },
SetDisplayName { name: String },
}
These are sent from RoomHandle to the actor via an mpsc channel.