livqenoDocs

MigrateFrom Agora

Migrate from Agora

Map Agora RTC channels, RtcTokenBuilder, RTM / Chat, Cloud Recording, Media Push, Media Gateway and NCS onto Livqeno, with side-by-side code.

This guide moves an app from Agora to Livqeno. It covers the Video / Voice SDK (agora-rtc-sdk-ng on the web), Signaling (RTM) and Agora Chat, Cloud Recording, Media Push, Media Gateway and the Notifications service (NCS). Read the overview first for the general approach.

Concept mapping

AgoraLivqenoNotes
App ID + App CertificateProject + API keyThe API key never leaves your backend.
Channel (exists once someone joins it)Room, created by your backendlivqeno.rooms.create({ name, getOrCreate: true }), then make tokens for room.id.
uid (number) or user account (string)identity (string)Use String(uid). Livqeno never assigns one for you, so there is no uid: 0.
RtcTokenBuilder.buildTokenWithUidlivqeno.tokens.create()Made by the API, not signed locally. Returns a grant with token, endpoint and iceServers.
Token privileges (join, publish audio / video / data)permissions: join, subscribe, publish, publishAudio, publishVideo, publishDataAnything you don't turn on is off.
Channel profile rtcA room
Channel profile live, host / audience roles (setClientRole)A live stream: hosts, co-hosts and viewersOr a room with view-only tokens. Moving an audience member up is the stage.
createMicrophoneAndCameraTracks + client.publishroom.enableMicrophone() / room.enableCamera()One call captures and publishes.
user-published + client.subscribetrackSubscribedLivqeno subscribes automatically.
track.setMuted() / setEnabled()track.mute() / room.disableMicrophone()
createScreenVideoTrackroom.enableScreenShare()Video only. Sent next to the camera from the same client.
Dual stream (enableDualStream, setRemoteVideoStreamType)Simulcast, on by default, and track.setLayer()See Tracks.
Data stream (sendStreamMessage, native SDKs)room.sendData() + dataReceivedNeeds publishData.
Signaling (RTM): message channels, presenceChat: conversations, presence, typingSeparate chat token.
Agora Chat (IM): groups, threads, reactionsChatThreads, reactions, receipts, attachments, moderation, push.
Cloud RecordingNo equivalent for callsSee Gaps.
Media Push (RTMP converter)RestreamLive streams only. Up to 5 destinations, fixed grid.
Media Gateway (OBS / encoder in)IngestWHIP or RTMP(S). Live streams only.
NCS (Notifications)WebhooksSwalvo-Signature, HMAC-SHA256.
Built-in media encryption (setEncryptionConfig)E2EEKeys stay on devices. Web and Flutter.
AI noise suppression / virtual background extensionsNot available. See Effects removed.
Area code (AgoraRTC.setArea)Automatic placementSee Regions.
Agora AnalyticsObservability API
Kicking a user (banning rule REST API)POST /v1/rooms/{roomId}/participants/{identity}/removePlus token revocation.

Server: make a token

const { RtcTokenBuilder, RtcRole } = require('agora-token');
 
const token = RtcTokenBuilder.buildTokenWithUid(
  process.env.AGORA_APP_ID,
  process.env.AGORA_APP_CERTIFICATE,
  channelName,
  uid,
  RtcRole.PUBLISHER,
  3600, // token expiry, seconds
  3600, // privilege expiry, seconds
);
import { Livqeno } from '@livqeno/server';
 
const livqeno = new Livqeno({ apiKey: process.env.LIVQENO_API_KEY, baseUrl: process.env.LIVQENO_API_URL });
 
// A channel name becomes a room name. getOrCreate makes concurrent joins safe.
const room = await livqeno.rooms.create({ name: channelName, getOrCreate: true });
 
const grant = await livqeno.tokens.create({
  room: room.id, // the id, not the name
  identity: String(uid),
  permissions: { join: true, subscribe: true, publish: true, publishData: true },
  expiresIn: 3600,
});
res.json(grant); // forward the whole grant: token, endpoint, iceServers

Things to know when you port the token endpoint:

  • One expiry, no separate privilege expiry. expiresIn is 30 seconds to 6 hours. Pass refreshToken to createRTCClient and the SDK fetches a new one before the old one runs out (RTC authentication). This replaces Agora's token-privilege-will-expire handler.
  • Audience members get { join: true, subscribe: true } and nothing else. For a host/audience product with many viewers, use a live stream instead of a room.

Client: join, publish and subscribe

import AgoraRTC from 'agora-rtc-sdk-ng';
 
