How to Create a Live Streaming App for Android

Discover how easy it can be to add chat to a live stream app on Android!

Samuel U.
Samuel U.
Published May 6, 2020 Updated June 9, 2021

As we seek to still connect with friends and family while we all #stayhome, live streaming and video conferencing have become insanely popular. These technologies have always been great tools for connecting with coworkers and loved ones who are not located nearby; however, we are now seeing incredible growth in their use for connecting with people near and far. Artists are live-streaming their performances; teachers are educating children via online classes; merchants are turning to live video commerce; friends and family are meeting online using group video chat, etc.

This post is still useful, but out of date. Stream now offers a Live Video Streaming API!

With this being the case, the apps that support these connections have also been taking off, and we thought it might be fun to show you just how easy it would be to build a live streaming application powered by video and chat! In this tutorial, we'll walk through how to create a live video streaming app on Android. What’s more, the app we build here won’t just be an ugly, bare-bones application; thanks to Stream's Chat API, which will allow for seamless communication between users within the live video chat, we’ll have a friendly and appealing UI!

Note: If you'd like to jump right into the code, you can find the complete open-source codebase on GitHub.

At the end of the tutorial, you will have created the following Android streaming application from scratch:

Sample Live Streaming Android App Video with Chat Overlay Powered by Stream

To incorporate chat into our application, this tutorial will leverage Stream’s Low-Level Android Client; if you’d like to read more about using it, check out Stream’s Android Chat Docs and our Android SDK!

Getting Started

Let’s start by cloning the step-1 branch of the app repo from GitHub, which will serve as our initial codebase for this tutorial. Head over to your preferred working directory on your machine and clone the project using the following snippet:

$ git clone -b step-1 git@github.com:GetStream/Livestream-Clone-Android.git

The step-1 branch contains the necessary project code with a basic UI that consists of the following:

  • A simple YouTube-style video streaming view
  • A customized input box with a send button, for adding comments
  • All of the required Gradle dependencies, including Stream’s Low-Level Android client – one of the core dependencies of this tutorial

To start, you will need to generate a file called secret.properties within the root directory of your project. Populate your new file with the following code:

StreamApiKey=<YOUR_STREAM_API_KEY>
UserToken=<YOUR_STREAM_USER_TOKEN>

Replace the <YOUR_STREAM_API_KEY> and <YOUR_STREAM_USER_TOKEN> defaults with the values from your own Stream Dashboard. The StreamApiKey is a public key which you can get by registering your chat app at https://getstream.io/dashboard/. The UserToken is the JWT token of your app user. To ease the process of generating a JWT on the fly, we’ve created a simple generator within our docs, which can be found here.

Once you have finished adding the necessary secrets, just run the ./gradlew build command. At this point, you should have the project building successfully!

After running the app, you should see the following screen with the video playing automatically and comment input controls present at the bottom:

Video image with comments overlay

Creating the UI

Let’s add the UI code responsible for displaying the chat messages over the video!

We will start by modifying the activity_main.xml to add a RecyclerView widget, which will display chat messages as a vertical list. The RecyclerView will be placed at the bottom of the screen, leaving the space at the top for the video, to give it more real estate and make it the focal point. Behind the list view, we are also going to add a gradient to improve the readability of the latest incoming messages, which happens to be useful when bright video frames are displayed on the device, as well.

Let’s start by adding the following views to the activity_main.xml file:

...    
<com.pierfrancescosoffritti.androidyoutubeplayer.core.player.views.YouTubePlayerView ... />
    
    <!-- Translucent gradient overlay background to make chat more visible -->
    <View
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:background="@drawable/bg_chat"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintTop_toTopOf="@+id/messagesList" />

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/messagesList"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_marginBottom="@dimen/margin_x_small"
        android:orientation="vertical"
        android:overScrollMode="never"
        android:paddingLeft="@dimen/margin_small"
        android:paddingRight="@dimen/margin_small"
        app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
        app:layout_constraintBottom_toTopOf="@+id/sendMessageButton"
        app:layout_constraintHeight_percent="0.35" />
