Real-time bidirectional communication with sub-50ms latency
Secure WebSocket connection with authentication
WebSocket URL
wss://ws.wave.inc/v1/streams// Using native WebSocket API
const ws = new WebSocket(
'wss://ws.wave.inc/v1/streams?api_key=wave_live_xxxxx'
);
ws.onopen = () => {
console.log('Connected to WAVE WebSocket');
// Subscribe to stream events
ws.send(JSON.stringify({
type: 'subscribe',
stream_id: 'stream_abc123',
events: ['stream.started', 'analytics.update', 'chat.message']
}));
};
ws.onmessage = (event) => {
const message = JSON.parse(event.data);
console.log('Received:', message.type, message.payload);
switch(message.type) {
case 'stream.started':
console.log('Stream went live!', message.payload);
break;
case 'analytics.update':
console.log('Viewers:', message.payload.concurrent_viewers);
break;
case 'chat.message':
console.log('Chat:', message.payload.username, message.payload.message);
break;
}
};
ws.onerror = (error) => {
console.error('WebSocket error:', error);
};
ws.onclose = (event) => {
console.log('Disconnected:', event.code, event.reason);
};
// Send chat message
function sendMessage(text) {
ws.send(JSON.stringify({
type: 'chat.message',
stream_id: 'stream_abc123',
message: text
}));
}
// Send reaction
function sendReaction(emoji) {
ws.send(JSON.stringify({
type: 'reaction.sent',
stream_id: 'stream_abc123',
reaction: emoji
}));
}3 event types
stream.startedFired when a stream goes live
Payload Schema
{
"stream_id": "string",
"started_at": "ISO 8601 timestamp",
"protocol": "string",
"ingest_endpoint": "string"
}stream.endedFired when a stream stops
Payload Schema
{
"stream_id": "string",
"ended_at": "ISO 8601 timestamp",
"duration": "number (seconds)",
"recording_id": "string | null"
}stream.healthReal-time stream health metrics (every 5s)
Payload Schema
{
"stream_id": "string",
"bitrate": "number (kbps)",
"fps": "number",
"packet_loss": "number (0-1)",
"latency": "number (ms)"
}3 event types
viewer.joinedNew viewer joined the stream
Payload Schema
{
"stream_id": "string",
"viewer_id": "string",
"country": "string",
"device": "string",
"joined_at": "ISO 8601 timestamp"
}viewer.leftViewer left the stream
Payload Schema
{
"stream_id": "string",
"viewer_id": "string",
"watch_time": "number (seconds)",
"left_at": "ISO 8601 timestamp"
}analytics.updateReal-time analytics update (every 10s)
Payload Schema
{
"stream_id": "string",
"concurrent_viewers": "number",
"total_views": "number",
"peak_viewers": "number",
"average_watch_time": "number"
}3 event types
chat.messageChat message sent/received
Payload Schema
{
"stream_id": "string",
"message_id": "string",
"user_id": "string",
"username": "string",
"message": "string",
"timestamp": "ISO 8601 timestamp"
}reaction.sentSend emoji reaction
Payload Schema
{
"stream_id": "string",
"reaction": "string (emoji)",
"user_id": "string"
}reaction.receivedReaction broadcast to all viewers
Payload Schema
{
"stream_id": "string",
"reaction": "string",
"count": "number",
"timestamp": "ISO 8601 timestamp"
}4 event types
pingKeepalive ping/pong
Payload Schema
{
"timestamp": "number"
}errorError notification
Payload Schema
{
"code": "string",
"message": "string",
"details": "object"
}subscribeSubscribe to stream events
Payload Schema
{
"stream_id": "string",
"events": "string[]"
}unsubscribeUnsubscribe from stream
Payload Schema
{
"stream_id": "string"
}Handle disconnections and maintain connection reliability
Normal Closure
Retry: No
Going Away
Retry: Yes, after 5s
Abnormal Closure
Retry: Yes, exponential backoff
Invalid Auth
Retry: No - fix API key
Rate Limited
Retry: Yes, after 60s
Server Error
Retry: Yes, exponential backoff
class WaveWebSocket {
constructor(apiKey, streamId) {
this.apiKey = apiKey;
this.streamId = streamId;
this.reconnectDelay = 1000; // Start at 1s
this.maxReconnectDelay = 30000; // Max 30s
this.connect();
}
connect() {
this.ws = new WebSocket(
`wss://ws.wave.inc/v1/streams?api_key=${this.apiKey}`
);
this.ws.onopen = () => {
console.log('Connected');
this.reconnectDelay = 1000; // Reset backoff
this.subscribe();
this.startHeartbeat();
};
this.ws.onclose = (event) => {
console.log('Disconnected:', event.code);
this.stopHeartbeat();
// Determine if we should reconnect
if (this.shouldReconnect(event.code)) {
setTimeout(() => {
this.reconnectDelay = Math.min(
this.reconnectDelay * 2,
this.maxReconnectDelay
);
this.connect();
}, this.reconnectDelay);
}
};
this.ws.onerror = (error) => {
console.error('WebSocket error:', error);
};
this.ws.onmessage = (event) => {
const msg = JSON.parse(event.data);
if (msg.type === 'pong') {
this.lastPong = Date.now();
} else {
this.handleMessage(msg);
}
};
}
shouldReconnect(code) {
// Don't reconnect on normal closure or auth errors
return code !== 1000 && code !== 4401;
}
startHeartbeat() {
this.heartbeatInterval = setInterval(() => {
if (this.ws.readyState === WebSocket.OPEN) {
this.ws.send(JSON.stringify({
type: 'ping',
timestamp: Date.now()
}));
// Check if pong received within 5s
setTimeout(() => {
if (Date.now() - this.lastPong > 5000) {
console.warn('No pong received, reconnecting');
this.ws.close(1006);
}
}, 5000);
}
}, 30000); // Every 30s
}
stopHeartbeat() {
if (this.heartbeatInterval) {
clearInterval(this.heartbeatInterval);
}
}
subscribe() {
this.ws.send(JSON.stringify({
type: 'subscribe',
stream_id: this.streamId,
events: ['stream.started', 'analytics.update']
}));
}
}