Initialize project and implement decentralized voice chat client (PipeWire, Opus, Iroh, Iced)

This commit is contained in:
2026-05-27 05:18:56 -04:00
commit 1220d94e91
15 changed files with 9177 additions and 0 deletions
+214
View File
@@ -0,0 +1,214 @@
use crate::network::{RoomState, NetError, PeerState, RoomEvent, PeerSpeakTicket};
use iroh::{Endpoint, EndpointId};
use iroh_gossip::net::Gossip;
use iroh_gossip::proto::TopicId;
use tokio::sync::{mpsc, Mutex};
use tokio::sync::mpsc::Receiver;
use std::sync::Arc;
use std::collections::HashMap;
use async_trait::async_trait;
use tokio_stream::StreamExt;
use serde::{Serialize, Deserialize};
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct GossipPayload {
pub author: EndpointId,
pub msg: GossipMessage,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
pub enum GossipMessage {
Announce(PeerState),
Leave,
}
pub struct IrohGossipState {
_endpoint: Endpoint,
gossip: Gossip,
address_lookup: iroh::address_lookup::memory::MemoryLookup,
self_state: Arc<Mutex<Option<PeerState>>>,
peers: Arc<Mutex<HashMap<EndpointId, PeerState>>>,
event_tx: mpsc::Sender<RoomEvent>,
event_rx: Mutex<Option<mpsc::Receiver<RoomEvent>>>,
active_topic: Mutex<Option<tokio::task::JoinHandle<()>>>,
active_topic_id: Mutex<Option<TopicId>>,
active_sender: Mutex<Option<iroh_gossip::api::GossipSender>>,
}
impl IrohGossipState {
pub fn new(
endpoint: Endpoint,
gossip: Gossip,
address_lookup: iroh::address_lookup::memory::MemoryLookup,
) -> Self {
let (event_tx, event_rx) = mpsc::channel(100);
Self {
_endpoint: endpoint,
gossip,
address_lookup,
self_state: Arc::new(Mutex::new(None)),
peers: Arc::new(Mutex::new(HashMap::new())),
event_tx,
event_rx: Mutex::new(Some(event_rx)),
active_topic: Mutex::new(None),
active_topic_id: Mutex::new(None),
active_sender: Mutex::new(None),
}
}
}
#[async_trait]
impl RoomState for IrohGossipState {
async fn join(&self, ticket_str: &str, self_state: PeerState) -> Result<(), NetError> {
let ticket = ticket_str.parse::<PeerSpeakTicket>()?;
let topic_id = TopicId::from_bytes(ticket.topic_id);
// Stop any currently running topic
let _ = self.leave().await;
// Add the host to the address book
self.address_lookup.add_endpoint_info(ticket.host_addr.clone());
// Join the gossip topic. If we are the host, bootstrap list will be empty
// or contain ourselves (which is fine), but let's bootstrap to the ticket host.
let bootstrap_peers = if ticket.host_addr.id == self_state.addr.id {
vec![]
} else {
vec![ticket.host_addr.id]
};
let gossip_topic = self.gossip.subscribe(topic_id, bootstrap_peers).await
.map_err(|e| NetError::Gossip(format!("Failed to join gossip topic: {}", e)))?;
let (gossip_sender, mut gossip_receiver) = gossip_topic.split();
*self.self_state.lock().await = Some(self_state.clone());
*self.active_topic_id.lock().await = Some(topic_id);
*self.active_sender.lock().await = Some(gossip_sender.clone());
let event_tx = self.event_tx.clone();
let peers = self.peers.clone();
let address_lookup = self.address_lookup.clone();
let self_state_clone = self.self_state.clone();
let gossip_sender_clone = gossip_sender.clone();
let handle = tokio::spawn(async move {
// Broadcast initial state
let payload = GossipPayload {
author: self_state_clone.lock().await.as_ref().unwrap().addr.id,
msg: GossipMessage::Announce(self_state_clone.lock().await.clone().unwrap()),
};
if let Ok(bytes) = serde_json::to_vec(&payload) {
let _ = gossip_sender_clone.broadcast(bytes.into()).await;
}
// Stream topic messages
while let Some(res) = gossip_receiver.next().await {
match res {
Ok(iroh_gossip::api::Event::Received(msg)) => {
if let Ok(payload) = serde_json::from_slice::<GossipPayload>(&msg.content) {
match payload.msg {
GossipMessage::Announce(state) => {
if payload.author == self_state_clone.lock().await.as_ref().unwrap().addr.id {
continue; // Ignore our own announcements
}
let mut peer_map = peers.lock().await;
let is_new = !peer_map.contains_key(&payload.author);
let state_changed = peer_map.get(&payload.author) != Some(&state);
if is_new {
address_lookup.add_endpoint_info(state.addr.clone());
peer_map.insert(payload.author, state.clone());
let _ = event_tx.send(RoomEvent::PeerJoined(payload.author, state)).await;
} else if state_changed {
peer_map.insert(payload.author, state.clone());
let _ = event_tx.send(RoomEvent::PeerUpdated(payload.author, state)).await;
}
}
GossipMessage::Leave => {
let mut peer_map = peers.lock().await;
if peer_map.remove(&payload.author).is_some() {
let _ = event_tx.send(RoomEvent::PeerLeft(payload.author)).await;
}
}
}
}
}
Ok(iroh_gossip::api::Event::NeighborUp(_peer_id)) => {
// Resend state on new neighbor connection to guarantee synchronization
if let Some(state) = self_state_clone.lock().await.as_ref() {
let payload = GossipPayload {
author: state.addr.id,
msg: GossipMessage::Announce(state.clone()),
};
if let Ok(bytes) = serde_json::to_vec(&payload) {
let _ = gossip_sender_clone.broadcast(bytes.into()).await;
}
}
}
_ => {}
}
}
});
*self.active_topic.lock().await = Some(handle);
Ok(())
}
async fn update_self_state(&self, self_state: PeerState) -> Result<(), NetError> {
let mut self_guard = self.self_state.lock().await;
*self_guard = Some(self_state.clone());
if let Some(sender) = self.active_sender.lock().await.as_ref() {
let payload = GossipPayload {
author: self_state.addr.id,
msg: GossipMessage::Announce(self_state),
};
if let Ok(bytes) = serde_json::to_vec(&payload) {
sender.broadcast(bytes.into()).await
.map_err(|e| NetError::Gossip(e.to_string()))?;
}
}
Ok(())
}
async fn leave(&self) -> Result<(), NetError> {
let mut handle_guard = self.active_topic.lock().await;
if let Some(handle) = handle_guard.take() {
handle.abort();
}
let mut topic_id_guard = self.active_topic_id.lock().await;
let _ = topic_id_guard.take();
let mut sender_guard = self.active_sender.lock().await;
if let Some(sender) = sender_guard.take() {
if let Some(self_state) = self.self_state.lock().await.as_ref() {
let payload = GossipPayload {
author: self_state.addr.id,
msg: GossipMessage::Leave,
};
if let Ok(bytes) = serde_json::to_vec(&payload) {
let _ = sender.broadcast(bytes.into()).await;
}
}
}
self.peers.lock().await.clear();
Ok(())
}
fn active_peers(&self) -> Vec<(EndpointId, PeerState)> {
let guard = self.peers.blocking_lock();
guard.iter().map(|(k, v)| (*k, v.clone())).collect()
}
async fn subscribe_events(&self) -> Result<Receiver<RoomEvent>, NetError> {
let mut rx_guard = self.event_rx.lock().await;
if let Some(rx) = rx_guard.take() {
Ok(rx)
} else {
Err(NetError::Other("Events already subscribed".to_string()))
}
}
}