# Video and Audio Calls Integration

## Introduction

Video calls are a common feature in chat applications. Stream offers both a [Chat SDK](/chat/docs/sdk/android/v6/) and a [Video SDK](/video/docs/android/) that work together seamlessly.

This guide covers adding video calling to an existing chat app. For the opposite (adding chat to a video app), see the [Video SDK documentation](/video/docs/android/).

## Permissions

Add camera and microphone permissions to your `AndroidManifest.xml`:

```xml
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
```

Request these permissions at runtime before starting a call.

## Setting Up Both SDKs

Both SDKs should use the same API key and user credentials. Here's how to initialize them together:

```kotlin
class MyApplication : Application() {
    override fun onCreate() {
        super.onCreate()

        val apiKey = "your_api_key"
        val userId = "user_id"
        val userToken = "user_token"

        // Initialize Chat SDK
        initializeChatClient(apiKey, userId, userToken)

        // Initialize Video SDK
        initializeStreamVideo(apiKey, userId, userToken)
    }

    private fun initializeChatClient(apiKey: String, userId: String, token: String) {
        val offlinePlugin = StreamOfflinePluginFactory(appContext = this)
        val statePlugin = StreamStatePluginFactory(
            config = StatePluginConfig(),
            appContext = this,
        )

        val chatClient = ChatClient.Builder(apiKey, this)
            .withPlugins(offlinePlugin, statePlugin)
            .build()

        val user = User(id = userId)
        chatClient.connectUser(user, token).enqueue()
    }

    private fun initializeStreamVideo(apiKey: String, userId: String, token: String) {
        val streamVideo = StreamVideoBuilder(
            context = this,
            apiKey = apiKey,
            user = io.getstream.video.android.model.User(id = userId),
            token = token,
        ).build()
    }
}
```

## Adding a Call Button (Compose)

Add a video call button to your message list header. Customize the `MessagesScreen` or create a custom header:

```kotlin
@Composable
fun ChatScreenWithVideo(
    channelId: String,
    onStartCall: (List<String>) -> Unit,
    onBackPressed: () -> Unit,
) {
    val viewModelFactory = MessagesViewModelFactory(
        context = LocalContext.current,
        channelId = channelId,
    )
    val listViewModel = viewModel(
        modelClass = MessageListViewModel::class.java,
        factory = viewModelFactory,
    )

    val channel = listViewModel.channel

    MessagesScreen(
        viewModelFactory = viewModelFactory,
        topBarContent = {
            MessageListHeader(
                channel = channel,
                currentUser = listViewModel.user.collectAsStateWithLifecycle().value,
                connectionState = listViewModel.connectionState.collectAsStateWithLifecycle().value,
                onBackPressed = onBackPressed,
                trailingContent = {
                    // Video call button
                    IconButton(
                        onClick = {
                            val memberIds = channel.members.map { it.user.id }
                            onStartCall(memberIds)
                        }
                    ) {
                        Icon(
                            imageVector = Icons.Default.VideoCall,
                            contentDescription = "Start video call",
                            tint = ChatTheme.colors.primaryAccent,
                        )
                    }
                },
            )
        },
    )
}
```

## Starting a Call

Create a call with the channel members. Setting `ring = true` sends push notifications to other members:

```kotlin
suspend fun startCall(memberIds: List<String>): Call? {
    val streamVideo = StreamVideo.instance()
    val callId = UUID.randomUUID().toString()

    val call = streamVideo.call(type = "default", id = callId)

    val result = call.create(
        memberIds = memberIds,
        ring = true,
    )

    return result.getOrNull()?.let { call }
}
```

## Handling Ringing Calls

The Video SDK provides `RingingCallContent` which automatically displays the correct UI based on call state:

- **Outgoing call**: Shows "Calling..." with a cancel button (for the caller)
- **Incoming call**: Shows caller info with accept/reject buttons (for recipients)

You need to handle call actions (`AcceptCall`, `DeclineCall`, `CancelCall`) in the `onCallAction` callback:

