ss-meet
v0.0.3
Published
WebRTC video conferencing library using Statement Store for decentralized signaling
Downloads
280
Maintainers
Readme
ss-meet
WebRTC video conferencing library using Statement Store for decentralized signaling.
Features
- Decentralized Signaling: Uses Substrate Statement Store instead of a centralized signaling server
- P2P Video Calls: WebRTC-based peer-to-peer video and audio
- End-to-End Encryption: Optional AES-GCM encryption for signaling data
- Auto-Discovery: Automatic peer discovery via presence channels
- Data Channels: Send arbitrary data between peers
- TURN Support: Cloudflare TURN integration with fallback to public STUN servers
- TypeScript: Full type safety with comprehensive types
Installation
npm install ss-meetQuick Start
import { SSMeet, generateRandomMnemonic } from 'ss-meet'
// Create a meeting instance
const meeting = new SSMeet({
roomId: 'my-meeting-room',
endpoint: 'wss://your-substrate-node.com',
identity: {
peerId: 'user-123',
mnemonic: generateRandomMnemonic(),
displayName: 'Alice'
}
})
// Listen for events
meeting.on('stream', (peerId, stream) => {
const video = document.getElementById(`video-${peerId}`)
if (video) video.srcObject = stream
})
meeting.on('peerJoined', (peer) => {
console.log(`${peer.displayName} joined`)
})
meeting.on('data', (peerId, data) => {
console.log(`Message from ${peerId}:`, data)
})
// Get local media and connect
const localStream = await navigator.mediaDevices.getUserMedia({
video: true,
audio: true
})
meeting.setLocalStream(localStream)
await meeting.connect()
// Send messages
meeting.broadcast({ type: 'chat', message: 'Hello!' })
// Disconnect when done
await meeting.disconnect()End-to-End Encryption
SS-Meet supports optional end-to-end encryption for signaling data. This encrypts SDPs (which contain IP addresses), presence information, and any data stored in the Statement Store.
import {
SSMeet,
generateRoomKey,
importRoomKey,
createShareableUrl,
extractRoomKeyFromUrl
} from 'ss-meet'
// Room creator generates a key
const roomKeyString = await generateRoomKey()
const shareUrl = createShareableUrl('https://app.com/room/abc', roomKeyString)
// Share this URL - the key is in the fragment (#) and never sent to servers
// Participants extract the key from URL
const keyFromUrl = extractRoomKeyFromUrl() // Uses window.location.hash
if (keyFromUrl) {
const roomKey = await importRoomKey(keyFromUrl)
const meeting = new SSMeet({
roomId: 'abc',
endpoint: 'wss://substrate-node.com',
identity: { peerId: 'user-1', mnemonic: '...' },
options: {
roomKey // Enable encryption
}
})
}Encryption Functions
import {
generateRoomKey, // Generate a new AES-256 key (returns base64url string)
importRoomKey, // Import key from base64url string to CryptoKey
encryptChannelData, // Encrypt a string
decryptChannelData, // Decrypt a string
isEncryptedData, // Check if data looks encrypted
isValidRoomKey, // Validate key format
extractRoomKeyFromUrl, // Extract key from URL fragment
createShareableUrl // Create URL with key in fragment
} from 'ss-meet'Configuration
SSMeetConfig
| Property | Type | Required | Description |
|----------|------|----------|-------------|
| roomId | string | Yes | Unique room identifier |
| endpoint | string | Yes | WebSocket URL to Substrate node |
| identity | SSMeetIdentity | Yes | User identity configuration |
| turn | SSMeetTurnConfig | No | TURN server configuration |
| options | SSMeetOptions | No | Advanced options |
Identity Configuration
{
peerId: 'unique-user-id', // Unique identifier for this user
mnemonic: '...', // BIP39 mnemonic for signing
displayName: 'Alice' // Optional display name
}TURN Configuration
{
keyId: 'cloudflare-turn-key',
apiToken: 'cloudflare-api-token'
}Options
{
forceRelay: false, // Force TURN relay mode
presenceExpiry: 600, // Presence expiry in seconds
debug: false, // Enable debug logging
roomKey: CryptoKey, // Optional encryption key
appTopic: 'my-app' // Custom app topic (default: 'ss-meet')
}Application Topic Isolation
By default, ss-meet uses the topic 'ss-meet' to filter messages in the Statement Store. If you're building a custom application, you should set a unique appTopic to isolate your app's messages from other applications:
const meeting = new SSMeet({
roomId: 'my-room',
endpoint: 'wss://substrate-node.com',
identity: { peerId: 'user-1', mnemonic: '...' },
options: {
appTopic: 'my-custom-app' // Your app's unique identifier
}
})This ensures that only participants using the same appTopic will discover each other.
Events
| Event | Parameters | Description |
|-------|------------|-------------|
| status | (status, message) | Connection status changed |
| peerJoined | (peer) | A peer joined the meeting |
| peerLeft | (peerId) | A peer left the meeting |
| peerConnected | (peerId) | WebRTC connection established |
| peerDisconnected | (peerId) | WebRTC connection closed |
| stream | (peerId, stream) | Received media stream |
| data | (peerId, data) | Received data message |
| log | (message, type) | Debug log message |
| error | (error) | An error occurred |
API Reference
SSMeet
Methods
// Connection
setLocalStream(stream: MediaStream | null): void
getLocalStream(): MediaStream | null
connect(): Promise<void>
disconnect(): Promise<void>
isConnected(): boolean
getStatus(): ConnectionStatus
// Peers
getConnectedPeers(): string[]
getPeerInfo(peerId: string): PeerInfo | undefined
getAllPeers(): PeerInfo[]
// Data
send(peerId: string, data: unknown): boolean
broadcast(data: unknown): void
// Info
getRoomId(): string
getPeerId(): string
getDisplayName(): string | undefined
// Events
on(event, callback): () => void // Returns unsubscribe function
off(event, callback): void
once(event, callback): () => void
removeAllListeners(): voidWallet Utilities
import {
generateRandomMnemonic,
generateSecureMnemonic,
deriveWallet,
validateMnemonic,
createWallet
} from 'ss-meet'
// Generate a 12-word mnemonic
const mnemonic = generateRandomMnemonic()
// Generate a 24-word mnemonic (more secure)
const secureMnemonic = generateSecureMnemonic()
// Derive wallet from mnemonic
const wallet = deriveWallet(mnemonic)
console.log(wallet.address) // SS58 address
// Validate a mnemonic
if (validateMnemonic(userInput)) {
// Valid mnemonic
}
// Create a new wallet with fresh mnemonic
const newWallet = createWallet()Error Handling
SS-Meet uses typed errors for better error handling:
import { SSMeet, SSMeetError } from 'ss-meet'
const meeting = new SSMeet(config)
meeting.on('error', (error) => {
if (error instanceof SSMeetError) {
switch (error.code) {
case 'CONNECTION_FAILED':
console.error('Failed to connect:', error.message)
break
case 'PEER_CONNECTION_FAILED':
console.error('Peer connection failed:', error.message)
break
case 'BLOCKCHAIN_ERROR':
console.error('Blockchain error:', error.message)
break
// ... handle other error codes
}
}
})
try {
await meeting.connect()
} catch (error) {
if (error instanceof SSMeetError) {
console.error(`[${error.code}] ${error.message}`)
if (error.cause) {
console.error('Caused by:', error.cause)
}
}
}Error Codes
| Code | Description |
|------|-------------|
| CONNECTION_FAILED | Failed to connect to meeting |
| SIGNALING_FAILED | Signaling operation failed |
| PEER_CONNECTION_FAILED | WebRTC peer connection failed |
| ICE_GATHERING_FAILED | ICE gathering timed out |
| DATA_CHANNEL_FAILED | Data channel error |
| MEDIA_FAILED | Media access error |
| INVALID_CONFIG | Invalid configuration |
| NOT_CONNECTED | Operation requires connection |
| ALREADY_CONNECTED | Already connected |
| BLOCKCHAIN_ERROR | Statement Store error |
| TURN_CREDENTIALS_FAILED | TURN credential fetch failed |
Testing
Run the test suite:
npm test # Run all tests
npm run test:watch # Watch mode
npm run test:coverage # With coverage reportArchitecture
SS-Meet uses a layered architecture:
┌─────────────────────────────────────┐
│ SSMeet │ ← High-level API
├─────────────────────────────────────┤
│ SSWebRTCProvider │ ← Orchestration
├──────────────┬──────────────────────┤
│ SignalingMgr │ PeerManager │ ← Signaling & WebRTC
├──────────────┴──────────────────────┤
│ StatementStore │ ← Blockchain I/O
├─────────────────────────────────────┤
│ @novasamatech/sdk-statement │ ← Statement SDK
└─────────────────────────────────────┘Signaling Protocol
- Presence: Each peer writes a presence message with their ID and join timestamp
- Older Peer Offers: The peer who joined first initiates WebRTC connections
- Handshake Channels: Shared channels for offer/answer exchange
- Non-Trickle ICE: ICE candidates are embedded in SDP
Statement Store Topics
{appTopic}- Application topic for filtering (default: 'ss-meet'){roomId}- Room-specific topic for isolation
Advanced Usage
Using Lower-Level Components
For advanced use cases, you can use the underlying components directly:
import {
SSWebRTCProvider,
StatementStore,
SignalingManager,
PeerManager,
TurnCredentials
} from 'ss-meet'
// Direct StatementStore usage
const store = new StatementStore({
endpoint: 'wss://substrate-node.com',
documentId: 'my-room',
mnemonic: '...'
})
await store.connect()
store.onStatement((value) => {
console.log('Received:', value)
})
await store.write('my-channel', { type: 'custom', data: '...' })Meeting Optimizations
SS-Meet includes optional optimization modules:
import {
ActiveSpeakerDetector,
VideoVisibilityManager,
AdaptiveBitrateController,
SimulcastManager,
LastNVideoManager,
QualityTierManager
} from 'ss-meet'Browser Support
- Chrome 80+
- Firefox 75+
- Safari 14+
- Edge 80+
Requires WebRTC and Web Crypto API support.
License
MIT
