y-ohgi's blog

TODO: ここになにかかく

サーバーレスでclubhouse みたいなボイスチャットサービス「mixroom」を開発しました

TL;DR

  • clubhouse のような複数人でのボイスチャットサービス「mixroom」をつくりました
  • 主にSkyWay・Firebase・Next.js を使いました。
  • https://mixroom.fun

「mixroom」 とは

f:id:y-ohgi:20210216141048p:plain

mixroom は"目的ベース"でボイスチャットをする場所を提供するサービスです。

昨今は人と話す機会が減り、友好関係も自分から能動的に獲得しにいかないと減るばかりで「孤独感を感じる」みたいなことが増えてきました。
そんなとき、ふらっと立ち寄れるなんの気構えしなくて良い場所があればなと思い、このサービスを作ってみました。
ボイスチャットをするモチベーションを"特定の人"ではなく、ゲーム・実況・もくもく会などの"目的"にすることで良い意味で言い訳ができ、人とのつながりを感じることができるんじゃないかなと考えています。

気構えしなくてよい・カジュアルに参加できる・シンプルにつかえることを重視しています。
ブラウザで完結するので何かをインストールする必要がなく、Twitter ログインをするだけでボイスチャットに参加可能になっています。
また、友人やパートナーなどの”人ベース”ではなく、"目的ベース"の場所にするために、このサイトにはユーザープロフィールやボイスチャット相手のTwitter へのリンクなどは基本的に表示していません。ユーザーのアイコンとディスプレイネーム("@" からはじまるid のほうではないです。)の2つのみ使用するようにしています。

誰かと話したいときにの場所として1つの選択肢に慣れば幸いです。
ぜひお気軽に使ってみてください

機能

α版として、複数人でボイスチャットをする機能を提供しています。
将来的に「目的ベース」でボイスチャットをするための機能を追加していきます。

ボイスチャット

youtu.be

誰かが立てたボイスチャットルームに入室するとボイスチャットができます。
話してる人のアイコンが光ったりします。

ルーム作成

youtu.be

ボイスチャットルームが作成できます。
ログイン済みのユーザーであれば誰でも作成でき、だれでもルームに入室できます。

今後

"目的ベース" でカジュアルに集まれる場所として機能追加をしていきます。
いまの段階では単純に「ブラウザでボイスチャットができるサービス」で、ようやくスタート地点に立てたところです。

また、特に重要だと思っていることが「治安」です。
ボイスチャットサービスは公序良俗に反するユーザーへの対応が必須だと考えており、どう適切にモラルを持ったユーザーを守るかにかなり悩んでいます。
そもそも自分のために作っているサービスでもあるので、治安の悪化は無視できないです。


mixroom を支える技術

以下、技術選定や使用した技術のおはなしです。

f:id:y-ohgi:20210212221758p:plain

SkyWay を使用して音声データの双方向通信

このサービスの要である音声データ(MediaStream)の双方向通信(WebRTC)を実現するために「SkyWay」を使用しています。
クライアント間で音声データをリアルタイムで受け渡す必要があり、そのクライアント間のコネクションを張ってくれるのがSkyWay です。

いくつか選択肢があるきがしはしたのですが、リアルタイム通信を行う以上日本にサーバーがあったほうが嬉しいと思ったことが一番の選定理由です。
SkyWay | アプリやWebサービスに、ビデオ・音声通話をかんたんに導入・実装できるSDK

mixroom はルーム単位でコネクションを行うのですが、そういった複数人でのコネクションを標準で提供してくれています。
以下がルームへ参加するコードの例です。
ざっくり、SkyWay へ接続して、ルームに参加して、既にルームに参加しているユーザーから音声(MediaStream)をうけとるような例です。

const peer = new Peer({
  key: "<API KEY>"
})

const room = peer.joinRoom("<ROOM ID>", {
  stream: myStream
})

room.on("stream", (stream) => {
  // ここに
})

既にはられているコネクションはSkyWay のWeb コンソール・API 経由で確認することができます。なにかあったときに緊急停止みたいなことができるのは安心ですね。
ECL API | API Reference | SkyWay(アプリやWebサービスに、ビデオ・音声通話をかんたんに導入・実装できるSDK)

mixroom では音声データのみ扱っていますが、映像データもSkyWay は対応しています。
実際にWebRTC を使用して音声をやりとりする開発でのつらみとして、「自分の声」がありました。どのマイク・スピーカーと音がつながっているのかわからない。そもそもPC とスマホで違うデバイスからアクセスしても同じ声なので違いがわからないし、自分の声聞くときの「これじゃない感」がすごい。みたいな。
なので開発時は音声を切って映像データを流してテストをしたりしていたのですが、開発終盤でひやひやしながら映像から音声に切り替えときすんなりいきSkyWay に感動したりしてました。

Firebase Realtime Database でアクティブなユーザーの管理

ボイスチャットルームに接続中のユーザーの取得をするために「Realtime Database」を使用しています。
Realtime Database は名前の通りWebSocket を使用してリアルタイムでデータを読み書き可能なデータベース(NoSQL)です。

