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
+93
View File
@@ -0,0 +1,93 @@
use iroh::EndpointId;
use bytes::Bytes;
use thiserror::Error;
use tokio::sync::mpsc::Receiver;
use async_trait::async_trait;
use serde::{Serialize, Deserialize};
use std::str::FromStr;
#[derive(Error, Debug)]
pub enum NetError {
#[error("Failed to initialize network: {0}")]
Init(String),
#[error("Failed to connect/dial peer: {0}")]
Connection(String),
#[error("Gossip swarm error: {0}")]
Gossip(String),
#[error("Serialization / Deserialization error: {0}")]
Serde(String),
#[error("Invalid ticket: {0}")]
InvalidTicket(String),
#[error("Other network error: {0}")]
Other(String),
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct PeerState {
pub name: String,
pub is_muted: bool,
pub addr: iroh::EndpointAddr,
}
#[derive(Debug, Clone)]
pub enum RoomEvent {
PeerJoined(EndpointId, PeerState),
PeerLeft(EndpointId),
PeerUpdated(EndpointId, PeerState),
}
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct PeerSpeakTicket {
pub host_addr: iroh::EndpointAddr,
pub topic_id: [u8; 32],
}
impl ToString for PeerSpeakTicket {
fn to_string(&self) -> String {
let serialized = serde_json::to_vec(self).unwrap();
// Convert to base64 URL-safe string
base64::Engine::encode(&base64::engine::general_purpose::URL_SAFE_NO_PAD, &serialized)
}
}
impl FromStr for PeerSpeakTicket {
type Err = NetError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let decoded = base64::Engine::decode(&base64::engine::general_purpose::URL_SAFE_NO_PAD, s)
.map_err(|e| NetError::InvalidTicket(e.to_string()))?;
let ticket: PeerSpeakTicket = serde_json::from_slice(&decoded)
.map_err(|e| NetError::InvalidTicket(e.to_string()))?;
Ok(ticket)
}
}
#[async_trait]
pub trait NetworkTransport: Send + Sync {
/// Send a low-latency unreliable datagram to a specific peer (for audio).
async fn send_datagram(&self, peer_id: EndpointId, data: Bytes) -> Result<(), NetError>;
/// Subscribes to incoming datagrams from any peer.
async fn receive_datagrams(&self) -> Result<Receiver<(EndpointId, Bytes)>, NetError>;
}
#[async_trait]
pub trait RoomState: Send + Sync {
/// Joins a room using a gossip ticket string and announces our state.
async fn join(&self, ticket: &str, self_state: PeerState) -> Result<(), NetError>;
/// Updates our local state (e.g. when user mutes/unmutes) and broadcasts it.
async fn update_self_state(&self, self_state: PeerState) -> Result<(), NetError>;
/// Leaves the room and announces departure.
async fn leave(&self) -> Result<(), NetError>;
/// Returns a list of currently active peers in the room.
fn active_peers(&self) -> Vec<(EndpointId, PeerState)>;
/// Subscribes to room events (peer joined, peer left, peer updated).
async fn subscribe_events(&self) -> Result<Receiver<RoomEvent>, NetError>;
}
pub mod iroh_impl;
pub mod gossip;