Add Engine.send_file(name, mime, data) and MessageHandler.on_file. Files stream over each open RTC data channel as binary frames — M<json meta> · C<16-byte id><16KB chunk>… · E<id> — and reassemble on the receiver in arrival order (the channel is reliable+ordered). The data channel now distinguishes string (clipboard) from binary (file) messages. Files are direct-only — never relayed, matching prefer-direct. Transport trait gains start(...files) + a default-noop send_file (only RTC implements it). The agent saves received files to ~/Downloads; the apps stub on_file (receive UI is a follow-up). All MessageHandler impls updated. Proven by examples/file_demo.rs: A sends a 50KB blob (4 chunks), B reassembles it to identical bytes over RTC. Follow-ups: file transfer over LAN (same-network without RTC); attach/preview UI; progress + size cap. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
63 lines
2.4 KiB
Rust
63 lines
2.4 KiB
Rust
//! Proves two engines establish a WebRTC data channel and clipboard flows P2P.
|
|
//! cargo run --example rtc_p2p -- http://192.168.11.233:8765
|
|
//!
|
|
//! Path discriminator: an RTC-delivered message has source == the peer's
|
|
//! from-id; the SSE copy has source == the peer's source-label. Since RTC is
|
|
//! direct (no server round-trip) it should win the dedup race, so a received
|
|
//! source equal to the from-id proves delivery came over the data channel.
|
|
|
|
use std::sync::mpsc;
|
|
use std::time::Duration;
|
|
|
|
use tethercore::{Engine, Message, MessageHandler};
|
|
|
|
struct Collector(mpsc::Sender<Message>);
|
|
impl MessageHandler for Collector {
|
|
fn on_message(&self, msg: Message) {
|
|
let _ = self.0.send(msg);
|
|
}
|
|
fn on_status(&self, _connected: bool) {}
|
|
fn on_file(&self, _name: String, _mime: String, _data: Vec<u8>) {}
|
|
}
|
|
|
|
fn main() {
|
|
let server = std::env::args()
|
|
.nth(1)
|
|
.unwrap_or_else(|| "http://192.168.11.233:8765".into());
|
|
let room = "rtc-p2p-spike".to_string();
|
|
|
|
let (tx, rx) = mpsc::channel();
|
|
let a = Engine::new(server.clone(), room.clone(), "peer-a".into(), "label-a".into(), "Peer A".into(), "".into());
|
|
a.start(Box::new(Collector(tx)));
|
|
|
|
// B's from-id and source-label are deliberately different so we can tell
|
|
// which transport delivered.
|
|
let b_from = "peer-b";
|
|
let b = Engine::new(server, room, b_from.into(), "label-b-sse".into(), "Peer B".into(), "".into());
|
|
b.start(Box::new(Collector(mpsc::channel().0))); // B needs to be live to negotiate
|
|
|
|
// Give presence + ICE + DTLS time to bring the data channel up.
|
|
eprintln!("waiting for RTC negotiation…");
|
|
std::thread::sleep(Duration::from_secs(6));
|
|
|
|
let payload = "hello-over-rtc";
|
|
b.send(payload.to_string());
|
|
|
|
match rx.recv_timeout(Duration::from_secs(5)) {
|
|
Ok(m) if m.text == payload => {
|
|
let via = if m.source == b_from { "RTC data channel ✅" } else { "SSE relay (RTC lost the race)" };
|
|
println!("received {:?}\n from={} source={} → delivered via {}", m.text, m.from, m.source, via);
|
|
if m.source != b_from {
|
|
println!(" note: message arrived, but over SSE — check the 'data channel open' log above to confirm RTC linked");
|
|
}
|
|
}
|
|
Ok(m) => println!("⚠️ unexpected: {m:?}"),
|
|
Err(_) => {
|
|
println!("❌ nothing received in time");
|
|
std::process::exit(1);
|
|
}
|
|
}
|
|
a.stop();
|
|
b.stop();
|
|
}
|