本文へスキップ

ボイス ​

0.2 で追加しました。Earshot は、声がゲームの世界の中にあるように聞こえるようにします。既存の RTCPeerConnection か、後述のボイスルームを使えます。私たちがサーバーをホストすることはありません。

ボイスチャット ​

ts
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 です。

マイクの操作 ​

マイクはゲームが求めるまでオフで、何も録音しません。

ts
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()このプレイヤーがボイスを使えるかの確認。必須
echoCancellationtrue、false、"all"、"remote-only"。"all" は他の音声のキャンセルも、可能な範囲でブラウザに求めますtrue
pushToTalkオンの間、マイクは talk(true) のときだけ送信しますfalse

mic.applied は、ブラウザが実際に適用した設定です。ノイズ抑制と自動ゲインは常に要求します。

ボイスルーム ​

接続の仕組みがまだなければ、joinVoiceRoom() を使えます。自分のアカウントにデプロイする小さな Cloudflare Worker でプレイヤーをつなぎます。私たちはホストしません。招待制のルーム、人数の上限、短命な TURN 認証情報を備えていて、ログインや BAN の確認は canJoin 関数に書きます。

ts
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.peersID ごとの各プレイヤーの 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-serveripPolicy が "relay" なのに、Worker が TURN サーバーを提供しませんでした
mic-not-startedmic.start() が成功していません
connection-failedWorker に接続できないか、接続を拒否されました

切断された接続は自動では再接続しないので、room.connection(peer) で状態を表示してください。

開発者の責任 ​

これらの機能だけでは、法令への準拠にはなりません。 プレイヤー間のライブ音声には、米国の COPPA や英国の Online Safety Act などの義務を伴う場合があります。子ども向けのゲームでは特に注意してください。年齢確認、BAN の強制、モデレーション、通報の受付はブラウザのライブラリでは扱えません。ゲーム側で計画してください。ボイスはデフォルトでオフにし、フレンドに限定してください。これは法的助言ではありません。

テスト環境 ​

テストしたのは、同じネットワーク上の PC と iPhone、およびデスクトップの Chromium と Firefox の間のリレー接続です。より大きなグループは未テストです。

Earshot 0.2 のリリース記事でも、これらの機能を紹介しています。