```kotlin
@Composable
fun RingingCallScreen(
    call: Call,
    onCallEnded: () -> Unit,
) {
    val scope = rememberCoroutineScope()

    RingingCallContent(
        call = call,
        onBackPressed = onCallEnded,
        onCallAction = { action ->
            scope.launch {
                when (action) {
                    is AcceptCall -> {
                        call.accept()
                        call.join()
                    }
                    is DeclineCall -> {
                        call.reject()
                        onCallEnded()
                    }
                    is CancelCall -> {
                        call.reject()
                        call.leave()
                        onCallEnded()
                    }
                    is ToggleCamera -> call.camera.setEnabled(action.isEnabled)
                    is ToggleMicrophone -> call.microphone.setEnabled(action.isEnabled)
                    else -> Unit
                }
            }
        },
        onAcceptedContent = {
            // Navigate when call disconnects (not immediately after leave())
            val connection by call.state.connection.collectAsStateWithLifecycle()
            LaunchedEffect(connection) {
                if (connection == RealtimeConnection.Disconnected) {
                    onCallEnded()
                }
            }

            CallContent(
                call = call,
                onBackPressed = { call.leave() },
            )
        },
        onRejectedContent = {
            // Call was rejected by all recipients
            LaunchedEffect(Unit) {
                onCallEnded()
            }
        },
        onNoAnswerContent = {
            // No one answered
            LaunchedEffect(Unit) {
                onCallEnded()
            }
        },
    )
}
```

The `onCallAction` handler processes button clicks:

| Action             | Triggered By              | Handler                          |
| ------------------ | ------------------------- | -------------------------------- |
| `AcceptCall`       | Accept button (incoming)  | `call.accept()` + `call.join()`  |
| `DeclineCall`      | Decline button (incoming) | `call.reject()`                  |
| `CancelCall`       | Cancel button (outgoing)  | `call.reject()` + `call.leave()` |
| `ToggleCamera`     | Camera toggle             | `call.camera.setEnabled()`       |
| `ToggleMicrophone` | Mic toggle                | `call.microphone.setEnabled()`   |

## Observing Ringing Calls

Monitor for ringing calls and navigate to the call screen:

```kotlin
@Composable
fun ObserveRingingCall(
    onRingingCall: (Call) -> Unit,
) {
    val ringingCall by StreamVideo.instance().state.ringingCall.collectAsStateWithLifecycle()

    LaunchedEffect(ringingCall) {
        ringingCall?.let { call ->
            onRingingCall(call)
        }
    }
}
```

## Complete Integration Example

Here's how it all comes together:

```kotlin
@Composable
fun ChatApp() {
    var currentScreen by remember { mutableStateOf<Screen>(Screen.ChannelList) }
    var activeCall by remember { mutableStateOf<Call?>(null) }

    val scope = rememberCoroutineScope()

    // Observe incoming ringing calls
    ObserveRingingCall { call ->
        activeCall = call
        currentScreen = Screen.Call
    }

    ChatTheme {
        VideoTheme {
            when (val screen = currentScreen) {
                Screen.ChannelList -> {
                    ChannelsScreen(
                        onChannelClick = { channel ->
                            currentScreen = Screen.Messages(channel.cid)
                        },
                    )
                }
                is Screen.Messages -> {
                    ChatScreenWithVideo(
                        channelId = screen.channelId,
                        onStartCall = { memberIds ->
                            scope.launch {
                                startCall(memberIds)?.let { call ->
                                    activeCall = call
                                    currentScreen = Screen.Call
                                }
                            }
                        },
                        onBackPressed = { currentScreen = Screen.ChannelList },
                    )
                }
                Screen.Call -> {
                    activeCall?.let { call ->
                        RingingCallScreen(
                            call = call,
                            onCallEnded = {
                                activeCall = null
                                currentScreen = Screen.ChannelList
                            },
                        )
                    }
                }
            }
        }
    }
}

sealed class Screen {
    data object ChannelList : Screen()
    data class Messages(val channelId: String) : Screen()
    data object Call : Screen()
}
```

The `RingingCallContent` inside `RingingCallScreen` handles all call states:

| State                | UI Shown                              |
| -------------------- | ------------------------------------- |
| Outgoing (caller)    | "Calling..." with cancel button       |
| Incoming (recipient) | Caller info with accept/reject        |
| Accepted             | `CallContent` via `onAcceptedContent` |
| Rejected/No answer   | Cleanup via callbacks                 |

## Push Notifications

For calls to work when the app is in the background, configure push notifications for both SDKs. See:

- [Chat Push Notifications](/chat/docs/sdk/android/v6/client/guides/push-notifications/)
- [Video Push Notifications](/video/docs/android/advanced/incoming-calls/push-notifications/)

Both can share the same Firebase project but need separate notification handling.

## Next Steps

- Explore the [Video SDK documentation](/video/docs/android/) for more call features
- Add [screen sharing](/video/docs/android/ui-components/call/call-content/#screen-sharing)
- Implement [call recording](/video/docs/android/advanced/recording/)
- Customize the [call UI](/video/docs/android/ui-components/overview/)


---

This page was last updated at 2026-04-17T17:33:31.874Z.

For the most recent version of this documentation, visit [https://getstream.io/chat/docs/sdk/android/v6/client/guides/video-integration/](https://getstream.io/chat/docs/sdk/android/v6/client/guides/video-integration/).