diff --git a/Cargo.toml b/Cargo.toml index f4ad618281f..5f655fcaf75 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -81,6 +81,7 @@ members = [ "examples/wasm2js", "examples/webaudio", "examples/webgl", + "examples/webrtc_datachannel", "examples/websockets", "examples/webxr", "examples/without-a-bundler", diff --git a/examples/webrtc_datachannel/Cargo.toml b/examples/webrtc_datachannel/Cargo.toml new file mode 100644 index 00000000000..603640d19d9 --- /dev/null +++ b/examples/webrtc_datachannel/Cargo.toml @@ -0,0 +1,27 @@ +[package] +name = "webrtc_datachannel" +version = "0.1.0" +authors = ["The wasm-bindgen Developers"] +edition = "2018" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +wasm-bindgen = "0.2.62" +js-sys = "0.3" +wasm-bindgen-futures = "0.4.12" + +[dependencies.web-sys] +version = "0.3.22" +features = [ + "MessageEvent", + "RtcPeerConnection", + "RtcSignalingState", + "RtcSdpType", + "RtcSessionDescriptionInit", + "RtcPeerConnectionIceEvent", + "RtcIceCandidate", + "RtcDataChannel", + "RtcDataChannelEvent", +] diff --git a/examples/webrtc_datachannel/README.md b/examples/webrtc_datachannel/README.md new file mode 100644 index 00000000000..3cdbea1ddd3 --- /dev/null +++ b/examples/webrtc_datachannel/README.md @@ -0,0 +1,15 @@ +# WebRTC DataChannel Example + +[View documentation for this example online][dox] or [View compiled example +online][compiled] + +[compiled]: https://rustwasm.github.io/wasm-bindgen/exbuild/webrtc_datachannel/ +[dox]: https://rustwasm.github.io/wasm-bindgen/examples/webrtc_datachannel.html + +You can build the example locally with: + +``` +$ npm run serve +``` + +and then visiting http://localhost:8080 in a browser should run the example! diff --git a/examples/webrtc_datachannel/index.html b/examples/webrtc_datachannel/index.html new file mode 100644 index 00000000000..78e810af774 --- /dev/null +++ b/examples/webrtc_datachannel/index.html @@ -0,0 +1,10 @@ + + + + + WebRTC DataChannel example + + +

Open DevTools and check the Console.

+ + diff --git a/examples/webrtc_datachannel/index.js b/examples/webrtc_datachannel/index.js new file mode 100644 index 00000000000..270c64ae677 --- /dev/null +++ b/examples/webrtc_datachannel/index.js @@ -0,0 +1,3 @@ +window.addEventListener('load', async () => { + await import('./pkg'); +}); diff --git a/examples/webrtc_datachannel/package.json b/examples/webrtc_datachannel/package.json new file mode 100644 index 00000000000..5f0670df5c9 --- /dev/null +++ b/examples/webrtc_datachannel/package.json @@ -0,0 +1,14 @@ +{ + "scripts": { + "build": "webpack", + "serve": "webpack-dev-server" + }, + "devDependencies": { + "@wasm-tool/wasm-pack-plugin": "1.0.1", + "text-encoding": "^0.7.0", + "html-webpack-plugin": "^3.2.0", + "webpack": "^4.29.4", + "webpack-cli": "^3.1.1", + "webpack-dev-server": "^3.1.0" + } +} diff --git a/examples/webrtc_datachannel/src/lib.rs b/examples/webrtc_datachannel/src/lib.rs new file mode 100644 index 00000000000..a6a1602c410 --- /dev/null +++ b/examples/webrtc_datachannel/src/lib.rs @@ -0,0 +1,166 @@ +use js_sys::Reflect; +use wasm_bindgen::prelude::*; +use wasm_bindgen::JsCast; +use wasm_bindgen_futures::JsFuture; +use web_sys::{ + MessageEvent, RtcDataChannelEvent, RtcPeerConnection, RtcPeerConnectionIceEvent, RtcSdpType, + RtcSessionDescriptionInit, +}; + +macro_rules! console_log { + ($($t:tt)*) => (log(&format_args!($($t)*).to_string())) +} +macro_rules! console_warn { + ($($t:tt)*) => (warn(&format_args!($($t)*).to_string())) +} + +#[wasm_bindgen] +extern "C" { + #[wasm_bindgen(js_namespace = console)] + fn log(s: &str); + #[wasm_bindgen(js_namespace = console)] + fn warn(s: &str); +} + +#[wasm_bindgen(start)] +pub async fn start() -> Result<(), JsValue> { + /* + * Set up PeerConnections + * pc1 <=> pc2 + * + */ + let pc1 = RtcPeerConnection::new()?; + console_log!("pc1 created: state {:?}", pc1.signaling_state()); + let pc2 = RtcPeerConnection::new()?; + console_log!("pc2 created: state {:?}", pc2.signaling_state()); + + /* + * Create DataChannel on pc1 to negotiate + * Message will be shonw here after connection established + * + */ + let dc1 = pc1.create_data_channel("my-data-channel"); + console_log!("dc1 created: label {:?}", dc1.label()); + + let dc1_clone = dc1.clone(); + let onmessage_callback = + Closure::wrap( + Box::new(move |ev: MessageEvent| match ev.data().as_string() { + Some(message) => { + console_warn!("{:?}", message); + dc1_clone.send_with_str("Pong from pc1.dc!").unwrap(); + } + None => {} + }) as Box, + ); + dc1.set_onmessage(Some(onmessage_callback.as_ref().unchecked_ref())); + onmessage_callback.forget(); + + /* + * If negotiaion has done, this closure will be called + * + */ + let ondatachannel_callback = Closure::wrap(Box::new(move |ev: RtcDataChannelEvent| { + let dc2 = ev.channel(); + console_log!("pc2.ondatachannel!: {:?}", dc2.label()); + + let onmessage_callback = + Closure::wrap( + Box::new(move |ev: MessageEvent| match ev.data().as_string() { + Some(message) => console_warn!("{:?}", message), + None => {} + }) as Box, + ); + dc2.set_onmessage(Some(onmessage_callback.as_ref().unchecked_ref())); + onmessage_callback.forget(); + + dc2.send_with_str("Ping from pc2.dc!").unwrap(); + }) as Box); + pc2.set_ondatachannel(Some(ondatachannel_callback.as_ref().unchecked_ref())); + ondatachannel_callback.forget(); + + /* + * Handle ICE candidate each other + * + */ + let pc2_clone = pc2.clone(); + let onicecandidate_callback1 = + Closure::wrap( + Box::new(move |ev: RtcPeerConnectionIceEvent| match ev.candidate() { + Some(candidate) => { + console_log!("pc1.onicecandidate: {:#?}", candidate.candidate()); + let _ = + pc2_clone.add_ice_candidate_with_opt_rtc_ice_candidate(Some(&candidate)); + } + None => {} + }) as Box, + ); + pc1.set_onicecandidate(Some(onicecandidate_callback1.as_ref().unchecked_ref())); + onicecandidate_callback1.forget(); + + let pc1_clone = pc1.clone(); + let onicecandidate_callback2 = + Closure::wrap( + Box::new(move |ev: RtcPeerConnectionIceEvent| match ev.candidate() { + Some(candidate) => { + console_log!("pc2.onicecandidate: {:#?}", candidate.candidate()); + let _ = + pc1_clone.add_ice_candidate_with_opt_rtc_ice_candidate(Some(&candidate)); + } + None => {} + }) as Box, + ); + pc2.set_onicecandidate(Some(onicecandidate_callback2.as_ref().unchecked_ref())); + onicecandidate_callback2.forget(); + + /* + * Send OFFER from pc1 to pc2 + * + */ + let offer = JsFuture::from(pc1.create_offer()).await?; + let offer_sdp = Reflect::get(&offer, &JsValue::from_str("sdp"))? + .as_string() + .unwrap(); + console_log!("pc1: offer {:?}", offer_sdp); + + let mut offer_obj = RtcSessionDescriptionInit::new(RtcSdpType::Offer); + offer_obj.sdp(&offer_sdp); + let sld_promise = pc1.set_local_description(&offer_obj); + JsFuture::from(sld_promise).await?; + console_log!("pc1: state {:?}", pc1.signaling_state()); + + /* + * Receive OFFER from pc1 + * Create and send ANSWER from pc2 to pc1 + * + */ + let mut offer_obj = RtcSessionDescriptionInit::new(RtcSdpType::Offer); + offer_obj.sdp(&offer_sdp); + let srd_promise = pc2.set_remote_description(&offer_obj); + JsFuture::from(srd_promise).await?; + console_log!("pc2: state {:?}", pc2.signaling_state()); + + let answer = JsFuture::from(pc2.create_answer()).await?; + let answer_sdp = Reflect::get(&answer, &JsValue::from_str("sdp"))? + .as_string() + .unwrap(); + console_log!("pc2: answer {:?}", answer_sdp); + + let mut answer_obj = RtcSessionDescriptionInit::new(RtcSdpType::Answer); + answer_obj.sdp(&answer_sdp); + let sld_promise = pc2.set_local_description(&answer_obj); + JsFuture::from(sld_promise).await?; + console_log!("pc2: state {:?}", pc2.signaling_state()); + + /* + * Receive ANSWER from pc2 + * + */ + let mut answer_obj = RtcSessionDescriptionInit::new(RtcSdpType::Answer); + answer_obj.sdp(&answer_sdp); + let srd_promise = pc1.set_remote_description(&answer_obj); + JsFuture::from(srd_promise).await?; + console_log!("pc1: state {:?}", pc1.signaling_state()); + + Ok(()) +} diff --git a/examples/webrtc_datachannel/webpack.config.js b/examples/webrtc_datachannel/webpack.config.js new file mode 100644 index 00000000000..25afd18002a --- /dev/null +++ b/examples/webrtc_datachannel/webpack.config.js @@ -0,0 +1,27 @@ +const path = require('path'); +const HtmlWebpackPlugin = require('html-webpack-plugin'); +const webpack = require('webpack'); +const WasmPackPlugin = require("@wasm-tool/wasm-pack-plugin"); + +module.exports = { + entry: './index.js', + output: { + path: path.resolve(__dirname, 'dist'), + filename: 'index.js', + }, + plugins: [ + new HtmlWebpackPlugin({ + template: 'index.html' + }), + new WasmPackPlugin({ + crateDirectory: path.resolve(__dirname, ".") + }), + // Have this example work in Edge which doesn't ship `TextEncoder` or + // `TextDecoder` at this time. + new webpack.ProvidePlugin({ + TextDecoder: ['text-encoding', 'TextDecoder'], + TextEncoder: ['text-encoding', 'TextEncoder'] + }) + ], + mode: 'development' +}; diff --git a/guide/src/examples/webrtc_datachannel.md b/guide/src/examples/webrtc_datachannel.md new file mode 100644 index 00000000000..efe3f0cc454 --- /dev/null +++ b/guide/src/examples/webrtc_datachannel.md @@ -0,0 +1,25 @@ +# WebRTC DataChannel Example + +[View full source code][code] or [view the compiled example online][online] + +[online]: https://rustwasm.github.io/wasm-bindgen/exbuild/webrtc_datachannel/ +[code]: https://github.com/rustwasm/wasm-bindgen/tree/master/examples/webrtc_datachannel/ + +This example creates 2 peer connections and 2 data channels in single browser tab. +Send ping/pong between `peer1.dc` and `peer2.dc`. + +## `Cargo.toml` + +The `Cargo.toml` enables features necessary to use WebRTC DataChannel and its negotiation. + +```toml +{{#include ../../../examples/webrtc_datachannel/Cargo.toml}} +``` + +## `src/lib.rs` + +The Rust code connects WebRTC data channel. + +```rust +{{#include ../../../examples/webrtc_datachannel/src/lib.rs}} +```