Clubhouse своими руками: для iOS, Android, Web и даже Unity

  • Tutorial

Меньше чем за год новая социальная сеть Clubhouse набрала больше 6 миллионов участников — и всё продолжает расти. В чём же секрет такой популярности?

Clubhouse — принципиально новый формат социальных сетей, такого ещё не было. Во время пандемии людям стало не хватать живого общения, поэтому в Clubhouse всё общение происходит голосом, через так называемые комнаты. Никаких привычных нам сообщений и диалогов. Всё общение происходит в реальном времени.

Комнаты могут быть закрытыми “для своих” или открытыми для всех желающих. Создатель (модератор) комнаты сам выбирает, кто может говорить, а присоединившиеся участники могут поднять руку, чтобы выразить желание что-то сказать, и если модератор захочет, включит желающему микрофон.

Однако попасть в Clubhouse могут не все. В настоящий момент официальное приложение доступно лишь владельцам айфонов, а зарегистрироваться можно только по приглашению от уже зарегистрированного пользователя. Но что, если у меня андроид? Или я хочу сидеть в соцсетях с компьютера?

Можно, конечно, терпеливо ждать, когда они решат выпустить версию для андроидов и десктопную версию, а можно поступить креативно – написать свой Clubhouse, с блэкджеком записью разговоров и поддержкой любых платформ. А Voximplant поможет с этим.

В этой статье я покажу, как можно создать комнату-конференцию наподобие Clubhouse для Web SDK, в качестве клиента я буду использовать браузер.

Обратите внимание, что Voximplant поддерживает множество SDK, начиная с Web, iOS, Android и заканчивая Flutter, React Native и даже Unity. То есть если вы пишете игру на Unity, есть возможность встроить аудио- или даже видеочат прямо в неё.

Настройки VoxEngine

Создаём комнату. Заходим в свой аккаунт Voximplant (или регистрируем новый) и создаём там приложение, нового пользователя для авторизации и начинаем писать сценарий.

Сначала подключим модуль конференций Voximplant и обработаем отключение участников:

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

Затем обработаем проверку разрешений:

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

Создание новой конференции и подключение нового участника к комнате:

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'});
   }
});

На этом с настройкой VoxEngine всё.

Настройка клиента

Далее перейдём к клиентской части. Мы используем Web SDK, так что клиент будет представлять собой HTML-страницу. Кнопки управления конференцией скроем до инициализации SDK:

<!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>

Далее самое интересное. Пишем сценарий в теге <script>. Для начала инициализируем наш SDK, авторизуемся и отобразим кнопки:

<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');
});

Задаём нужные константы для конференции. Имя пользователя для участника комнаты я возьму из тега с id=”myname”. Затем определим, когда видны определённые кнопки:

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;
   }
}

Обработаем завершение конференции:

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

Обработаем добавление нового участника в комнату и удаление из нее:

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);
   })
}

Обрабатываем действие по кнопке выхода из комнаты и другие события:

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();
   });
}

И напоследок обработаем кнопки, которые задают роль участника комнаты (говорящий или слушатель):

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>

Готово. На выходе получаем комнату-аудиоконференцию, в которой участники могут быть говорящими или просто слушателями, управление ролями происходит с помощью кнопок, а ниже виден список говорящих с их именами и ID.

Остаётся только сделать базу данных пользователей и комнат, красивый выделяющийся интерфейс — и ваш собственный конкурент Clubhouse для любой платформы готов!

Voximplant
Облачная платформа голосовой и видеотелефонии

Comments 14

    +5
    Не понимаю этого, как машину без колес, которую все хвалят за необычность и дерзость решения.
    в Clubhouse всё общение происходит голосом, через так называемые комнаты. Никаких привычных нам сообщений и диалогов.
    Комнаты могут быть закрытыми “для своих” или открытыми для всех желающих. Создатель (модератор) комнаты сам выбирает, кто может говорить, а присоединившиеся участники могут поднять руку, чтобы выразить желание что-то сказать, и если модератор захочет, включит желающему микрофон.

    Миллениалы изобрели закрытые комнаты teamspeak/mumble, да еще и с ограниченными возможностями и рады этому словно прозрению. О, человечество, воистину ты ходишь по кругу!
      +2

      Я даже больше скажу, до покупки скайпа майкрософтом — там были уже скайпкасты, где один человек мог говорить, остальные слушать и давать право говорить кому то еще, только паралельно еще и текстовый чат был.

        +1
        Откуда столько снобизма? Достаточно удобное решение для своих нужд.
        Вы знали что ватсап это перезобретенный XMPP а Discord переизобретенный IRC. И что? Они удобны.
          +1
          а правда, для каких нужд?
            +1
            Ну как видите, некоторые известные люди проводят там свои записи, потому что в отличии от трансляции там нет необходимости смотреть в камеру и напрягаться, а в отличии от подкаста там всё в реалтайме и легко настраивается.

            Для пользователей тоже удобство. Можно менять комнаты с разной тематикой на лету. Условно в дискорде для этого каждый раз нужно искать разные серверы и проходить различные соглашения с правилами итд. Да и не все всегда мутят по умолчанию. А тут это как место реалтайм подкастов, заходи и слушай.

            Люди любят подобные приложения за то, что информацию в массы становится доносить проще. Со всех сторон.
              0
              Но вот это мне и непонятно.
              Есть кейсы, которые требуют реалтайма: какие-нибудь публичные дебаты, радио (не зря «эхо москвы» так уцепилось за КХ).
              Но если известный человек хочет обнародовать свою точку зрения, реалтайм ему только мешает. Да и видео, честно говоря, хоть и требует напряжения, но добавляет убедительности (лично я не смотрю видео и не слушаю аудио, т.к. это слишком большой расход времени по сравнению с текстом, но это уже другой вопрос).
              А для пользователя КХ получается аналогом радио, а не средством общения.
                0
                Типа того. Всё зависит от предпочтений человека, кому-то нравится смотреть, кому-то нравится слушать (ведь твич собирает миллионы пользователей, хотя до него было TV). Лично мне КХ не симпатизирует с технической стороны (она бедная), а как идея, я бы не сказал что это что-то революционное, но достаточно оригинально. Возможно КХ создаст за собой волну новых аудиальных сервисов.
            0
            Согласен. Они удобны и ими пользуются. Тот же Discord позиционирует себя как соц.сеть для геймеров (хотя в последнем их заявлении они от этой классификации отходят), и сейчас пытаются сделать соц.сеть для всех — начиная от музыкантов заканчивая еще чем-то.
            0
            Ну, Twitter как-то взлетел… Тоже все удивлялись, кому захочется общаться с такими тупыми ограничениями…
              +1
              мы недооценили потребность в соц. сети для сплетен
            0
            А радиолюбители по-видимому никогда голосом не общались на определённых частотах…
              +3
              Clubhouse — принципиально новый формат социальных сетей, такого ещё не было. Во время пандемии людям стало не хватать живого общения, поэтому в Clubhouse всё общение происходит голосом, через так называемые комнаты. Никаких привычных нам сообщений и диалогов. Всё общение происходит в реальном времени.

              Денис Попов — перелогинься!
              • UFO just landed and posted this here
                  0
                  а зачем?

                  Only users with full accounts can post comments. Log in, please.