Prerequisites
Before proceeding, ensure that your development environment meets the following requirements:
- Android Studio Arctic Fox (2020.3.1) or later.
- Android SDK API Level 21 or higher.
- A mobile device that runs Android 5.0 or later.
Important: One should have a VideoSDK account to generate token. Visit VideoSDK dashboard to generate token
Getting Started with the Code!
Follow the steps to create the environment necessary to add audio calls into your app. Also you can find the code sample for quickstart here.
Create new Android Project
For a new project in Android Studio, create a Phone and Tablet Android project with an Empty Activity.

Caution: After creating the project, Android Studio automatically starts gradle sync. Ensure that the sync succeeds before you continue.
Integrate Video SDK
1dependencyResolutionManagement {
2 repositories {
3 google()
4 mavenCentral()
5 maven {url= uri("https://maven.aliyun.com/repository/jcenter")}
6 }
7}
81dependencyResolutionManagement{
2 repositories {
3 google()
4 maven { url = uri("https://www.jitpack.io") }
5 mavenCentral()
6 maven {url= uri("https://maven.aliyun.com/repository/jcenter")}
7 }
8}
9- Add the following dependency in your app's
app/build.gradle.
1dependencies {
2 implementation ("live.videosdk:rtc-android-sdk:2.2.0")
3
4 // library to perform Network call to generate a meeting id
5 implementation ("com.amitshekhar.android:android-networking:1.0.2")
6
7 // other app dependencies
8 }
9Important: Android SDK compatible with armeabi-v7a, arm64-v8a, x8664 architectures. If you want to run the application in an emulator, choose ABI x8664 when creating a device.
Add permissions into your project
- In
/app/Manifests/AndroidManifest.xml, add the following permissions.
1<uses-permission android:name="android.permission.INTERNET"/>
2<uses-permission android:name="android.permission.RECORD_AUDIO"/>
3<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS"/>
4Note: An audio only app publishes the microphone alone, so the camera feature and the
CAMERApermission are not required. Leaving them out means Android never prompts the user for camera access.
Note: If your project has set
android.useAndroidX=true, then setandroid.enableJetifier=truein thegradle.propertiesfile to migrate your project to AndroidX and avoid duplicate class conflict.
Structure of the project
Your project structure should look like this.
1app/
2βββ src/main/
3β βββ java/com/example/app/
4β β βββ components/
5β β βββ screens/
6β β βββ navigation/
7β β βββ ui/
8β β β βββ theme/
9β β βββ model/
10β β βββ MainApplication.kt
11β β βββ MainActivity.kt
12β β βββ NetworkClass.kt
13β βββ res/
14β βββ AndroidManifest.xml
15βββ build.gradle
16βββ settings.gradle
17App Architecture
Step 1: Initialize VideoSDK
- Create
MainApplicationclass which will extend theandroid.app.Application.
- Create field
sampleTokeninMainApplicationwhich will hold the generated token from the VideoSDK dashboard. This token will use in VideoSDK config as well as in generating meetingId.
1import android.app.Application
2import live.videosdk.rtc.android.VideoSDK
3
4class MainApplication: Application() {
5
6 val sampleToken = "YOUR_TOKEN" //paste your token here
7
8 override fun onCreate() {
9 super.onCreate()
10 VideoSDK.initialize(applicationContext)
11 }
12}
13- Add
MainApplicationtoAndroidManifest.xml
1<application
2 android:name=".MainApplication" >
3 <!-- ... -->
4</application>
5Step 2: Managing Permissions
The MainActivity is the entry point of the application. It handles the permission request for accessing the microphone.
On creation, it checks if the required permission (RECORD_AUDIO) is granted.
If not, it requests it. Once the permission is granted, it sets the content view using Jetpack Compose and initializes the MyApp composable, which contains the app's navigation and UI logic.
1class MainActivity : ComponentActivity() {
2 override fun onCreate(savedInstanceState: Bundle?) {
3 super.onCreate(savedInstanceState)
4 checkSelfPermission(REQUESTED_PERMISSIONS[0], PERMISSION_REQ_ID)
5
6 setContent {
7 Videosdk_android_compose_quickstartTheme {
8 MyApp(this)
9 }
10 }
11 }
12
13 private fun checkSelfPermission(permission: String, requestCode: Int): Boolean {
14 if (ContextCompat.checkSelfPermission(this, permission) !=
15 PackageManager.PERMISSION_GRANTED
16 ) {
17 ActivityCompat.requestPermissions(this,
18 REQUESTED_PERMISSIONS, requestCode)
19 return false
20 }
21 return true
22 }
23
24 companion object {
25 private const val PERMISSION_REQ_ID = 22
26 private val REQUESTED_PERMISSIONS = arrayOf(
27 Manifest.permission.RECORD_AUDIO
28 )
29 }
30}
31
32@Composable
33fun MyApp(context: Context) {
34 NavigationGraph(context = context)
35}
36Step 3: Setting Up Navigation
The NavigationGraph composable manages navigation between the JoinScreen and MeetingScreen using NavController.
1@Composable
2fun NavigationGraph(navController: NavHostController = rememberNavController(),context: Context) {
3 NavHost(navController = navController, startDestination = "join_screen") {
4 composable("join_screen") {
5 JoinScreen(navController,context)
6 }
7 composable("meeting_screen?meetingId={meetingId}") { backStackEntry ->
8 val meetingId = backStackEntry.arguments?.getString("meetingId")
9 meetingId?.let {
10 MeetingScreen(viewModel = MeetingViewModel(),navController, meetingId, context)
11 }
12 }
13 }
14}
15Step 4: Creating Components
- Before building the individual screens, it's essential to develop reusable composable components that will be used throughout the project.
- Create a
ReusableComponents.ktfile to organize all reusable components for the project, such as buttons, spacers, and customizable text.
1@Composable
2fun MyAppButton(task: () -> Unit, buttonName: String) {
3 Button(onClick = task) {
4 Text(text = buttonName)
5 }
6}
7@Composable
8fun MySpacer() {
9 Spacer(
10 modifier = Modifier
11 .fillMaxWidth()
12 .height(1.dp)
13 .background(color = Color.Gray))}
14@Composable
15fun MyText(text: String, fontSize: TextUnit = 23.sp) {
16 Text(
17 text = text,
18 fontSize = fontSize,
19 fontWeight = FontWeight.Normal,
20 modifier = Modifier.padding(4.dp),
21 style = MaterialTheme.typography.bodyMedium.copy(fontSize = 16.sp),
22 color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f)
23 )}
24- Create a
ParticipantAudioView.ktfile that includes two composable functions:ParticipantAudioViewfor rendering an individual participant tile, andParticipantsGridfor displaying a grid of participant tiles.
Info:
- An audio only call has no video track to render, so there is no
VideoViewand noAndroidViewwrapper here. The tile is plain Compose UI showing the participant's name and microphone state.- Remote participant audio is played back by the Android SDK automatically once the participant joins, so no player or view has to be attached for the audio to be heard.
- The
ParticipantEventListeneris used to keep the microphone indicator in sync when a participant mutes or unmutes.
1@Composable
2fun ParticipantAudioView(
3 participant: Participant
4) {
5 var isMicEnabled by remember { mutableStateOf(false) }
6
7 LaunchedEffect(participant) {
8 isMicEnabled = participant.streams.any { (_, stream) ->
9 stream.kind.equals("audio", ignoreCase = true) && stream.track != null
10 }
11
12 participant.addEventListener(object : ParticipantEventListener() {
13 override fun onStreamEnabled(stream: Stream) {
14 if (stream.kind.equals("audio", ignoreCase = true)) {
15 isMicEnabled = true
16 }
17 }
18
19 override fun onStreamDisabled(stream: Stream) {
20 if (stream.kind.equals("audio", ignoreCase = true)) {
21 isMicEnabled = false
22 }
23 }
24 })
25 }
26
27 Box(
28 modifier = Modifier
29 .fillMaxWidth()
30 .height(200.dp)
31 .background(if (isMicEnabled) Color.DarkGray else Color.Gray),
32 contentAlignment = Alignment.Center
33 ) {
34 Text(
35 text = if (isMicEnabled) "Mic On" else "Mic Off",
36 color = Color.White
37 )
38
39 Box(
40 modifier = Modifier
41 .align(Alignment.BottomCenter)
42 .fillMaxWidth()
43 .background(Color(0x99000000))
44 .padding(4.dp)
45 ) {
46 Text(
47 text = participant.displayName,
48 color = Color.White,
49 modifier = Modifier.align(Alignment.Center)
50 )
51 }
52 }
53}
54@Composable
55fun ParticipantsGrid(
56 participants: List<Participant>,
57 modifier: Modifier = Modifier
58) {
59 LazyVerticalGrid(
60 columns = GridCells.Fixed(2),
61 verticalArrangement = Arrangement.spacedBy(16.dp),
62 horizontalArrangement = Arrangement.spacedBy(16.dp),
63 modifier = modifier
64 .fillMaxWidth()
65 .padding(8.dp)
66 ) {
67 items(participants.size) { index ->
68 ParticipantAudioView(
69 participant = participants[index],
70 )
71 }
72 }
73}
74Step 5: Creating Joining Screen
We will create a simple Join screen UI that supports two primary roles:
- Create Meeting: Starts a new meeting.
- Join Meeting: Enter a Meeting ID to join an existing meeting.
The Joining screen will include :
- Create Button - This button will create a new meeting for you.
- TextField for Meeting Id - This text field will contain the meeting Id you want to join.
- Join Button - This button will join the meeting with
meetingIdyou provided.
1@Composable
2fun JoinScreen(
3 navController: NavController, context: Context
4) {
5 val app = context.applicationContext as MainApplication
6 val token = app.sampleToken
7 Box(
8 modifier = Modifier
9 .fillMaxSize()
10 .padding(8.dp),
11 contentAlignment = Alignment.Center
12 ) {
13 Column(
14 modifier = Modifier.padding(4.dp),
15 horizontalAlignment = Alignment.CenterHorizontally,
16 verticalArrangement = Arrangement.SpaceEvenly
17 ) {
18 var input by rememberSaveable { mutableStateOf("") }
19 CreateMeetingBtn(navController, token)
20 Text(text = "OR")
21 InputMeetingId(input) { updateInput ->
22 input = updateInput
23 }
24 JoinMeetingBtn(navController, input)
25 }
26 }
27}
28
29@Composable
30fun CreateMeetingBtn(navController: NavController, token: String) {
31 MyAppButton({
32 NetworkManager.createMeetingId(token) { meetingId ->
33 navController.navigate("meeting_screen?meetingId=$meetingId")
34 }
35 }, "Create Meeting")
36}
37
38@Composable
39fun InputMeetingId(input: String, onInputChange: (String) -> Unit) {
40 OutlinedTextField(value = input,
41 onValueChange = onInputChange,
42 label = { Text(text = "Enter Meeting Id") })
43}
44
45@Composable
46fun JoinMeetingBtn(navController: NavController, meetingId: String) {
47 MyAppButton({
48 if (meetingId.isNotEmpty()) {
49 navController.navigate("meeting_screen?meetingId=$meetingId")
50 }
51 }, "Join Meeting")
52}
53Output

