@ajna-inc/webrtc
v0.2.1
Published
DIDComm v2 signaling protocol for WebRTC P2P calls with full NAT/firewall traversal support.
Readme
@ajna-inc/webrtc
DIDComm v2 signaling protocol for WebRTC P2P calls with full NAT/firewall traversal support.
- PIURI:
https://didcomm.org/webrtc/1.0 - Messages:
propose,offer,answer,ice,renegotiate,end - Advertises support via Discover Features; works with mediators and message pickup.
Features
- Module-level ICE server defaults - Configure STUN/TURN servers once
- ICE policy control -
all,relay-preferred, orrelay-onlyfor strict firewall environments - ICE restart support - Recover from failed connections
- Trickle ICE - Efficient candidate exchange
- Propose message - Share ICE servers before SDP exchange
Install
This package is part of the monorepo. Add the module to your Agent options:
import { WebRTCModule } from '@ajna-inc/webrtc'
const agent = new Agent({
config: { /* ... */ },
dependencies: agentDependencies,
modules: {
webrtc: new WebRTCModule({
// Configure default ICE servers (optional - has sensible defaults)
iceServers: [
{ urls: 'stun:stun.l.google.com:19302' },
{ urls: 'turn:turn.example.org:3478', username: 'user', credential: 'pass' },
],
// Default ICE policy: 'all' | 'relay-preferred' | 'relay-only'
defaultPolicy: 'relay-preferred',
// Enable trickle ICE by default
defaultTrickle: true,
}),
},
})NAT/Firewall Traversal
For peers behind restrictive NAT or firewalls, use TURN servers:
const agent = new Agent({
modules: {
webrtc: new WebRTCModule({
iceServers: [
// STUN for simple NAT
{ urls: 'stun:stun.l.google.com:19302' },
// TURN for symmetric NAT and firewalls
{ urls: 'turn:turn.yourserver.com:3478', username: 'user', credential: 'pass' },
// TURNS (TLS) for stricter firewalls
{ urls: 'turns:turn.yourserver.com:5349', username: 'user', credential: 'pass' },
],
// Force relay-only for maximum compatibility (hides IP addresses too)
defaultPolicy: 'relay-only',
}),
},
})Frontend usage (browser)
Below is a setup for P2P WebRTC using Credo TS for signaling.
// Get configured ICE servers from module
const iceServers = agent.modules.webrtc.getDefaultIceServers()
// Subscribe to inbound signaling
agent.events.on('WebRTCEvents.IncomingPropose', async ({ payload }) => {
const { thid, iceServers, policy, media } = payload
// Caller is proposing a call - you can accept by sending an offer
// The iceServers from propose can be used to configure RTCPeerConnection
console.log('Incoming call proposal:', { media, iceServers })
})
agent.events.on('WebRTCEvents.IncomingOffer', async ({ payload }) => {
const { thid, sdp, iceServers, policy, context } = payload
// Use ICE servers from offer (or fall back to module defaults)
const pc = new RTCPeerConnection({
iceServers: iceServers ?? agent.modules.webrtc.getDefaultIceServers(),
iceTransportPolicy: policy === 'relay-only' ? 'relay' : 'all',
})
peers.set(thid, pc)
await pc.setRemoteDescription({ type: 'offer', sdp })
const answer = await pc.createAnswer()
await pc.setLocalDescription(answer)
await agent.modules.webrtc.acceptCall({
connectionId: context.connection!.id,
threadId: thid,
sdp: answer.sdp!,
})
})
agent.events.on('WebRTCEvents.IncomingAnswer', async ({ payload }) => {
const { thid, sdp, iceServers } = payload
const pc = peers.get(thid)
if (!pc) return
// If callee provided additional ICE servers, you might want to restart ICE
await pc.setRemoteDescription({ type: 'answer', sdp })
})
agent.events.on('WebRTCEvents.IncomingIce', async ({ payload }) => {
const { thid, candidate, endOfCandidates } = payload
const pc = peers.get(thid)
if (!pc) return
if (endOfCandidates) await pc.addIceCandidate(null)
else await pc.addIceCandidate(candidate as RTCIceCandidateInit)
})
// Handle renegotiation requests (e.g., ICE restart)
agent.events.on('WebRTCEvents.RenegotiateRequested', async ({ payload }) => {
const { thid, iceRestart, iceServers, reason } = payload
const pc = peers.get(thid)
if (!pc) return
if (iceRestart) {
// Perform ICE restart
const offer = await pc.createOffer({ iceRestart: true })
await pc.setLocalDescription(offer)
// Send new offer...
}
})
// Start call (caller)
async function startCall(connectionId: string, localStream: MediaStream) {
const thid = crypto.randomUUID()
// Use module's default ICE servers
const iceServers = agent.modules.webrtc.getDefaultIceServers()
const pc = new RTCPeerConnection({ iceServers })
peers.set(thid, pc)
// Handle ICE connection failures
pc.oniceconnectionstatechange = () => {
if (pc.iceConnectionState === 'failed') {
// Request ICE restart
agent.modules.webrtc.restartIce({ connectionId, threadId: thid })
}
}
// Render remote stream
pc.ontrack = e => (remoteVideo.srcObject = e.streams[0])
// Send ICE via DIDComm
pc.onicecandidate = e => agent.modules.webrtc.sendIce({
connectionId,
threadId: thid,
candidate: e.candidate ?? undefined,
endOfCandidates: e.candidate == null,
})
// Add local tracks
for (const track of localStream.getTracks()) pc.addTrack(track, localStream)
const offer = await pc.createOffer({ offerToReceiveVideo: true, offerToReceiveAudio: true })
await pc.setLocalDescription(offer)
// Start call - ICE servers are automatically included from module config
await agent.modules.webrtc.startCall({
connectionId,
threadId: thid,
sdp: offer.sdp!,
// Optionally override policy for this specific call
policy: 'relay-preferred',
})
}
// Request ICE restart when connection fails
async function handleConnectionFailure(connectionId: string, thid: string) {
await agent.modules.webrtc.restartIce({
connectionId,
threadId: thid,
// Optionally provide new ICE servers
iceServers: [{ urls: 'turn:backup-turn.example.org:3478', username: 'u', credential: 'p' }],
})
}API Reference
WebRTCApi Methods
| Method | Description |
|--------|-------------|
| proposeCall() | Send a call proposal with ICE servers before offer |
| startCall() | Send SDP offer with ICE servers |
| acceptCall() | Send SDP answer |
| sendIce() | Send ICE candidate |
| renegotiate() | Request renegotiation (track changes, codec changes) |
| restartIce() | Request ICE restart (connection recovery) |
| endCall() | End the call |
| getDefaultIceServers() | Get configured default ICE servers |
Events
| Event | Description |
|-------|-------------|
| IncomingPropose | Call proposal received (includes ICE servers) |
| IncomingOffer | SDP offer received |
| IncomingAnswer | SDP answer received |
| IncomingIce | ICE candidate received |
| RenegotiateRequested | Renegotiation requested (e.g., ICE restart) |
| CallEnded | Call ended |
Notes
- This module moves only signaling over DIDComm; media flows via WebRTC/DTLS-SRTP.
- For NAT traversal, configure TURN servers in the module config or per-call.
- Use
relay-onlypolicy for maximum firewall compatibility (also hides IP addresses). - Works with mediators and message pickup (offline delivery), as with any DIDComm message.
