Build multi-modal AI applications using our new open-source Vision AI SDK.

Android Livestreaming Tutorial

The following tutorial shows you how to quickly build a Livestreaming app leveraging Stream's Video API and the Stream Video Android components. The underlying API is very flexible and allows you to build nearly any type of video experience.

Prefer to skip the setup? Add the Stream skill and let your AI agent build your Android livestream.

In this tutorial we'll build a low-latency in-app livestreaming experience with Jetpack Compose and the Stream Video SDK. The livestream is broadcast over Stream's global edge network.

This page works for both humans and AI coding agents: every step is a file operation, shell command, or a clearly-marked manual action, and each step has a verification checkpoint.

The tutorial has two parts:

Part 1 — View a livestream — create a livestream in the dashboard, push video into it with OBS over RTMP, and watch it on Android.

Part 2 — Broadcast from a device — publish a livestream directly from Android over WebRTC, with backstage and go-live.

Livestreaming is more hands-on than the other tutorials. Part 1 needs the Stream dashboard and OBS — both are manual, human-only steps that no CLI or agent can do for you. They're marked with 🧑 below.

Choose your path

Install the Stream CLI once, then add the skills, so Claude Code, Cursor, or Codex build the Android code against current SDK APIs:

bash
1
2
3
4
5
6
7
# Install the getstream CLI curl -fsSL https://getstream.io/cli.sh | bash # Install the skills (router, docs, and builder) for your agent. # --universal for Cursor, Codex, and other AGENTS-ecosystem tools; --claude for Claude Code. getstream skills --universal getstream skills stream-android --universal

Then ask your agent:

plaintext
1
2
3
4
/stream-android Add a Stream livestream viewer to my Jetpack Compose app using LivestreamPlayer, then a broadcaster screen that publishes from the device camera with backstage + go-live. Use my CLI credentials, or the tutorial demo credentials if I'm not logged in.

Where you come in. The agent writes the Android code, but you create the livestream in the dashboard and run OBS (Part 1), log in when getstream init opens the browser, and run the app granting camera/mic access (Part 2).

Path B — Build it manually

Follow the steps below.

For AI assistants reading this page: append .md to any Stream docs URL for a clean Markdown version. A condensed index for the Android Video SDK is at https://getstream.io/video/docs/android/llms.txt. Prefer these over parsing HTML.

Important Building Blocks

  • StreamVideo — the client, built with StreamVideoBuilder. For a viewer it can use User.anonymous(); a broadcaster needs a user with a broadcast-capable role (e.g. admin).
  • Call of type livestream — created with client.call("livestream", id). Backstage mode (on by default) lets hosts set up before call.goLive() opens the stream to viewers.
  • LivestreamPlayer — a ready-made viewer component. Customize it with overlayContent and rendererContent, or drop to VideoRenderer for a fully custom player.
  • call.stateconnection, backstage, duration, totalParticipants, livestream, localParticipant, and more, as StateFlows.

Part 1 — View a livestream

Step 1 — Create a livestream in the dashboard

Open the dashboard and select Video & Audio → Overview. You'll see buttons to create different call types.

Screenshot shows dashboard overview

Click Create Livestream. The next screen shows the livestream's details:

Screenshot shows dashboard livestream credentials

You'll need the RTMP URL and RTMP Stream Key (for OBS in Step 2), plus the API Key, Viewer Token, and Livestream ID (for the app in Step 3).

🧑 Human checkpoint: creating the livestream and copying its credentials is done in the Stream dashboard — there's no CLI or agent path for it. Agents: ask the human to create a livestream and paste back the RTMP URL, stream key, API key, viewer token, and call id.

Step 2 — Set up the livestream in OBS

OBS is a popular livestreaming package; we'll use it to publish video over RTMP.

After installing it, set up a capture device in the Sources section:

Screenshot shows capturing device setup

Select Video Capture Device to stream from your camera (or Screen Capture for your screen). Then click Settings in the Controls section:

