Skip to content

Repository files navigation

engawa(縁側)

ブラウザだけで使える、社内向けの仮想オフィス。

2D マップ上でアバターを動かし、誰かに近づくと自動で音声・映像・画面共有がつながります。 Slack のハドルや Gather.town のような「ちょっと話しかける」体験を、軽量・低コストで実現することを目指したツールです。

        ┌─────────────────────────────────────────┐
        │   . . . . . . . . . . . . . . . . . . .   │
        │        (•‿•) ───近づくと通話──→ (^_^)      │
        │         あなた                  田中さん    │
        │   . . . . . . . . . . . . . . . . . . .   │
        └─────────────────────────────────────────┘

🍵 名前の由来

縁側は、家の内と外のあいだにある板の間。 用がなくても腰掛けられて、通りかかった人となんとなく言葉を交わす——そんな場所でした。

リモートワークで失われがちな「ばったり会って、ちょっと話す」を、画面の中に取り戻したい。 engawa は、誰かのとなりに気軽に居られる、オフィスの縁側です。


✨ 主な機能

機能 説明
🚶 アバター移動 矢印キー / WASD でマップ上を歩く
🔊 近接音声通話 一定距離まで近づくと自動で音声がつながる(離れると自動で切断)
📷 ビデオ通話 カメラ ON で顔出し。話している人は枠が光る
🖥 画面共有 ワンクリックで共有。小窓 / サイドパネル / 全画面を切り替え可能
録画 画面全体(フロア+画面共有+全カメラ)と全員の音声を合成してブラウザ上で録画
🟢 ステータス表示 オンライン / 取り込み中 / 離席中 を切り替え
🔔 近接チャイム 人が近づいた / 離れたときに軽い効果音
🔒 入室パスワード ACCESS_PASSWORD を設定すると入室時にパスワードを確認(任意。未設定ならオープン)
👥 大人数対応 会議室ゾーンや 5 人以上の集まりは自動で SFU 配信に切り替え(要 Cloudflare Realtime SFU 設定。未設定なら全員 P2P)
📲 アプリとしてインストール デスクトップ Chrome / Edge でインストール可能(PWA)。独立ウィンドウで起動でき、Dock / タスクバーに常駐できる

🎮 使い方

  1. ブラウザでアプリを開く
  2. 名前(と、必要ならパスワード)を入力して入室
  3. 矢印キー / WASD でアバターを動かす
  4. 話したい相手に近づくだけで通話が始まります

ツールバーからマイク・カメラ・画面共有・録画・ステータスを操作できます。

ACCESS_PASSWORD を設定している場合は、名前入力の前にパスワードの確認画面が表示されます。

アプリとしてインストール: デスクトップ Chrome / Edge ではアドレスバーのインストールアイコン(またはメニューの「アプリをインストール」)から、ブラウザの枠のない独立ウィンドウで起動できます。


🚀 動かす(開発環境)

アプリの操作はすべて Docker 経由で行います。docker-compose.ymlMakefile はリポジトリ最上位にあります。

cp .env.example .env   # (任意)TURN やパスワードを使う場合のみ編集
make up                # 起動(バックグラウンド)

開発環境は Caddy リバースプロキシが HTTPS を終端 します。次の URL で開きます:

  • アプリ: https://engawa.localhost ← これが開発の入口
  • 初回は Caddy のローカル CA を信頼していないため証明書警告が出ます。下記「ローカル CA を信頼する」を参照。

.localhost ドメインは OS が自動で 127.0.0.1 に解決するため、hosts ファイルの編集は不要です。

