<-- Web Sockets

## web-sys: WebRTC DataChannel

This example connects to an echo server on wss://echo.websocket.org, sends a ping message, and receives the response.
_ wasm-bindgen Guide{target="_blank"}

web-sys: WebRTC DataChannel{target="_blank"}

setup the project

cargo new webrtc_datachannel --lib
cd webrtc_datachannel
mkdir -p www/js www/html
cargo add wasm-bindgen js-sys wasm-bindgen-futures
  • Edit Cargo.toml
[lib]
crate-type = ["cdylib"]

[dependencies]
js-sys = "0.3.66"
wasm-bindgen = "0.2.89"
wasm-bindgen-futures = "0.4.39"

[dependencies.web-sys]
version = "0.3.66"
features = [
  "MessageEvent",
  "RtcPeerConnection",
  "RtcSignalingState",
  "RtcSdpType",
  "RtcSessionDescriptionInit",
  "RtcPeerConnectionIceEvent",
  "RtcIceCandidate",
  "RtcDataChannel",
  "RtcDataChannelEvent",
]

The code

  • index.html
<!doctype html>
<html>
  <head>
    <meta content="text/html;charset=utf-8" http-equiv="Content-Type"/>
    <title>WebRTC DataChannel: nobundle</title>
  </head>
  <body>
    <p>Open DevTools and check the Console.</p>
    <script type="module" src="../js/index.js"></script>
  </body>
</html>
  • index.js
import init from '../pkg/webrtc_datachannel.js';

window.addEventListener('load', async () => {
    await init('../pkg/webrtc_datachannel_bg.wasm');
});

/*
async function run() {
    const wasm = await init().catch(console.error);

}
run();
*/

Note with target web its webrtc_datachannel_bg.wasm we don't have a webrtc_datachannel.wasm file

  • Rust side
#![allow(unused)]
fn main() {
// src/lib.rs
use js_sys::Reflect;
use wasm_bindgen::prelude::*;
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)]
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 shown 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::<dyn FnMut(_)>::new(move |ev: MessageEvent| {
        if let Some(message) = ev.data().as_string() {
            console_warn!("{:?}", message);
            dc1_clone.send_with_str("Pong from pc1.dc!").unwrap();
        }
    });
    dc1.set_onmessage(Some(onmessage_callback.as_ref().unchecked_ref()));
    onmessage_callback.forget();

    /*
     * If negotiation has done, this closure will be called
     *
     */
    let ondatachannel_callback = Closure::<dyn FnMut(_)>::new(move |ev: RtcDataChannelEvent| {
        let dc2 = ev.channel();
        console_log!("pc2.ondatachannel!: {:?}", dc2.label());

        let onmessage_callback = Closure::<dyn FnMut(_)>::new(move |ev: MessageEvent| {
            if let Some(message) = ev.data().as_string() {
                console_warn!("{:?}", message);
            }
        });
        dc2.set_onmessage(Some(onmessage_callback.as_ref().unchecked_ref()));
        onmessage_callback.forget();

        let dc2_clone = dc2.clone();
        let onopen_callback = Closure::<dyn FnMut()>::new(move || {
            dc2_clone.send_with_str("Ping from pc2.dc!").unwrap();
        });
        dc2.set_onopen(Some(onopen_callback.as_ref().unchecked_ref()));
        onopen_callback.forget();
    });
    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::<dyn FnMut(_)>::new(move |ev: RtcPeerConnectionIceEvent| {
            if let Some(candidate) = ev.candidate() {
                console_log!("pc1.onicecandidate: {:#?}", candidate.candidate());
                let _ = pc2_clone.add_ice_candidate_with_opt_rtc_ice_candidate(Some(&candidate));
            }
        });
    pc1.set_onicecandidate(Some(onicecandidate_callback1.as_ref().unchecked_ref()));
    onicecandidate_callback1.forget();

    let pc1_clone = pc1.clone();
    let onicecandidate_callback2 =
        Closure::<dyn FnMut(_)>::new(move |ev: RtcPeerConnectionIceEvent| {
            if let Some(candidate) = ev.candidate() {
                console_log!("pc2.onicecandidate: {:#?}", candidate.candidate());
                let _ = pc1_clone.add_ice_candidate_with_opt_rtc_ice_candidate(Some(&candidate));
            }
        });
    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(())
}

}

build and serve

wasm-pack build --target web --no-typescript --out-dir www/pkg

http www

open index.html

firefox http://localhost:8000/html/

What's next?

Next example: web-sys: A requestAnimationFrame Loop -->