333 lines
8.3 KiB
Markdown
333 lines
8.3 KiB
Markdown
# MediaControl WebRTC Signaling Protocol
|
|
|
|
This document describes the clean, pluggable API for the MediaControl component used for peer-to-peer audio/video communication.
|
|
|
|
## Overview
|
|
|
|
The MediaControl component provides a reusable WebRTC-based media communication system that can be integrated into any application. It handles:
|
|
- Peer-to-peer audio and video streaming
|
|
- WebRTC connection management (offers, answers, ICE candidates)
|
|
- Signaling via WebSocket
|
|
- User controls for muting/unmuting and video on/off
|
|
|
|
## Server-Side Protocol
|
|
|
|
### Required WebSocket Message Types
|
|
|
|
#### 1. Join Request (Client → Server)
|
|
```typescript
|
|
{
|
|
type: "join",
|
|
data: {
|
|
has_media?: boolean // Whether this peer provides audio/video (default: true)
|
|
}
|
|
}
|
|
```
|
|
|
|
#### 2. Join Status Response (Server → Client)
|
|
```typescript
|
|
{
|
|
type: "join_status",
|
|
status: "Joined" | "Joining" | "Error",
|
|
message?: string
|
|
}
|
|
```
|
|
|
|
#### 3. Add Peer (Server → Client)
|
|
Sent to all existing peers when a new peer joins, and sent to the new peer for each existing peer.
|
|
```typescript
|
|
{
|
|
type: "addPeer",
|
|
data: {
|
|
peer_id: string, // Unique identifier (session name)
|
|
peer_name: string, // Display name for the peer
|
|
has_media: boolean, // Whether this peer provides media
|
|
should_create_offer: boolean, // If true, create an RTC offer to this peer
|
|
// Legacy fields (optional, for backward compatibility):
|
|
hasAudio?: boolean,
|
|
hasVideo?: boolean
|
|
}
|
|
}
|
|
```
|
|
|
|
#### 4. Remove Peer (Server → Clients)
|
|
Sent when a peer disconnects.
|
|
```typescript
|
|
{
|
|
type: "removePeer",
|
|
data: {
|
|
peer_id: string,
|
|
peer_name: string
|
|
}
|
|
}
|
|
```
|
|
|
|
#### 5. Relay ICE Candidate (Client → Server → Peer)
|
|
```typescript
|
|
// Client sends:
|
|
{
|
|
type: "relayICECandidate",
|
|
data: {
|
|
peer_id: string, // Target peer
|
|
candidate: RTCIceCandidateInit
|
|
}
|
|
}
|
|
|
|
// Server relays to target peer:
|
|
{
|
|
type: "iceCandidate",
|
|
data: {
|
|
peer_id: string, // Source peer
|
|
peer_name: string,
|
|
candidate: RTCIceCandidateInit
|
|
}
|
|
}
|
|
```
|
|
|
|
#### 6. Relay Session Description (Client → Server → Peer)
|
|
```typescript
|
|
// Client sends:
|
|
{
|
|
type: "relaySessionDescription",
|
|
data: {
|
|
peer_id: string, // Target peer
|
|
session_description: RTCSessionDescriptionInit
|
|
}
|
|
}
|
|
|
|
// Server relays to target peer:
|
|
{
|
|
type: "sessionDescription",
|
|
data: {
|
|
peer_id: string, // Source peer
|
|
peer_name: string,
|
|
session_description: RTCSessionDescriptionInit
|
|
}
|
|
}
|
|
```
|
|
|
|
#### 7. Peer State Update (Client → Server → All Peers)
|
|
```typescript
|
|
// Client sends:
|
|
{
|
|
type: "peer_state_update",
|
|
data: {
|
|
peer_id: string,
|
|
muted: boolean,
|
|
video_on: boolean
|
|
}
|
|
}
|
|
|
|
// Server broadcasts to all other peers:
|
|
{
|
|
type: "peer_state_update",
|
|
data: {
|
|
peer_id: string,
|
|
peer_name: string,
|
|
muted: boolean,
|
|
video_on: boolean
|
|
}
|
|
}
|
|
```
|
|
|
|
## Server Implementation Requirements
|
|
|
|
### Peer Registry
|
|
The server must maintain a registry of connected peers for each room/game:
|
|
|
|
```typescript
|
|
interface PeerInfo {
|
|
ws: WebSocket; // WebSocket connection
|
|
has_media: boolean; // Whether peer provides media
|
|
hasAudio?: boolean; // Legacy: has audio
|
|
hasVideo?: boolean; // Legacy: has video
|
|
}
|
|
|
|
const peers: Record<string, PeerInfo> = {};
|
|
```
|
|
|
|
### Join Handler
|
|
```typescript
|
|
function join(peers: any, session: any, config: {
|
|
has_media?: boolean,
|
|
hasVideo?: boolean,
|
|
hasAudio?: boolean
|
|
}) {
|
|
const { has_media, hasVideo, hasAudio } = config;
|
|
const peerHasMedia = has_media ?? (hasVideo || hasAudio);
|
|
|
|
// Notify all existing peers about new peer
|
|
for (const peer in peers) {
|
|
peers[peer].ws.send(JSON.stringify({
|
|
type: "addPeer",
|
|
data: {
|
|
peer_id: session.name,
|
|
peer_name: session.name,
|
|
has_media: peerHasMedia,
|
|
should_create_offer: false
|
|
}
|
|
}));
|
|
}
|
|
|
|
// Notify new peer about all existing peers
|
|
for (const peer in peers) {
|
|
session.ws.send(JSON.stringify({
|
|
type: "addPeer",
|
|
data: {
|
|
peer_id: peer,
|
|
peer_name: peer,
|
|
has_media: peers[peer].has_media,
|
|
should_create_offer: true
|
|
}
|
|
}));
|
|
}
|
|
|
|
// Add new peer to registry
|
|
peers[session.name] = {
|
|
ws: session.ws,
|
|
has_media: peerHasMedia,
|
|
hasAudio,
|
|
hasVideo
|
|
};
|
|
|
|
// Send success status
|
|
session.ws.send(JSON.stringify({
|
|
type: "join_status",
|
|
status: "Joined",
|
|
message: "Successfully joined"
|
|
}));
|
|
}
|
|
```
|
|
|
|
## Client-Side Integration
|
|
|
|
### Session Type
|
|
```typescript
|
|
interface Session {
|
|
id: string;
|
|
name: string | null;
|
|
has_media?: boolean; // Whether this session provides audio/video
|
|
// ... other fields
|
|
}
|
|
```
|
|
|
|
### MediaControl Components
|
|
|
|
#### MediaAgent
|
|
Handles WebRTC signaling and connection management. Does not render UI.
|
|
|
|
```typescript
|
|
import { MediaAgent } from './MediaControl';
|
|
|
|
<MediaAgent
|
|
socketUrl={socketUrl}
|
|
session={session}
|
|
peers={peers}
|
|
setPeers={setPeers}
|
|
/>
|
|
```
|
|
|
|
#### MediaControl
|
|
Renders video feed and controls for a single peer.
|
|
|
|
```typescript
|
|
import { MediaControl } from './MediaControl';
|
|
|
|
<MediaControl
|
|
isSelf={peer.local}
|
|
peer={peer}
|
|
sendJsonMessage={sendJsonMessage}
|
|
remoteAudioMuted={peer.muted}
|
|
remoteVideoOff={!peer.video_on}
|
|
/>
|
|
```
|
|
|
|
### Peer State Management
|
|
```typescript
|
|
const [peers, setPeers] = useState<Record<string, Peer>>({});
|
|
```
|
|
|
|
## API Endpoints
|
|
|
|
### GET /api/v1/games/
|
|
Returns session information including media capability:
|
|
```typescript
|
|
{
|
|
id: string,
|
|
name: string | null,
|
|
has_media: boolean, // Default: true for regular users
|
|
lobbies: Room[]
|
|
}
|
|
```
|
|
|
|
## Migration Guide
|
|
|
|
### From Legacy API
|
|
If your server currently uses `hasVideo`/`hasAudio`:
|
|
|
|
1. Add `has_media` field to all messages (computed as `hasVideo || hasAudio`)
|
|
2. Add `peer_name` field to all peer-related messages (same as `peer_id`)
|
|
3. Add `join_status` response to join requests
|
|
4. Update session endpoint to return `has_media`
|
|
|
|
### Backward Compatibility
|
|
The protocol supports both old and new field names:
|
|
- Server accepts `has_media`, `hasVideo`, and `hasAudio`
|
|
- Server sends both old and new fields during transition period
|
|
|
|
## Reconnection Handling
|
|
|
|
The server handles WebSocket reconnections gracefully:
|
|
|
|
1. **On Reconnection**: When a peer reconnects (new WebSocket for existing session):
|
|
- Old peer is removed from registry via `part()`
|
|
- New WebSocket reference is updated in peer registry
|
|
- `join_status` response sent with "Reconnected" message
|
|
- All existing peers are sent to reconnecting client via `addPeer`
|
|
- All other peers are notified about the reconnected peer
|
|
|
|
2. **Client Behavior**: Clients should:
|
|
- Wait for `join_status` before considering themselves joined
|
|
- Handle `addPeer` messages even after initial join (for reconnections)
|
|
- Re-establish peer connections when receiving `addPeer` for known peers
|
|
|
|
## Security Considerations
|
|
|
|
1. **STUN/TURN Configuration**: Configure ICE servers in MediaControl.tsx (lines 462-474)
|
|
2. **Peer Authentication**: Validate peer identities before relaying signaling messages
|
|
3. **Rate Limiting**: Limit signaling message frequency to prevent abuse
|
|
4. **Room Isolation**: Ensure peers can only connect to others in the same room/game
|
|
|
|
## Troubleshooting
|
|
|
|
### Peers Not Connecting
|
|
1. Check `join_status` response is "Joined" or "Reconnected"
|
|
2. Verify `addPeer` messages include both `peer_id` and `peer_name`
|
|
3. Check ICE candidates are being relayed correctly
|
|
4. Verify STUN/TURN servers are accessible
|
|
5. Check server logs for "Already joined" messages (indicates reconnection scenario)
|
|
|
|
### No Video/Audio
|
|
1. Check `has_media` is set correctly in session
|
|
2. Verify browser permissions for camera/microphone
|
|
3. Check peer state updates are being broadcast
|
|
4. Verify tracks are enabled in MediaStream
|
|
|
|
### "Already Joined" Issues
|
|
If peers show as "Already joined" but can't connect:
|
|
1. Check that old WebSocket connections are being cleaned up on reconnect
|
|
2. Verify `part()` is called when WebSocket is replaced
|
|
3. Ensure peer registry is updated with new WebSocket reference
|
|
|
|
## Testing
|
|
|
|
### Manual Test Checklist
|
|
- [ ] Join shows "Joined" status
|
|
- [ ] Existing peers appear in peer list
|
|
- [ ] New peer appears to existing peers
|
|
- [ ] Video/audio streams connect
|
|
- [ ] Mute/unmute works locally and remotely
|
|
- [ ] Video on/off works locally and remotely
|
|
- [ ] Peer removal cleans up connections
|
|
- [ ] Reconnection after disconnect works
|