mirror of
https://github.com/fluencelabs/rust-libp2p
synced 2025-06-19 04:51:22 +00:00
feat(webrtc): add WebRTC for WASM environments
This PR implements `Transport` for WebRTC for browsers by using web-sys. Only the `webrtc-direct` spec is implemented. The `webrtc` spec for connecting two browsers with each other is left to a future PR. Related: https://github.com/libp2p/specs/issues/475. Related #2617. Supersedes: #4229. Pull-Request: #4248. Co-authored-by: Thomas Eizinger <thomas@eizinger.io>
This commit is contained in:
104
examples/browser-webrtc/src/lib.rs
Normal file
104
examples/browser-webrtc/src/lib.rs
Normal file
@ -0,0 +1,104 @@
|
||||
#![cfg(target_arch = "wasm32")]
|
||||
|
||||
use futures::StreamExt;
|
||||
use js_sys::Date;
|
||||
use libp2p::core::Multiaddr;
|
||||
use libp2p::identity::{Keypair, PeerId};
|
||||
use libp2p::ping;
|
||||
use libp2p::swarm::{keep_alive, NetworkBehaviour, SwarmBuilder, SwarmEvent};
|
||||
use std::convert::From;
|
||||
use std::io;
|
||||
use wasm_bindgen::prelude::*;
|
||||
use web_sys::{Document, HtmlElement};
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub async fn run(libp2p_endpoint: String) -> Result<(), JsError> {
|
||||
wasm_logger::init(wasm_logger::Config::default());
|
||||
|
||||
let body = Body::from_current_window()?;
|
||||
body.append_p("Let's ping the WebRTC Server!")?;
|
||||
|
||||
let local_key = Keypair::generate_ed25519();
|
||||
let local_peer_id = PeerId::from(local_key.public());
|
||||
let mut swarm = SwarmBuilder::with_wasm_executor(
|
||||
libp2p_webrtc_websys::Transport::new(libp2p_webrtc_websys::Config::new(&local_key)).boxed(),
|
||||
Behaviour {
|
||||
ping: ping::Behaviour::new(ping::Config::new()),
|
||||
keep_alive: keep_alive::Behaviour,
|
||||
},
|
||||
local_peer_id,
|
||||
)
|
||||
.build();
|
||||
|
||||
log::info!("Initialize swarm with identity: {local_peer_id}");
|
||||
|
||||
let addr = libp2p_endpoint.parse::<Multiaddr>()?;
|
||||
log::info!("Dialing {addr}");
|
||||
swarm.dial(addr)?;
|
||||
|
||||
loop {
|
||||
match swarm.next().await.unwrap() {
|
||||
SwarmEvent::Behaviour(BehaviourEvent::Ping(ping::Event { result: Err(e), .. })) => {
|
||||
log::error!("Ping failed: {:?}", e);
|
||||
|
||||
break;
|
||||
}
|
||||
SwarmEvent::Behaviour(BehaviourEvent::Ping(ping::Event {
|
||||
peer,
|
||||
result: Ok(rtt),
|
||||
..
|
||||
})) => {
|
||||
log::info!("Ping successful: RTT: {rtt:?}, from {peer}");
|
||||
body.append_p(&format!("RTT: {rtt:?} at {}", Date::new_0().to_string()))?;
|
||||
}
|
||||
evt => log::info!("Swarm event: {:?}", evt),
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(NetworkBehaviour)]
|
||||
struct Behaviour {
|
||||
ping: ping::Behaviour,
|
||||
keep_alive: keep_alive::Behaviour,
|
||||
}
|
||||
|
||||
/// Convenience wrapper around the current document body
|
||||
struct Body {
|
||||
body: HtmlElement,
|
||||
document: Document,
|
||||
}
|
||||
|
||||
impl Body {
|
||||
fn from_current_window() -> Result<Self, JsError> {
|
||||
// Use `web_sys`'s global `window` function to get a handle on the global
|
||||
// window object.
|
||||
let document = web_sys::window()
|
||||
.ok_or(js_error("no global `window` exists"))?
|
||||
.document()
|
||||
.ok_or(js_error("should have a document on window"))?;
|
||||
let body = document
|
||||
.body()
|
||||
.ok_or(js_error("document should have a body"))?;
|
||||
|
||||
Ok(Self { body, document })
|
||||
}
|
||||
|
||||
fn append_p(&self, msg: &str) -> Result<(), JsError> {
|
||||
let val = self
|
||||
.document
|
||||
.create_element("p")
|
||||
.map_err(|_| js_error("failed to create <p>"))?;
|
||||
val.set_text_content(Some(msg));
|
||||
self.body
|
||||
.append_child(&val)
|
||||
.map_err(|_| js_error("failed to append <p>"))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn js_error(msg: &str) -> JsError {
|
||||
io::Error::new(io::ErrorKind::Other, msg).into()
|
||||
}
|
157
examples/browser-webrtc/src/main.rs
Normal file
157
examples/browser-webrtc/src/main.rs
Normal file
@ -0,0 +1,157 @@
|
||||
#![allow(non_upper_case_globals)]
|
||||
|
||||
use anyhow::Result;
|
||||
use axum::extract::{Path, State};
|
||||
use axum::http::header::CONTENT_TYPE;
|
||||
use axum::http::StatusCode;
|
||||
use axum::response::{Html, IntoResponse};
|
||||
use axum::{http::Method, routing::get, Router};
|
||||
use futures::StreamExt;
|
||||
use libp2p::{
|
||||
core::muxing::StreamMuxerBox,
|
||||
core::Transport,
|
||||
identity,
|
||||
multiaddr::{Multiaddr, Protocol},
|
||||
ping,
|
||||
swarm::{keep_alive, NetworkBehaviour, SwarmBuilder, SwarmEvent},
|
||||
};
|
||||
use libp2p_webrtc as webrtc;
|
||||
use rand::thread_rng;
|
||||
use std::net::{Ipv4Addr, SocketAddr};
|
||||
use tower_http::cors::{Any, CorsLayer};
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> anyhow::Result<()> {
|
||||
env_logger::builder()
|
||||
.parse_filters("browser_webrtc_example=debug,libp2p_webrtc=info,libp2p_ping=debug")
|
||||
.parse_default_env()
|
||||
.init();
|
||||
|
||||
let id_keys = identity::Keypair::generate_ed25519();
|
||||
let local_peer_id = id_keys.public().to_peer_id();
|
||||
let transport = webrtc::tokio::Transport::new(
|
||||
id_keys,
|
||||
webrtc::tokio::Certificate::generate(&mut thread_rng())?,
|
||||
)
|
||||
.map(|(peer_id, conn), _| (peer_id, StreamMuxerBox::new(conn)))
|
||||
.boxed();
|
||||
|
||||
let behaviour = Behaviour {
|
||||
ping: ping::Behaviour::new(ping::Config::new()),
|
||||
keep_alive: keep_alive::Behaviour,
|
||||
};
|
||||
|
||||
let mut swarm = SwarmBuilder::with_tokio_executor(transport, behaviour, local_peer_id).build();
|
||||
|
||||
let address_webrtc = Multiaddr::from(Ipv4Addr::UNSPECIFIED)
|
||||
.with(Protocol::Udp(0))
|
||||
.with(Protocol::WebRTCDirect);
|
||||
|
||||
swarm.listen_on(address_webrtc.clone())?;
|
||||
|
||||
let address = loop {
|
||||
if let SwarmEvent::NewListenAddr { address, .. } = swarm.select_next_some().await {
|
||||
if address
|
||||
.iter()
|
||||
.any(|e| e == Protocol::Ip4(Ipv4Addr::LOCALHOST))
|
||||
{
|
||||
log::debug!("Ignoring localhost address to make sure the example works in Firefox");
|
||||
continue;
|
||||
}
|
||||
|
||||
log::info!("Listening on: {address}");
|
||||
|
||||
break address;
|
||||
}
|
||||
};
|
||||
|
||||
let addr = address.with(Protocol::P2p(*swarm.local_peer_id()));
|
||||
|
||||
// Serve .wasm, .js and server multiaddress over HTTP on this address.
|
||||
tokio::spawn(serve(addr));
|
||||
|
||||
loop {
|
||||
tokio::select! {
|
||||
swarm_event = swarm.next() => {
|
||||
log::trace!("Swarm Event: {:?}", swarm_event)
|
||||
},
|
||||
_ = tokio::signal::ctrl_c() => {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(NetworkBehaviour)]
|
||||
struct Behaviour {
|
||||
ping: ping::Behaviour,
|
||||
keep_alive: keep_alive::Behaviour,
|
||||
}
|
||||
|
||||
#[derive(rust_embed::RustEmbed)]
|
||||
#[folder = "$CARGO_MANIFEST_DIR/static"]
|
||||
struct StaticFiles;
|
||||
|
||||
/// Serve the Multiaddr we are listening on and the host files.
|
||||
pub(crate) async fn serve(libp2p_transport: Multiaddr) {
|
||||
let listen_addr = match libp2p_transport.iter().next() {
|
||||
Some(Protocol::Ip4(addr)) => addr,
|
||||
_ => panic!("Expected 1st protocol to be IP4"),
|
||||
};
|
||||
|
||||
let server = Router::new()
|
||||
.route("/", get(get_index))
|
||||
.route("/index.html", get(get_index))
|
||||
.route("/:path", get(get_static_file))
|
||||
.with_state(Libp2pEndpoint(libp2p_transport))
|
||||
.layer(
|
||||
// allow cors
|
||||
CorsLayer::new()
|
||||
.allow_origin(Any)
|
||||
.allow_methods([Method::GET]),
|
||||
);
|
||||
|
||||
let addr = SocketAddr::new(listen_addr.into(), 8080);
|
||||
|
||||
log::info!("Serving client files at http://{addr}");
|
||||
|
||||
axum::Server::bind(&addr)
|
||||
.serve(server.into_make_service())
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct Libp2pEndpoint(Multiaddr);
|
||||
|
||||
/// Serves the index.html file for our client.
|
||||
///
|
||||
/// Our server listens on a random UDP port for the WebRTC transport.
|
||||
/// To allow the client to connect, we replace the `__LIBP2P_ENDPOINT__` placeholder with the actual address.
|
||||
async fn get_index(
|
||||
State(Libp2pEndpoint(libp2p_endpoint)): State<Libp2pEndpoint>,
|
||||
) -> Result<Html<String>, StatusCode> {
|
||||
let content = StaticFiles::get("index.html")
|
||||
.ok_or(StatusCode::NOT_FOUND)?
|
||||
.data;
|
||||
|
||||
let html = std::str::from_utf8(&content)
|
||||
.expect("index.html to be valid utf8")
|
||||
.replace("__LIBP2P_ENDPOINT__", &libp2p_endpoint.to_string());
|
||||
|
||||
Ok(Html(html))
|
||||
}
|
||||
|
||||
/// Serves the static files generated by `wasm-pack`.
|
||||
async fn get_static_file(Path(path): Path<String>) -> Result<impl IntoResponse, StatusCode> {
|
||||
log::debug!("Serving static file: {path}");
|
||||
|
||||
let content = StaticFiles::get(&path).ok_or(StatusCode::NOT_FOUND)?.data;
|
||||
let content_type = mime_guess::from_path(path)
|
||||
.first_or_octet_stream()
|
||||
.to_string();
|
||||
|
||||
Ok(([(CONTENT_TYPE, content_type)], content))
|
||||
}
|
Reference in New Issue
Block a user