Get started
Migrating from Agora to VoltKit RTC
Move an Agora voice or video app to VoltKit RTC: how channels, uids, roles, tokens, audio routing and callbacks map, with Android (Kotlin) examples.
The concepts carry over almost one to one: an Agora channel is a VoltKit room, a uid is an identity (a string you choose), a broadcaster is a host or cohost, an audience member is a viewer. The main difference is where decisions are made: roles, moderation and room lifetime are enforced by the VoltKit service from what your backend put in the token, not by the client.
The steps
- Backend. Replace your Agora token builder with two REST calls: make sure the room exists, and mint a token per user with their role. See Issue tokens from your backend.
- App. Replace
RtcEnginewith oneStreamClientper call or room (Android, Flutter, JavaScript). Test in a build without the Agora SDK: two call engines in one app compete for the microphone and the audio routing. - Callbacks. Replace
IRtcEngineEventHandlercallbacks with the client's flows (table below). - Ringing, push and call history stay yours, as they were with Agora. Webhooks can feed call records.
Concepts
| Agora | VoltKit RTC |
|---|---|
| App ID + App Certificate | API base URL + API secret (server only) |
| Channel name | roomId (create it with PUT /v1/rooms/{roomId}) |
| uid (int) | identity (your user id, a string) |
| RTC token from the token builder | room token from POST /v1/rooms/{roomId}/tokens, with the role inside |
CLIENT_ROLE_BROADCASTER | host (moderates too) or cohost |
CLIENT_ROLE_AUDIENCE | viewer |
setClientRole on the client | the host calls promote(identity, role); the server enforces it |
Channel profile COMMUNICATION / LIVE_BROADCASTING | one room model for both: everyone publishes in a call; only speakers in a live room |
| RTM (messaging) | built in: sendMessage / onMessage on the same connection |
| Cloud recording | Private recording: the camera recorded on the device straight to your R2 (Flutter SDK today; not in the Android SDK yet) |
Android calls
Agora RtcEngine | VoltKit StreamClient |
|---|---|
RtcEngine.create(config) | StreamClient(context, wsUrl, token) |
joinChannel(token, channel, uid, options) | connect(metadata) |
leaveChannel(), RtcEngine.destroy() | close() |
enableAudio() + publish the microphone | publish(PublishOptions(video = false)) |
enableVideo() + setupLocalVideo | publish() + localVideoTrack.value?.addSink(renderer) |
setupRemoteVideo(VideoCanvas) | attachVideo(track, renderer) when onTrack delivers the track |
muteLocalAudioStream(muted) | setMicEnabled(!muted) |
muteLocalVideoStream(muted) | setCamEnabled(!muted) |
muteRemoteAudioStream(uid, muted) | RemoteTrack.setVolume(0.0) on that user's audio track |
adjustUserPlaybackSignalVolume(uid, v) | RemoteTrack.setVolume(v / 100.0) |
switchCamera() | switchCamera() |
setEnableSpeakerphone(on) | setSpeakerphoneOn(on) |
setDefaultAudioRoutetoSpeakerphone(on) | StreamOptions(speakerphone = on) |
setAudioProfile(SPEECH_STANDARD) | AudioQuality.std (the default) |
setAudioProfile(MUSIC_HIGH_QUALITY) | AudioQuality.hd (where the project allows it) |
enableAudioVolumeIndication(...) | collect onAudioLevels (sampled while collected) |
setVideoEncoderConfiguration(...) | VideoPublishOptions(quality, frameRate), setVideoQuality, setVideoFrameRate |
setRemoteVideoStreamType(uid, LOW) | setReceiveQuality(producerId, VideoQuality.p480), or adaptive stream via attachVideo |
setRemoteSubscribeFallbackOption(AUDIO_ONLY) | setAudioOnlyFallback(true) (on by default) |
setLocalPublishFallbackOption(AUDIO_ONLY) | setPublishFallback(true) |
renewToken(token) | updateToken(token) (rarely needed: the service refreshes tokens) |
startEchoTest / startLastmileProbeTest | StreamClient.preflight(context, wsUrl, token) |
getCallId + log upload | getDiagnostics().toJson() |
Callbacks
| Agora callback | VoltKit flow |
|---|---|
onJoinChannelSuccess | connect() returns the JoinResult |
onUserJoined / onUserOffline(uid, reason) | onParticipantJoined / onParticipantLeft: USER_OFFLINE_QUIT is reason left, USER_OFFLINE_DROPPED is disconnected (they may come back) |
onRemoteAudioStateChanged, onUserMuteAudio | onTrackMuted (and onTrack, onTrackEnded) |
onRemoteVideoStateChanged, onUserMuteVideo | onTrackMuted |
onAudioVolumeIndication | onAudioLevels |
onActiveSpeaker | onActiveSpeakers |
onAudioRouteChanged | audioOutput (a StateFlow) |
onConnectionStateChanged | onConnectionStateChanged (idle, connecting, connected, reconnecting, closed) |
onConnectionLost / onRejoinChannelSuccess | reconnecting / onReconnected |
onNetworkQuality | onNetworkQuality |
onTokenPrivilegeWillExpire | onTokenExpiring |
onClientRoleChanged | onRoleChanged |
onLocalAudioStateChanged (error) | onMediaWarning, onError |
onError | onError (a registry code), and the call's own RpcError |
onLeaveChannel | onClosed(reason) |
What is different
- No uid allocation. Use your own user id as
identity; it is a string and appears as-is to everyone, in webhooks and in moderation. - One room per call. A one-to-one call is a room of two; end it with
endRoom()orDELETE /v1/rooms/{roomId}. - Moderation is server side.
forceMute,kickand role changes are enforced by the service; the muted person cannot undo them. Raise-hand, the stage and seats are built in (Group audio rooms). - No subscribe calls. Every remote track is received as soon as it is published; adaptive stream and
setReceiveQualitychoose how much video. Remote audio plays by itself. - Audio routing is automatic. Bluetooth and wired headsets are followed while connected;
manageAudio = falsehands routing back to your app. - Billing. Participant-minutes by the media used: standard audio, HD audio, video. See Usage and billing.
Moving gradually
Run both in different builds rather than in one: two call engines in one app compete for the microphone, audio focus and routing. A common path is an internal build on VoltKit for testing, with your backend minting VoltKit tokens next to Agora ones, and switching users over by app version.
Last updated . Something unclear or wrong? Tell us.