@openvisio/sdk-js
v2.1.0
Published
Official JavaScript SDK for OpenVisio — an open video meeting platform built on LiveKit. Server and browser clients for room management, moderation, recording, and transcription.
Readme
@openvisio/sdk-js
Official JavaScript SDK for OpenVisio — an open video meeting platform built on LiveKit. Server and browser clients with typed methods for room management, participant moderation, recording, and transcription.
TL;DR
- Your backend uses
OpenvisioServerto create rooms, mint LiveKit join tokens, and run moderation / recording / interactions. It holds the API key. - Your frontend joins the call. Two patterns:
- Headless (recommended) — connect to LiveKit directly with the minted token
via
livekit-client. Render the UI yourself. No iframe, no SSO popup, no third-party cookies. - Iframe (alternative) — embed the OpenVisio web app with
OpenvisioBrowser. Convenient (you get the full UI) but you inherit cross-origin auth.
- Headless (recommended) — connect to LiveKit directly with the minted token
via
- Authorization is your responsibility. The OpenVisio proxy trusts the API key
holder; who may moderate is decided by your backend, using
is_administrableon the room as the source of truth.
Architecture
Your frontend ──── Your backend ──── OpenVisio proxy ──── OpenVisio backend
(OpenvisioServer) LiveKit server
│ │
└────────── media (WebRTC) ─────────────┘The SDK ships as one package with two isolated entry points so the API key cannot leak into the browser bundle:
| Import | Runs on | Holds the key |
|---|---|---|
| @openvisio/sdk-js/server | your backend (Node 18+) | yes |
| @openvisio/sdk-js/browser | your frontend | no |
The browser never calls the proxy directly — it calls your backend, which uses the server client.
Installation
npm install @openvisio/sdk-jsOr load the browser bundle directly from a CDN (no build step required):
<script type="module">
import { OpenvisioBrowser } from 'https://esm.sh/@openvisio/[email protected]/browser';
// or: https://unpkg.com/@openvisio/[email protected]/dist/browser.js
// or: https://cdn.jsdelivr.net/npm/@openvisio/[email protected]/dist/browser.js
</script>Backend — OpenvisioServer
import { OpenvisioServer } from '@openvisio/sdk-js/server';
const openvisio = new OpenvisioServer({
proxyUrl: 'https://openvisio-proxy.example.com',
apiKey: process.env.OPENVISIO_API_KEY!, // never sent to the browser
});Create or resolve a room
// Host — create the room AS the logged-in user (forward their OIDC token) so they
// become the room owner -> admin. Without a token, the proxy's service account owns it.
const room = await openvisio.createRoom(undefined, userToken);
// { room_name, slug, is_administrable }
// Guest / join — resolve a slug; pass the user token to learn their room role.
const resolved = await openvisio.resolveRoom('aaa-bbbb-ccc', userToken);
// { room_name, slug, is_administrable }Mint a join token (10-minute TTL)
const { token, livekit_url } = await openvisio.createToken(room.room_name, 'user_42', {
displayName: 'Alice',
});createToken powers the headless join below. Generate it right before the
user connects, not at room-creation time.
In-meeting actions
// interactions.* — soft, the participant can undo. Returns StatusResponse.
await openvisio.interactions.chat(room.room_name, 'The meeting ends in 5 minutes.');
await openvisio.interactions.raiseHand(room.room_name, 'user_42', true);
await openvisio.interactions.reaction(room.room_name, 'party-popper');
// moderation.* — server-side permission revoke; only another admin call restores it.
await openvisio.moderation.muteMic(room.room_name, 'user_99');
await openvisio.moderation.kick(room.room_name, 'user_99');
// Recording / transcription — user_id must be a live participant. Keep the egress_id.
const { egress_id } = await openvisio.startRecording(room.room_name, 'user_42');
await openvisio.stopRecording(egress_id);
await openvisio.listRooms(); // active rooms + participant identities
await openvisio.leave(room.room_name, 'user_42');Full surface:
interactions.{muteMic, unmuteMic, disableCamera, enableCamera, raiseHand, reaction, chat},
moderation.{muteMic, unmuteMic, disableCamera, enableCamera, stopScreenShare, kick},
createRoom, resolveRoom, createToken, listRooms, leave,
start/stopRecording, start/stopTranscription, health.
Authorization — read this
The OpenVisio proxy authenticates your application with a single API key and trusts it. Authorization (who can kick, mute, record) is enforced by your backend before calling the SDK.
Key principle: authentication ≠ authorization. Being logged in — or being in the call — does not make a user an admin.
The source of truth for "may this user moderate this room?" is the room role,
exposed as is_administrable on createRoom / resolveRoom:
// Host: create AS the user -> they own the room -> is_administrable === true.
const room = await openvisio.createRoom(undefined, userToken);
const isAdmin = room.is_administrable;
// Guest joining someone else's room -> is_administrable === false -> no admin powers.
const resolved = await openvisio.resolveRoom(slug, userToken);
// Enforce it in YOUR routes, before touching the proxy:
function requireAdmin(req, res, next) {
if (!req.session.isAdmin) return res.status(403).json({ error: 'forbidden' });
next();
}
app.post('/api/openvisio/kick', requireAdmin, (req, res) =>
openvisio.moderation.kick(/* ... */));The proxy creates rooms with its own service account by default, which makes the service account the owner — not your user. Forward the user's OIDC token to
createRoomso the user becomes the owner andis_administrableis true for them.
Frontend — Headless (recommended)
Your frontend connects to LiveKit directly with the token from your backend and
renders the call itself. No iframe, no SSO popup, no SameSite=None cookies. The
participant identity is the one you minted, so you always know who is who.
import { Room, RoomEvent } from 'livekit-client';
// or @livekit/components-react for a turnkey <VideoConference /> UI.
// 1. Your backend returns { token, livekit_url } from openvisio.createToken(...).
const { token, livekitUrl } = await fetch('/api/openvisio/token').then((r) => r.json());
// 2. Connect and render.
const room = new Room({ adaptiveStream: true, dynacast: true });
await room.connect(livekitUrl, token);
await room.localParticipant.enableCameraAndMicrophone();
// 3. Self-actions are client-side (do NOT route them through the proxy).
await room.localParticipant.setMicrophoneEnabled(false);
room.localParticipant.publishData(
new TextEncoder().encode(JSON.stringify({
type: 'reactionReceived',
data: { emoji: 'party-popper' },
})),
{ reliable: true },
);
// 4. Live UI feedback from the room state:
room
.on(RoomEvent.TrackMuted, (pub, p) => { /* show muted icon */ })
.on(RoomEvent.ParticipantAttributesChanged, (changed, p) => {
if (changed.handRaisedAt) { /* show raised hand */ }
})
.on(RoomEvent.RoomMetadataChanged, () => {
/* JSON.parse(room.metadata).recording_status === 'started' -> show REC */
});Actions on other participants (moderation, soft mutes, recording) go through
the proxy via your backend; self mic/cam/screen and animated reactions are
done client-side on room.localParticipant. See the
openvisio-sdk-js-sample
app for a full working reference.
Frontend — Iframe (alternative)
Embeds the OpenVisio web app. You get the full UI for free, but the iframe runs
its own OIDC login and needs a SameSite=None session cookie — fragile and
increasingly blocked by browsers. Prefer headless unless you specifically need the
native UI.
import { OpenvisioBrowser } from '@openvisio/sdk-js/browser';
const browser = new OpenvisioBrowser({ openvisioUrl: 'https://openvisio.example.com' });
await browser.authenticate(); // SSO popup (silent if already logged in)
const iframe = browser.mountIframe(document.getElementById('openvisio-container')!, {
slug: 'aaa-bbbb-ccc',
accessToken, // from your backend (createToken)
});
browser.leave(iframe); // destroy iframe FIRST -> no "excluded" banner
// ...then call your backend so it runs openvisio.leave(room_name, userId).Error handling
Every server call throws OpenvisioError (with .status and .body) on a
non-2xx response. The proxy's error detail is preserved in the message.
import { OpenvisioError } from '@openvisio/sdk-js';
try {
await openvisio.interactions.unmuteMic(roomName, userId);
} catch (e) {
if (e instanceof OpenvisioError && e.status === 404) {
// participant joined with mic off, or isn't in the room — nothing to unmute
}
}Common codes: 404 — participant not in room / track not published (moderation and interactions are consistent here); 403 — guest blocked by your authz; 409 — your backend's "no active recording to stop"; 502/503 — OpenVisio backend or identity provider unreachable.
Reactions
interactions.reaction() broadcasts a reaction to everyone, but a server-sent
reaction shows only the toast — the animated emoji on tiles needs a sender
participant. In the headless pattern you have the LiveKit client, so publish
the reaction yourself with room.localParticipant.publishData(...) (payload
above) and the animation plays. The server-side call is still useful for
reactions triggered by your backend.
Sample app
A runnable reference lives at
vopenia-io/openvisio-sdk-js-sample —
OIDC login (Authorization Code + PKCE), is_administrable admin gating, live
mic/cam/hand/chat/recording feedback, plus a toggle to switch between the
headless flow and the iframe flow (which loads OpenvisioBrowser from the CDN
to demonstrate the published package).
Development
npm install
npm run build # -> dist/ (ESM + CJS + .d.ts, one bundle per entry point)
npm test # vitest — locks the request shapes against the proxy
npm run typecheck # tsc --noEmitPublishing
Releasing is a single manual step. Prepare the version, then run the workflow:
# 1. Bump the version + add a matching CHANGELOG.md entry, commit, merge to main.
# (Only edit package.json's version — the tag is created by the workflow.)Then open the Actions tab → Release → Run workflow (on main). The
.github/workflows/release.yml workflow reads the version from
package.json, refuses if that tag already exists or has no ## [version]
CHANGELOG entry, runs typecheck/tests/build, publishes to npm, tags vX.Y.Z,
and creates a GitHub Release from the CHANGELOG section.
Everything happens in that one run — there is no separate "push a tag to publish"
step. (A tag pushed by CI's default token can't trigger another workflow, so the
publisher runs directly on the manual dispatch instead.) The published tarball
ships dist/, README.md, CHANGELOG.md and LICENSE (per files in
package.json) — sources, tests, and examples stay out.
One-time setup (no secret): on npmjs.com the package uses Trusted Publishing
(OIDC) — package Settings → Trusted Publisher → GitHub Actions,
repo vopenia-io/openvisio-sdk-js, workflow release.yml. The workflow then
authenticates automatically via its OIDC token; there is no NPM_TOKEN to store.
License
MIT