コマンド 内容
make up 起動(docker compose up -d
make down 停止
make restart 再起動
make test テスト実行(server / client 両方)
make build クライアントの本番ビルド

WebRTC は HTTPS または localhost でのみ 動作します。アプリ自身は TLS を持たず、TLS 終端はリバースプロキシの責務 です。本番でも各自のリバースプロキシ(Caddy / nginx / クラウドの LB など)で HTTPS(wss:// を含む)を終端してください。

ローカル CA を信頼する

tls internal が発行する証明書はローカル CA 署名です。警告を消すには CA ルート証明書を OS に取り込みます:

# Caddy コンテナから CA ルート証明書を取り出す
docker compose cp caddy:/data/caddy/pki/authorities/local/root.crt ./caddy-root.crt
  • macOS: caddy-root.crt をダブルクリック →「キーチェーンアクセス」で「常に信頼」に設定
  • Windows: 「信頼されたルート証明機関」ストアにインポート
  • Linux: /usr/local/share/ca-certificates/ に置いて sudo update-ca-certificates

LAN 内の別端末(スマホ等)から実機テストする

別端末で WebRTC を試すには HTTPS が必須です。tls internal の証明書は他端末では既定で信頼されないため、次の準備が必要です:

  1. ホスト PC の LAN IP を調べる(例: 192.168.1.10
  2. 別端末の hosts に 192.168.1.10 engawa.localhost を追加する(証明書は engawa.localhost 向けに発行されるため、IP 直アクセスではなくこの名前で開く)
    • iOS など hosts を編集できない端末では、社内 DNS で同名を引かせる等の代替が必要です
  3. 上記の caddy-root.crt をその端末に転送してインストール・信頼する
  4. 別端末で https://engawa.localhost を開く

これらの設定(hosts・CA 配布)は端末環境ごとに異なるため、各自の環境に合わせて行ってください。本リポジトリは特定ホスティング前提のデプロイ設定は持ちません。


🧱 技術スタック

サーバー

  • Bun + 標準の WebSocket(外部依存ほぼなし)
  • 役割は「位置同期」「WebRTC シグナリング中継」「グループ方式(mesh/SFU)の判定・配信」「TURN/SFU 制御のプロキシ」のみ(メディアは通さない)

クライアント

  • TypeScript + Vite
  • 描画は素の Canvas 2D API(UI フレームワークなし)
  • WebRTC は simple-peer(P2P メッシュ)+ Cloudflare Realtime SFU(大人数・会議室、simulcast 多レイヤ)

インフラ

  • STUN: Google 公開 STUN
  • TURN: Cloudflare Realtime(NAT 越えできない場合のみ経由)
  • SFU: Cloudflare Realtime SFU(会議室・大人数グループの配信。未設定ならすべて P2P メッシュ)

🏗 アーキテクチャ

メディア(音声・映像・画面共有)は engawa のサーバーを経由しません。屋外の少人数近接は P2P メッシュ、会議室ゾーンや 5 人以上の集まりは Cloudflare Realtime SFU 経由——どちらでもメディアは自前サーバーを通りません。サーバーがやるのは、出会いの仲介・位置同期・グループ方式の判定だけです。

[ブラウザA]                 [サーバー (Bun)]                 [ブラウザB]
   │  WebSocket  ←───────→  ├ 位置のブロードキャスト  ←──────→  │  WebSocket
   │                        ├ WebRTC シグナリング中継(mesh)    │
   │                        ├ グループ方式(mesh/SFU)の判定・配信  │
   │                        ├ /api/turn-credentials             │
   │                        └ /api/sfu/*(SFU 制御のプロキシ)    │
   │                                                            │
   ├── 屋外・少人数: WebRTC P2P メッシュ ───────────────────────┤
   └── 会議室 / 5 人以上: Cloudflare Realtime SFU 経由 ──────────┘
            ※ メディアは engawa を経由しない(P2P / Cloudflare のみ)

サーバーの責務

  1. 静的ファイル配信
  2. 位置同期 — クライアントの座標を受け取り全員にブロードキャスト
  3. WebRTC シグナリング中継 — offer / answer / ICE candidate を相手に転送(メッシュ)
  4. グループ方式の判定・配信 — 位置と会議室ゾーンから近接グループを求め、mesh / SFU を決めて配る
  5. TURN / SFU の発行・プロキシ — Cloudflare API を叩いて短期トークンを返す/SFU 制御を中継(API キーはサーバーのみ保持)

📁 ディレクトリ構成

engawa/
├ client/                # Vite + TypeScript フロントエンド
│  └ src/
│     ├ app.ts           # トップレベルのオーケストレータ(ゲームループ・各サブシステムの配線)
│     ├ toolbar.ts       # マイク / カメラ / 画面共有 / 録画ボタンと各種メニュー
│     ├ remote-media.ts  # リモートのビデオ / 音声 / 画面共有タイルの DOM 管理
│     ├ panels.ts        # フローティングパネルの配置
│     ├ speaking.ts      # 発話検出
│     ├ canvas.ts        # Canvas 描画
│     ├ player.ts        # プレイヤー状態
│     ├ media.ts         # マイク / カメラ / 画面共有
│     ├ webrtc.ts        # WebRTC 接続管理(P2P メッシュ)
│     ├ sfu.ts           # Cloudflare Realtime SFU 接続管理(大人数・会議室、simulcast)
│     ├ network.ts       # WebSocket 通信
│     ├ proximity.ts     # 近接判定(接続 / 切断のしきい値)
│     ├ recorder.ts      # 録画
│     ├ sounds.ts        # 効果音
│     ├ tilemap.ts       # マップ
│     ├ input.ts         # キー入力
│     └ types.ts         # 共有型定義
└ server/
   └ src/
      ├ index.ts         # Bun サーバーエントリ
      ├ websocket.ts     # WS メッセージハンドラ
      ├ logic.ts         # 入室・位置同期などのロジック
      ├ turn.ts          # Cloudflare TURN クレデンシャル発行
      ├ sfu.ts           # Cloudflare Realtime SFU 制御のプロキシ
      └ types.ts         # サーバー側型定義

⚙️ 設定(環境変数)

.env.env.example をコピーして作成):

PORT=3000

# Cloudflare TURN(任意 — 設定しなければ STUN のみで動作)
CLOUDFLARE_TURN_TOKEN_ID=
CLOUDFLARE_TURN_TOKEN_SECRET=

# Cloudflare Realtime SFU(任意 — 大人数・会議室の通話。未設定なら全員 P2P メッシュ)
CLOUDFLARE_REALTIME_APP_ID=
CLOUDFLARE_REALTIME_APP_TOKEN=

# 入室パスワード(任意 — 設定すると入室時にパスワードを求める。未設定ならオープン)
ACCESS_PASSWORD=
  • TURN を使う場合は Cloudflare Dashboard → Realtime → TURN で Token を作成して設定します。
  • SFU を使う場合は Cloudflare Dashboard → Realtime → SFU で App を作成し、App ID と Token を設定します。未設定でも全機能はメッシュで動作します(大人数時のスケールのみ制限されます)。
  • ACCESS_PASSWORD を設定すると、入室時にパスワードが必要になります(名前入力の前に確認)。未設定ならパスワードは求めません。

🐳 本番デプロイ(コンテナ)

本番用の host 非依存なコンテナイメージ を同梱しています(Dockerfile)。Bun サーバーがクライアントのビルド成果物を ./public から配信し、静的ファイル・/ws/api単一ポート / 同一オリジンで提供します。

docker build -t engawa .
docker run -p 3000:3000 \
  -e CLOUDFLARE_TURN_TOKEN_ID=xxx -e CLOUDFLARE_TURN_TOKEN_SECRET=yyy \
  -e CLOUDFLARE_REALTIME_APP_ID=zzz -e CLOUDFLARE_REALTIME_APP_TOKEN=www \
  engawa

v* タグを push すると GitHub Actions(.github/workflows/release.yml)が GHCR にイメージを公開します: ghcr.io/<owner>/engawa

Caddy 付きの本番サンプル(production_sample/docker_compose/

VPS などにそのまま置いて動かせるサンプルを production_sample/docker_compose/ に同梱しています。Caddy が Let's Encrypt で HTTPS を自動終端し、GHCR の公開イメージへリバースプロキシします。

# 実ドメインが解決でき、80/443 がインターネットから到達できるホストで:
cd production_sample/docker_compose
cp .env.example .env   # ENGAWA_DOMAIN / ACME_EMAIL / TURN などを設定
docker compose up -d
  • ENGAWA_DOMAIN の DNS を当ホストに向け、ポート 80/443 を開けておくと、Caddy が証明書を自動取得・更新します。
  • イメージは ENGAWA_IMAGE で指定(既定 ghcr.io/iitenkida7/engawa:latest)。本番では :vX.Y.Z のようにバージョンタグを固定するのを推奨。
  • server コンテナはホストにポート公開せず、Caddy 経由のみで到達します(単一オリジンで static・/ws/api を配信)。
  • 詳しい前提・構成は production_sample/docker_compose/README.md を参照。開発用の docker-compose.yml / Caddyfile(リポジトリ最上位。tls internal / engawa.localhost)とは別物です。

注意点:

  • TLS 終端はリバースプロキシ / プラットフォームのエッジの責務。アプリ自身は TLS を持ちません。fly.toml などの特定ホスティング向け設定は本リポジトリに含めません(各自の環境側で管理)。
  • ⚠️ 必ず 1 インスタンスで動かす。位置同期・シグナリングはインメモリの単一プロセスで、水平スケールすると別インスタンスのユーザーが見えなくなります(メディアは P2P なので台数に依存しません)。
  • TURN トークンや ACCESS_PASSWORD はイメージに焼かず、実行時の環境変数 / シークレットで渡します。

🧭 設計の指針(変えるときに外さないでほしい点)

  1. 音声 / 映像 / 画面共有は絶対に engawa サーバーを経由しない — P2P / TURN、または Cloudflare Realtime SFU 経由のみ(SFU でも自前サーバーはメディアを通さない)。
  2. シグナリングサーバーは状態を持たない — DB 不要。グループ情報や SFU の一時状態もメモリ上で完結し、再起動でリセットして良い。
  3. Cloudflare の API キー(TURN / SFU とも)はサーバー側のみ保持 — ブラウザには短期クレデンシャル / プロキシ経由のアクセスだけを渡す。
  4. HTTPS 必須(localhost 開発を除く)。

🎨 クレジット

アバター(キャラメイク)のドット絵素材は LPC(Universal-LPC-Spritesheet-Character-Generator を使用しています。

  • パーツごとの作者・ライセンス・出典 URL は engawa/client/src/assets/lpc/CREDITS.csv に個別に記録しています。各ライセンスの全文も同じディレクトリに同梱しています(LICENSE-CC-BY-SA-3.0.txt / LICENSE-CC-BY-3.0.txt / LICENSE-CC-BY-4.0.txt / LICENSE-OGA-BY-3.0.txt / LICENSE-CC0-1.0.txt / LICENSE-GPL-3.0.txt / LICENSE-GPL-2.0.txt)。
  • LPC アートのライセンスは CC-BY-SA 3.0 / CC-BY 3.0 / CC-BY 4.0 / OGA-BY 3.0 / GPL 2.0 / GPL 3.0 / CC0 がパーツごとに混在します。CC-BY-SA 3.0 は ShareAlike(合成・色変えなどの派生も同ライセンス)です。キャラメイク画面の下部にも「素材: LPC · クレジット」リンクを表示しています。
  • マップのタイル素材は Kenney の Roguelike Indoors(CC0) を使用しています(engawa/client/src/assets/KENNEY-roguelike-indoors-LICENSE.txt)。
  • これらの素材ライセンスは、engawa 本体のコードのライセンスとは分離して扱います。

🤝 開発に参加する

  • コミットメッセージ・コード中のコメントは英語で書きます。
  • TypeScript strict モード / セミコロンあり / シングルクォート / インデント 2 スペース。
  • アプリに関する操作(ビルド・テスト・依存追加など)はすべて Docker 経由で行います。

詳しいルールは CLAUDE.md を参照してください。

About

virtual office

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages