livqenoDocs

MigrateFrom Twilio Video

Migrate from Twilio Video

Map Twilio Programmable Video rooms, access tokens, tracks and status callbacks onto Livqeno, with side-by-side code.

This guide moves a web app from Twilio Programmable Video (twilio-video in the browser, the twilio helper library on the server) to Livqeno. Read the overview first for the general approach.

Concept mapping

Twilio VideoLivqenoNotes
Account SID + API Key SID / SecretProject + API keyOne API key per project and environment. It never leaves your backend.
Room (created on first connect by name, or with the Rooms REST API)Room, created by your backendlivqeno.rooms.create({ name, getOrCreate: true }), then make tokens for room.id.
Room types (Group, Go, P2P)One room typeEvery room goes through Livqeno's media server (SFU). There's no peer-to-peer mode.
AccessToken + VideoGrantlivqeno.tokens.create()Made by the API, not signed locally. Returns a grant with token, endpoint and iceServers.
identityidentitySame idea: your user id, unique in the room.
Participant SIDidentityLivqeno addresses participants by identity.
connect(token, { name })createRTCClient(grant).join()The room comes from the token.
LocalAudioTrack / LocalVideoTrackroom.enableMicrophone() / room.enableCamera()One call captures and publishes.
trackSubscribed on a participanttrackSubscribed on the room(track, participant).
track.disable() / enable()track.mute() / unmute()Others get trackMuted / trackUnmuted.
Screen share via getDisplayMedia + publishTrackroom.enableScreenShare()Video only.
LocalDataTrackroom.sendData() + dataReceivedNeeds the publishData permission.
Conversations API (chat)ChatSeparate token: livqeno.chat.createToken().
Bandwidth Profile, track priorityadaptiveStream (on by default) and track.setLayer()See Tracks.
Dominant speakeractiveSpeakersChangedLoudest first, up to eight.
Network Quality APIconnectionQualityChangedSee Reconnection & network quality.
Recording Rules, CompositionsNo equivalent for callsSee Gaps.
Status callbacks (StatusCallback URL)WebhooksConfigured per project and environment in the dashboard.
X-Twilio-SignatureSwalvo-SignatureHMAC-SHA256 over <timestamp>.<raw body>.
Media regions (gll, us1, …)Automatic placementSee Regions.
Video InsightsObservability APIlivqeno.connections.list(), livqeno.errors.list().
Remove participant (REST)POST /v1/rooms/{roomId}/participants/{identity}/remove
Complete a room (REST)DELETE /v1/rooms/{roomId}Disconnects everyone.

Server: make a token

const twilio = require('twilio');
const { AccessToken } = twilio.jwt;
const { VideoGrant } = AccessToken;
 
const token = new AccessToken(
  process.env.TWILIO_ACCOUNT_SID,
  process.env.TWILIO_API_KEY_SID,
  process.env.TWILIO_API_KEY_SECRET,
  { identity: user.id, ttl: 3600 },
);
token.addGrant(new VideoGrant({ room: roomName }));
res.json({ token: token.toJwt() });
import { Livqeno } from '@livqeno/server';
 
const livqeno = new Livqeno({ apiKey: process.env.LIVQENO_API_KEY, baseUrl: process.env.LIVQENO_API_URL });
 
// Rooms are explicit. getOrCreate makes concurrent joins safe.
const room = await livqeno.rooms.create({ name: roomName, getOrCreate: true });
 
const grant = await livqeno.tokens.create({
  room: room.id, // the id, not the name
  identity: user.id,
  permissions: { join: true, subscribe: true, publish: true, publishData: true },
  expiresIn: 600,
});
res.json(grant); // forward the whole grant: token, endpoint, iceServers

Two things change for the client, not just the server:

  • Return the whole grant, not only the token string. The client SDK needs endpoint and iceServers from it. Don't build ICE servers yourself.
  • Permissions are explicit. Twilio's VideoGrant lets anyone in the room publish. In Livqeno, anything you don't turn on is off. A view-only user gets { join: true, subscribe: true }. See Permissions.

Client: join, publish and subscribe

import { connect } from 'twilio-video';
 
const room = await connect(token, { name: roomName, audio: true, video: true });
 
function onParticipant(participant) {
  participant.on('trackSubscribed', (track) => grid.appendChild(track.attach()));
  participant.on('trackUnsubscribed', (track) => track.detach().forEach((el) => el.remove()));
}
room.participants.forEach(onParticipant);
room.on('participantConnected', onParticipant);

join() resolves once you're admitted to the room, a moment before media is connected. Drive "connected" UI from the connected event, or await room.waitUntilConnected(). See Rooms & participants.

Leaving: Twilio's room.disconnect() becomes await room.leave().

Mute and unmute

room.localParticipant.audioTracks.forEach((pub) => pub.track.disable());
room.localParticipant.audioTracks.forEach((pub) => pub.track.enable());

To mute someone else from your backend (moderation), call POST /v1/rooms/{roomId}/participants/{identity}/mute with { "kind": "audio", "muted": true }. The user's app gets localTrackMutedByServer.

Screen share

import { LocalVideoTrack } from 'twilio-video';
 
const stream = await navigator.mediaDevices.getDisplayMedia();
const screenTrack = new LocalVideoTrack(stream.getVideoTracks()[0], { name: 'screen' });
await room.localParticipant.publishTrack(screenTrack);
// stop:
room.localParticipant.unpublishTrack(screenTrack);
screenTrack.stop();

Data and chat messages

import { LocalDataTrack } from 'twilio-video';
 
const dataTrack = new LocalDataTrack();
await room.localParticipant.publishTrack(dataTrack);
dataTrack.send(JSON.stringify({ type: 'raise-hand' }));
 
// Receiving: a RemoteDataTrack fires 'message'
track.on('message', (data) => console.log(data));

Add the dataReceived listener right after joining: it is what sets up your side of the data channel. For saved messages (history, read receipts), use Chat instead of sendData(). If you used the Twilio Conversations API next to Video, Chat is its replacement. See Chat quickstart.

Recording

const twilioClient = require('twilio')(apiKeySid, apiKeySecret, { accountSid });
 
// Record everyone in the room
await twilioClient.video.v1.rooms(roomSid).recordingRules.update({
  rules: [{ type: 'include', all: true }],
});
 
// Later: compose the recordings into one file
await twilioClient.video.v1.compositions.create({
  roomSid,
  audioSources: ['*'],
  videoLayout: { grid: { video_sources: ['*'] } },
  format: 'mp4',
});

If you record ordinary group calls, Livqeno can't replace that today. Keep recorded rooms on Twilio, or record outside Livqeno, until it can. See Feature gaps.

RTMP out

Twilio Video has no built-in RTMP output as far as we know. If you built one (for example, a headless browser that joins the room and pushes to YouTube), Livqeno's restream replaces it, but only for live streams, not for plain rooms:

const stream = await livqeno.liveStreams.create({ title: 'Launch', hostIdentity: 'alice' });
const hostGrant = await livqeno.liveStreams.addHost(stream.id, { identity: 'alice' });
await livqeno.liveStreams.start(stream.id);
 
await livqeno.liveStreams.restreams.create(stream.id, {
  url: 'rtmps://a.rtmp.youtube.com/live2',
  streamKey: process.env.YOUTUBE_STREAM_KEY!,
  label: 'YouTube',
});

The host joins with LiveStream.join(hostGrant) from @livqeno/live. See Build a live stream.

Webhooks

const twilio = require('twilio');
 
// Status callbacks are form-encoded and signed over the full URL + params.
const ok = twilio.validateRequest(
  process.env.TWILIO_AUTH_TOKEN,
  req.header('X-Twilio-Signature'),
  'https://example.com/twilio/status',
  req.body,
);
if (ok && req.body.StatusCallbackEvent === 'participant-connected') {
  // ...
}

Every server SDK ships this check (verifyWebhook in TypeScript, verify_webhook in Python, livqeno.VerifyWebhookRequest, WebhookVerifier.verify, Webhook::constructEvent). See Webhooks.

Twilio status callbackLivqeno webhook
room-createdroom.started (first participant joins)
room-endedroom.ended
participant-connectedroom.participant_joined
participant-disconnectedroom.participant_left (with a reason)
track-addedroom.track_published
track-removedroom.track_unpublished
recording-*, composition-*None
track-enabled / track-disabledNone: mute is a client event only

E2EE

As far as we know, Twilio's Group Rooms route media through Twilio's servers without end-to-end encryption (check their docs for your room type). Livqeno rooms can be end-to-end encrypted on the web and Flutter: create the room with e2ee: true and give the SDK a key. Recording, HLS, restream and ingest are refused for such a room. See End-to-end encryption.

Feature gaps

What Twilio Video has that Livqeno doesn't, as of today:

Twilio featureOn Livqeno
Recording Rules, per-track recordingsNot available. No room recording of any kind.
CompositionsNot available. The only composite output is a live stream's restream grid (1280×720, fixed layout).
Peer-to-peer roomsNot available. Every room uses the SFU.
PSTN / SIP participantsNot available.
Mute and unmute webhooks (track-enabled / track-disabled)Client events only.
System or tab audio in screen shareNot sent yet.
Large rooms50 participants per room by default. Capacity has been tested to 100 participants with simulated media, not on real networks (Known limitations).
Browser coverageOnly Chromium is tested with real media.

Migration checklist

  • Inventory. Find every use of twilio-video, AccessToken, VideoGrant, the Rooms/Recordings/Compositions REST APIs and your status-callback handlers.
  • Credentials. Create a Livqeno project, an API key per environment, and store them as LIVQENO_API_KEY and LIVQENO_API_URL (API credentials).
  • Token service. Add a Livqeno branch to your token endpoint: rooms.create({ name, getOrCreate: true }), then tokens.create(). Return the whole grant. Map each Twilio "role" in your app to an explicit permission set.
  • Client. Wrap join, mute, screen share and data in one interface with a Twilio and a Livqeno implementation. Choose per room, so a call never mixes providers.
  • Chat. If you used Conversations, move it to Chat: conversations, members, then a chat token.
  • Webhooks. Register a Livqeno endpoint, verify Swalvo-Signature, deduplicate on Livqeno-Event-Id, and map events with the table above.
  • Recording. Keep recorded rooms on Twilio until you have a plan for them. Don't cut over a recorded product on the assumption it's coming.
  • Dual-run. Send a small share of new rooms to Livqeno. Watch connections and errors and your own call-quality metrics.
  • Cut over. Make Livqeno the default for new rooms. Remove the Twilio path once no Twilio room has been active for a full token lifetime.

Pricing

Livqeno's rates are on Usage. Compare them with Twilio's current pricing page; this guide doesn't quote it.

Next steps