...

Next, we will need to implement the adapter class to be responsible for rendering the chat messages in the RecyclerView widget. To do this, we will add a “MessagesListAdapter.kt” file with the following code:

package io.getstream.livestreamclone

import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import io.getstream.chat.android.client.models.Message
import kotlinx.android.synthetic.main.item_message.view.*

class MessagesListAdapter() : ListAdapter<Message, MessageViewHolder>(DiffCallback) {

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MessageViewHolder {
        val itemView = LayoutInflater.from(parent.context)
            .inflate(R.layout.item_message, parent, false)
        return MessageViewHolder(itemView)
    }

    override fun onBindViewHolder(holder: MessageViewHolder, position: Int) {
        holder.bindMessage(getItem(position))
    }

    companion object {
        val DiffCallback = object : DiffUtil.ItemCallback<Message>() {
            override fun areItemsTheSame(oldItem: Message, newItem: Message): Boolean {
                return oldItem.id == newItem.id
            }

            override fun areContentsTheSame(oldItem: Message, newItem: Message): Boolean {
                return oldItem == newItem
            }
        }
    }
}

class MessageViewHolder(view: View) : RecyclerView.ViewHolder(view) {
    fun bindMessage(message: Message) {
        message.apply {
            itemView.avatar.loadUrl(user.image, R.drawable.ic_person_white_24dp)
            itemView.userName.text = user.name
            itemView.message.text = text
        }
    }
}

As you can see, we are using the ListAdapter class from the androidx.recyclerview package – this is a wrapper over the standard RecyclerView.Adapter class and is a great way to optimize the way items render when you update the list.

To read more about the ListAdapter class, head over to the Android docs.

We also declared the layout for the single chat message element as R.layout.item_message. Let’s define that layout now! Create a new layout file called “item_message.xml” in in the app/res/layout directory and add the following code:

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="wrap_content">

    <androidx.cardview.widget.CardView
        android:id="@+id/avatarWrapper"
        android:layout_width="@dimen/avatar_size"
        android:layout_height="@dimen/avatar_size"
        android:layout_margin="@dimen/margin_x_small"
        app:cardCornerRadius="@dimen/avatar_circle_radius"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintTop_toTopOf="parent">
        <androidx.appcompat.widget.AppCompatImageView
            android:id="@+id/avatar"
            android:layout_width="@dimen/avatar_size"
            android:layout_height="@dimen/avatar_size" />
    </androidx.cardview.widget.CardView>


    <TextView
        android:id="@+id/userName"
        style="@style/TextAppearance.AppCompat.Title"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_margin="@dimen/margin_x_small"
        android:ellipsize="end"
        android:lines="1"
        app:layout_constraintLeft_toRightOf="@+id/avatarWrapper"
        app:layout_constraintTop_toTopOf="@+id/avatarWrapper" />

    <TextView
        android:id="@+id/message"
        style="@style/TextAppearance.AppCompat.Caption"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_margin="@dimen/margin_x_small"
        android:ellipsize="end"
        android:maxLines="3"
        app:layout_constraintLeft_toRightOf="@+id/avatarWrapper"
        app:layout_constraintTop_toBottomOf="@+id/userName" />

</androidx.constraintlayout.widget.ConstraintLayout>

At the end of this step, we are going to instantiate the adapter and set it to the RecyclerView in the LiveStreamActivity.onCreate() hook method. We are also going to add the updateMessagesList(messages: List<Message>) function, which will be responsible for updating the message items in the list:

class LiveStreamActivity : AppCompatActivity(R.layout.activity_main) {
    private val adapter = MessagesListAdapter()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        loadMockVideoStream()
        messagesList.adapter = adapter

        val viewModel: LiveStreamViewModel by viewModels()

