メインコンテンツまでスキップ

Signaling API

シグナリングサーバーとの通信API定義。


1. 概要

Signalingモジュールは以下の責務を持つ:

  • ルームの作成・参加・退出
  • ICE候補の交換
  • セッション情報の交換
  • 参加者リストの管理

シグナリングはP2P接続確立のための補助であり、音声データは経由しない。

1.1 通信フロー


2. プロトコル

項目仕様
トランスポートWebSocket over TLS
フォーマットJSON
エンドポイントwss://signaling.jamjam.example/v1

3. クライアント API

実装状況: 基本的なシグナリング機能(SignalingClient, SignalingConnection)は実装済み。 高レベルAPI(create_room(), join_room_by_code()等)は SignalingMessage enum 経由で提供。

3.1 接続

/// シグナリングサーバーに接続
///
/// スレッド: 非リアルタイムスレッドから呼び出すこと
/// ブロッキング: No(非同期)
async fn connect(server_url: &str) -> Result<SignalingClient, SignalingError>;

3.2 ルーム作成

実装状況: SignalingMessage::CreateRoom で実装済み。 招待コード生成も実装済み。招待URLは将来の拡張として計画中。

/// ルームを作成(現在の実装)
///
/// SignalingMessage::CreateRoom を使用:
/// - room_name: ルーム名
/// - password: パスワード(オプション)
/// - peer_name: 参加者名
///
/// 戻り値: SignalingMessage::RoomCreated { room_id, peer_id, invite_code }

// --- 将来の拡張(計画中)---

/// 高レベルAPI(計画中)
async fn create_room(&self, options: CreateRoomOptions) -> Result<CreateRoomResult, SignalingError>;

struct CreateRoomOptions {
/// ルーム名(オプション)
name: Option<String>,
/// パスワード(オプション、設定するとパスワード保護)
password: Option<String>,
/// 最大参加者数
max_participants: u32,
/// パブリックルームとして公開するか
is_public: bool,
}

struct CreateRoomResult {
/// ルームID
room_id: String,
/// 招待コード(実装済み)
invite_code: String,
/// 招待URL(計画中)
invite_url: String,
}

3.3 ルーム参加

実装状況: SignalingMessage::JoinRoom で実装済み。 招待コードでの参加(JoinRoomByCode)も実装済み。

/// ルームに参加(現在の実装)
///
/// SignalingMessage::JoinRoom を使用:
/// - room_id: ルームID
/// - password: パスワード(オプション)
/// - peer_name: 参加者名
///
/// 戻り値: SignalingMessage::RoomJoined { room_id, peer_id, peers: Vec<PeerInfo> }

// --- 高レベルAPI(計画中)---

/// ルームに参加(招待コードで、SignalingMessage::JoinRoomByCode で実装済み)
async fn join_room_by_code(
&self,
invite_code: &str,
options: JoinRoomOptions,
) -> Result<JoinRoomResult, SignalingError>;

/// ルームに参加(ルームIDで、高レベルAPI計画中)
async fn join_room_by_id(
&self,
room_id: &str,
options: JoinRoomOptions,
) -> Result<JoinRoomResult, SignalingError>;

struct JoinRoomOptions {
/// 表示名
display_name: String,
/// パスワード(パスワード保護されたルームの場合)
password: Option<String>,
}

/// 参加結果(計画中の拡張版)
struct JoinRoomResult {
/// 自分のセッションID
session_id: String,
/// 自分の参加者ID(UUID)
peer_id: Uuid,
/// ルーム情報
room: RoomDetails,
/// 既存の参加者一覧
peers: Vec<PeerInfo>,
}

/// ルーム詳細(計画中の拡張版)
struct RoomDetails {
room_id: String,
name: Option<String>,
host_id: Uuid,
created_at: u64,
}

Note: 現在の実装では PeerInfo を参加者情報として使用。詳細は Section 5 を参照。

3.4 ルーム退出

/// ルームから退出
async fn leave_room(&self) -> Result<(), SignalingError>;

3.5 ルーム終了(ホストのみ)

/// ルームを終了(全員を切断)
///
/// ホストのみ実行可能
async fn close_room(&self) -> Result<(), SignalingError>;

4. ICE候補交換 API

実装状況: 計画中。現在の実装ではICE候補交換はシグナリングサーバー経由ではなく、 直接接続(UpdatePeerInfoでアドレス情報を交換)で対応。 WebRTC互換のICE/SDP交換は将来の拡張として計画中。

4.1 ICE候補送信

/// ICE候補を送信
///
/// # 引数
/// - target: 送信先参加者ID
/// - candidate: ICE候補
async fn send_ice_candidate(
&self,
target: ParticipantId,
candidate: IceCandidate,
) -> Result<(), SignalingError>;

struct IceCandidate {
/// 候補文字列
candidate: String,
/// SDPミッドライン
sdp_mid: String,
/// SDPミッドラインインデックス
sdp_mline_index: u32,
}

4.2 SDP交換

/// SDPオファーを送信
async fn send_offer(
&self,
target: ParticipantId,
offer: SessionDescription,
) -> Result<(), SignalingError>;

/// SDPアンサーを送信
async fn send_answer(
&self,
target: ParticipantId,
answer: SessionDescription,
) -> Result<(), SignalingError>;

struct SessionDescription {
/// SDP種別(offer / answer)
sdp_type: String,
/// SDP文字列
sdp: String,
}

5. メッセージ型

実装状況: 実装済み。src/network/signaling.rs で定義。

シグナリングプロトコルで使用されるメッセージ型。 クライアント↔サーバー間の双方向通信で使用される。

/// シグナリングメッセージ
///
/// WebSocket JSON形式で送受信される。
/// serde: adjacently tagged format - `#[serde(tag = "type", content = "data")]`
enum SignalingMessage {
// --- Client → Server ---
/// ルーム一覧を取得
ListRooms,
/// ルームを作成
CreateRoom {
room_name: String,
password: Option<String>,
peer_name: String,
},
/// ルームに参加
JoinRoom {
room_id: String,
password: Option<String>,
peer_name: String,
},
/// ルームから退出
LeaveRoom,
/// ピア情報を更新(アドレス候補付き)
UpdatePeerInfo {
/// アドレス候補リスト(優先度順)
candidates: Vec<AddressCandidate>,
/// 後方互換用のパブリックアドレス
public_addr: Option<SocketAddr>,
/// 後方互換用のローカルアドレス
local_addr: Option<SocketAddr>,
},

// --- Server → Client ---
/// ルーム一覧
RoomList { rooms: Vec<RoomInfo> },
/// ルーム作成完了
RoomCreated {
room_id: String,
peer_id: Uuid,
/// 招待コード(ルーム参加に使用)
invite_code: String,
},
/// ルーム参加完了
RoomJoined {
room_id: String,
peer_id: Uuid,
peers: Vec<PeerInfo>,
},
/// ピアが参加
PeerJoined { peer: PeerInfo },
/// ピアが退出
PeerLeft { peer_id: Uuid },
/// ピア情報が更新された
PeerUpdated { peer: PeerInfo },
/// エラー
Error { message: String },
}

/// ピア情報(複数アドレス候補対応)
struct PeerInfo {
id: Uuid,
name: String,
/// アドレス候補リスト(優先度順、IPv4/IPv6デュアルスタック対応)
candidates: Vec<AddressCandidate>,
/// 後方互換用のパブリックアドレス
public_addr: Option<SocketAddr>,
/// 後方互換用のローカルアドレス
local_addr: Option<SocketAddr>,
}

/// アドレス候補
struct AddressCandidate {
/// ソケットアドレス
address: SocketAddr,
/// 候補タイプ(Host = ローカル, ServerReflexive = STUN経由)
candidate_type: CandidateType,
/// RFC 5245準拠の優先度(大きいほど優先)
priority: u32,
}

enum CandidateType {
/// ローカルネットワークアドレス
Host,
/// STUN経由で取得したパブリックアドレス
ServerReflexive,
}

/// ルーム情報
struct RoomInfo {
id: String,
name: String,
peer_count: usize,
max_peers: usize,
has_password: bool,
/// 招待コード(ルーム参加に使用)
invite_code: String,
}

/// メッセージリスナーを設定
fn set_message_listener<F>(&self, listener: F)
where
F: Fn(SignalingMessage) + Send + 'static;

5.1 接続状態遷移


6. エラー

enum SignalingError {
/// 接続失敗
ConnectionFailed(String),
/// ルームが見つからない
RoomNotFound,
/// ルームが満員
RoomFull,
/// パスワードが違う
InvalidPassword,
/// 権限がない
Unauthorized,
/// タイムアウト
Timeout,
/// サーバーエラー
ServerError(String),
/// 内部エラー
Internal(String),
}

7. サーバーサイドプロトコル

7.1 メッセージフォーマット

{
"type": "message_type",
"payload": { ... },
"timestamp": 1234567890
}

7.2 クライアント → サーバー メッセージ

create_room

{
"type": "create_room",
"payload": {
"name": "My Room",
"password_hash": "sha256_hash_or_null",
"max_participants": 10,
"is_public": false
}
}

join_room

{
"type": "join_room",
"payload": {
"room_id": "uuid",
"invite_code": "ABC123",
"display_name": "Player1",
"password_hash": "sha256_hash_or_null"
}
}

leave_room

{
"type": "leave_room",
"payload": {}
}

ice_candidate

{
"type": "ice_candidate",
"payload": {
"target": "participant_id",
"candidate": {
"candidate": "candidate:...",
"sdp_mid": "0",
"sdp_mline_index": 0
}
}
}

offer

{
"type": "offer",
"payload": {
"target": "participant_id",
"sdp": {
"type": "offer",
"sdp": "v=0\r\n..."
}
}
}

answer

{
"type": "answer",
"payload": {
"target": "participant_id",
"sdp": {
"type": "answer",
"sdp": "v=0\r\n..."
}
}
}

7.3 サーバー → クライアント メッセージ

room_created

{
"type": "room_created",
"payload": {
"room_id": "uuid",
"invite_code": "ABC123",
"invite_url": "jamjam://join/ABC123"
}
}

room_joined

{
"type": "room_joined",
"payload": {
"session_id": "uuid",
"participant_id": "uuid",
"room": {
"room_id": "uuid",
"name": "My Room",
"host_id": "uuid",
"created_at": 1234567890
},
"participants": [
{
"id": "uuid",
"display_name": "Player1",
"joined_at": 1234567890
}
]
}
}

participant_joined

{
"type": "participant_joined",
"payload": {
"id": "uuid",
"display_name": "Player2",
"joined_at": 1234567890
}
}

participant_left

{
"type": "participant_left",
"payload": {
"id": "uuid"
}
}

ice_candidate

{
"type": "ice_candidate",
"payload": {
"from": "participant_id",
"candidate": { ... }
}
}

offer

{
"type": "offer",
"payload": {
"from": "participant_id",
"sdp": { ... }
}
}

answer

{
"type": "answer",
"payload": {
"from": "participant_id",
"sdp": { ... }
}
}

room_closed

{
"type": "room_closed",
"payload": {
"reason": "host_closed"
}
}

error

{
"type": "error",
"payload": {
"code": "room_not_found",
"message": "Room not found"
}
}

8. 使用例

// シグナリングサーバーに接続
let client = SignalingClient::new("wss://signaling.jamjam.example/v1");
let mut conn = client.connect().await?;

// ルーム作成
conn.send(SignalingMessage::CreateRoom {
room_name: "Guitar Session".into(),
password: None,
peer_name: "Host".into(),
}).await?;

match conn.recv().await? {
SignalingMessage::RoomCreated { room_id, peer_id, invite_code } => {
println!("Room created: {} (peer: {}, invite: {})", room_id, peer_id, invite_code);
}
SignalingMessage::Error { message } => {
return Err(anyhow::anyhow!("Failed: {}", message));
}
_ => {}
}

// または、ルーム参加
conn.send(SignalingMessage::JoinRoom {
room_id: "ABC123".into(),
password: None,
peer_name: "Player1".into(),
}).await?;

match conn.recv().await? {
SignalingMessage::RoomJoined { room_id, peer_id, peers } => {
println!("Joined room: {} as {}", room_id, peer_id);
for peer in &peers {
println!(" Peer: {} ({})", peer.name, peer.id);
}
}
SignalingMessage::Error { message } => {
return Err(anyhow::anyhow!("Failed: {}", message));
}
_ => {}
}

// 退出
conn.send(SignalingMessage::LeaveRoom).await?;