Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -145,3 +145,13 @@ bench = false
name = "swap-tracks"
path = "examples/swap-tracks/swap-tracks.rs"
bench = false

[[example]]
name = "mdns-local-peers"
path = "examples/mdns-local-peers/mdns-local-peers.rs"
bench = false

[[example]]
name = "ice-tcp"
path = "examples/ice-tcp/ice-tcp.rs"
bench = false
223 changes: 223 additions & 0 deletions examples/ice-tcp/ice-tcp.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,223 @@
//! ICE over TCP (RFC 6544) example
//!
//! Demonstrates two in-process WebRTC peers that pass TCP addresses via
//! `PeerConnectionBuilder::with_tcp_addrs()`.
//!
//! **Current limitation:** The async driver layer does not yet bind or drive TCP
//! sockets (the `_tcp_addrs` parameter in `PeerConnectionImpl::new` is a
//! placeholder). Until TCP transport is wired through the driver, both peers
//! also bind a UDP socket so that ICE connectivity checks can succeed.
//!
//! Once the driver gains TCP support, the UDP fallback can be removed and this
//! example will demonstrate pure TCP ICE (RFC 6544).
//!
//! ## How to run
//!
//! ```sh
//! cargo run --example ice-tcp
//! ```

use std::sync::Arc;
use std::time::Duration;

use webrtc::data_channel::{DataChannel, DataChannelEvent};
use webrtc::peer_connection::{
PeerConnection, PeerConnectionBuilder, PeerConnectionEventHandler, RTCIceGatheringState,
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PeerConnection is imported here but not referenced anywhere in the example (only PeerConnectionBuilder/traits are used). Removing it avoids an unused-import warning when compiling the example.

Suggested change
PeerConnection, PeerConnectionBuilder, PeerConnectionEventHandler, RTCIceGatheringState,
PeerConnectionBuilder, PeerConnectionEventHandler, RTCIceGatheringState,

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PeerConnection is a trait that must be in scope for method dispatch — removing it causes compilation errors. The import is required.

RTCPeerConnectionState,
};
use webrtc::runtime::{Runtime, Sender, block_on, channel, default_runtime, sleep, timeout};

const TEST_MESSAGE: &str = "Hello over TCP ICE!";

// ── Offerer handler ────────────────────────────────────────────────────────────

struct OffererHandler {
gather_tx: Sender<()>,
connected_tx: Sender<()>,
}

#[async_trait::async_trait]
impl PeerConnectionEventHandler for OffererHandler {
async fn on_ice_gathering_state_change(&self, state: RTCIceGatheringState) {
if state == RTCIceGatheringState::Complete {
let _ = self.gather_tx.try_send(());
}
}
async fn on_connection_state_change(&self, state: RTCPeerConnectionState) {
eprintln!("Offerer connection state: {}", state);
if state == RTCPeerConnectionState::Connected {
let _ = self.connected_tx.try_send(());
}
}
}

// ── Answerer handler ───────────────────────────────────────────────────────────

struct AnswererHandler {
gather_tx: Sender<()>,
connected_tx: Sender<()>,
msg_tx: Sender<String>,
runtime: Arc<dyn Runtime>,
}

#[async_trait::async_trait]
impl PeerConnectionEventHandler for AnswererHandler {
async fn on_ice_gathering_state_change(&self, state: RTCIceGatheringState) {
if state == RTCIceGatheringState::Complete {
let _ = self.gather_tx.try_send(());
}
}
async fn on_connection_state_change(&self, state: RTCPeerConnectionState) {
eprintln!("Answerer connection state: {}", state);
if state == RTCPeerConnectionState::Connected {
let _ = self.connected_tx.try_send(());
}
}
async fn on_data_channel(&self, dc: Arc<dyn DataChannel>) {
let label = dc.label().await.unwrap_or_default();
eprintln!("Answerer: received data channel '{}'", label);
let msg_tx = self.msg_tx.clone();
self.runtime.spawn(Box::pin(async move {
while let Some(event) = dc.poll().await {
match event {
DataChannelEvent::OnOpen => eprintln!("Answerer: data channel opened"),
DataChannelEvent::OnMessage(msg) => {
let text = String::from_utf8(msg.data.to_vec()).unwrap_or_default();
eprintln!("Answerer received: '{}'", text);
msg_tx.try_send(text).ok();
}
DataChannelEvent::OnClose => break,
_ => {}
}
}
}));
}
}

