Synento docs
Examples

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.

View as Markdown

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 server

2. 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 devices

The 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.

On this page