Screenshot shows OBS settings

Select Stream. For Service choose Custom, and enter the Server (RTMP URL) and Stream Key from Step 1:

Screenshot shows OBS settings setup

Press Start Streaming in the Controls section, then return to the dashboard — you should see the OBS feed:

Screenshot shows livestream that is live

By default the livestream starts immediately; you can enable backstage for the livestream call type in the dashboard.

🧑 Human checkpoint: installing and configuring OBS is a manual desktop step. Agents: ask the human to start streaming from OBS and confirm the feed appears in the dashboard before continuing.

Checkpoint: the dashboard shows your OBS video feed playing live.

Step 3 — Build the livestreaming app

  1. Open Android Studio and create a new project.
  2. Select Phone & Tablet → Empty Activity (the Compose one) and click Next.
  3. Name your project LivestreamApp and click Finish.

🧑 Human checkpoint: the project is created in Android Studio's New Project wizard. Agents: detect the project shape first (find . -maxdepth 3 -name "settings.gradle*" -o -name "build.gradle*"); if there's no Gradle project, ask the human to create it.

Step 3.1 — Add the Stream SDK

Add the SDK to the app module's build.gradle. Replace <latest_version> with the latest release:

kotlin
1
2
3
4
5
6
7
8
9
10
11
dependencies { // Stream Video Compose SDK implementation("io.getstream:stream-video-android-ui-compose:<latest_version>") // Jetpack Compose (Android Studio often adds these automatically) implementation(platform("androidx.compose:compose-bom:2024.09.02")) implementation("androidx.activity:activity-compose:1.9.0") implementation("androidx.compose.ui:ui") implementation("androidx.compose.ui:ui-tooling") implementation("androidx.compose.material3:material3") }

Use recent Android Gradle Plugin, Kotlin, and Compose compiler versions so the SDK can target compileSdk = 35. In the root build.gradle.kts:

kotlin
1
2
3
4
5
plugins { id("com.android.application") version "8.7.2" apply false id("org.jetbrains.kotlin.android") version "2.0.21" apply false id("org.jetbrains.kotlin.plugin.compose") version "2.0.21" apply false }

Apply the Compose plugin in the app module's build.gradle.kts:

kotlin
1
2
3
4
5
plugins { id("com.android.application") id("org.jetbrains.kotlin.android") id("org.jetbrains.kotlin.plugin.compose") }

Add the INTERNET permission in AndroidManifest.xml, before the application tag:

xml
1
<uses-permission android:name="android.permission.INTERNET" />

Sync the project.

Checkpoint: ./gradlew :app:assembleDebug succeeds and import io.getstream.video.android.compose.ui.components.livestream.LivestreamPlayer resolves.

Step 3.2 — Initialize the SDK

You initialize the SDK by building a StreamVideo client. For a production app, create it in your Application class or a DI module. To keep this simple we create it in MainActivity in the next step. You can omit the user parameter to default to an anonymous user.

Step 3.3 — View a Livestream on an Android Device

Open MainActivity.kt and replace the MainActivity class with the following. It initializes the SDK and plays the stream with LivestreamPlayer:

kotlin
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.compose.runtime.Composable import io.getstream.video.android.compose.permission.LaunchCallPermissions import io.getstream.video.android.compose.theme.VideoTheme import io.getstream.video.android.compose.ui.components.livestream.LivestreamPlayer import io.getstream.video.android.core.Call import io.getstream.video.android.core.StreamVideoBuilder import io.getstream.video.android.model.User class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) // Initialize StreamVideo. For a production app, we recommend adding the client to your Application class or DI module. val client = StreamVideoBuilder( context = applicationContext, user = User.anonymous(), apiKey = "YOUR_API_KEY", token = "YOUR_VIEWER_TOKEN", ).build() val call = client.call("livestream", "YOUR_CALL_ID") setContent { VideoTheme { LiveAudience(call) } } } } @Composable fun LiveAudience(call: Call) { // Ensure the call has the correct permissions to run LaunchCallPermissions(call = call, onAllPermissionsGranted = { // Join the livestream. call.join() }) LivestreamPlayer(call = call) }

