ぽっと通話のしくみ
ぽっと通話は、運営が通信経路としてのサーバを持たない音声通話です。配っているのは静的な HTML 1 枚で、声も、相手探しも、在室の合図も、運営の設備を通りません。このページはその「どうやって」を、リポジトリを読む手前の粒度で書いたものです。
1ファイル(通話アプリ本体 index.html)
1依存ライブラリ(Trystero)
0運営のサーバ(通信経路として)
80+自動テストの判定(回帰テスト)
1. 全体像
太い線=声(ふだんは中継を通る。相手にあなたの IP は見えない)、点線=合図、薄い点線=中継が使えないときだけの直接接続。運営が動かしているのは破線の枠の Worker だけで、それは両方のブラウザに短命の鍵を発行するだけです。
ブラウザに読み込まれるのは index.html 1 枚と、実行時に CDN から読む Trystero というライブラリだけです。ビルドも、パッケージ管理も、バックエンドもありません。
2. 相手を見つける
同じ部屋を開いた人どうしが出会うには、どこかで「私はここにいます」と合図を交わす必要があります。ぽっと通話はそれを公開の Nostr リレーで行います。Nostr は署名付きの小さな JSON を公開リレーに置いて購読する仕組みで、誰でも運営でき、誰のものでもありません。
- Trystero が、部屋 ID から導いた話題に暗号化した合図を流し、同じ話題を聞いている相手と WebRTC の接続を組み立てます(相手探しとシグナリング)。
- リレーは 7 本に多重化しています。公開リレーは止まったり制限を掛けたりするので、半年を目安に実測で点検し、入れ替えています。
- 合図は接続の手順だけで、声も名前も通りません。接続ができた後の再交渉は、できあがった WebRTC のデータチャネルの中で行います。
3. 声の通り道
声は WebRTC で、ブラウザどうしを直接つなぎます。音声は DTLS-SRTP で暗号化され、途中に運営のサーバはありません。
- 既定は Cloudflare の TURN 中継を通します。狙いは IP アドレスのプライバシーで、中継を挟むと相手にあなたの IP は見えません。中継は暗号化されたバイト列を運ぶだけで、中身は読めません。
- 中継の利用には短命の鍵が要ります。それを配るのが運営の小さな Cloudflare Worker(
pot-turn)です。ブラウザは起動時にここから鍵を受け取り、WebRTC に渡します。Worker は鍵を発行するだけで、声も合図も在室も通りません。
- 鍵が取れなかったときだけ、通常の直接接続(STUN)に落ちます。そのときは相手に IP が見えるので、参加者の行に ↔ の印を出して自己申告します。ふだんは ☁️ です。
4. ロビーもサーバなし
「通話中の部屋」の一覧も、集計サーバはありません。ページを開いた人は全員が __lobby__ という隠し部屋の WebRTC メッシュに入り、通話中の人が数秒ごとに在室(部屋 ID・名前・人数・話題タグ)を流します。各自のブラウザがそれを集めて一覧にしています。「いま見ている人数」も同じメッシュのピア数です。
この方式は同時閲覧者が数十人で頭打ちになります。伸ばす必要が出たら、運営サーバを立てるのではなく、在室を公開 Nostr リレーに直接流す方向に倒す、と決めています。
5. 部屋の正体
- 部屋の実体はランダムな 16 文字の ID で、URL の
#room=ID&name=名前 に入ります。名前は付け替え可能なメタデータで、同じ名前の部屋が同時にあっても大丈夫です。だから合流は名前ではなくリンクで行います。
- 古い形の
#名前 のリンクは「入口」として動きます。その名前の部屋がいま開いていれば入り、なければ新しく作る道を案内します。
- ID が名前そのもの(ハッシュではない)の部屋は「いつも同じ部屋」になります。案内役の部屋や、はじめましての席はこの形で、リンクを開けば確認なしにそのまま入れます。
- 人がいなくなった部屋は消えます。どこにも保存されないので、復活もしません。
6. 配信部屋と署名
「主だけが話し、ほかは聞く」を、サーバの権限管理なしで成立させています。
- 主のブラウザが ECDSA P-256 の鍵対を作り、公開鍵の指紋(SHA-256 の先頭 16 バイト、22 文字)を部屋 ID の一部にします(
ID~pk~指紋)。同じ部屋にいる全員が、同じ主の鍵を前提に集まっていることが URL で保証されます。
- 主は「話してよい人」の一覧(ピア ID の名簿)に署名して、データチャネルで配ります。各自のブラウザは署名を確かめ、名簿に載っていない人の音声は再生しません。名乗るだけでは主にも登壇者にもなれません。
- 秘密鍵はその端末のブラウザにだけ保存されます。別の端末やデータ消去のあとでは、同じ部屋の主には戻れません。
- 鍵の生成・署名・検証はすべてブラウザの
crypto.subtle で行い、名簿は既存の WebRTC データチャネルを通ります。サーバは増えていません。
7. 無視は受け取る側で完結
「この人の声を受け取らない」は、相手に何も送らず、自分のブラウザがその人からの音声・ひとこと・相づちを描画しないだけです。接続は切りません(切るとライブラリが即座に張り直すため、実測で捨てた案です)。配信部屋の名簿も同じ関門を通るので、「誰の音を鳴らすか」を決める場所はコード上一か所です。
8. 人数の上限
接続はフルメッシュで、全員が全員につながります。接続数は人数の二乗で増え、音声も人数分アップロードするので、上限を明示的に置いています。
| 部屋の種類 | 上限 | 快適な範囲 |
| 雑談部屋 | 8 人 | 4〜6 人 |
| 配信部屋 | 15 人 | 主 1 人+聞き役 |
満室のとき抜けるのは、あとから入った側だけです(全員が同じ判定を回すと全員が抜けてしまうため、到着順で決めます)。大人数化には SFU(音声を集めて配り直すサーバ)が要ります。それは設備を持たない設計の外側なので、いまは上限のほうを選んでいます。
9. 在室の合図(バナー)
通話中は、その部屋が「いま開いているか」を外のページ(はじめにのバナーなど)が表示できるよう、在室を公開 Nostr リレーに 30 秒ごとに流します。載せるのは部屋名の SHA-256 ハッシュと絵文字だけで、部屋名も人の名前も送りません。リレーは保存しない種類のイベント(ephemeral)を使い、鍵はページごとの使い捨てです。
ハッシュなので、部屋名を知っている人だけがその部屋を見張れます。全部屋の一覧を第三者が覗くことはできません。バナー側は購読するだけで、運営サーバは介しません。
10. スマホ向けの工夫
- 自動再生の制限:iPhone はページを開き直した直後、ユーザー操作なしに音を鳴らせないことがあります。再生が拒否されたら「音声を有効化」を出し、画面のどこかをタップすれば鳴るようにしています。
- 音の出口を 1 本に:相手の声・BGM・通知音を Web Audio でまとめ、1 つの
<audio> 要素から出します。iOS はこの出口の扱いが厳しく、AudioContext の再開を 1 本に束ねないと音が連なります。
- 画面ロック:通話中は Wake Lock を取り、背面から戻ったときに取り直します。
- 小窓:部屋名を描いた canvas を映像トラックにして Picture-in-Picture に入れ、ほかのアプリを見ている間も通話中だと分かるようにしています。
- 声が来ないときの自己修復:受信トラックが届いても中身(RTP)が流れないことがあります。「要素があるか」ではなく「トラックが live で muted でないか」を見張り、数秒来なければ相手に送り直しを頼みます。送り直す側は一度外して新しい MediaStream に包んで入れ直し、受け取る側はライブラリを通さず WebRTC の track イベントから直接受けます。
- アプリ内ブラウザ:LINE・X・Instagram などの中のブラウザはマイクが使えないことがあるので、判定して Safari や Chrome で開き直す案内を出します。
11. 設備なしで成立させる、という設計
ぽっと通話の設計の芯は、運営が動かす設備を可能な限りゼロにしたまま、通話を成立させることです。配るのは HTML 1 枚。相手探しは公開リレー、声はブラウザどうし、中継は第三者、ロビーは参加者のブラウザのメッシュ。運営のコードで動いているのは中継の鍵を配る小さな Worker だけで、通信そのものはどこも通りません。
この形にすると、いくつかのことが自然に成り立ちます。
- 運用コストがほぼゼロ:サーバの監視も、スケールアウトも、障害対応もありません。人が増えれば、その人たちのブラウザが仕事をします。
- 集めていないものは漏れない:声も会話も在室も運営の手元に来ないので、守るべきデータベースがありません。プライバシーは設定ではなく構造で決まります。
- 運営が止まっても、通話は止まらない:運営の PC が落ちても、いま話している人たちの通話は続きます。リポジトリを fork して自分の GitHub Pages に置けば、同じものがもう一つ動きます。
- 読める大きさ:バックエンドが無いぶん、全体が 1 ファイルに収まります。何が起きているかを最後まで追えます。
引き換えに、手放したものもあります。過去の会話は残らず、部屋は人がいなくなると消えます。大人数の部屋は作れません。公開リレーや Cloudflare という第三者に依存し、そこが止まれば相手探しや中継も止まります(そのときは直接接続に落ちます)。
だから設計のルールは一つです。自前のサーバを足して解決しない。伸ばしたいものが出てきたら、まず参加者のブラウザと公開の仕組みでできないかを考えます(ロビーの拡張は公開リレーへの直接 publish、在室の表示は購読、といった具合に)。それでも足りないものは、いまは「やらない」と決めています。
12. テストと公開
- コードは GitHub で MIT ライセンスとして公開しています。自由に使用・改変・再配布できます。同梱している第三者ソフトウェアの表示もリポジトリにあります。
- 回帰テストはヘッドレス Chrome で 2〜9 タブを実際につないで確かめるもので、判定は 80 件を超えています(接続、再接続、無視、配信部屋の名簿、おたより、昼夜の配色のコントラストまで)。
- 自分で立てたい人向けの手順(GitHub Pages への公開、差し替えるべき値、TURN Worker のソース)は README にあります。
- アクセス数の測定は Cloudflare Web Analytics(Cookie 不使用・個人を特定しない)で、その集計は README で公開しています。
はじめにへ リポジトリを見る