ボイス
0.2 で追加しました。Earshot は、声がゲームの世界の中にあるように聞こえるようにします。既存の RTCPeerConnection か、後述のボイスルームを使えます。私たちがサーバーをホストすることはありません。
ボイスチャット
import { createVoiceMic } from "@debuffjp/earshot";
const mic = createVoiceMic({ canUseVoice: () => settings.voiceAllowed });
const { ok, error } = await mic.start(); // タップやキー入力から呼びます。
const stream = mic.stream;
if (ok && stream) stream.getTracks().forEach((track) => connection.addTrack(track, stream));
connection.ontrack = (event) => {
const voice = ear.voice(event.streams[0], { pos: () => friend });
// voice.state、voice.speaking、voice.levelDb は UI に使えます。
};声は他の音源と同じように壁、部屋、パンを通り、同じように ear.update() が必要です。ear.voice() には OfflineAudioContext ではなく AudioContext が必要です。
| ボイスのプロパティ | 意味 |
|---|---|
voice.muted | このプレイヤーを手元でミュートします |
voice.levelDb | 距離と壁を通す前の音量(dB) |
voice.speaking | 発話インジケーター用。距離と壁を無視するので、範囲が重要なら state も確認してください |
voice.state、voice.mix、voice.reverb | 他の音源と同じです |
voice.remove() | このボイスを切り離します |
speaking は speakingDb を超えるとオンになります。speakingDb は ear.voice() のオプションで、デフォルトは -50 です。
マイクの操作
マイクはゲームが求めるまでオフで、何も録音しません。
mic.muted = true;
mic.pushToTalk = true;
mic.talk(true); // pushToTalk がオンの間、押している間だけ話せます。
mic.talk(false);
mic.live; // 音声を送信中なら true。プレイヤーに表示してください。
mic.stop();canUseVoice は必須で、false の間はマイクを開きません。同意の確認や、ポータルの disableChat などのフラグに使ってください。このとき start() は { ok: false, error: "not-allowed" } を返します。確認するのは start() を呼んだときだけなので、後から許可がなくなったら自分で mic.stop() を呼んでください。
start() は reject しません。ブラウザに拒否されると、error はブラウザのエラー名です。
| オプション | 意味 | デフォルト |
|---|---|---|
canUseVoice() | このプレイヤーがボイスを使えるかの確認。必須 | |
echoCancellation | true、false、"all"、"remote-only"。"all" は他の音声のキャンセルも、可能な範囲でブラウザに求めます | true |
pushToTalk | オンの間、マイクは talk(true) のときだけ送信します | false |
mic.applied は、ブラウザが実際に適用した設定です。ノイズ抑制と自動ゲインは常に要求します。
ボイスルーム
接続の仕組みがまだなければ、joinVoiceRoom() を使えます。自分のアカウントにデプロイする小さな Cloudflare Worker でプレイヤーをつなぎます。私たちはホストしません。招待制のルーム、人数の上限、短命な TURN 認証情報を備えていて、ログインや BAN の確認は canJoin 関数に書きます。
import { createRoomCode, joinVoiceRoom, type Point } from "@debuffjp/earshot";
const code = createRoomCode(); // フレンドに共有します。
const joined = await joinVoiceRoom({
url: "wss://earshot-voice.you.workers.dev",
code,
mic,
listener: ear,
ipPolicy: "relay", // または "direct"(後述)。
pos: (peer) => players.get(peer) ?? spawn,
onMessage: (peer, position) => players.set(peer, position as Point),
});
if (joined.ok) setInterval(() => joined.room.send(player), 66);参加する前に mic を開始しておいてください。pos は最初の位置が届く前のプレイヤーにも呼ばれるので、代わりの値を返せるようにします。コードを知っていれば誰でも参加できるので、共有はフレンドだけにしてください。
joinVoiceRoom() は reject しません。ok: true はルームに入ったことを表し、各プレイヤーとの接続はまだ確立中かもしれません。
| オプション | 意味 |
|---|---|
onPeer(peer, voice) | プレイヤーの声が届きました |
onLeave(peer) | プレイヤーが退出しました。自分が退出するときも全員分呼ばれます |
onMessage(peer, message) | room.send の小さなメッセージ。遅れて届くことも、届かないこともあります |
| ルームのプロパティ | 意味 |
|---|---|
room.id | このプレイヤーの ID |
room.peers | ID ごとの各プレイヤーの Voice |
room.connection(peer) | UI 用の接続状態:"connecting"、"connected"、"failed" など |
room.send(message) | 接続中の全プレイヤーに JSON を送ります。Worker を通さず直接送ります |
room.leave() | ルームから退出します。マイクは mic.stop() まで開いたままです |
ipPolicy
ipPolicy はプライバシーに関わる判断なので必須です。"relay" は他のプレイヤーから IP アドレスを隠し、中継できないときは接続に失敗します。すべての音声は TURN のトラフィックとして課金されます。"direct" は安く済みますが、互いの IP アドレスが見えます。
費用の表はセルフホストにあります。
接続とエラー
全員が全員に接続するので、人数に応じてアップロードが増えます。十分に離れたプレイヤー(silentRadius の 1.25 倍より遠く)への送信は止まります。
エラーは joined.error に返ります。
| エラー | 意味 |
|---|---|
room-full | ルームが Worker の MAX_PLAYERS に達しています |
no-turn-server | ipPolicy が "relay" なのに、Worker が TURN サーバーを提供しませんでした |
mic-not-started | mic.start() が成功していません |
connection-failed | Worker に接続できないか、接続を拒否されました |
切断された接続は自動では再接続しないので、room.connection(peer) で状態を表示してください。
開発者の責任
これらの機能だけでは、法令への準拠にはなりません。 プレイヤー間のライブ音声には、米国の COPPA や英国の Online Safety Act などの義務を伴う場合があります。子ども向けのゲームでは特に注意してください。年齢確認、BAN の強制、モデレーション、通報の受付はブラウザのライブラリでは扱えません。ゲーム側で計画してください。ボイスはデフォルトでオフにし、フレンドに限定してください。これは法的助言ではありません。
テスト環境
テストしたのは、同じネットワーク上の PC と iPhone、およびデスクトップの Chromium と Firefox の間のリレー接続です。より大きなグループは未テストです。
Earshot 0.2 のリリース記事でも、これらの機能を紹介しています。