Step 6: Creating MeetingId
The NetworkManager singleton handles meeting creation by making a POST request to the VideoSDK API:
- Meeting Creation: Sends a request to the API with an authorization token to create a meeting.
- Response Handling: Extracts the roomId from the response and triggers the
onMeetingCreatedcallback.
1object NetworkManager {
2 fun createMeetingId(token: String, onMeetingIdCreated: (String) -> Unit) {
3 AndroidNetworking.post("https://api.videosdk.live/v2/rooms")
4 .addHeaders("Authorization", token)
5 .build()
6 .getAsJSONObject(object : JSONObjectRequestListener {
7 override fun onResponse(response: JSONObject) {
8 try {
9 val meetingId = response.getString("roomId")
10 onMeetingIdCreated(meetingId)
11 } catch (e: JSONException) {
12 e.printStackTrace()
13 }
14 }
15
16 override fun onError(anError: ANError) {
17 anError.printStackTrace()
18 Log.d("TAG", "onError: $anError")
19 }
20 })
21 }
22}
23Note: Don't confuse with Room and Meeting keyword, both are same thing π
Step 7: Creating Meeting Screen
We will create a simple Meeting screen UI with two main sections:
- Participants' Tiles: Displays the name and microphone state of each meeting participant.
- Media Control Buttons: Includes buttons to control the mic and leave the meeting.
The MeetingScreen composable is the main UI for an audio call meeting, handling meeting details and user actions. The MeetingScreen composable is organized into three sections, each serving a specific purpose:
Header: Displays the meeting ID at the top of the screen for reference.ParticipantsGrid: Shows the list of participants in a grid layout, adjusting dynamically.MediaControlButtons: A set of buttons to control the microphone and leave the meeting.
1@Composable
2fun MeetingScreen(viewModel: MeetingViewModel, navController: NavController, meetingId: String, context: Context) {
3
4 val app = context.applicationContext as MainApplication
5 val isMeetingLeft = viewModel.isMeetingLeft
6
7 LaunchedEffect(isMeetingLeft) {
8 if (isMeetingLeft) {
9 navController.navigate("join_screen")
10 }
11 }
12 Column(modifier = Modifier.fillMaxSize()) {
13 Header(meetingId)
14 MySpacer()
15 ParticipantsGrid(viewModel.participants, Modifier.weight(1f))
16 MySpacer()
17 MediaControlButtons(
18 onJoinClick = {
19 viewModel.initMeeting(context, app.sampleToken, meetingId)
20 },
21 onMicClick = { viewModel.toggleMic() },
22 onLeaveClick = {
23 viewModel.leaveMeeting()
24 }
25 )
26 }
27}
28@Composable
29fun Header(meetingId: String) {
30 Box(
31 modifier = Modifier
32 .fillMaxWidth()
33 .padding(8.dp),
34 contentAlignment = Alignment.TopStart
35 ) {
36 Row {
37 MyText("MeetingID: ", 28.sp)
38 MyText(meetingId, 25.sp)
39 }
40 }
41}
42@Composable
43fun MediaControlButtons(onJoinClick:()->Unit,onMicClick: () -> Unit, onLeaveClick: () -> Unit) {
44 Row(
45 modifier = Modifier
46 .fillMaxWidth()
47 .padding(6.dp),
48 verticalAlignment = Alignment.CenterVertically,
49 horizontalArrangement = Arrangement.SpaceAround
50 ) {
51 MyAppButton(onJoinClick,"Join")
52 MyAppButton(onMicClick,"ToggleMic")
53 MyAppButton(onLeaveClick,"Leave")
54 }
55}
56Step 8: Setting Up MeetingViewModel
The MeetingViewModel manages the meeting state, handles participants, and controls the microphone setting to ensure a smooth audio call experience.
Managing Meeting State and Media Control:
- Meeting Initialization: Initializes the meeting with the VideoSDK and adds an event listener.
- Participant Management: Tracks participants, adding/removing them as they join/leave.
- Media Control: Toggles the microphone state.
- Leave Meeting: Allows users to leave the meeting.
1class MeetingViewModel : ViewModel() {
2
3 private var meeting: Meeting? = null
4 private var micEnabled by mutableStateOf(true)
5 val participants = mutableStateListOf<Participant>()
6
7 //used in MeetingScreen to handle navigation
8 var isMeetingLeft by mutableStateOf(false)
9 private set
10
11 fun initMeeting(context: Context, token: String, meetingId: String ) {
12 VideoSDK.config(token)
13 if(meeting==null){
14 //webcamEnabled is passed as false because this is an audio only call
15 meeting = VideoSDK.initMeeting(
16 context, meetingId, "John Doe",
17 micEnabled, false, null, null, true, null, null)
18 }
19 meeting!!.addEventListener(meetingEventListener)
20 meeting!!.join()
21 }
22
23 private val meetingEventListener: MeetingEventListener = object : MeetingEventListener() {
24 override fun onMeetingJoined() {
25 Log.d("#meeting", "onMeetingJoined()")
26 meeting?.let { participants.add(it.localParticipant) }
27 }
28 override fun onMeetingLeft() {
29 Log.d("#meeting", "onMeetingLeft()")
30 meeting = null
31 isMeetingLeft = true
32 }
33
34 override fun onParticipantJoined(participant: Participant) {
35 participants.add(participant)
36 }
37
38 override fun onParticipantLeft(participant: Participant) {
39 participants.remove(participant)
40 }
41 }
42
43 fun toggleMic() {
44 if (micEnabled) {
45 meeting?.muteMic()
46 } else {
47 meeting?.unmuteMic()
48 }
49 micEnabled = !micEnabled
50 }
51
52 fun leaveMeeting() {
53 meeting?.leave()
54 }
55}
56Final Output
We are done with implementation of customised audio calling app in Android using Video SDK. To explore more features go through Basic and Advanced features.
Tip: You can checkout the complete quick start example here.
