Building a Real-Time Video Calling App with Agora UIKit in Flutter

Axiftaj
6 min readFeb 6, 2024

--

In this tutorial, we’ll guide you through the process of integrating Agora UIKit, a comprehensive UI toolkit for the Agora Real-Time Communication (RTC) SDK, into your Flutter app to implement a video calling feature. Following these steps will enable you to create a seamless and engaging video calling experience for your users.

Prerequisites:

  1. Basic knowledge of Flutter and Dart.
  2. An Agora account to obtain the App ID.
  3. Node.js installed for token generation backend.

Step 1: Setting Up the Flutter Project

Begin by creating a new Flutter project. Open your pubspec.yaml file and add the Agora UIKit Flutter package to your dependencies:

agora_uikit: latest:)

Run flutter pub get to install the dependency.

Step 2: Adding Permissions for Android and iOS

For a smooth video calling experience, ensure the necessary permissions are added to your AndroidManifest.xml and Info.plist files. Add the following to the AndroidManifest.xml:

<manifest> 
<!-- Other manifest configurations -->
<uses-permissionandroid:name="android.permission.READ_PHONE_STATE"/>
<uses-permissionandroid:name="android.permission.INTERNET" />
<uses-permissionandroid:name="android.permission.RECORD_AUDIO" />
<uses-permissionandroid:name="android.permission.CAMERA" />
<uses-permissionandroid:name="android.permission.MODIFY_AUDIO_SETTINGS" />
<uses-permissionandroid:name="android.permission.ACCESS_NETWORK_STATE" />
<!-- The Agora SDK requires Bluetooth permissions in case users are using Bluetooth devices. -->
<uses-permissionandroid:name="android.permission.BLUETOOTH" />
</manifest>

Add the following to the Info.plist file:

<key>NSMicrophoneUsageDescription</key>
<string>We need access to your microphone for voice calls.</string>
<key>NSCameraUsageDescription</key>
<string>We need access to your camera for video calls.</string>

Step 3: Token Generation Backend

Create a token generation backend using Node.js. Here’s an example index.js file:

const express = require('express');
const { RtcTokenBuilder, RtcRole } = require('agora-access-token');
const APP_ID = '***add APP_ID from agora dashboard*****';
const APP_CERTIFICATE = 'add APP_CERTIFICATE from agora dashboard';const app = express();const nocache = (req, resp, next) => {
resp.header('Cache-Control', 'private, no-cache, no-store, must-revalidate');
resp.header('Express', '-1');
resp.header('Pragma', 'no-cache');
next();
};const generateAccessToken = (req, resp) => {
resp.header('Access-Control-Allow-Origin', '*'); const channelName = req.query.channelName;
if (!channelName) {
return resp.status(500).json({ 'error': 'channel is required' });
} let uid = req.query.uid;
if (!uid || uid == '') {
uid = 0;
} let role = RtcRole.SUBSCRIBER;
if (req.query.role == 'publisher') {
role = RtcRole.PUBLISHER;
} let expireTime = req.query.expireTime;
if (!expireTime || expireTime == '') {
expireTime = 3600;
} else {
expireTime = parseInt(expireTime, 10);
} const currentTime = Math.floor(Date.now() / 1000);
const privilegeExpireTime = currentTime + expireTime; const token = RtcTokenBuilder.buildTokenWithUid(APP_ID, APP_CERTIFICATE, channelName, uid, role, privilegeExpireTime); return resp.json({ 'token': token });
};app.get('/access_token', nocache, generateAccessToken);const PORT = process.env.PORT || 5000;
app.listen(PORT, () => {
console.log('listening on ' + PORT);
});

This backend generates tokens required for Agora’s authentication. You can host this Node.js application on a platform like Heroku.

Step 4: Initiating a Video Call

Now that we have set up the project, added necessary permissions, and created a token generation backend, let’s proceed to initiate a video call. In this step, we will implement a function to make a call and navigate to the call screen.

// Step 4: Initiating a Video Call
Future<void> makeCall(BuildContext context, String receiverName,
String receiverUid, String receiverProfilePic, bool isGroupChat) async {
// Generate a unique call ID using Uuid
String callId = const Uuid().v1();
// Prepare call data for both the sender and receiver
Call senderCallData = Call(
callerId: SessionController().userId.toString(),
callerName: SessionController().name.toString(),
callerPic: SessionController().profilePic.toString(),
receiverId: receiverUid,
receiverName: receiverName,
receiverPic: receiverProfilePic,
callId: callId,
hasDialled: true,
);

Call receiverCallData = Call(
callerId: SessionController().userId.toString(),
callerName: SessionController().name.toString(),
callerPic: SessionController().profilePic.toString(),
receiverId: receiverUid,
receiverName: receiverName,
receiverPic: receiverProfilePic,
callId: callId,
hasDialled: false,
);
// Call the function to handle the call and navigate to the call screen
await callUser(senderCallData, context, receiverCallData);
}
// Function to handle the call and navigate to the call screen
Future callUser(
Call senderCallData,
BuildContext context,
Call receiverCallData,
) async {
try {
// Store call data in Firestore
await FirebaseFirestore.instance
.collection('call')
.doc(senderCallData.callerId)
.set(senderCallData.toMap());
await FirebaseFirestore.instance
.collection('call')
.doc(senderCallData.receiverId)
.set(receiverCallData.toMap());
// Navigate to the call screen
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => CallScreen(
channelId: senderCallData.callId,
call: senderCallData,
isGroupChat: false,
),
),
);
} catch (e) {
// Handle any errors
Utils.toasstMessage(e.toString());
}
}