        sendMessageButton.setOnClickListener {
            viewModel.sendButtonClicked(messageInput.text.toString())
            messageInput.setText("")
            messageInput.clearFocus()
            messageInput.hideKeyboard()
        }
    }

    private fun updateMessagesList(messages: List<Message>) {
        adapter.submitList(messages)
        adapter.notifyDataSetChanged()
        val scrollTarget = adapter.itemCount
        messagesList.scrollToPosition(scrollTarget)
    }
    
    ...
  }

The Message type in the updateMessagesList() function belongs to the io.getstream.chat.android.client.models package from the Stream Low-Level client library.

In the next part of this tutorial, we are going to implement simple message sending logic and handle incoming message events!

Adding Chat with Stream

Now that you have completed the previous steps, your code should be the same as the step-2 branch of the app repo.

Building your own app? Get early access to our Livestream or Video Calling API and launch in days!

You can check out the step-2 branch if you’d like to start from this part and skip the previous portion of the tutorial.

Let’s start by initiating the ChatClient instance. Let’s add the following line inside the onCreate() function of our App class:

ChatClient.Builder(BuildConfig.STREAM_API_KEY, this).build()

Next, let’s jump into the LiveStreamViewModel.kt file. This is the place where the actual connection with the chat backend will be managed. Let’s start by defining the User and Channel metadata in the companion object at the bottom of the class:

    companion object {
        private const val USER_ID = "bob"
        private const val CHANNEL_TYPE = "livestream"
        private const val CHANNEL_ID = "livestream-clone-android" // You'll want to make it unique per video
        private const val USER_TOKEN = BuildConfig.USER_TOKEN

        private val chatUser = User(id = USER_ID).apply {
            name = USER_ID
            image = getDummyAvatar(USER_ID)
        }

        private fun getDummyAvatar(id: String) = "https://api.adorable.io/avatars/285/$id.png"
    }

Then, obtain the ChatClient instance and assign it into a class property called chatClient. Now, let’s use the ChatClient instance to initiate the Channel. We will do so by calling the setUser() function within theinit {} block:

class LiveStreamViewModel() : ViewModel() {
    private val chatClient = ChatClient.instance()
    private val _viewState = MutableLiveData<State>()

    private lateinit var channelController: ChannelController

    val viewState: LiveData<State> = _viewState

    init {
        chatClient.setUser(chatUser, USER_TOKEN, object : InitConnectionListener() {
            override fun onSuccess(data: ConnectionData) {
                channelController = chatClient.channel(CHANNEL_TYPE, CHANNEL_ID)
                requestChannel()
                subscribeToNewMessageEvent()
            }

            override fun onError(error: ChatError) {
                _viewState.postValue(State.Error("User setting error"))
                Timber.e(error)
            }
        })
    }
    ...
  }

If the chatClient.setUser() operation is successful, we are going to receive the ConnectionData object in the onSuccess() callback.

At this point, we set the channel by calling the chatClient.channel() function. This function takes two parameters: the CHANNEL_TYPE and CHANNEL_ID. For the purposes of our sample application, we are setting CHANNEL_TYPE to “livestream” and CHANNEL_ID to a hardcoded value.

In your production application, you will want to pass a unique ID specific to the video stream you wish to present.

This function returns the ChannelController instance, which we assign to the private lateinit var channelController: ChannelController class property.

We will also use this instance later, to perform requests to the chat backend.

When we receive the onSuccess callback, we are calling two functions: requestChannel() and subscribeToNewMessageEvent():

    private fun requestChannel() {
        val channelData = mapOf("name" to "Live stream chat")
        val request = QueryChannelRequest()
            .withData(channelData)
            .withMessages(20)
            .withWatch()

        channelController.query(request).enqueue {
            if (it.isSuccess) {
                _viewState.postValue(State.Messages(it.data().messages))
            } else {
                _viewState.postValue(State.Error("QueryChannelRequest error"))
                Timber.e(it.error())
            }
        }
    }

    private fun subscribeToNewMessageEvent() {
        chatClient.events().subscribe {
            if (it is NewMessageEvent) {
                _viewState.postValue(State.NewMessage(it.message))
            }
        }
    }

