From 98d256d1b92c17f076c03254195783491ed94b05 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Wed, 2 Sep 2026 21:48:19 +0800 Subject: [PATCH] webrtc: add an answerer example for the web client's e2e Reads a `webrtc://` offer from stdin, answers it, bridges ICE candidates as JSON lines over stdin/stdout and echoes every message. rustdesk-web-js drives it from Chrome (js/e2e/webrtc) to check that the browser transport and WebRTCStream still agree on the envelope, the signed fingerprint, the candidate JSON and the fragment framing. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01JXJJGEGdgu26wgCppvUXdZ --- examples/webrtc_echo.rs | 80 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 examples/webrtc_echo.rs diff --git a/examples/webrtc_echo.rs b/examples/webrtc_echo.rs new file mode 100644 index 000000000..ba20cc705 --- /dev/null +++ b/examples/webrtc_echo.rs @@ -0,0 +1,80 @@ +// Temporary e2e helper for the web client: answers a browser offer read from +// stdin, bridges ICE candidates over stdin/stdout as JSON lines, then echoes +// every message it receives. +extern crate hbb_common; + +#[cfg(feature = "webrtc")] +#[hbb_common::tokio::main] +async fn main() -> hbb_common::anyhow::Result<()> { + use hbb_common::{ + anyhow::anyhow, + serde_json::{self, json, Value}, + tokio, + webrtc::WebRTCStream, + }; + use std::io::BufRead; + + let mut offer = String::new(); + std::io::stdin().lock().read_line(&mut offer)?; + if offer.trim().is_empty() { + return Err(anyhow!("no offer")); + } + let mut stream = WebRTCStream::new(offer.trim(), false, 30000).await?; + let out = json!({ + "answer": stream.local_endpoint(), + "session_key": stream.session_key(), + "local_fingerprint": stream.local_dtls_fingerprint().await?, + "remote_fingerprint": stream.remote_dtls_fingerprint().await?, + }); + println!("{}", out); + let mut ice_rx = stream + .take_local_ice_rx() + .ok_or_else(|| anyhow!("no ice rx"))?; + tokio::spawn(async move { + while let Some(c) = ice_rx.recv().await { + println!("{}", json!({ "candidate": c })); + } + }); + let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel::(); + std::thread::spawn(move || { + let stdin = std::io::stdin(); + for line in stdin.lock().lines().flatten() { + if tx.send(line).is_err() { + break; + } + } + }); + let for_ice = stream.clone(); + tokio::spawn(async move { + while let Some(line) = rx.recv().await { + let Ok(v) = serde_json::from_str::(&line) else { + continue; + }; + if let Some(c) = v.get("candidate").and_then(Value::as_str) { + if let Err(e) = for_ice.add_remote_ice_candidate(c).await { + eprintln!("add_remote_ice_candidate failed: {e}"); + } + } + } + }); + stream.wait_connected(30000).await?; + println!( + "{}", + json!({ + "connected": true, + "relayed": stream.is_relayed().await, + "remote_ipv6": stream.is_remote_ipv6().await, + }) + ); + while let Some(res) = stream.next().await { + let bytes = res?; + let len = bytes.len(); + stream.send_bytes(bytes.freeze()).await?; + eprintln!("echoed {len} bytes"); + } + stream.close().await; + Ok(()) +} + +#[cfg(not(feature = "webrtc"))] +fn main() {}