Skip to content

Commit

Permalink
doc(example): add webrtc example
Browse files Browse the repository at this point in the history
  • Loading branch information
Totodore committed Jul 25, 2024
1 parent d796728 commit cc068c8
Show file tree
Hide file tree
Showing 5 changed files with 370 additions and 0 deletions.
17 changes: 17 additions & 0 deletions examples/webrtc-node-app/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
[package]
name = "webrtc-node-app"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
socketioxide = { path = "../../socketioxide" }
axum.workspace = true
tokio = { workspace = true, features = ["rt-multi-thread", "macros"] }
tower-http = { version = "0.5.0", features = ["cors", "fs"] }
tower.workspace = true
tracing-subscriber.workspace = true
tracing.workspace = true
serde.workspace = true
serde_json.workspace = true
9 changes: 9 additions & 0 deletions examples/webrtc-node-app/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# WebRTC Node App

Simple implementation of a webrtc application with socket.io for the signaling part.
The implementation is taken from this article:

https://acidtango.com/thelemoncrunch/how-to-implement-a-video-conference-with-webrtc-and-node/

Here is the repository of the original author:
https://github.com/borjanebbal/webrtc-node-app
177 changes: 177 additions & 0 deletions examples/webrtc-node-app/public/client.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
// DOM elements.
const roomSelectionContainer = document.getElementById(
"room-selection-container",
);
const roomInput = document.getElementById("room-input");
const connectButton = document.getElementById("connect-button");

const videoChatContainer = document.getElementById("video-chat-container");
const localVideoComponent = document.getElementById("local-video");
const remoteVideoComponent = document.getElementById("remote-video");

// Variables.
const socket = io();
const mediaConstraints = {
audio: true,
video: { width: 1280, height: 720 },
};
let localStream;
let remoteStream;
let isRoomCreator;
let rtcPeerConnection; // Connection between the local device and the remote peer.
let roomId;

// Free public STUN servers provided by Google.
const iceServers = {
iceServers: [
{ urls: "stun:stun.l.google.com:19302" },
{ urls: "stun:stun1.l.google.com:19302" },
{ urls: "stun:stun2.l.google.com:19302" },
{ urls: "stun:stun3.l.google.com:19302" },
{ urls: "stun:stun4.l.google.com:19302" },
],
};

// BUTTON LISTENER ============================================================
connectButton.addEventListener("click", () => {
joinRoom(roomInput.value);
});

// SOCKET EVENT CALLBACKS =====================================================
socket.on("room_created", async () => {
console.log("Socket event callback: room_created");

await setLocalStream(mediaConstraints);
isRoomCreator = true;
});

socket.on("room_joined", async () => {
console.log("Socket event callback: room_joined");

await setLocalStream(mediaConstraints);
socket.emit("start_call", roomId);
});

socket.on("full_room", () => {
console.log("Socket event callback: full_room");

alert("The room is full, please try another one");
});

socket.on("start_call", async () => {
console.log("Socket event callback: start_call");

if (isRoomCreator) {
rtcPeerConnection = new RTCPeerConnection(iceServers);
addLocalTracks(rtcPeerConnection);
rtcPeerConnection.ontrack = setRemoteStream;
rtcPeerConnection.onicecandidate = sendIceCandidate;
await createOffer(rtcPeerConnection);
}
});

socket.on("webrtc_offer", async (event) => {
console.log("Socket event callback: webrtc_offer");

if (!isRoomCreator) {
rtcPeerConnection = new RTCPeerConnection(iceServers);
addLocalTracks(rtcPeerConnection);
rtcPeerConnection.ontrack = setRemoteStream;
rtcPeerConnection.onicecandidate = sendIceCandidate;
rtcPeerConnection.setRemoteDescription(new RTCSessionDescription(event));
await createAnswer(rtcPeerConnection);
}
});

socket.on("webrtc_answer", (event) => {
console.log("Socket event callback: webrtc_answer");

rtcPeerConnection.setRemoteDescription(new RTCSessionDescription(event));
});

socket.on("webrtc_ice_candidate", (event) => {
console.log("Socket event callback: webrtc_ice_candidate");

// ICE candidate configuration.
const candidate = new RTCIceCandidate({
sdpMLineIndex: event.label,
candidate: event.candidate,
});
rtcPeerConnection.addIceCandidate(candidate);
});

// FUNCTIONS ==================================================================
function joinRoom(room) {
if (room === "") {
alert("Please type a room ID");
} else {
roomId = room;
socket.emit("join", room);
showVideoConference();
}
}

function showVideoConference() {
roomSelectionContainer.style.display = "none";
videoChatContainer.style.display = "block";
}

async function setLocalStream(mediaConstraints) {
try {
localStream = await navigator.mediaDevices.getUserMedia(mediaConstraints);
localVideoComponent.srcObject = localStream;
} catch (error) {
console.error("Could not get user media", error);
}
}

function addLocalTracks(rtcPeerConnection) {
localStream.getTracks().forEach((track) => {
rtcPeerConnection.addTrack(track, localStream);
});
}