Inside requestChannel(), we are creating an instance of QueryChannelRequest() and calling channelController.query(request) followed by an enqueue() call, which executes our function. The request contains channel data that specifies the name of the channel, the number of latest messages we want to obtain, and the watch flag. The withWatch() method informs our app that we want to receive channel events (e.g. new message events).

subscribeToNewMessageEvent() calls chatClient.events().subscribe(), in order to subscribe to the stream of chat events. It uses only the events of the NewMessageEvent type and updates the LiveData channel with newly received Message objects. In addition, once the QueryChannelRequest is successful, we obtain the list of messages and pass it to the LiveData.

Now, our Activity will subscribe to this channel and update the screen to show the received messages on the screen! Inside the LiveStreamActivity onCreate() hook, we obtain an instance of LiveStreamViewModel and subscribe to the LiveStreamViewModel.viewState’s LiveData. In doing so, the Activity will receive State updates informing it about new messages and errors:

class LiveStreamActivity : AppCompatActivity(R.layout.activity_main) {
    private val adapter = MessagesListAdapter()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        loadMockVideoStream()
        messagesList.adapter = adapter

        val viewModel: LiveStreamViewModel by viewModels()
        viewModel.viewState.observe(this, Observer {
            when (it) {
                is State.Messages -> updateMessagesList(it.messages)
                is State.NewMessage -> updateMessagesList(adapter.currentList + it.message)
                is State.Error -> showToast(it.message)
            }
        })

        sendMessageButton.setOnClickListener {
            viewModel.sendButtonClicked(messageInput.text.toString())
            messageInput.setText("")
            messageInput.clearFocus()
            messageInput.hideKeyboard()
        }
    }
    ...
}

We call the updateMessageList() function whenever we receive State.Messages or State.NewMessage updates. Note that, in the case of receiving a State.NewMessage, we are adding the new message to the existing list that we get from the messagesList adapter.

At this point, when running the application, we should be able to post new messages to the chat, and new messages should be displayed in the chat view. In the next step, we are going to add a little animation to make the chat list updates more smooth!

Cleaning up the UI

At this point, your code should correspond to the step-3 branch of the app repo; you can checkout this branch if you’d like to start from this section of the tutorial.

Let’s declare an instance of LinearSmoothScroller as a class property in the LiveStreamActivity class:

class LiveStreamActivity : AppCompatActivity(R.layout.activity_main) {
    private val adapter = MessagesListAdapter()

    private val messageListSmoothScroller by lazy {
        object : LinearSmoothScroller(this) {
            val MILLISECONDS_PER_INCH = 400f

            override fun calculateSpeedPerPixel(displayMetrics: DisplayMetrics?): Float {
                return MILLISECONDS_PER_INCH / displayMetrics!!.densityDpi
            }
        }
    }
    ...
}

We are now going to modify the updateMessagesList() so that we can use it to animate the list to scroll to the latest message after notifying the adapter about new data:

    private fun updateMessagesList(messages: List<Message>) {
        adapter.submitList(messages)
        adapter.notifyDataSetChanged()
        val scrollTarget = adapter.itemCount
        messageListSmoothScroller.targetPosition = scrollTarget
        messagesList.layoutManager?.startSmoothScroll(messageListSmoothScroller)
    }

That’s it! We have just implemented the chat feature! Your app should now look like this:

Android - Livestream Video with Chat Overlay Powered by Stream

Hint: Click the play button to view on YouTube!

Wrapping Up

I hope getting through this tutorial gave you a sense of how fast it can be to add a live chat feature into a video streaming app on Android. Thanks to Stream and the Low-Level Android Chat Client, we were able to avoid dealing with any complex chat backend or with WebSockets, entirely!

Note: For the production live streaming application, you’ll need to tune up this demo app a bit. Ideally, you will want to create a separate chat channel for each group, and allow multiple users to join the conversation.

Here are a few more links to resources which will help you get up and running quickly:

Thanks for reading, and happy coding!

Integrating Video With Your App?
We've built a Video and Audio solution just for you. Check out our APIs and SDKs.
Learn more ->