Home
/ Blog /
Building a Clubhouse Clone with Svelte and 100msMarch 16, 202214 min read
Share
In this article, we will go through the process of building a Clubhouse clone with 100ms and Svelte. Clubhouse is a popular app that enables people to speak together in audio rooms over the internet.
Svelte is a new framework that institutes a whole new way for frameworks to interact with the DOM. It doesn't use a VDOM but surgically updates the DOM based on what you want to display.
We also have step-by-step guides to build a Clubhouse like app with different technologies
It’s also lightweight and, thus, faster because it doesn't ship svelte to the front end. Instead, it ships the code that performs the update.
100ms provides video conferencing infrastructure designed to help businesses build powerful video applications in hours. Its SDK is tailored to suit numerous use cases like game streaming, virtual events, audio rooms, classrooms, and much more. It abstracts the complexities involved in building these apps and reduces development time drastically.
To top it all off, 100ms has been created and is managed by the team who created live infrastructure at Disney and Facebook (Meta).
There are a couple of terms used by 100ms that we need to get familiar with to understand this article:
You can read about the other terms here
Click on any of the Join as buttons to test out the platform.
To save time, head to the Developer tab of the dashboard, copy the token endpoint and store it in a safe place. Additionally, head to the Rooms tab, and store the Room ID of the room we just created.
To get started, clone this starter pack. It contains the main setup needed for the app, like SCSS and page routing as well as its components. After cloning, run
yarn
to install all dependencies of the starter pack.
Run
yarn dev
to start the project. You should see the following:
Under src/services/hms.js
, we have set up the basic 100ms functions. These functions enable us to connect our components to 100ms.
Head into the App.svelte
file in src
and replace its content with:
<script>
import router from "page";
import Home from "./pages/home.svelte";
import Room from "./pages/room.svelte";
//NEW LINE HERE
import { onMount } from "svelte";
import { hmsStore } from "./services/hms";
import { selectIsConnectedToRoom } from "@100mslive/hms-video-store";
//NEW LINE ENDS
let page;
router("/", () => (page = Home));
router("/room", () => (page = Room));
router.start();
//NEW LINE HERE
const onRoomStateChange = (connected) => {
if (connected) router.redirect("/room");
else router.redirect("/");
};
onMount(async () => {
hmsStore.subscribe(onRoomStateChange, selectIsConnectedToRoom);
});
//NEW LINE ENDS
</script>
<svelte:component this={page} />
Going from the top, 3 new variables are imported:
componentDidMount
in React). You mainly use it to subscribe to listeners or make requests to API endpoints.boolean
value that tells you if you're connected to a room or not.You can read about other selectors here.
In the onMount function, we set a listener that calls onRoomStateChange whenever the connection state changes. The onRoomStateChange reacts to this by redirecting you to the appropriate page based on its input.
Head to the home.svelte
file and replace its contents with:
<script>
import { hmsActions } from "./../services/hms";
import { getToken } from "./../utils/utils";
let userName = "";
let role = "";
const submitForm = async () => {
if (!userName || !role) return;
try {
const authToken = await getToken(role, userName);
const config = {
userName,
authToken,
settings: {
isAudioMuted: true,
isVideoMuted: false,
},
rememberDeviceSelection: true,
};
hmsActions.join(config);
} catch (error) {
console.log("Token API Error", error);
}
};
</script>
<main>
<form>
<header>Join Room</header>
<label for="username">
Username
<input
bind:value={userName}
id="username"
type="text"
placeholder="Username"
/>
</label>
<label>
Role
<select bind:value={role} name="role">
<option value="speaker">Speaker</option>
<option value="listener">Listener</option>
<option value="moderator">Moderator</option>
</select>
</label>
<button on:click|preventDefault={submitForm}> Join </button>
</form>
</main>
Here we import:
We also have a function, submitForm
, that couples the config
variable and adds us to the room using hmsAction
.
In the markup, you'll notice we have bind
: in the input. This is called a directive and Svelte gives us numerous directives to make our lives easier.
The bind:value
directive links the value of the input to the specified variable.
In your case, this variable is the username
variable. You also use it in the select
element. The on:click
directive, on the other hand, attaches the specified function as the handler to the click event on that button.
Svelte also gives us modifiers like |preventDefault
that customize the directive to our taste. In our case, |preventDefault
calls the event.preventDefault
function before running the handler.
You'll also notice that we haven't implemented the getToken
function, so let's get to it. Create a utils.js
file in the directory src/utils
and paste the following:
const TOKEN_ENDPOINT = process.env.TOKEN_ENDPOINT;
const ROOM_ID = process.env.ROOM_ID;
export const getToken = async (userRole, userName) => {
const role = userRole.toLowerCase();
const user_id = userName;
const room_id = ROOM_ID;
let payload = {
user_id,
role,
room_id,
};
let url = `${TOKEN_ENDPOINT}api/token`;
const response = await fetch(url, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(payload),
});
let resp = await response.json();
return resp.token;
};
First, you extract the environment variables from process.env
. Then, make a call to the endpoint provided to you by 100ms. This endpoint responds with the needed token.
But we haven't set up our environmental variables. We can do this easily by installing some packages. Run
yarn -D dotenv @rollup/plugin-replace
to get them installed. Then open the rollup.config.js in the root of the folder and paste the following:
//NEW LINE STARTS
import replace from "@rollup/plugin-replace";
import { config } from "dotenv";
//NEW LINE ENDS
const production = !process.env.ROLLUP_WATCH;
//CODE OMITTED FOR BREVITY
export default {
input: "src/main.js",
output: {
sourcemap: true,
format: "iife",
name: "app",
file: "public/build/bundle.js",
},
plugins: [
//NEW LINE STARTS
replace({
"process.env.NODE_ENV": JSON.stringify("production"),
"process.env.TOKEN_ENDPOINT": JSON.stringify(
config().parsed?.TOKEN_ENDPOINT || process.env.TOKEN_ENDPOINT
),
"process.env.ROOM_ID": JSON.stringify(
config().parsed?.ROOM_ID || process.env.ROOM_ID
),
}),
//NEW LINE ENDS
svelte({
preprocess: preprocess(),
compilerOptions: {
dev: !production,
},
}),
Our getToken
function should be up and running now.
Next, replace the code in room.svelte
with the following:
<script>
import page from "page";
import Peer from "./../components/peer.svelte";
import { hmsActions, hmsStore } from "./../services/hms";
import { selectPeers,selectLocalPeerRole,
selectIsLocalAudioEnabled, } from "@100mslive/hms-video-store";
import { onMount, onDestroy } from "svelte";
import { PeerStore } from "./../stores";
let peers = [];
let localPeerRole = "";
let audioEnabled = null;
const handlePeers = (iPeers) => {
let res = hmsStore.getState(selectLocalPeerRole);
localPeerRole = res ? res.name : "";
audioEnabled = hmsStore.getState(selectIsLocalAudioEnabled);
PeerStore.set(iPeers);
};
const handleMute = async () => {
await hmsActions.setLocalAudioEnabled(!audioEnabled);
audioEnabled = hmsStore.getState(selectIsLocalAudioEnabled);
};
onMount(async () => {
hmsStore.subscribe(handlePeers, selectPeers);
});
const leaveRoom = () => hmsActions.leave();
onDestroy(leaveRoom);
$: peers = $PeerStore;
</script>
<main>
<h1>Welcome To The Room</h1>
<section class="peers">
{#each peers as peer (peer.id)}
<Peer {localPeerRole} {peer} />
{/each}
</section>
<div class="buttons">
{#if localPeerRole != "listener"}
<button on:click={handleMute} class="mute"
>{audioEnabled ? "Mute" : "Unmute"}</button
>
{/if}
<button on:click={leaveRoom} class="leave">Leave Room</button>
</div>
</main>
This page houses the most important features of our app. First, we import the required variables. Some of these are:
onMount
except it is called immediately before the component is unmounted.The handlePeers
function does three things:
localPeerRole
variable.audioEnabled
variable.PeerStore
store.The handleMute
function simply toggles the audio state of the local peer. A leaveRoom
is called when the component is to be unmounted or when the Leave Room
button is clicked.
The $:
syntax helps us create reactive statements. These statements run immediately before the component updates, whenever the values that they depend on have changed.
We have 2 new syntaxes in our markup:
{#each peers as peer (peer.id)}
:This helps us map out each peer in the peers
array using the peer's ID as a key.{#if localPeerRole != "listener"}
:This renders the component between the if
block, if the condition is true. Therefore, it renders the Mute
button if the local peer is not a listener.On to the last component, peer.svelte. For the last time, copy the code below into the file:
<script>
import {
selectIsPeerAudioEnabled,
} from "@100mslive/hms-video-store";
import { onMount } from "svelte";
import { hmsActions, hmsStore } from "../services/hms";
export let peer = null;
export let localPeerRole = "";
let isContextOpen = false;
let firstCharInName = "";
let isPeerMuted = false;
const togglePeerAudio = () => {
hmsActions.setRemoteTrackEnabled(peer.audioTrack, isPeerMuted);
};
const changeRole = (role) => {
hmsActions.changeRole(peer.id, role, true);
};
onMount(async () => {
hmsStore.subscribe((isPeerAudioEnabled) => {
isPeerMuted = !isPeerAudioEnabled;
}, selectIsPeerAudioEnabled(peer?.id));
});
$: firstCharInName = peer ? peer.name.split(" ")[0][0].toUpperCase() : "";
</script>
<div class="peer">
<div on:click={() => (isContextOpen = !isContextOpen)} class="content">
<div class="image">
<p>{firstCharInName}</p>
</div>
<p>{peer ? peer.name : ""}{peer && peer.isLocal ? " (You)" : ""}</p>
</div>
{#if localPeerRole == "moderator" && !peer.isLocal}
<div class="context" class:open={isContextOpen}>
<button on:click={togglePeerAudio}
>{isPeerMuted ? "Unmute" : "Mute"}</button
>
<button on:click={() => changeRole("speaker")}>Make Speaker</button>
<button on:click={() => changeRole("listener")}>Make Listener</button>
</div>
{/if}
</div>
Once again, all needed variables are imported. You are expecting 2 props: peer and localPeerRole.
2 functions are declared: togglePeerAudio and changeRole. They do exactly what their names describe. In the onMount function, a handler is added to update the **isPeerMuted **state of a peer.
Each peer component has a context menu that has options for muting the peer or changing their role. But this menu is only available to moderators as only they should have such permissions.
At this point, we are done.
You can run
yarn dev
in the terminal to see the application.
Engineering
Share
Related articles
See all articles