Replace the placeholders with values from the dashboard (Step 1): YOUR_API_KEY = API Key, YOUR_VIEWER_TOKEN = Viewer Token, YOUR_CALL_ID = Livestream ID.

Screenshot shows livestream call data

That's everything needed to play a livestream. LivestreamPlayer plays the stream given just the call id and type.

Checkpoint: run the app — you see the livestream published from OBS playing on the device.

Step 3.4 — Customizing the UI

The default player UI is a good starting point. Customize LivestreamPlayer with overlayContent or rendererContent.

Customizing Overlay Content

Provide custom overlayContent to display extra information, such as viewer counts or duration:

kotlin
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp import io.getstream.video.android.compose.theme.VideoTheme import io.getstream.video.android.compose.ui.components.livestream.LivestreamPlayer import io.getstream.video.android.core.Call @Composable fun CustomOverlayContent(call: Call) { val totalParticipants by call.state.totalParticipants.collectAsState() Column( modifier = Modifier .fillMaxWidth() .padding(16.dp), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Top ) { Text(text = "Custom overlay Live Stream", style = VideoTheme.typography.bodyL, color = Color.Red) Spacer(modifier = Modifier.height(8.dp)) Text(text = "Viewers: $totalParticipants", color = Color.White) } } @Composable fun CustomLivestreamPlayer(call: Call) { LivestreamPlayer( call = call, overlayContent = { CustomOverlayContent(call = call) } ) }

Customizing Renderer Content

Provide rendererContent to change how the host's video is rendered. Here we render with VideoRenderer while keeping the custom overlay:

