Personal playlist on a dedicated music player (browse/play/prev/next/ seek/volume/reorder/remove, .pls/.m3u import), plus per-person shared listening: broadcast your track over presence, peers tune in and stream it point-to-point over the files plane. Playback is timeline-synced (play/pause/skip/seek mirror with no drift) with gapless prefetch of the next track and independent per-source volume per listener. In the 3-column layout the playlist gets its own card stacked under the chat, with a resizable divider and its own scrollbar; other layouts keep it in the Controls panel. Breaking wire change: gossip protocol v5 (presence gains music fields), so 0.6.0 peers cannot share a swarm with 0.5.x. Version bumped 0.5.1 -> 0.6.0; CHANGELOG updated. Untrusted-input handling: broadcast track name sanitized and size cap-checked at gossip ingest, fetched bytes confirmed audio before decode, only the descriptor rides gossip (bytes go point-to-point, one fetch in flight). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
112 lines
3.5 KiB
Rust
112 lines
3.5 KiB
Rust
use peerspeak::network::{
|
|
gossip::IrohGossipState,
|
|
RoomState, PeerState,
|
|
};
|
|
use iroh::{Endpoint, endpoint::presets};
|
|
use iroh_gossip::net::Gossip;
|
|
use tokio::time::{self, Duration};
|
|
|
|
#[tokio::main]
|
|
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|
println!("Starting network loopback test...");
|
|
|
|
// 1. Node A (Host) Setup
|
|
let lookup_a = iroh::address_lookup::memory::MemoryLookup::new();
|
|
let secret_a = iroh::SecretKey::generate();
|
|
let endpoint_a = Endpoint::builder(presets::N0)
|
|
.secret_key(secret_a.clone())
|
|
.address_lookup(lookup_a.clone())
|
|
.bind()
|
|
.await?;
|
|
|
|
endpoint_a.online().await;
|
|
println!("Node A online. ID: {}", endpoint_a.id());
|
|
|
|
let gossip_a = Gossip::builder().spawn(endpoint_a.clone());
|
|
let _router_a = iroh::protocol::Router::builder(endpoint_a.clone())
|
|
.accept(iroh_gossip::net::GOSSIP_ALPN, gossip_a.clone())
|
|
.spawn();
|
|
|
|
let room_a = IrohGossipState::new(endpoint_a.clone(), gossip_a.clone(), lookup_a.clone(), secret_a);
|
|
|
|
// 2. Node B (Client) Setup
|
|
let lookup_b = iroh::address_lookup::memory::MemoryLookup::new();
|
|
let secret_b = iroh::SecretKey::generate();
|
|
let endpoint_b = Endpoint::builder(presets::N0)
|
|
.secret_key(secret_b.clone())
|
|
.address_lookup(lookup_b.clone())
|
|
.bind()
|
|
.await?;
|
|
|
|
endpoint_b.online().await;
|
|
println!("Node B online. ID: {}", endpoint_b.id());
|
|
|
|
let gossip_b = Gossip::builder().spawn(endpoint_b.clone());
|
|
let _router_b = iroh::protocol::Router::builder(endpoint_b.clone())
|
|
.accept(iroh_gossip::net::GOSSIP_ALPN, gossip_b.clone())
|
|
.spawn();
|
|
|
|
let room_b = IrohGossipState::new(endpoint_b.clone(), gossip_b.clone(), lookup_b.clone(), secret_b);
|
|
|
|
// 3. Create room on Node A
|
|
let topic_id = rand::random();
|
|
let ticket = peerspeak::network::PeerSpeakTicket {
|
|
host_addr: endpoint_a.addr(),
|
|
topic_id,
|
|
name: String::new(),
|
|
};
|
|
let ticket_str = ticket.to_string();
|
|
println!("Ticket generated: {}", ticket_str);
|
|
|
|
let state_a = PeerState {
|
|
name: "Alice".to_string(),
|
|
is_muted: false,
|
|
addr: endpoint_a.addr(),
|
|
sharing: None,
|
|
avatar: Default::default(),
|
|
game: None,
|
|
music: None,
|
|
};
|
|
room_a.join(&ticket_str, state_a, vec![]).await?;
|
|
println!("Node A joined topic.");
|
|
|
|
// Subscribe to events on Node A
|
|
let mut rx_a = room_a.subscribe_events().await?;
|
|
tokio::spawn(async move {
|
|
while let Some(event) = rx_a.recv().await {
|
|
println!("Node A Event: {:?}", event);
|
|
}
|
|
});
|
|
|
|
// 4. Join room on Node B
|
|
let state_b = PeerState {
|
|
name: "Bob".to_string(),
|
|
is_muted: false,
|
|
addr: endpoint_b.addr(),
|
|
sharing: None,
|
|
avatar: Default::default(),
|
|
game: None,
|
|
music: None,
|
|
};
|
|
room_b.join(&ticket_str, state_b, vec![]).await?;
|
|
println!("Node B joined topic.");
|
|
|
|
// Subscribe to events on Node B
|
|
let mut rx_b = room_b.subscribe_events().await?;
|
|
tokio::spawn(async move {
|
|
while let Some(event) = rx_b.recv().await {
|
|
println!("Node B Event: {:?}", event);
|
|
}
|
|
});
|
|
|
|
// Wait and check connection
|
|
println!("Waiting 10 seconds for Gossip sync...");
|
|
time::sleep(Duration::from_secs(10)).await;
|
|
|
|
println!("Alice's peers: {:?}", room_a.active_peers());
|
|
println!("Bob's peers: {:?}", room_b.active_peers());
|
|
|
|
println!("Test finished.");
|
|
Ok(())
|
|
}
|