sono.land

WebRTC

WebRTC

In this tutorial we will create a video chat app with video using sono.io and the WebRTC API. We will be initiating peer-to-peer connections, and showing off the capabilities of sono.io by using the SonoClient library, the Sono server instance, and the SonoRTC wrapper. Do note that the SonoRTC is in its beta version and use at your own risk. We will be building off of our work from the previous chatroom tutorial, so make sure to have gone through that one.

The Server

We don't need to add too much to the server-side, we simply need to open a channel, and we'll call it secret. This is what your server file should look like:

src/server.ts
1import { serve } from "https://deno.land/std@0.95.0/http/server.ts";
2import { serveFile } from "https://deno.land/std@0.95.0/http/file_server.ts";
3import { Sono } from "https://deno.land/x/sono@v1.0/mod.ts"
4
5const server = serve({ port: 3000 });
6const sono = new Sono();
7
8sono.channel('secret', ()=> {console.log('secret opened')})
9
10for await (const req of server) {
11 if (req.method === "GET" && req.url === "/") {
12 const path = `${Deno.cwd()}/static/index.html`
13 const content = await serveFile(req, path);
14 req.respond(content)
15 }
16 else if (req.method === "GET" && req.url === "/ws") {
17 sono.connect(req, () => {
18 sono.emit('new client connected')
19 });
20 }
21 else if (req.method === "GET" && req.url === "/favicon.ico") {
22 // Do nothing in case of favicon request
23 }
24 else {
25 const path = `${Deno.cwd()}/static/${req.url}`;
26 const content = await serveFile(req, path);
27 req.respond(content)
28 }
29}

The HTML

Onto the client side. Grab this index.html file, where we've intiatiated a couple containers for the local video element and the remote video elements.

src/static/index.html
1<!DOCTYPE html>
2<html lang="en">
3<head>
4 <meta charset="UTF-8">
5 <meta http-equiv="X-UA-Compatible" content="IE=edge">
6 <meta name="viewport" content="width=device-width, initial-scale=1.0">
7 <title>Document</title>
8</head>
9<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/tailwindcss/dist/tailwind.min.css">
10<style>
11 body { font-family: "Courier New"; }
12 #messages_container { height: 350px; }
13 .message {
14 border-bottom: 1px solid #000000;
15 padding: 5px;
16 }
17 #videocontainer {
18 display: flex;
19 flex-direction: row;
20 align-items: center;
21 justify-content:center;
22
23 }
24 #maincontainer {
25 display: flex;
26 flex-direction: column;
27 align-items: center;
28 justify-content:center;
29 }
30 video {
31 height: '600';
32 width: '600';
33 }
34</style>
35<body>
36 <div id='maincontainer'>
37 <div id='videocontainer'>
38 <div id='localvideocontainer'>
39 <video playsinline autoplay muted="true" id='localVideo'></video>
40 </div>
41 <div id='remotevideocontainer'>
42 </div>
43 </div>
44 <div id='buttoncontainer'>
45 <button class="p-2 text-white bg-black w-full mt-1" id='start'>Start My Video</button>
46 <button class="p-2 text-white bg-black w-full mt-1" id="connectSecret">Connect Secret</button>
47 <button class="p-2 text-white bg-black w-full mt-1" id="connectHome">Connect Home</button>
48 <div id='currentChannel'>Not Connected</div>
49 </div>
50 <div class="mx-auto border border-black mt-10 p-2" id="container" style="max-width: 800px">
51 <div class="font-bold bg-black text-white text-center p-3" id="banner">Sono.io Chat</div>
52 <div class="overflow-x-auto" id="messages_container">
53 <ul id="messageBoard"></ul>
54 </div>
55 <div class="flex">
56 <input class="w-1/2 border border-black p-2" id="username" type="text" placeholder="username" />
57 <input class="w-1/2 border border-black p-2" id="input" type="text" placeholder="message" />
58 </div>
59 <button class="p-2 text-white bg-black w-full mt-1" id="button">send message</button>
60 </div>
61 </div>
62
63</body>
64<script type='module' src="main.js"></script>
65</html>

Above, we can also see that we have two buttons: one for connecting to the 'home' channel, which will always be the default channel you are connected to when you first initiate a WebSocket connection, and the second for connecting to 'secret'.

The JavaScript

Now to set up the actual logic behind peer-to-peer connections. The highlighted lines are the only added lines.

src/static/main.js
1import { SonoClient } from "https://deno.land/x/sono@v1.1/src/sonoClient.js"
2import { webRTC } from "https://deno.land/x/sono@v1.1/src/sonoRTC.js"
3
4const sono = new SonoClient('ws://localhost:8080/ws');
5
6sono.onconnection(()=>{
7 sono.message('client connected')
8})
9
10sono.on('message', (payload) => {
11 const messageBoard = document.getElementById('messageBoard');
12 const newMessage = document.createElement('li')
13 console.log(payload)
14 if(payload.message.username) newMessage.innerHTML = `<strong>${payload.message.username}:</strong> ${payload.message.message}`;
15 else newMessage.innerHTML = payload.message;
16 messageBoard.appendChild(newMessage);
17})
18
19document.getElementById('button').addEventListener('click', () => {
20 const message = document.getElementById('input').value;
21 const username = document.getElementById('username').value;
22 sono.broadcast({message, username});
23 const messageBoard = document.getElementById('messageBoard');
24 const newMessage = document.createElement('li')
25 newMessage.innerHTML = `<strong>${username}:</strong> ${message}`;
26 newMessage.style.cssFloat='right';
27 messageBoard.appendChild(newMessage);
28 messageBoard.appendChild(document.createElement('br'));
29});
30
31const serverConfig = 'stun:stun2.l.google.com:19302';
32const localVideo = document.getElementById('localVideo');
33const constraints = {audio: true, video: true};
34const remotevideocontainer = document.getElementById('remotevideocontainer')
35
36const rtc= new webRTC(serverConfig, sono, localVideo, remotevideocontainer, constraints);
37
38document.getElementById('start').onclick = () => {
39 rtc.startLocalMedia();
40};
41
42document.getElementById('connectSecret').onclick = () => {
43 document.getElementById('currentChannel').innerText = 'Connected to Secret'
44 rtc.changeChannel('secret');
45}
46
47document.getElementById('connectHome').onclick = () => {
48 document.getElementById('currentChannel').innerText = 'Connected to Home'
49 rtc.changeChannel('home');
50}

Notice that here we are using a free STUN server hosted by Google on line 31. Read more about STUN servers here. Now, when the server is ran and you go to your host, you should see a live video and audio chat application capable of multi-to-multi connections.

Edit this page on GitHub