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:
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. TheStreamApiKey
is a public key which you can get by registering your chat app at https://getstream.io/dashboard/. TheUserToken
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:
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 theupdateMessagesList()
function belongs to theio.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.
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:
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:
- Kotlin Chat Docs
- High-Level UI SDK Introduction
- How to Build a WhatsApp Clone for Android Tutorial
- Protocols for Streaming Media
Thanks for reading, and happy coding!