kotlin
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import io.getstream.video.android.compose.ui.components.livestream.LivestreamPlayer import io.getstream.video.android.compose.ui.components.video.VideoRenderer import io.getstream.video.android.core.Call @Composable fun CustomLivestreamPlayer(call: Call) { val livestream by call.state.livestream.collectAsState() LivestreamPlayer( call = call, rendererContent = { VideoRenderer( modifier = Modifier.fillMaxSize(), call = call, video = livestream, videoFallbackContent = { // Content for when the video is not available. }, ) }, overlayContent = { CustomOverlayContent(call = call) }, ) }

To use these customizations, replace LivestreamPlayer with CustomLivestreamPlayer in LiveAudience.

State & Participants

call.state exposes the StateFlows you need:

  • call.state.connection — realtime connection status (use it for a loading UI).
  • call.state.backstage — whether the call is in backstage mode.
  • call.state.duration — how long the call has been running.
  • call.state.totalParticipants — the number of viewers.
  • call.state.localParticipant — the participant on this device.

call.state.participants gives access to all participants (name, image, video track, roles). The participant state docs explain more.

Writing a Fully Custom Livestream Player

You can also write a fully custom player for complete control. The example below uses a Scaffold with a top bar (viewer count + duration) over the video:

kotlin
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp import io.getstream.video.android.compose.theme.VideoTheme import io.getstream.video.android.compose.ui.components.video.VideoRenderer import io.getstream.video.android.core.Call @Composable private fun FullyCustomLivestreamPlayer(call: Call) { val totalParticipants by call.state.totalParticipants.collectAsState() val duration by call.state.duration.collectAsState() val livestream by call.state.livestream.collectAsState() Scaffold( modifier = Modifier .fillMaxSize() .background(VideoTheme.colors.baseSheetPrimary) .padding(6.dp), contentColor = VideoTheme.colors.baseSheetPrimary, topBar = { Box( modifier = Modifier .fillMaxWidth() .padding(6.dp), ) { Text( modifier = Modifier .align(Alignment.CenterEnd) .background( color = VideoTheme.colors.brandPrimary, shape = RoundedCornerShape(6.dp), ) .padding(horizontal = 12.dp, vertical = 4.dp), text = "Live $totalParticipants", color = Color.White, ) Text( modifier = Modifier.align(Alignment.Center), text = "Live for $duration", color = VideoTheme.colors.brandPrimaryDk, ) } }, ) { VideoRenderer( modifier = Modifier .fillMaxSize() .padding(it) .clip(RoundedCornerShape(6.dp)), call = call, video = livestream, videoFallbackContent = { // Content for when the video is not available. }, ) } }

Part 2 — Broadcast from a device

In Part 1 we published via RTMP and authenticated through the dashboard. In a real app you'd generate tokens programmatically with a server-side SDK. Part 2 broadcasts directly from an Android device over WebRTC.

Step 0 — Credentials for broadcasting

The broadcaster needs an API key, a user token for a user with a broadcast-capable role, a user id, and a call id. Provision your own via the CLI, or use the pre-filled tutorial credentials.

Option 1 — Your own app, via the CLI:

bash
1
2
3
getstream init # authenticate + create/select org & app (opens a browser) getstream env --target android # writes STREAM_API_KEY to local.properties getstream token tutorial_user # mint a user token

🧑 Human checkpoint: getstream init opens a browser to authenticate. Agents: run it, then ask the human to finish logging in before continuing.

Option 2 — Pre-filled tutorial credentials: the whole-app broadcast block below is filled in with working demo credentials — copy it as-is. Swap in your own before building anything real.

Step 4 — Livestreaming from Android devices

We'll send video from the device over WebRTC using the backstage flow.

Step 4.1 — Permissions setup

Publishing needs camera and microphone access. Add to AndroidManifest.xml:

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

Add the camera feature declaration:

xml
1
<uses-feature android:name="android.hardware.camera" android:required="false" />

These permissions must be granted for the livestream to work; we'll request them with Stream components shortly.

Step 4.2 — Broadcasting a livestream

This replaces the viewer code from Part 1 — the app now publishes video instead of watching it. Update MainActivity to create the broadcaster client with a user in the admin role (so they're allowed to broadcast):

kotlin
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.compose.material3.Text import io.getstream.video.android.compose.theme.VideoTheme import io.getstream.video.android.core.StreamVideoBuilder import io.getstream.video.android.model.User class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val userId = "REPLACE_WITH_USER_ID" val userName = "Broadcaster" val userToken = "REPLACE_WITH_TOKEN" // Step 1 - Create a user. val user = User( id = userId, // predefined string name = userName, // Name and image are used in the UI role = "admin", ) // Step 2 - Initialize StreamVideo. For a production app, we recommend adding the client to your Application class or DI module. val client = StreamVideoBuilder( context = applicationContext, apiKey = "REPLACE_WITH_API_KEY", user = user, token = userToken, ).build() setContent { VideoTheme { Text(text = "TODO: Render host content") } } } }

Run the app now and you'll see TODO: Render host content.

Creating the call

Stream uses the same call object for livestreaming, audio rooms, and video calling:

kotlin
1
2
val call = client.call("livestream", "REPLACE_WITH_CALL_ID") call.join(create = true)

Specify the call type as livestream and a unique callId. call.join(create = true) creates the call on the server and sets up realtime transport. You can also add members with roles — see the call creation docs.

Rendering the UI

Create a new file LiveHostContent.kt with a composable that renders the broadcast. Unlike watching, there's no default broadcast component, so we build one.

kotlin
1
2
3
4
@Composable fun LiveHostContent(call: Call) { // We will define our content here }

Collect State Properties — track connection, participant count, backstage, local participant, video, and duration. Add inside LiveHostContent:

kotlin
1
2
3
4
5
6
7
val connection by call.state.connection.collectAsState() val totalParticipants by call.state.totalParticipants.collectAsState() val backstage by call.state.backstage.collectAsState() val localParticipant by call.state.localParticipant.collectAsState() val video = localParticipant?.video?.collectAsState() val duration by call.state.duration.collectAsState() val scope = rememberCoroutineScope()

Set Up the Scaffold with a top bar, bottom bar, and content area:

kotlin
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Scaffold( modifier = Modifier .fillMaxSize() .background(VideoTheme.colors.baseSheetPrimary) // On targetSdk 35 the app draws edge-to-edge, behind the system bars. systemBarsPadding // keeps the top bar below the status bar and the broadcast button above the navigation bar. .systemBarsPadding() .padding(6.dp), contentColor = VideoTheme.colors.baseSheetPrimary, topBar = { // Will define the topBar content here }, bottomBar = { // Will define bottomBar content here }, ) { // Main content, will be the VideoRenderer (ignore the padding warning) } }

Create the Top Bar — show live status, participant count, and duration when connected; a backstage message otherwise:

kotlin
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
if (connection == RealtimeConnection.Connected) { if (!backstage) { Box( modifier = Modifier .fillMaxWidth() .padding(6.dp), ) { Text( modifier = Modifier .align(Alignment.CenterEnd) .background( color = VideoTheme.colors.brandPrimary, shape = RoundedCornerShape(6.dp), ) .padding(horizontal = 12.dp, vertical = 4.dp), text = "Live $totalParticipants", color = Color.White, ) Text( modifier = Modifier.align(Alignment.Center), text = "Live for $duration", color = VideoTheme.colors.basePrimary, ) } } else { Text( text = "The livestream is not started yet", color = VideoTheme.colors.basePrimary, ) } } else if (connection is RealtimeConnection.Failed) { Text( text = "Connection failed", color = VideoTheme.colors.basePrimary, ) }

Create the Bottom Bar — a button to start/stop the broadcast based on backstage state:

kotlin
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Button( colors = ButtonDefaults.buttonColors( contentColor = VideoTheme.colors.brandPrimary, containerColor = VideoTheme.colors.brandPrimary ), onClick = { scope.launch { if (backstage) call.goLive() else call.stopLive() } }, ) { Text( text = if (backstage) "Start Broadcast" else "Stop Broadcast", color = Color.White, ) }

Render the Video Content with VideoRenderer:

kotlin
1
2
3
4
5
6
7
8
9
10
11
VideoRenderer( modifier = Modifier .fillMaxSize() .padding(it) .clip(RoundedCornerShape(6.dp)), call = call, video = video?.value, videoFallbackContent = { // Content for when the video is not available. }, )

The complete MainActivity

Now wire LiveHostContent into MainActivity. We add a LiveHost() composable that requests camera and microphone permissions before joining:

kotlin
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import io.getstream.video.android.compose.permission.LaunchPermissionRequest import io.getstream.video.android.compose.theme.VideoTheme import io.getstream.video.android.core.Call import io.getstream.video.android.core.StreamVideoBuilder import io.getstream.video.android.model.User class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) // Step 1 - Create a user with the admin role so they can broadcast. val user = User( id = "REPLACE_WITH_USER_ID", name = "Broadcaster", role = "admin", ) // Step 2 - Initialize StreamVideo. For a production app, we recommend adding the client to your Application class or DI module. val client = StreamVideoBuilder( context = applicationContext, apiKey = "REPLACE_WITH_API_KEY", user = user, token = "REPLACE_WITH_TOKEN", ).build() val call = client.call("livestream", "REPLACE_WITH_CALL_ID") setContent { VideoTheme { LiveHost(call) } } } @Composable fun LiveHost(call: Call) { LaunchPermissionRequest( permissions = listOf( android.Manifest.permission.RECORD_AUDIO, android.Manifest.permission.CAMERA ) ) { AllPermissionsGranted { LaunchedEffect(call) { call.join(create = true) } LiveHostContent(call = call) } NoneGranted { // Handle permission explanation } } } }

🧑 Human checkpoint: the camera and microphone prompts are Android system dialogs, and a real device gives a real camera feed. Agents: install the app, then ask the human to launch it and allow camera and microphone access.

Note: When you go live, the SDK keeps the broadcast alive in a foreground service, which posts an ongoing notification. On Android 13+ this requires the POST_NOTIFICATIONS runtime permission — the SDK declares and requests it for you. Denying it never blocks the broadcast; the user just won't see the notification.

Checkpoint: the broadcaster screen shows your camera feed with a Start Broadcast button (backstage), and the top bar reads The livestream is not started yet until you go live.

Step 5 — Backstage and Go Live

Backstage mode lets you and co-hosts set up before going live. Only after call.goLive() can regular viewers join. (Disable backstage for the livestream call type in the dashboard if you want calls to start immediately.) LiveHostContent reads call.state.backstage: when true, it shows the setup screen with a Start Broadcast button that calls call.goLive().

Step 6 — Preview in the browser

Press Start Broadcast in the Android app. You'll see:

Livestream

Open the stream in your browser to watch:

For testing you can join the call on our web-app:

Checkpoint: after Start Broadcast, the top bar switches to Live <count> and the browser shows your device's video.

Advanced Features

Verify the whole build

bash
1
./gradlew :app:installDebug

Then confirm end to end: Part 1 — OBS pushes to the dashboard livestream and the app plays it. Part 2 — the broadcaster app requests camera/mic, shows the backstage setup, and after Start Broadcast the browser (Step 6) shows your device's video.

Troubleshooting

  • stream project is not initialized — CLI onboarding not run (Part 2 CLI path). Run getstream init first.
  • Player shows nothing (Part 1) — OBS isn't streaming, or the API key/viewer token/call id don't match the dashboard livestream. Re-copy all three from the livestream page.
  • Can't go live / not authorized to broadcast — the broadcaster user needs a broadcast-capable role. Create the user with role = "admin" (Step 4.2).
  • No camera/mic prompt (Part 2) — permissions missing or denied. Add CAMERA and RECORD_AUDIO to the manifest (Step 4.1) and grant them.
  • Black video when broadcasting — run on a real device; the emulator has no real camera.
  • Viewers can't join — the stream is still in backstage. Press Start Broadcast (call.goLive()).
  • Compose build errors — align Kotlin, Compose compiler, and AGP versions (Step 3.1).

Next steps

Calls run on Stream's global edge network. Pricing is flexible with multiple tiers.

Final Thoughts

You built low-latency in-app livestreaming two ways — watching an RTMP/OBS stream with LivestreamPlayer, and broadcasting from a device over WebRTC with backstage and go-live. WebRTC is optimal for latency; HLS buffers better on poor connections. The Video SDK for Android and the API support recording, HLS, co-hosts, moderation, and more.

Machine-readable resources

For AI agents and coding assistants working with this SDK:

  • CLI + skills: curl -fsSL https://getstream.io/cli.sh | bash, then getstream skills --universal and getstream skills stream-android --universal for the Android pack (use --claude instead for Claude Code). (Alternative: npx skills add GetStream/agent-skills -s stream.)
  • Provisioning (broadcaster): getstream initgetstream env --target androidgetstream token <user_id>; the viewer credentials in Part 1 come from the dashboard livestream page
  • Manual steps that have no CLI/agent path: creating the livestream in the dashboard and running OBS (Part 1)
  • Android integration skill: invoke /stream-android in your agent; /stream-docs searches live SDK docs with citations
  • Docs index for LLMs: https://getstream.io/video/docs/android/llms.txt
  • Markdown endpoints: append .md to any docs URL for a clean, token-efficient version
  • Source of truth for APIs: the SDK repository — check the pinned version's source rather than assuming APIs from training data

Give us feedback!

Did you find this tutorial helpful in getting you up and running with your project? Either good or bad, we're looking for your honest feedback so we can improve.

Start coding for free

No credit card required.
If you're interested in a custom plan or have any questions, please contact us.