const client = AgoraRTC.createClient({ mode: 'rtc', codec: 'vp8' });
await client.join(appId, channelName, token, uid);
 
const [micTrack, camTrack] = await AgoraRTC.createMicrophoneAndCameraTracks();
await client.publish([micTrack, camTrack]);
 
client.on('user-published', async (user, mediaType) => {
  await client.subscribe(user, mediaType);
  if (mediaType === 'video') user.videoTrack.play(`tile-${user.uid}`);
  if (mediaType === 'audio') user.audioTrack.play();
});

Event names map like this: user-joined / user-left → participantJoined / participantLeft; user-published / user-unpublished → trackSubscribed / trackUnsubscribed; connection-state-change → connectionStateChanged; volume-indicator → activeSpeakersChanged; network-quality → connectionQualityChanged. The full list is in Events.

To receive without sending video, set autoSubscribe (default on) and simply don't enable the camera. To stop receiving one track, call track.setEnabled(false) on the remote track.

Mute and unmute

await micTrack.setMuted(true);
await micTrack.setMuted(false);
 
// Or stop capture entirely:
await micTrack.setEnabled(false);

Screen share

const screenTrack = await AgoraRTC.createScreenVideoTrack({ encoderConfig: '1080p_1' });
// The Web SDK publishes one video track per client, so apps usually swap the
// camera out or use a second client for the screen (check Agora's docs).
await client.unpublish(camTrack);
await client.publish(screenTrack);

Dual stream vs. simulcast

await client.enableDualStream();
client.setRemoteVideoStreamType(remoteUid, 1); // 1 = low-quality stream

Messages: data streams, RTM and Agora Chat

There are two Livqeno equivalents, depending on whether the message should be kept:

  • Ephemeral, in-call (Agora data stream, RTM message channel used for signaling like "raise hand"): room.sendData().
  • Saved (RTM message history, Agora Chat): Chat.
import AgoraRTM from 'agora-rtm';
 
const rtm = new AgoraRTM.RTM(appId, userId);
await rtm.login({ token: rtmToken });
await rtm.subscribe(channelName);
 
rtm.addEventListener('message', (event) => console.log(event.publisher, event.message));
await rtm.publish(channelName, 'hello');

On your backend, a chat user must be a member of a conversation before their token reaches it. This replaces RTM's separate RTM token:

const conversation = await livqeno.chat.createConversation({
  name: channelName,
  roomId: room.id, // optional: attach the chat to the call's room
  getOrCreate: true,
});
await livqeno.chat.addMember(conversation.publicId, { userId: String(uid) });
const chatGrant = await livqeno.chat.createToken({ userId: String(uid), conversations: [conversation.publicId] });

RTM presence maps to chat presence and typing. RTM storage (channel and user metadata) and locks have no direct equivalent: see Gaps.

Cloud Recording

// Cloud Recording is a REST flow: acquire a resource, start, later stop.
const base = `https://api.agora.io/v1/apps/${appId}/cloud_recording`;
const headers = { Authorization: `Basic ${restCredential}`, 'Content-Type': 'application/json' };
 
const { resourceId } = await fetch(`${base}/acquire`, {
  method: 'POST', headers,
  body: JSON.stringify({ cname: channelName, uid: recorderUid, clientRequest: {} }),
}).then((r) => r.json());
 
const { sid } = await fetch(`${base}/resourceid/${resourceId}/mode/mix/start`, {
  method: 'POST', headers,
  body: JSON.stringify({ cname: channelName, uid: recorderUid, clientRequest: { token, storageConfig } }),
}).then((r) => r.json());
 
// ...later
await fetch(`${base}/resourceid/${resourceId}/sid/${sid}/mode/mix/stop`, {
  method: 'POST', headers,
  body: JSON.stringify({ cname: channelName, uid: recorderUid, clientRequest: {} }),
});

If you record ordinary calls with Cloud Recording (mixed or individual mode), Livqeno can't replace that today. Keep recorded channels on Agora until it can. The replay of a broadcast-mode stream is an HLS playlist hosted by Livqeno, not a file written to your own bucket.

Media Push (RTMP out)

// Media Push: create a converter that mixes the channel and pushes RTMP.
// Check Agora's docs for the region path and the transcodeOptions fields.
await fetch(`https://api.agora.io/${region}/v1/projects/${appId}/rtmp-converters`, {
  method: 'POST', headers,
  body: JSON.stringify({
    converter: {
      name: 'youtube',
      transcodeOptions: { rtcChannel: channelName /* audioOptions, videoOptions */ },
      rtmpUrl: `rtmp://a.rtmp.youtube.com/live2/${youtubeKey}`,
    },
  }),
});