In this step, we’ve implemented the makeCall function, which generates a unique call ID and prepares call data for both the sender and receiver. The callUser function is then called to store the call data in Firestore and navigate to the call screen.

Step 5: Enhancing the Video Call Screen

In this step, we will enhance the video call screen by integrating Agora UIKit and adding video call functionalities.

// Step 5: Enhancing the Video Call Screen
class CallScreen extends StatefulWidget {
// Existing code for CallScreen
}
class _CallScreenState extends State<CallScreen> {
AgoraClient? client;
String? token;
String baseUrl = 'https://mybackend.herokuapp.com'; // Function to fetch Agora token from the server
Future<void> getToken() async {
final response = await http.get(Uri.parse(
'$baseUrl/access_token?channelName=video${SessionController().userId}&role=subscriber&uid=0',
));
if (response.statusCode == 200) {
setState(() {
token = jsonDecode(response.body)['token'];
});
// Initialize Agora
initAgora();
}
}
@override
void initState() {
super.initState();
Future.delayed(const Duration(milliseconds: 1000)).then(
(_) {
// Fetch Agora token from the server
getToken();
},
);
// Initialize AgoraClient with connection data
client = AgoraClient(
agoraConnectionData: AgoraConnectionData(
appId: AgoraConfig.appId,
channelName: widget.channelId,
tempToken: token,
),
);
}
// Function to initialize Agora
void initAgora() async {
await client!.initialize();
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: client == null
? const LoadingWidget()
: SafeArea(
child: Stack(
children: [
// Agora Video Viewer to display video stream
AgoraVideoViewer(client: client!),
// Agora Video Buttons for in-call functionalities
AgoraVideoButtons(
client: client!,
// Disconnect button
disconnectButtonChild: IconButton(
onPressed: () async {
await client!.engine.leaveChannel();
// End the call and navigate back
endCall(
widget.call.callerId,
widget.call.receiverId,
context,
);
Navigator.pop(context);
},
icon: const Icon(Icons.call_end),
),
),
],
),
),
);
}
// Function to end the call and perform cleanup
void endCall(
String callerId,
String receiverId,
BuildContext context,
) async {
try {
// Remove call data from Firestore
await FirebaseFirestore.instance
.collection('call')
.doc(callerId)
.delete();
await FirebaseFirestore.instance
.collection('call')
.doc(receiverId)
.delete();
} catch (e) {
// Handle any errors
Utils.toasstMessage(e.toString());
}
}
}

In this step, we’ve enhanced the Call Screen by integrating Agora UIKit. The screen now fetches the Agora token from the server using the getTokenfunction, which communicates with the specified server URL.

Step 6: Implementing Call Synchronization Widget

In this step, we’ll implement a widget that synchronizes call data from the backend. This widget will determine if there’s an incoming call for the current user and display the appropriate screen.

Firstly, let’s create a new widget named CallSyncWidget:

// Step 6: Implementing Call Synchronization Widget
class CallSyncWidget extends StatelessWidget {
final Widget scaffold;
const CallSyncWidget({
Key? key,
required this.scaffold,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return StreamBuilder<DocumentSnapshot>(
stream: FirebaseFirestore.instance
.collection('call')
.doc(SessionController().userId)
.snapshots(),
builder: (context, snapshot) {
if (snapshot.hasData && snapshot.data!.data() != null) {
Call call = Call.fromMap(snapshot.data!.data() as Map<String, dynamic>);
if (!call.hasDialled) {
// If there's an incoming call, display the Call Pickup Screen
return CallPickupScreen(call: call);
}
}
// If no incoming call, display the provided scaffold
return scaffold;
},
);
}
}

Now, let’s integrate this widget into our existing screens. Update the main build method in each screen to use CallSyncWidget:

// In Call Screen
@override
Widget build(BuildContext context) {
return CallSyncWidget(
scaffold: Scaffold(
// Existing Call Screen code
),
);
}
// In Call Pickup Screen
@override
Widget build(BuildContext context) {
return CallSyncWidget(
scaffold: Scaffold(
// Existing Call Pickup Screen code
),
);
}
// In any other screen where you want to check for incoming calls
@override
Widget build(BuildContext context) {
return CallSyncWidget(
scaffold: Scaffold(
// Existing screen code
),
);
}

By integrating CallSyncWidget, your app will now dynamically switch between screens based on incoming calls for the current user. The widget checks for incoming calls and displays the appropriate screen accordingly.

Congratulations! You’ve successfully implemented call synchronization in your Flutter app using Agora UIKit. Your users will now experience a seamless and engaging video calling experience.

Feel free to explore additional customization and features to enhance your video calling app further. If you have any questions or encounter issues, refer to the Agora documentation or seek assistance from the community.

This concludes our tutorial on Building a Real-Time Video Calling App with Agora UIKit in Flutter. We hope you found this guide helpful, and happy coding!

--

--

Axiftaj
Axiftaj

Written by Axiftaj

Hello, this is Asif Taj a tech enthusiasts providing the quality services for the Android and iOS apps development, UI/UX Research and Designs, and Video ads.

Responses (1)