mixroom ではアクティブなユーザーの管理のために使用しています。
「WebRTC とWebSocket 両方使う必要はないのでは?」というのはその通りなのですが、クライアントの切断処理をWebRTC ではなくWebSocket で行うことでサーバー側でより確実な状態の管理を実現したかったためです(たとえばルーム一覧画面でルーム内のユーザーを表示するためにWebRTC を使い、各ルームへコネクションを張ってしまうのは効率が悪いですよね)。
「接続状態をデータベースへ適切な権限を持って変更すること」「WebRTC のレイヤーで接続管理を行うとクライアントがいつ切断したのかをサーバーサイドで検知すること」この2つをコスパが良いと判断しました。
ただ、Realtime Database はユーザーの接続管理だけになっており、永続化のために使用しておりません。あくまでユーザーとサーバー間でコネクションが切れたタイミングを検知するために使用しています。

「接続状態をデータベースへ適切な権限を持って変更すること」

これはFirebase のつよみであるサービス間連携を利用しました。 Realtime Database へのアクセス権限をAuthentication でログイン中のユーザーによって変更することがデフォルトで可能です。
説明するよりRealtime Database のルール(JSON で書きます)をみてもらったほうが早いでしょう。

{
  "rules": {
    "users": {
      "$uid": {
        ".read": true,
        ".write": "auth.uid === $uid"
      }
    }
  }
}

これは /users/$uid への読み込みはすべて許可し、書き込みは $uid とAuthenticaton でログイン中のユーザーID が一致している場合にのみ許可するルールです。

Firebase Realtime Database ルールについて

「WebRTC のレイヤーで接続管理を行うとクライアントがいつ切断したのかをサーバーサイドで検知すること」

これはRealtime Database のプレゼンス機能を使用しています。
接続時に値を追加し切断時に削除するだけであればこれだけです。ルールで自分自身のみ書き込み可能なパスにプレゼンスで接続ステータスを書き換えると接続状態の管理が終わります。

const con = firebase.database().ref(`users/${uid}`).set({active: true})
con.onDisconnect().remove()

JavaScript でのオフライン機能の有効化  |  Firebase Realtime Database

Firebase エミュレータ

Firebase の各サービスのエミュレータが公式で提供されており、ローカルでFirestore やAuthentication のような主要なサービスをローカルで実行することができます。
Firebase Local Emulator Suite の概要

Authentication を使用したサードパティログインもエミュレータ上で動かすことが可能で、実際にTwitter ログインを呼び出したのが以下の動画です。

ただ、Firebase の強みであるサービス同士の連携がエミュレータ上だとサポートされないことがいくつかありました。
そのため途中までローカルのdocker-compose でエミュレータを動かして開発していたのですが、諦めてエミュレータを使用しない道を選択しました。
Firebase エミュレータはかなり優秀で、使わないという判断をするに至るまでまでかなり時間を使った気がします。

ogp の自動生成

f:id:y-ohgi:20210216141153p:plain

ルーム作成時毎にCloud Functions を呼び出しCloud Storage へアップロードしています。
Cloud Run で実行しているNext.js のコンテナがalpine のnode で、その中に画像周りのミドルウェアを追加する道がつらそうだったため早々にFunctions を採用しました。

Next.js x material-ui

ogp を動的に生成したかったことと、単純にNext.js を書いたことがなかったので採用しました。

なにもわからないままはじめたのですがいまもなにもわからないです。
SkyWay(WebRTC)・Realtime Database (WebSockt)を使用し、ogpを動的に生成するためにSSRを有効化する関係上そもそも可能なのかみたいなところからはじめて、この試行錯誤にかなり時間を使いました。最終的に動いたものの、正直まだ理解しきれていないので大きな課題として残っています

その他

基本的に1人で開発を行い、ココナラでキャッチコピーとカラーコードだけ依頼しました。
お金は人が来なければドメイン代だけなので実質タダです。マネタイズは広告をそのうちいれる、かも。

イラスト

f:id:y-ohgi:20210216142310g:plain

unDraw を使用させていただきました。
配色を自由に変更できるため使用しやすかったためです。

Illustrations | unDraw

ロゴ

DesignEvo を使用させていただきました。
無料使用可能なロゴメーカーは規約を読んでみるとちょっと気が引けてしまったので、買い切りで商用利用可なDesignEvo を使用しました。

無料オンライン・ロゴメーカー、カスタム・ロゴデザイン作成 - DesignEvo

所感

まず、動作確認に協力してくださった各位、各位が居なかったら投げ捨ててました。本当に感謝です。

個人開発ははじめてなのですが(個人開発とよべるほどの規模ではないかもですが...)、原点を思い出す開発でした

好きだったオンラインゲームのギルドのホームページつくりたかったところから僕のエンジニアの道ははじまったのかなとかとか。。。
閉塞的な田舎で息苦しさを感じていた自分にはインターネットという広い世界でいろんな人と関わることの魅力に惹かれたのでした
ぼくのインターネットが好きだからエンジニアになったんでした

もともとボイスチャットのための場所を作りたい気持ちは心にあったのですが時間だけが過ぎていき、clubhouse を見てこれを逃したらこの先もチャンスを見送っていくのかなと思うと虚無がすごくて開発をはじめました

最初は「ボイスチャットぐらい秒」とか思ってた記憶があるのですが実際に開発してみると、最初は自分の力量不足がストレスとして積み重なっていって、最後は「本当にこんなものを僕は世に出して良いのか・・・?」みたいな羞恥心がストレスとして積み重なっていって、、、とても心臓に悪いですね

https://twitter.com/_y_ohgi/status/1360119490122448896?s=20

あくまでここがスタートラインだと思っているのでがんばっていきたいですね。
メイン機能として、1人1票任意のテーマを投票して、19時ぐらいから1位になったテーマでマッチングをするみたいな機能を作りたいと考えています。

f:id:y-ohgi:20210212221751p:plain