Web Quickstart — Live Video in a Web Page
Join a live Synento session from the browser with two-way audio and video, using a short-lived connection token minted by your server.
Join a live Synento session from the browser with two-way audio and video. It
takes two pieces: a small server route that mints a short-lived connection
token, and a page that uses @synento/client to publish and receive media.
Never put your API key in the browser
Your sk_live_… key can create sessions, read recordings, and change billing.
It must stay on your server. The browser only ever receives a connection
token: a short-lived credential scoped to one session and one user.
browser ──POST /api/connection──▶ your server ──sk_live_…──▶ Synento API
◀── { wsUrl, token } ──────────────────────────────────────────┘1. Install
npm install @synento/client # browser
npm install @synento/node # your server2. Mint a token on your server
This example is a Next.js route handler; any server framework works the same way. Create the session once (or look up an existing one), then mint a token for the user who is joining.
// app/api/connection/route.ts
import { createSynentoClient, resolveWsUrl } from '@synento/node'
// Reads SYNENTO_API_KEY and SYNENTO_API_URL from the environment.
const synento = createSynentoClient()
// Without this, Next may statically optimise the route and serve a stale token.
export const dynamic = 'force-dynamic'
export async function POST(request: Request) {
const { sessionId, userId } = await request.json()
// Authenticate the caller here — whoever gets a token can join this session.
const token = await synento.createConnectionToken(sessionId, { userId })
return Response.json({
expiresIn: token.expiresIn,
token: token.accessToken,
// Let the SDK build this. Assembling the URL by hand works in production
// and then fails on localhost, because the browser and the API commonly
// disagree about how to spell loopback — and the rejection arrives as a
// close(1008) *after* a successful upgrade, so it surfaces as a call that
// silently never connects rather than as an error.
wsUrl: resolveWsUrl({
apiBaseUrl: process.env.SYNENTO_API_URL ?? 'https://api.synento.com',
requestOrigin: request.headers.get('origin'),
sessionId,
userId,
}),
})
}3. Join from the browser
SynentoRoom handles encoding, transport, decoding, backpressure, and
reconnection. Note that getToken is a function: the SDK calls it again on
every reconnect, so tokens rotate without any work from you.
import { SynentoRoom } from '@synento/client'
const support = SynentoRoom.checkSupport()
if (!support.supported) {
throw new Error(support.reason) // e.g. this browser lacks WebCodecs
}
const userId = 'alice'
const sessionId = 'YOUR_SESSION_ID'
const room = new SynentoRoom({
userId,
getToken: async () => {
const res = await fetch('/api/connection', {
body: JSON.stringify({ sessionId, userId }),
headers: { 'Content-Type': 'application/json' },
method: 'POST',
})
if (!res.ok) throw new Error('could not mint a connection token')
return res.json() // { wsUrl, token }
},
})
// Render each remote participant into a canvas you provide.
room.on('participantsChanged', (participants) => {
for (const id of participants) {
if (document.querySelector(`canvas[data-user="${id}"]`)) continue
const canvas = document.createElement('canvas')
canvas.width = 640
canvas.height = 480
canvas.dataset.user = id
document.body.append(canvas)
room.attachParticipant(id, canvas)
}
})
await room.connect()
// Publish camera and microphone. The <video> element is your local preview.
const stream = await navigator.mediaDevices.getUserMedia({ audio: true, video: true })
const preview = document.querySelector('#preview')
preview.srcObject = stream
await room.publishCamera(stream, preview)Open the page in two browser tabs with different user ids and you will see and hear both participants.
Other things you can do
room.setCameraEnabled(false) // mute video, keep audio flowing
await room.publishScreen() // share the screen (prompts the user)
room.unpublishScreen()
room.sendChat('hello everyone') // chat is persisted with the recording
room.on('chat', (senderId, message) => console.log(senderId, message.message))
room.close() // leave and release devicesThe chat event fires on the sender too — the server broadcasts the stored
record to everyone in the session, so senderId is your own user id for your
own messages. Render from the event alone; appending the text locally when you
call sendChat shows your messages twice.
Connection state is observable, which is what you want for a status indicator:
room.on('connected', () => setStatus('Connected'))
room.on('reconnecting', (attempt) => setStatus(`Reconnecting (${attempt})…`))
room.on('disconnected', (reason) => setStatus(`Disconnected: ${reason}`))
room.on('error', (err) => console.error(err))Using React
@synento/react wraps the same client:
import { SynentoProvider, VideoTile, useParticipants } from '@synento/react'
function Call() {
const participants = useParticipants()
return participants.map((id) => <VideoTile key={id} participantId={id} />)
}
export default function App() {
return (
<SynentoProvider getToken={getToken} userId="alice">
<Call />
</SynentoProvider>
)
}Browser support
Synento encodes with WebCodecs, so it needs Chrome/Edge 94+, Safari 16.4+, or
Firefox 133+. SynentoRoom.checkSupport() tells you before you try, so you can
show a clear message rather than failing at connect time.
When the session ends
Nothing else to do — the session's durable streams are the recording. It is immediately replayable and exportable to MP4, with no encoding step. See Recordings & Replay.