Skip to content

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

  1. 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.
  2. App. Replace RtcEngine with one StreamClient per 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.
  3. Callbacks. Replace IRtcEngineEventHandler callbacks with the client's flows (table below).
  4. Ringing, push and call history stay yours, as they were with Agora. Webhooks can feed call records.

Concepts

AgoraVoltKit RTC
App ID + App CertificateAPI base URL + API secret (server only)
Channel nameroomId (create it with PUT /v1/rooms/{roomId})
uid (int)identity (your user id, a string)
RTC token from the token builderroom token from POST /v1/rooms/{roomId}/tokens, with the role inside
CLIENT_ROLE_BROADCASTERhost (moderates too) or cohost
CLIENT_ROLE_AUDIENCEviewer
setClientRole on the clientthe host calls promote(identity, role); the server enforces it
Channel profile COMMUNICATION / LIVE_BROADCASTINGone 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 recordingPrivate recording: the camera recorded on the device straight to your R2 (Flutter SDK today; not in the Android SDK yet)

Android calls

Agora RtcEngineVoltKit StreamClient
RtcEngine.create(config)StreamClient(context, wsUrl, token)
joinChannel(token, channel, uid, options)connect(metadata)
leaveChannel(), RtcEngine.destroy()close()
enableAudio() + publish the microphonepublish(PublishOptions(video = false))
enableVideo() + setupLocalVideopublish() + 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 / startLastmileProbeTestStreamClient.preflight(context, wsUrl, token)
getCallId + log uploadgetDiagnostics().toJson()

Callbacks

Agora callbackVoltKit flow
onJoinChannelSuccessconnect() 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, onUserMuteAudioonTrackMuted (and onTrack, onTrackEnded)
onRemoteVideoStateChanged, onUserMuteVideoonTrackMuted
onAudioVolumeIndicationonAudioLevels
onActiveSpeakeronActiveSpeakers
onAudioRouteChangedaudioOutput (a StateFlow)
onConnectionStateChangedonConnectionStateChanged (idle, connecting, connected, reconnecting, closed)
onConnectionLost / onRejoinChannelSuccessreconnecting / onReconnected
onNetworkQualityonNetworkQuality
onTokenPrivilegeWillExpireonTokenExpiring
onClientRoleChangedonRoleChanged
onLocalAudioStateChanged (error)onMediaWarning, onError
onErroronError (a registry code), and the call's own RpcError
onLeaveChannelonClosed(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() or DELETE /v1/rooms/{roomId}.
  • Moderation is server side. forceMute, kick and 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 setReceiveQuality choose how much video. Remote audio plays by itself.
  • Audio routing is automatic. Bluetooth and wired headsets are followed while connected; manageAudio = false hands 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.