DIY Clubhouse: for iOS, Android, and even Unity
Tags: ios, android, unity3d, webdev
Published: 2021-03-09 10:00:03

The pandemic changed us all. Working and studying remotely, *we craved live communications* and seeing our friends and colleagues in person. Maybe that's why a new social network, [**Clubhouse**](https://www.joinclubhouse.com), has gathered more than 6 million people in less than a year.

Clubhouse is an all-new type of social networking. No texting at all, communicate only by voice. All the communication happens in so-called "rooms", which can be **open** for everyone or **private**. The room's owner (moderator) decides who can talk, and the listeners can "raise a hand" to indicate that they want to ask a question.

However, Clubhouse has its limitations. For now, **it's only available for iOS** users, and to join, you **need to receive an invitation** from an existing user. But what if you are an Android user?

You can wait until they release an Android or desktop version, or you can **make your own Clubhouse**, with ~~blackjack~~ better security, voice recording, AI, and support for any platform. And [**Voximplant**](https://voximplant.com) will help you.

*In this article*, you’ll see how to create a Clubhouse-like audio room with Voximplant's Web SDK, so clients can use a common browser as a client.

> Please note that Voximplant supports multiple SDKs, from Web, iOS, and Android to React Native, Flutter, and even Unity. Imagine, you can embed an audio (or video) chat into your game design

## Set up VoxEngine

**VoxEngine** is a cloud platform to enable serverless communications. Let's create a room. [Log into your account](https://manage.voximplant.com/auth) (or [create one](https://manage.voximplant.com/auth/sign_up)) and [create a new application](https://voximplant.com/docs/introduction/introduction_to_voximplant/basic_concepts/applications). Then [create a user](https://voximplant.com/docs/introduction/introduction_to_voximplant/basic_concepts/users) for authorization and [a new scenario](https://voximplant.com/docs/introduction/introduction_to_voximplant/basic_concepts/programmable_voice_and_video/scenarios).

First [import the conference module](https://voximplant.com/docs/references/voxengine/conference). Then implement user disconnecting:

```js
require(Modules.Conference)
let owner = null;
const onEndpointDisconnected = (event)=>{
   const members = conference.getList();
   if(members.length === 1){
       VoxEngine.terminate();
       return;
   }
}
```

Then implement permissions check:

```js
const checkPermissions = ({call,headers}) =>{
   return new Promise((resolve)=>{
       setTimeout(()=>{resolve(true)},500);
   });
}
```

Implement [room creation](https://voximplant.com/docs/references/voxengine/voxengine/createconference) and [adding a new user](https://voximplant.com/docs/references/voxengine/conference#add):

```js
let logURL = ''; // for debug reason
let conference = null;
VoxEngine.addEventListener(AppEvents.Started, event => {
   logURL = event.logURL;
   conference = VoxEngine.createConference({hd_audio:true});
})
VoxEngine.addEventListener(AppEvents.CallAlerting, async event => {
   const permissions = await checkPermissions(event);
   if(permissions) {
       event.call.addEventListener(CallEvents.Disconnected, onEndpointDisconnected);
       event.call.answer();
       conference.add({
           call: event.call,
           displayName: event.headers['X-Name'],
           mode: "FORWARD",
           direction: "BOTH",
           scheme: event.scheme
       });
       if(conference.getList().length === 1){
           owner = conference.getList()[0].id();
           conference.getList()[0].getCall().sendMessage('owner');
       }
       conference.get(owner).getCall()
       .sendMessage(conference.getList().length);)
   } else {
       event.call.hangup({'X-Reason':'DENIED'});
   }
});
```

The first part is done. Now let's create a client.

## Set up the client

As I decided to make an example with **Web SDK**, my client will be an HTML-page. I'll hide the control buttons until the SDK initialization.

```html
<!DOCTYPE html>
<html lang="en">
<head>
   <style>
       .hidden {
           display: none !important;
       }
   </style>
   <meta charset="UTF-8">
   <title>The demo</title>
</head>
<body>
<div id="btns" class="hidden">
   <p id="myname">Avi</p>
   <button id="viewer">Join as listener</button>
   <button id="speaker">Join as speaker</button>
   <button id="leave" disabled>Leave</button>
</div>
<div id="audio"></div>
<h3>Current speakers <span id="countSpeakers">0</span></h3>
<div id="endpoints"></div>
</body>
<script src="*****"></script>
```

An interesting part. Let's [initialize our SDK](https://voximplant.com/docs/references/websdk/voximplant/client#init) in the <script> tag and show the control buttons if the initialization is successful.

```js
<script>
// sdk init
const sdk = VoxImplant.getInstance();
let user = 'user*****';
const init = async () => {
   await sdk.init({ showDebugInfo: true, serverIp: 'url*****' });
   await sdk.connect();
   await sdk.login(`${user}@app**.acc**.voximplant.com`, 'pass*****');
}
init().then(() => {
   document.getElementById('btns').classList.remove('hidden');
});
```

Create some necessary constants. I will get the user name from an HTML-tag with id=”myname”. Then let's define when each button is displayed:

```js
let currentCall;
let currentRole;
let countSpeakers = 0;
const confNumber = 'Test room';
const speakerBtn = document.getElementById('speaker');
const viewerBtn = document.getElementById('viewer');
const leaveBtn = document.getElementById('leave');
document.getElementById('myname').innerText = myName;
let setRole = (role) => {
   currentRole = role;
   if(role === 'speaker') {
       speakerBtn.disabled = true;
       leaveBtn.disabled = false;
       viewerBtn.disabled = false;
   }
   if(role === 'viewer') {
       speakerBtn.disabled = false;
       leaveBtn.disabled = false;
       viewerBtn.disabled = true;
   }
   if(role === 'start') {
       speakerBtn.disabled = false;
       viewerBtn.disabled = false;
       leaveBtn.disabled = true;
   }
}
```

Handle [leaving the conversation](https://voximplant.com/docs/references/websdk/voximplant/call#hangup):

```js
let endCall = () => {
   if(currentCall && currentCall.state() !== 'ENDED') {
       document.getElementById('endpoints').innerText = '';
       currentCall.hangup();
       setRole('start');
   }
}
```

Then [adding](https://voximplant.com/docs/references/websdk/voximplant/endpointevents#remotemediaadded) and [removing a user](https://voximplant.com/docs/references/websdk/voximplant/endpointevents#remotemediaremoved) to/from a conversation:

```js
let onEndpointAdded = (e) => {
   console.warn('Endpoint added', e.endpoint.id);
   const nameTable = document.getElementById('endpoints');
   let p = document.createElement('p');
   p.id = e.endpoint.id;
   p.innerText = `Name: ${e.endpoint.displayName}, id: ${e.endpoint.id}`;
   nameTable.append(p);
   document.getElementById('countSpeakers').innerText = countSpeakers + 1;
   e.endpoint.addEventListener(VoxImplant.EndpointEvents.RemoteMediaAdded, (ev)=> {
       console.warn('RemoteMediaAdded', ev.mediaRenderer);
       const nodeCall = document.getElementById('audio');
       ev.mediaRenderer.render(nodeCall);
   })
   e.endpoint.addEventListener(VoxImplant.EndpointEvents.RemoteMediaRemoved, (ev)=>{
       console.warn(`Endpoint ${e.endpoint.id} media removed ${ev.mediaRenderer}`);
   })
   // ENDPOINT REMOVED
   e.endpoint.addEventListener(VoxImplant.EndpointEvents.Removed, (ev)=>{
       console.warn(`Endpoint ${e.endpoint.id} removed`);
       let removeP = document.getElementById(e.endpoint.id);
       nameTable.removeChild(removeP);
   })
}
```

Handle the Leave button and some necessary events:

```js
const setCall = () => {
   leaveBtn.onclick = endCall;
   currentCall.addEventListener(VoxImplant.CallEvents.EndpointAdded, onEndpointAdded);
   currentCall.addEventListener(VoxImplant.CallEvents.MessageReceived, (e) => {
       console.warn('MessageReceived', e.text);
   });
   //handle connection
   currentCall.addEventListener(VoxImplant.CallEvents.Connected, () => {
       console.warn(`Call connected successfully`);
   });
   //other call event listeners
   currentCall.addEventListener(VoxImplant.CallEvents.Disconnected, () => {
       console.warn(`Call disconnected`);
       endCall();
   });
   currentCall.addEventListener(VoxImplant.CallEvents.Failed, (e) => {
       console.warn(`Call failed`);
       endCall();
   });
}
```

And handle the role buttons (Speaker or Listener):

```js
speakerBtn.onclick = async () => {
   document.getElementById('endpoints').innerText = '';
   if(currentCall) {
       document.getElementById('endpoints').innerText = '';
       await currentCall.hangup();
   }
   setTimeout(() => {
       setRole('speaker');
       currentCall = sdk.callConference({
           number: confNumber,
           extraHeaders: {'X-Name': myName}
       });
       setCall();
   }, 300)
}
viewerBtn.onclick = async () => {
   if(currentCall) {
       document.getElementById('endpoints').innerText = '';
       await currentCall.hangup();
   }
   setTimeout(() => {
       setRole('viewer');
       currentCall = sdk.joinAsViewer(confNumber);
       setCall();
   }, 300)
}
</script>
</html>
```

**Done**. Now we have a working room with two roles (Speaker and Listener) simple button controls and a list of active speakers. All we need to do to make it a complete app is to make a decent interface and add some user/room management with databases and search.

And do not forget that the functionality of the Voximplant engine goes far beyond this: you can schedule a voice room to record if you're unable to join due to a scheduling conflict, you can add messages if you need, you can even create interactive voice menus with speech synthesis and recognition, which can be lots of fun.