// ── Main ───────────────────────────────────────────────────────────────────────

fn main() {
block_on(run()).unwrap();
}

async fn run() -> anyhow::Result<()> {
env_logger::builder()
.filter_level(log::LevelFilter::Info)
.init();

let runtime =
default_runtime().ok_or_else(|| std::io::Error::other("no async runtime found"))?;

let (offerer_gather_tx, mut offerer_gather_rx) = channel::<()>(1);
let (offerer_connected_tx, mut offerer_connected_rx) = channel::<()>(1);
let (offerer_dc_open_tx, mut offerer_dc_open_rx) = channel::<()>(1);
let (answerer_gather_tx, mut answerer_gather_rx) = channel::<()>(1);
let (answerer_connected_tx, mut answerer_connected_rx) = channel::<()>(1);
let (answerer_msg_tx, mut answerer_msg_rx) = channel::<String>(8);

// ── Answerer ────────────────────────────────────────────────────────────────
// Pass TCP addresses via with_tcp_addrs() — these will be used once the
// driver gains TCP transport support. A UDP socket is also bound as a
// fallback so the example can run today.
let answerer_pc = PeerConnectionBuilder::new()
.with_handler(Arc::new(AnswererHandler {
gather_tx: answerer_gather_tx,
connected_tx: answerer_connected_tx,
msg_tx: answerer_msg_tx,
runtime: runtime.clone(),
}))
.with_runtime(runtime.clone())
.with_udp_addrs(vec!["127.0.0.1:0".to_string()])
.with_tcp_addrs(vec!["127.0.0.1:0".to_string()])
.build()
.await?;
Comment on lines +129 to +133
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

with_tcp_addrs(...) currently won't make this example work because the async PeerConnectionBuilder/driver only binds and drives UDP sockets. In src/peer_connection/mod.rs, the tcp_addrs passed to PeerConnectionImpl::new are ignored (_tcp_addrs), and PeerConnectionDriver::new errors if no UDP sockets are provided ("no sockets available"). As written, running the example should fail at .build().await? rather than establishing ICE-TCP connectivity. Either implement TCP socket support in the driver (and use tcp_addrs) or adjust the example/docs to match what the library supports today.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed: tcp_addrs limitation is now documented with a log::warn at runtime and in the example's header comments. UDP fallback sockets added so the example runs. Should be marked outdated.

eprintln!("Answerer: peer connection created (UDP + TCP addrs configured)");

// ── Offerer ────────────────────────────────────────────────────────────────
// Same as the answerer: TCP addrs are passed but only UDP is active today.
let offerer_pc = PeerConnectionBuilder::new()
.with_handler(Arc::new(OffererHandler {
gather_tx: offerer_gather_tx,
connected_tx: offerer_connected_tx,
}))
.with_runtime(runtime.clone())
.with_udp_addrs(vec!["127.0.0.1:0".to_string()])
.with_tcp_addrs(vec!["127.0.0.1:0".to_string()])
.build()
.await?;
Comment on lines +122 to +147
Copy link

Copilot AI Apr 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The PR description says this example “Demonstrates how to configure RTCSettingEngine::set_ice_tcp_mux()”, but the example doesn’t configure a setting engine / ICE-TCP mux at all. If this configuration is required (or recommended) to ensure RFC 6544 framing/behavior, the example should explicitly set it and pass it into the PeerConnectionBuilder so the example aligns with the stated purpose.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed: PR description updated to remove reference to set_ice_tcp_mux(). The example documents the current TCP limitation. Should be marked outdated.


let offerer_dc = offerer_pc.create_data_channel("chat", None).await?;
eprintln!("Offerer: created data channel");

{
let dc = offerer_dc.clone();
let open_tx = offerer_dc_open_tx;
runtime.spawn(Box::pin(async move {
while let Some(event) = dc.poll().await {
if let DataChannelEvent::OnOpen = event {
eprintln!("Offerer: data channel opened");
open_tx.try_send(()).ok();
}
}
}));
}

// ── Signaling (in-process) ─────────────────────────────────────────────────
let offer = offerer_pc.create_offer(None).await?;
offerer_pc.set_local_description(offer).await?;
timeout(Duration::from_secs(5), offerer_gather_rx.recv())
.await
.map_err(|_| anyhow::anyhow!("Timeout waiting for offerer ICE gathering"))?
.ok_or_else(|| anyhow::anyhow!("Offerer ICE gathering channel closed"))?;
let offer_sdp = offerer_pc.local_description().await.expect("offerer SDP");
eprintln!("Offerer: ICE gathering complete");

answerer_pc.set_remote_description(offer_sdp).await?;
let answer = answerer_pc.create_answer(None).await?;
answerer_pc.set_local_description(answer).await?;
timeout(Duration::from_secs(5), answerer_gather_rx.recv())
.await
.map_err(|_| anyhow::anyhow!("Timeout waiting for answerer ICE gathering"))?
.ok_or_else(|| anyhow::anyhow!("Answerer ICE gathering channel closed"))?;
let answer_sdp = answerer_pc.local_description().await.expect("answerer SDP");
Comment on lines +175 to +182
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same issue as offerer side: timeout(..., answerer_gather_rx.recv()) returns Ok(Option<()>), and Ok(None) (channel closed) is currently treated as a successful completion because only the timeout error is handled. Consider erroring on Ok(None) to avoid continuing with a potentially incomplete SDP.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed: ice-tcp uses .ok_or_else() to error on None for all timeout calls. Should be marked outdated.

eprintln!("Answerer: ICE gathering complete");

offerer_pc.set_remote_description(answer_sdp).await?;

// ── Wait for connection ────────────────────────────────────────────────────
timeout(Duration::from_secs(15), offerer_connected_rx.recv())
.await
.map_err(|_| anyhow::anyhow!("Timeout waiting for offerer to connect"))?
.ok_or_else(|| anyhow::anyhow!("Offerer connection channel closed"))?;
eprintln!("Offerer: connected!");

timeout(Duration::from_secs(5), answerer_connected_rx.recv())
.await
.map_err(|_| anyhow::anyhow!("Timeout waiting for answerer to connect"))?
.ok_or_else(|| anyhow::anyhow!("Answerer connection channel closed"))?;
eprintln!("Answerer: connected!");

// ── Send message ───────────────────────────────────────────────────────────
timeout(Duration::from_secs(10), offerer_dc_open_rx.recv())
.await
.map_err(|_| anyhow::anyhow!("Timeout waiting for data channel to open"))?
.ok_or_else(|| anyhow::anyhow!("Data channel open notification channel closed"))?;

eprintln!("Offerer: sending '{}'", TEST_MESSAGE);
offerer_dc.send_text(TEST_MESSAGE).await?;

let received = timeout(Duration::from_secs(10), answerer_msg_rx.recv())
.await
.map_err(|_| anyhow::anyhow!("Timeout waiting for message"))?
.ok_or_else(|| anyhow::anyhow!("Message channel closed"))?;

assert_eq!(received, TEST_MESSAGE);
eprintln!("✅ Message received: '{}'", received);

sleep(Duration::from_millis(100)).await;
offerer_pc.close().await?;
answerer_pc.close().await?;

eprintln!("✅ ice-tcp example completed");
Ok(())
}
Loading