async function createOffer(rtcPeerConnection) {
try {
const sessionDescription = await rtcPeerConnection.createOffer();
rtcPeerConnection.setLocalDescription(sessionDescription);

socket.emit("webrtc_offer", {
type: "webrtc_offer",
sdp: sessionDescription,
roomId,
});
} catch (error) {
console.error(error);
}
}

async function createAnswer(rtcPeerConnection) {
try {
const sessionDescription = await rtcPeerConnection.createAnswer();
rtcPeerConnection.setLocalDescription(sessionDescription);

socket.emit("webrtc_answer", {
type: "webrtc_answer",
sdp: sessionDescription,
roomId,
});
} catch (error) {
console.error(error);
}
}

function setRemoteStream(event) {
remoteVideoComponent.srcObject = event.streams[0];
remoteStream = event.stream;
}

function sendIceCandidate(event) {
if (event.candidate) {
socket.emit("webrtc_ice_candidate", {
roomId,
label: event.candidate.sdpMLineIndex,
candidate: event.candidate.candidate,
});
}
}
72 changes: 72 additions & 0 deletions examples/webrtc-node-app/public/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>WebRTC</title>

<style type="text/css">
body {
margin: 0;
font-size: 20px;
}

.centered {
position: absolute;
top: 40%;
left: 50%;
transform: translate(-50%, -50%);
}

.video-position {
position: absolute;
top: 35%;
left: 50%;
transform: translate(-50%, -50%);
}

#video-chat-container {
width: 100%;
background-color: black;
}

#local-video {
position: absolute;
height: 30%;
width: 30%;
bottom: 0px;
left: 0px;
}

#remote-video {
height: 100%;
width: 100%;
}
</style>
</head>

<body>
<div id="room-selection-container" class="centered">
<h1>WebRTC video conference</h1>
<label>Enter the number of the room you want to connect</label>
<input id="room-input" type="text" />
<button id="connect-button">CONNECT</button>
</div>

<div
id="video-chat-container"
class="video-position"
style="display: none"
>
<video id="local-video" autoplay="autoplay" muted="muted"></video>
<video id="remote-video" autoplay="autoplay"></video>
</div>

<script
src="https://cdn.socket.io/4.6.0/socket.io.min.js"
integrity="sha384-c79GN5VsunZvi+Q/WObgk2in0CbZsHnjEqvFxC5DxHn9lTfNce2WW6h2pH6u/kF+"
crossorigin="anonymous"
></script>
<script type="text/javascript" src="client.js"></script>
</body>
</html>
95 changes: 95 additions & 0 deletions examples/webrtc-node-app/src/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
use socketioxide::{
extract::{Data, SocketRef},
SocketIo,
};
use tower::ServiceBuilder;
use tower_http::{cors::CorsLayer, services::ServeDir};
use tracing::info;
use tracing_subscriber::FmtSubscriber;

#[derive(serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "camelCase")]
struct Event {
room_id: String,
sdp: serde_json::Value,
}

#[derive(serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "camelCase")]
struct IceCandidate {
room_id: String,
#[serde(flatten)]
data: serde_json::Value,
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let subscriber = FmtSubscriber::new();

tracing::subscriber::set_global_default(subscriber)?;

info!("Starting server");

let (layer, io) = SocketIo::new_layer();

io.ns("/", |s: SocketRef| {
s.on("join", |s: SocketRef, Data(room_id): Data<String>| {
let socket_cnt = s.within(room_id.clone()).sockets().unwrap().len();
if socket_cnt == 0 {
tracing::info!("creating room {room_id} and emitting room_created socket event");
s.join(room_id.clone()).unwrap();
s.emit("room_created", room_id).unwrap();
} else if socket_cnt == 1 {
tracing::info!("joining room {room_id} and emitting room_joined socket event");
s.join(room_id.clone()).unwrap();
s.emit("room_joined", room_id).unwrap();
} else {
tracing::info!("Can't join room {room_id}, emitting full_room socket event");
s.emit("full_room", room_id);
}
});

s.on("start_call", |s: SocketRef, Data(room_id): Data<String>| {
tracing::info!("broadcasting start_call event to peers in room {room_id}");
s.to(room_id.clone()).emit("start_call", room_id);
});
s.on("webrtc_offer", |s: SocketRef, Data(event): Data<Event>| {
tracing::info!(
"broadcasting webrtc_offer event to peers in room {}",
event.room_id
);
s.to(event.room_id).emit("webrtc_offer", event.sdp);
});
s.on("webrtc_answer", |s: SocketRef, Data(event): Data<Event>| {
tracing::info!(
"broadcasting webrtc_answer event to peers in room {}",
event.room_id
);
s.to(event.room_id).emit("webrtc_answer", event.sdp);
});
s.on(
"webrtc_ice_candidate",
|s: SocketRef, Data(event): Data<IceCandidate>| {
tracing::info!(
"broadcasting ice_candidate event to peers in room {}",
event.room_id
);
s.to(event.room_id.clone())
.emit("webrtc_ice_candidate", event);
},
);
});

let app = axum::Router::new()
.nest_service("/", ServeDir::new("public"))
.layer(
ServiceBuilder::new()
.layer(CorsLayer::permissive()) // Enable CORS policy
.layer(layer),
);

let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
axum::serve(listener, app).await.unwrap();

Ok(())
}

0 comments on commit cc068c8

Please sign in to comment.