@anonotf/connect
@anonotf/connect
Client SDK for AnonOtF — handles calls (1-on-1 and group, up to 9 people), live streaming to many viewers, live chat, and voice notes/recordings, without struggling with raw webrtc.
v0.2.0 — breaking change: group calls are now a mesh (everyone connects to everyone).
calls.on('remoteStream', ...)now receives{ userId, stream }instead of a barestream— update any v0.1.x integration accordingly. See the Calls section below.
Install
npm install @anonotf/connect
Architecture — read this before wiring it up
This SDK runs in the browser. Two things must never end up there:
- Your AnonOtF x-api-key (it can create/delete apps, read all data)
- Your Your API secret
So your own backend needs two small pieces:
- An endpoint that mints a short-lived socket token —
call AnonOtF's
POST /api/apps/:appId/socket-tokenwith your x-api-key, return the token to your frontend. - A thin REST proxy for everything under
/api/*(rooms, media clips, streaming tokens) — your frontend calls your backend, your backend attaches the x-api-key and forwards to AnonOtF.
This SDK never sends your x-api-key from the browser. The serverUrl you
pass in is only used for the Socket.IO connection (authenticated by the
token, not the key); apiBase should point at your own backend's proxy.
Quick start
To get app id and api key visit
https://an0n0tf-connect.vercel.app
import { AnonOtFConnect } from '@anonotf/connect';
// `token` came from YOUR backend, which called AnonOtF's
// POST /api/apps/:appId/socket-token using your x-api-key.
const client = new AnonOtFConnect({
serverUrl: 'https://your-anonotf-server.com',
token: theTokenYourBackendGaveYou,
apiBase: 'https://your-backend.com/api', // your own proxy, see above
});
client.connect();
await client.register({ name: 'Alex' });
Calls (1-on-1, grows to real group calls — full mesh)
Every accepted call gets a room behind the scenes — that's what lets you add people later without restarting the call. For 2+ people this is a genuine mesh: everyone gets a separate peer connection to everyone else, so everyone sees/hears everyone, the same way WhatsApp/Telegram group calls behave.
// Caller
client.calls.on('callAccepted', ({ roomId }) => console.log('connected', roomId));
client.calls.on('remoteStream', ({ userId, stream }) => {
// one event per remote participant — render a tile per userId
renderVideoTile(userId, stream);
});
client.calls.on('peerLeft', ({ userId }) => removeVideoTile(userId));
await client.calls.call('user_456', { callType: 'video' });
// Callee
client.calls.on('incomingCall', async ({ from, callType, roomId }) => {
await client.calls.accept(from, { callType, roomId });
});
// Mid-call — ring a third person into the SAME call (cap: 9 total)
// Everyone already in the call automatically meshes with the newcomer —
// nothing else to wire up.
client.calls.addToCall('user_789', { callType: 'video' });
// Ends the call for yourself — closes every peer connection in your mesh
client.calls.end();
client.calls.participantIds gives you the current list of remote user
IDs in the mesh at any time, if you need to render an initial roster.
Live streaming (broadcast to many viewers)
Whether you can publish (vs. just watch) is decided by the server based on your room role — broadcaster, moderator, and approved contributors can publish; plain viewers are subscribe-only.
const { role } = await client.streaming.join(roomId, myUserId);
client.streaming.on('trackSubscribed', ({ track, participant }) => {
const el = document.createElement(track.kind === 'video' ? 'video' : 'audio');
track.attach(el);
container.appendChild(el);
});
// ...later
await client.streaming.leave();
Live chat + raise hand
Ephemeral — Everything is Live, nothing is saved server-side.
client.chat.on('message', ({ fromUserId, text }) => renderMessage(fromUserId, text));
client.chat.send(roomId, 'hello!');
client.chat.raiseHand(roomId);
// Broadcaster side
client.chat.on('handRaised', ({ userId }) => showRaisedHand(userId));
client.chat.approveContributor(roomId, userId);
Voice notes & call recordings
const recorder = new MediaRecorder(stream);
const chunks = [];
recorder.ondataavailable = (e) => chunks.push(e.data);
recorder.onstop = async () => {
const blob = new Blob(chunks, { type: 'audio/webm' });
await client.mediaClips.upload(blob, {
type: 'voice_note',
mediaType: 'audio',
fromUserId: myUserId,
roomId, // or toUserId for a DM
});
};
Cleanup
client.disconnect();