Files
alknet/docs/research/references/iroh/iroh-live/04-rooms.md

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 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.

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 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

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

  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:

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.