The host joins with LiveStream.join(hostGrant) from @livqeno/live and turns on camera and mic on stream.room. Media Push's custom layouts (transcodeOptions) have no equivalent: Livqeno's output is a fixed grid at 1280×720. See Restream.

Media Gateway (encoder in)

Agora's Media Gateway gives an encoder a streaming key for a channel. Livqeno's ingest does the same for a live stream, over WHIP or RTMP(S):

const ingress = await livqeno.liveStreams.ingress.create(stream.id, { protocol: 'RTMP' });
 
// Shown only now (and after reset()). Paste into OBS.
console.log(ingress.rtmpsUrl, ingress.streamKey);

H.264 only, with B-frames turned off, up to 1080p30 and 8 Mbps. The video is passed through without transcoding, so viewers get exactly what the encoder sends.

Webhooks: NCS → Livqeno

const crypto = require('node:crypto');
 
// NCS signs the raw body with your NCS secret (check their docs for headers).
const expected = crypto.createHmac('sha256', process.env.AGORA_NCS_SECRET).update(rawBody).digest('hex');
if (expected === req.header('Agora-Signature-V2')) {
  const { eventType, payload } = JSON.parse(rawBody);
  // e.g. 103 / 104: a user joined / left the channel
}
Agora NCS event (check their docs for your channel profile)Livqeno webhook
Channel created / destroyedroom.started / room.ended
Broadcaster or user joins / leavesroom.participant_joined / room.participant_left
Audience joins / leaves (live profile)live_stream.viewer_joined / live_stream.viewer_left for live streams
Client role changelive_stream.stage_promoted / live_stream.stage_demoted
Cloud Recording eventslive_stream.recording_ready for broadcast-mode streams only
Media Push converter eventslive_stream.restream_active / live_stream.restream_failed
Media Gateway stream eventslive_stream.ingress_connected / _disconnected / _failed

The full list is the Event catalogue.

Feature gaps

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

Agora featureOn Livqeno
Cloud Recording (mixed, individual, web page)Not available for calls. Broadcast-mode live streams get an HLS replay.
Recording to your own storage bucketNot available.
Media Push custom layouts / transcoding optionsFixed grid at 1280×720 only. Up to 5 destinations.
Media Push or Media Gateway for an ordinary channelRestream and ingest work on live streams only, not plain rooms.
Ingest codecs other than H.264 (no B-frames), SRT inputNot available.
Channel media relay (cross-channel)Not available.
RTM storage (channel / user metadata) and locksNot available. Chat has message metadata, not a key-value store.
Spatial audio, raw audio/video frame APIs, extensions marketplaceNot available. See Effects removed.
Real-time transcriptionNot available.
Very large interactive channels50 participants per room by default; live streams for bigger audiences (Limits).
Game-engine SDKs (Unity, Unreal)Not available. Livqeno has Web, React, React Native, Flutter (mobile and desktop), iOS and Android (SDKs).
E2EE on React NativeNot yet.

Migration checklist

  • Inventory. List every use of agora-rtc-sdk-ng, agora-rtm / Agora Chat, the token builders, the Cloud Recording, Media Push and Media Gateway REST calls, and your NCS handler.
  • Identities. Decide how numeric uids become string identities (usually String(uid)), and make sure they're unique per room.
  • Credentials. Create a Livqeno project and an API key per environment (API credentials).
  • Token service. Add a Livqeno branch next to RtcTokenBuilder: rooms.create({ name: channelName, getOrCreate: true }), then tokens.create(). Return the whole grant. Map Agora roles to permission sets. Add refreshToken on the client.
  • Client. Wrap join, mute, screen share, dual stream and data in one interface. Pick the provider per channel, never per user inside one channel.
  • Messaging. Move RTM channels or Agora Chat groups to conversations; replace the RTM token with a chat token.
  • Live and RTMP. Move channels that use Media Push or Media Gateway to live streams.
  • Webhooks. Register a Livqeno endpoint, verify Swalvo-Signature, deduplicate on Livqeno-Event-Id, map NCS event types with the table above.
  • Recording. Keep Cloud Recording channels on Agora until you have a plan for them.
  • Dual-run, then cut over. Ramp new channels to Livqeno while watching observability. Remove Agora once no channel has used it for a full token lifetime.

Pricing

Livqeno's rates are on Usage. Compare them with Agora's current pricing pages; this guide doesn't quote them.

Next steps