# セルフホスト

`joinVoiceRoom()` には小さな Cloudflare Worker が必要です。自分のアカウントにデプロイし、費用も自分で負担します。私たちは何もホストしません。Worker はプレイヤー間の接続メッセージを中継し、人数の上限を守らせ、TURN の認証情報を発行します。音声はプレイヤー間か TURN を通り、Worker は通りません。

## デプロイ {#deploy}

Cloudflare アカウントが必要です。

1. [`templates/worker`](https://github.com/debuffjp/earshot/tree/main/templates/worker) をプロジェクトにコピーし、そこで `npm install` を実行します。
2. Cloudflare のダッシュボードの Realtime で TURN キーを作成します。キー ID と API トークンが発行されます。
3. `wrangler.jsonc` の `ALLOWED_ORIGINS` にゲームのアドレスを設定します。
4. シークレットを追加してデプロイします。

```bash
npx wrangler secret put TURN_KEY_ID
```

```bash
npx wrangler secret put TURN_KEY_API_TOKEN
```

```bash
npx wrangler deploy
```

5. Worker のアドレスを `wss://earshot-voice..workers.dev` の形でゲームに渡します。

2 つの異なるネットワークからテストしてください。Cloudflare で使用量のアラートも設定してください。

シークレットが 2 つそろっていないと、Worker は公開 STUN サーバーだけを提供します。このとき `ipPolicy: "relay"` は `no-turn-server` で失敗します。`"direct"` は、ネットワークが許す範囲でプレイヤーをつなげます。

## 設定 {#settings}

`wrangler.jsonc` の `vars` です。値はすべて文字列です。

| 変数 | 意味 | テンプレートの値 |
| --- | --- | --- |
| `ALLOWED_ORIGINS` | 参加を許可するオリジン（カンマ区切り）。例：`https://mygame.example` | `http://localhost:4710` |
| `MAX_PLAYERS` | ルームあたりの人数。これを超えると `room-full` になります | `8` |
| `TURN_TTL_SECONDS` | TURN 認証情報の有効期間 | `14400` |

オリジンは、スキーム、ホスト、ポートまで完全に一致する必要があり、末尾にスラッシュはつけません。開発中はローカルと本番のオリジンを並べて書いてください。

各ルームを保持する Durable Object のバインディングとマイグレーションは、すでにファイルに入っています。

## canJoin {#canjoin}

公開する前に、`src/index.js` の `canJoin` を編集してください。テンプレートのままでは、ルームコードを知っていれば誰でも参加でき、BAN や年齢については何もしません。false を返すと接続を拒否します。

```js
async function canJoin(request, env, { room, peer }) {
  return isLoggedIn(request.headers.get("Cookie"), env); // 自分のログイン確認。
}
```

`isLoggedIn` は自分で書くコードの例です。接続 URL は `joinVoiceRoom()` が組み立てるので、必要な情報はリクエストのヘッダーから読んでください。拒否されたプレイヤーには `connection-failed` が返ります。

- オリジンの確認で防げるのは他の Web サイトで、独自のクライアントは防げません。その気になれば、開いた Worker から TURN の認証情報を取得できます。費用が重要ならログインを必須にしてください。
- 知らない人同士のマッチングはありません。ルームコードはフレンド向けです。モデレーションができないなら、そのままにしてください。
- プライバシーの告知と、プレイヤーのいる地域の子どもやオンラインの安全に関する法律は、開発者の責任です。これは法的助言ではありません。詳しくは[ボイス](./voice#your-duties)を見てください。

## 費用 {#costs}

費用がかかるのはリレーの設定です。これは最悪の場合の計算で、全員がリレーを使い、話し続けるとします。パケットのオーバーヘッドを含む 32 kbps の Opus で、1 ストリームあたり 1 時間に約 24 MB です。Cloudflare は TURN の送信トラフィックに対し、毎月 1,000 GB の無料枠を超えた分へ 1 GB あたり $0.05 を課金します（[料金](https://developers.cloudflare.com/realtime/sfu/platform/pricing/)、2026-11-04 時点）。頼る前に最新の料金を確認してください。全員が全員に接続するので、n 人のルームには n x (n - 1) 本のストリームが流れます。4 人で 12 本、8 人で 56 本です。

| ルーム（全員リレー） | 1 時間あたりの GB | 無料枠を超えた後の 1 時間あたりの費用 | 無料枠で使えるルーム時間 |
| --- | --- | --- | --- |
| 4 人 | 0.29 | $0.014 | 約 3,470 |
| 8 人 | 1.34 | $0.067 | 約 740 |

実際のルームの費用はこれより少なくなります。十分に離れたプレイヤーへの送信は止まり、プッシュトゥトークはキーを離している間は無音を送るからです。無音はデータ量が少ないものの、トラフィックにはなります。この表は TURN だけの費用です。ルームサーバーの Workers と Durable Objects の使用量は別に課金されます。

## トラブルシューティング {#troubleshooting}

| `joined.error` | 確認すること |
| --- | --- |
| `connection-failed` | URL、次にページの正確なオリジンと `ALLOWED_ORIGINS`、その次に `canJoin`。`createRoomCode()` のコードは必ず Worker の形式チェックを通ります。自作のコードは英数字、`-`、`_` で 16〜64 文字にしてください |
| `no-turn-server` | シークレットが 2 つとも設定されていることを確認し、再度デプロイします |
| `room-full` | `MAX_PLAYERS` |