docs(research): add iroh suite deep-dive references for iroh, irpc, iroh-blobs, iroh-gossip, iroh-live, and iroh-docs
This commit is contained in:
185
docs/research/references/iroh/iroh-live/04-rooms.md
Normal file
185
docs/research/references/iroh/iroh-live/04-rooms.md
Normal file
@@ -0,0 +1,185 @@
|
||||
# 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.
|
||||
|
||||
```rust
|
||||
// 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 next `RoomEvent`
|
||||
- `try_recv()` — Non-blocking event check
|
||||
- `ticket()` — Get a ticket that includes this peer as a bootstrap node
|
||||
- `split()` — Decompose into `(RoomEvents, RoomHandle)` for use in separate tasks
|
||||
- `publish(name, &LocalBroadcast)` — Publish a broadcast to the room
|
||||
- `set_chat_publisher(publisher)` — Register a chat publisher
|
||||
- `send_chat(text)` — Send a chat message
|
||||
|
||||
### `RoomHandle`
|
||||
|
||||
Cloneable handle for publishing into a room. Obtained from `Room::split()`. Can be shared across tasks.
|
||||
|
||||
```rust
|
||||
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`
|
||||
|
||||
```rust
|
||||
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 bootstrap
|
||||
- `RoomTicket::new(topic_id, bootstrap)` — Specific topic and peers
|
||||
- `RoomTicket::new_from_env()` — From `IROH_LIVE_ROOM` or `IROH_LIVE_TOPIC` env vars
|
||||
|
||||
### `RoomEvent`
|
||||
|
||||
```rust
|
||||
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
|
||||
|
||||
```rust
|
||||
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"`:
|
||||
|
||||
```rust
|
||||
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
|
||||
|
||||
1. Peer A publishes a broadcast via `handle.publish("camera", &broadcast)`
|
||||
2. Actor publishes to MoQ AND updates gossip KV with `PeerState { broadcasts: ["camera"], display_name: ... }`
|
||||
3. Peer B's gossip KV stream receives the update
|
||||
4. Peer B's actor checks `known_peers` — if new, emits `PeerJoined`
|
||||
5. Peer B's actor checks `active_subscribe` — if new broadcast, initiates `live.subscribe(remote, name)`
|
||||
6. When subscription succeeds, Peer B emits `BroadcastSubscribed`
|
||||
7. 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 from `active_subscribe`
|
||||
- If this was the last broadcast from that peer, `PeerLeft` is 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:
|
||||
|
||||
```rust
|
||||
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
|
||||
|
||||
```rust
|
||||
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.
|
||||
Reference in New Issue
Block a user