Documentation menu
Quickstart
Ten minutes to a working call: add the package, point it at a gateway, mint a token, join a channel. If you already have an Agora app, read the migration guide instead — it is shorter.
You need a VoxaRTC endpoint and a project on it. The managed endpoint is https://gate.zamansheikh.com; your App ID, certificate and test tokens come from the console.
Add the package
dependencies:
flutter:
sdk: flutter
voxa_rtc_engine: ^0.2.8flutter pub getPoint the SDK at your gateway
This is the only non-Agora line your app will contain. It lives in a separate import so the main barrel stays byte-compatible with Agora's.
import 'package:flutter/material.dart';
import 'package:voxa_rtc_engine/voxa.dart';
void main() {
VoxaRtc.serverUrl = 'https://gate.zamansheikh.com';
runApp(const MyApp());
}Declare platform permissions
Android needs nothing: the package's own manifest contributes CAMERA, RECORD_AUDIO, MODIFY_AUDIO_SETTINGS, INTERNET, ACCESS_NETWORK_STATE and the Bluetooth permissions, exactly as agora_rtc_engine does. You still request them at runtime as usual.
iOS and macOS need the usual usage descriptions:
<key>NSCameraUsageDescription</key>
<string>Camera access is used for video calls.</string>
<key>NSMicrophoneUsageDescription</key>
<string>Microphone access is used for calls.</string>Create a project and mint a test token
Every channel join is authenticated. In the console, create a project — it gives you an App ID and an app certificate — then mint a short-lived publisher token for a test channel from the Tokens tab.
Join a channel
From here on the code is ordinary Agora code. If you have written an Agora integration before, nothing below will surprise you.
import 'package:flutter/material.dart';
import 'package:voxa_rtc_engine/voxa_rtc_engine.dart';
class CallPage extends StatefulWidget {
const CallPage({super.key});
@override
State<CallPage> createState() => _CallPageState();
}
class _CallPageState extends State<CallPage> {
late final RtcEngine _engine;
final _remoteUids = <int>[];
@override
void initState() {
super.initState();
_init();
}
Future<void> _init() async {
_engine = createAgoraRtcEngine();
await _engine.initialize(const RtcEngineContext(
appId: '<your appId>',
channelProfile: ChannelProfileType.channelProfileLiveBroadcasting,
));
_engine.registerEventHandler(RtcEngineEventHandler(
onJoinChannelSuccess: (connection, elapsed) {
debugPrint('joined ${connection.channelId}');
},
onUserJoined: (connection, remoteUid, elapsed) {
setState(() => _remoteUids.add(remoteUid));
},
onUserOffline: (connection, remoteUid, reason) {
setState(() => _remoteUids.remove(remoteUid));
},
));
await _engine.setClientRole(
role: ClientRoleType.clientRoleBroadcaster,
);
await _engine.enableVideo();
await _engine.startPreview();
await _engine.joinChannel(
token: '<token from step 4>',
channelId: 'demo',
uid: 0, // 0 = let the token/server assign
options: const ChannelMediaOptions(),
);
}
@override
void dispose() {
_engine.leaveChannel();
_engine.release();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Stack(children: [
AgoraVideoView(
controller: VideoViewController(
rtcEngine: _engine,
canvas: const VideoCanvas(uid: 0), // local preview
),
),
for (final uid in _remoteUids)
AgoraVideoView(
controller: VideoViewController.remote(
rtcEngine: _engine,
canvas: VideoCanvas(uid: uid),
connection: const RtcConnection(channelId: 'demo'),
),
),
]);
}
}Run it on two devices
Mint a second token with a different uid for the same channel, and run the app on a second device. Each should fire onUserJoined for the other and render its video.
Next
- Tokens & auth — wire your own token server so you can stop using the admin endpoint.
- API compatibility — what is implemented, and what throws.
- Troubleshooting — the failures people actually hit.