Quick Start for Http Live Streaming(HLS) in Android
This quick start shows how to add HTTP Live Streaming (HLS) to your Android app, step by step.
For low-latency interactive live streaming (under 100ms), follow this documentation.
Prefer Jetpack Compose? The same app built with Compose in five Kotlin files is in the HLS Compose quick start.
Prerequisites
Before proceeding, ensure that your development environment meets the following requirements:
- Java Development Kit.
- Android Studio Narwhal 3 Feature Drop (2025.1.3) or later.
- Android SDK API Level 24 or higher.
- A mobile device that runs Android 7.0 or later with an ARM (
arm64-v8aorarmeabi-v7a) processor, or an emulator with anarm64-v8asystem image.
You need a VideoSDK account to generate a token. Generate the token on the VideoSDK dashboard.
Getting Started with the Code!
Follow the steps to create the environment necessary to add video calls into your app.
Create new Android Project
For a new project in Android Studio, create a Phone and Tablet Android project with an Empty Views Activity. This template adds AppCompat and Material Components, which this quick start uses.

After creating the project, Android Studio automatically starts gradle sync. Ensure that the sync succeeds before you continue.
Integrate Video SDK
dependencyResolutionManagement {
repositories {
// ...
google()
mavenCentral()
}
}
- Add the following dependency in your app's
app/build.gradle.kts.
dependencies {
implementation("live.videosdk:rtc-android-sdk:3.0.0")
// library to perform Network call to generate a room id
implementation("com.squareup.okhttp3:okhttp:4.12.0")
// lifecycleScope, to call the SDK's suspend functions
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.8.7")
// RecyclerView, to show the hosts
implementation("androidx.recyclerview:recyclerview:1.3.2")
// other app dependencies
}
Android SDK ships its native libraries for arm64-v8a and armeabi-v7a. If you want to run the application in an emulator, choose an arm64-v8a system image when creating a device. x86 / x86_64 emulator images are not supported.
Add permissions into your project
- In
/app/Manifests/AndroidManifest.xml, add the following permissions after</application>. The SDK doesn't addINTERNETorMODIFY_AUDIO_SETTINGSto your app, so declare them here.
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.CAMERA" />
Structure of the project
Your project structure should look like this.
app
├── java
│ ├── packagename
│ ├── JoinActivity
│ ├── RoomActivity
│ ├── SpeakerAdapter
│ ├── SpeakerFragment
| ├── ViewerFragment
├── res
│ ├── layout
│ │ ├── activity_join.xml
│ │ ├── activity_room.xml
| | ├── fragment_speaker.xml
| | ├── fragment_viewer.xml
│ │ ├── item_remote_peer.xml
You have to set JoinActivity as Launcher activity. Also add android:configChanges="orientation|screenSize|screenLayout|smallestScreenSize|keyboardHidden" to the RoomActivity entry in AndroidManifest.xml. A rotation would otherwise recreate RoomActivity, and its second join() is refused while the first room is still joined.
App Architecture

Step 1: Creating Joining Screen
Create a new Activity named JoinActivity.
The Joining screen will include:
- Create Button - This button will create a new room for you.
- TextField for Room ID - This text field will contain the room ID you want to join.
- Join as Host Button - This button will join the room as host with the
roomIdyou provided. - Join as Viewer Button - This button will join the room as viewer with the
roomIdyou provided.
In /app/res/layout/activity_join.xml file, replace the content with the following.
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/createorjoinlayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/black"
android:gravity="center"
android:orientation="vertical">
<Button
android:id="@+id/btnCreateRoom"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Create Room"
android:textAllCaps="false" />
<TextView
android:id="@+id/tvText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingVertical="5sp"
android:text="OR"
android:textColor="@color/white"
android:textSize="20sp" />
<EditText
android:id="@+id/etRoomId"
android:theme="@android:style/Theme.Holo"
android:layout_width="250dp"
android:layout_height="wrap_content"
android:hint="Enter Room ID"
android:textColor="@color/white"
android:textColorHint="@color/white" />
<Button
android:id="@+id/btnJoinHostRoom"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8sp"
android:text="Join as Host"
android:textAllCaps="false" />
<Button
android:id="@+id/btnJoinViewerRoom"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Join as Viewer"
android:textAllCaps="false" />
</LinearLayout>
Integration of Create Room API
- Create field
sampleTokeninJoinActivitywhich will hold the generated token from the VideoSDK dashboard. This token is passed toVideoSDK.createRoom()and is also used to generate aroomId.
This quick start pastes a temporary token from the VideoSDK Dashboard into the client and lets the user choose the mode. Use this setup only for development. In production, your client must obtain its token from your backend server, and your server must decide whether a participant joins as a host or as an audience member. A token cannot restrict the mode, so any client that holds a valid token can join in SEND_AND_RECV mode. See Authentication and Tokens.
class JoinActivity : AppCompatActivity() {
//Replace with the token you generated from the VideoSDK Dashboard
private var sampleToken = ""
override fun onCreate(savedInstanceState: Bundle?) {
//...
}
}
-
On Join Button as Host
onClickevents, we will navigate toRoomActivitywith the token,roomIdand modeMode.SEND_AND_RECV. -
On Join Button as Viewer
onClickevents, we will navigate toRoomActivitywith the token,roomIdand modeMode.SIGNALLING_ONLY. A viewer watches the HLS stream, so it does not receive or send media in the room. The mode travels in theIntentas its name, andRoomActivityturns it back into aModewithMode.valueOf().
class JoinActivity : AppCompatActivity() {
//Replace with the token you generated from the VideoSDK Dashboard
private var sampleToken = ""
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_join)
val btnCreate = findViewById<Button>(R.id.btnCreateRoom)
val btnJoinHost = findViewById<Button>(R.id.btnJoinHostRoom)
val btnJoinViewer = findViewById<Button>(R.id.btnJoinViewerRoom)
val etRoomId = findViewById<EditText>(R.id.etRoomId)
// create a room and join as Host
btnCreate.setOnClickListener {
createRoomId(
sampleToken
)
}
// Join as Host
btnJoinHost.setOnClickListener {
val intent = Intent(this@JoinActivity, RoomActivity::class.java)
intent.putExtra("token", sampleToken)
intent.putExtra("roomId", etRoomId.text.toString().trim { it <= ' ' })
intent.putExtra("mode", Mode.SEND_AND_RECV.name)
startActivity(intent)
}
// Join as Viewer
btnJoinViewer.setOnClickListener {
val intent = Intent(this@JoinActivity, RoomActivity::class.java)
intent.putExtra("token", sampleToken)
intent.putExtra("roomId", etRoomId.text.toString().trim { it <= ' ' })
intent.putExtra("mode", Mode.SIGNALLING_ONLY.name)
startActivity(intent)
}
}
private fun createRoomId(token: String) {
// we will explore this method in the next step
}
}
- For Create Button, under
createRoomIdmethod we will generate aroomIdby calling the API and navigate toRoomActivitywith the token, generatedroomIdand modeMode.SEND_AND_RECV.
class JoinActivity : AppCompatActivity() {
//...onCreate
private fun createRoomId(token: String) {
// we will make an API call to VideoSDK Server to get a roomId
lifecycleScope.launch {
try {
val roomId = withContext(Dispatchers.IO) {
val request = Request.Builder()
.url("https://api.videosdk.live/v2/rooms")
.header("Authorization", token) //we will pass the token in the Headers
.post("".toRequestBody())
.build()
OkHttpClient().newCall(request).execute().use { response ->
val body = response.body?.string().orEmpty()
check(response.isSuccessful) { "HTTP ${response.code}: $body" }
// response will contain `roomId`
JSONObject(body).getString("roomId")
}
}
// starting the RoomActivity with received roomId and our sampleToken
val intent = Intent(this@JoinActivity, RoomActivity::class.java)
intent.putExtra("token", sampleToken)
intent.putExtra("roomId", roomId)
intent.putExtra("mode", Mode.SEND_AND_RECV.name)
startActivity(intent)
} catch (e: Exception) {
Toast.makeText(this@JoinActivity, e.message, Toast.LENGTH_SHORT).show()
}
}
}
}
The VideoSDK dashboard and some REST API fields still say "meeting" (for example meetingId). A meeting there is the same thing as a room.
- A host publishes the microphone and camera, so request the
RECORD_AUDIOandCAMERAruntime permissions inJoinActivity.
class JoinActivity : AppCompatActivity() {
companion object {
private const val PERMISSION_REQ_ID = 22
private val REQUESTED_PERMISSIONS = arrayOf(
Manifest.permission.RECORD_AUDIO,
Manifest.permission.CAMERA
)
}
private fun checkSelfPermission(permission: String, requestCode: Int) {
if (ContextCompat.checkSelfPermission(this, permission) !=
PackageManager.PERMISSION_GRANTED
) {
ActivityCompat.requestPermissions(this, REQUESTED_PERMISSIONS, requestCode)
}
}
override fun onCreate(savedInstanceState: Bundle?) {
//... button listeners
checkSelfPermission(REQUESTED_PERMISSIONS[0], PERMISSION_REQ_ID)
checkSelfPermission(REQUESTED_PERMISSIONS[1], PERMISSION_REQ_ID)
}
}
Output

Step 2: Creating Room Screen
Create a new Activity named RoomActivity.
In /app/res/layout/activity_room.xml file, replace the content with the following.
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/mainLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/black"
tools:context=".RoomActivity">
<TextView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:text="Creating a room for you"
android:textColor="@color/white"
android:textFontWeight="700"
android:textSize="20sp" />
</RelativeLayout>
The next step is to create the room, after getting the token and roomId from JoinActivity:
- Initialize VideoSDK.
- Create the room with
VideoSDK.createRoom(), passingroomId,token,participantName,micEnabled,camEnabled,modeand more. It throwsVideoSDKExceptionfor an emptytokenorroomId. - Add a
RoomEventListenerfor listening to theonRoomJoinedevent. - Join the room with
room.join()method. It is asuspendfunction, so call it from a coroutine and catchVideoSDKException. - Check the mode. If it is
Mode.SEND_AND_RECV, we will replace mainLayout withSpeakerFragment, otherwise withViewerFragment. - Leave the room when the user presses Back.
class RoomActivity : AppCompatActivity() {
var room: Room? = null
private set
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_room)
val roomId = intent.getStringExtra("roomId").orEmpty()
val token = intent.getStringExtra("token").orEmpty()
val mode = Mode.valueOf(intent.getStringExtra("mode") ?: Mode.SIGNALLING_ONLY.name)
val localParticipantName = "John Doe"
val streamEnable = mode == Mode.SEND_AND_RECV
// initialize VideoSDK
VideoSDK.initialize(applicationContext)
try {
// create the VideoSDK room
room = VideoSDK.createRoom(
roomId = roomId,
token = token,
participantName = localParticipantName,
micEnabled = streamEnable,
camEnabled = streamEnable,
mode = mode,
multiStream = false,
)
} catch (e: VideoSDKException) {
// thrown for an empty token or room id
Toast.makeText(this, e.message, Toast.LENGTH_LONG).show()
finish()
return
}
// if mode is SEND_AND_RECV then replace mainLayout with SpeakerFragment otherwise with ViewerFragment
room!!.addEventListener(object : RoomEventListener() {
override fun onRoomJoined(switchRoomId: String?) {
// onRoomJoined fires again after a reconnect, so add the fragment only once
if (room == null || supportFragmentManager.findFragmentById(R.id.mainLayout) != null) return
if (mode == Mode.SEND_AND_RECV) {
//pin the local participant
lifecycleScope.launch {
try {
room?.localParticipant?.pin(PinType.SHARE_AND_CAM)
} catch (e: VideoSDKException) {
Log.e("RoomActivity", "pin failed: ${e.message}")
}
}
supportFragmentManager
.beginTransaction()
.replace(R.id.mainLayout, SpeakerFragment(), "MainFragment")
.commit()
} else if (mode == Mode.SIGNALLING_ONLY) {
supportFragmentManager
.beginTransaction()
.replace(R.id.mainLayout, ViewerFragment(), "viewerFragment")
.commit()
}
}
})
// the Back button leaves the room too, instead of leaving it running
onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
lifecycleScope.launch {
try {
room?.leave()
} catch (e: VideoSDKException) {
// the join never completed, so there is nothing to leave
}
finish()
}
}
})
// join the room. join() is a suspend function, so call it from a coroutine.
lifecycleScope.launch {
try {
room?.join()
} catch (e: VideoSDKException) {
Toast.makeText(this@RoomActivity, "Could not join: ${e.message}", Toast.LENGTH_LONG).show()
finish()
}
}
}
}
Output

Step 3: Implement SpeakerView
After successfully joining the room, it's time to render the speaker's view and manage controls such as toggle camera/mic, start/stop HLS and leave the room.
-
Create a new fragment named
SpeakerFragment. -
In
/app/res/layout/fragment_speaker.xmlfile, replace the content with the following.
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/black"
android:gravity="center"
android:orientation="vertical"
tools:context=".SpeakerFragment">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginVertical="8sp"
android:paddingHorizontal="10sp">
<TextView
android:id="@+id/tvRoomId"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="Room ID : "
android:textColor="@color/white"
android:textSize="18sp"
android:layout_weight="3"/>
<Button
android:id="@+id/btnLeave"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="Leave"
android:textAllCaps="false"
android:layout_weight="1"/>
</LinearLayout>
<TextView
android:id="@+id/tvHlsState"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Current HLS State : NOT_STARTED"
android:textColor="@color/white"
android:textSize="18sp" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rvParticipants"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_marginVertical="10sp"
android:layout_weight="1" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center">
<Button
android:id="@+id/btnHLS"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Start HLS"
android:textAllCaps="false" />
<Button
android:id="@+id/btnWebcam"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginHorizontal="5sp"
android:text="Toggle Webcam"
android:textAllCaps="false" />
<Button
android:id="@+id/btnMic"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Toggle Mic"
android:textAllCaps="false" />
</LinearLayout>
</LinearLayout>
- Now, let's set listener for buttons which will allow the participant to toggle media. These
Roomactions (muteMic(),unmuteMic(),enableCam(),disableCam(),startHLS(),stopHLS(),leave()) aresuspendfunctions, so each listener launches a coroutine withlifecycleScope.startHLS()takes anHLSConfigthat sets the layout, orientation, theme and quality of the stream.
class SpeakerFragment : Fragment() {
private var hlsEnabled = false
private var btnMic: Button? = null
private var btnWebcam: Button? = null
private var btnHls: Button? = null
private var btnLeave: Button? = null
private var tvRoomId: TextView? = null
private var tvHlsState: TextView? = null
override fun onAttach(context: Context) {
super.onAttach(context)
mContext = context
if (context is Activity) {
mActivity = context
// getting the room object from RoomActivity
room = (mActivity as RoomActivity?)!!.room
}
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// Inflate the layout for this fragment
val view = inflater.inflate(R.layout.fragment_speaker, container, false)
btnMic = view.findViewById(R.id.btnMic)
btnWebcam = view.findViewById(R.id.btnWebcam)
btnHls = view.findViewById(R.id.btnHLS)
btnLeave = view.findViewById(R.id.btnLeave)
tvRoomId = view.findViewById(R.id.tvRoomId)
tvHlsState = view.findViewById(R.id.tvHlsState)
if (room != null) {
tvRoomId!!.text = "Room ID : " + room!!.id
setActionListeners()
}
return view
}
// The room actions are suspend functions, so each button launches a coroutine.
private fun setActionListeners() {
btnMic!!.setOnClickListener {
val room = room ?: return@setOnClickListener
lifecycleScope.launch {
try {
if (room.isMicEnabled) {
room.muteMic()
Toast.makeText(mContext, "Mic Muted", Toast.LENGTH_SHORT).show()
} else {
room.unmuteMic()
Toast.makeText(mContext, "Mic Enabled", Toast.LENGTH_SHORT).show()
}
} catch (e: VideoSDKException) {
Toast.makeText(mContext, e.message, Toast.LENGTH_SHORT).show()
}
}
}
btnWebcam!!.setOnClickListener {
val room = room ?: return@setOnClickListener
lifecycleScope.launch {
try {
if (room.isCamEnabled) {
room.disableCam()
Toast.makeText(mContext, "Camera Disabled", Toast.LENGTH_SHORT).show()
} else {
room.enableCam()
Toast.makeText(mContext, "Camera Enabled", Toast.LENGTH_SHORT).show()
}
} catch (e: VideoSDKException) {
Toast.makeText(mContext, e.message, Toast.LENGTH_SHORT).show()
}
}
}
btnLeave!!.setOnClickListener {
lifecycleScope.launch {
try {
room?.leave()
} catch (e: VideoSDKException) {
Toast.makeText(mContext, e.message, Toast.LENGTH_SHORT).show()
}
}
}
btnHls!!.setOnClickListener {
val room = room ?: return@setOnClickListener
lifecycleScope.launch {
try {
if (!hlsEnabled) {
val config = HLSConfig(
layout = ConfigLayout(
type = ConfigLayoutGrid.SPOTLIGHT,
priority = ConfigLayoutPriority.PIN,
gridSize = 4,
),
orientation = ConfigOrientation.PORTRAIT,
theme = ConfigTheme.DARK,
quality = ConfigQuality.HIGH,
)
room.startHLS(config)
} else {
room.stopHLS()
}
} catch (e: VideoSDKException) {
Toast.makeText(mContext, e.message, Toast.LENGTH_SHORT).show()
}
}
}
}
companion object {
private var mActivity: Activity? = null
private var mContext: Context? = null
private var room: Room? = null
}
}
- After adding listeners for buttons, let's add a
RoomEventListenerto the room and remove all listeners inonDestroy()method.onHlsStateChangedreports eachHLSStateas the stream starts and stops.
class SpeakerFragment : Fragment() {
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
//...
if (room != null) {
//...
// add Listener to the room
room!!.addEventListener(roomEventListener)
}
return view
}
private val roomEventListener: RoomEventListener = object : RoomEventListener() {
override fun onRoomLeft(reason: LeaveReason) {
if (isAdded) {
val intents = Intent(mContext, JoinActivity::class.java)
intents.addFlags(
Intent.FLAG_ACTIVITY_NEW_TASK
or Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_CLEAR_TASK
)
startActivity(intents)
mActivity!!.finish()
}
}
override fun onHlsStateChanged(state: HLSState, hlsUrl: HLSUrl?) {
tvHlsState!!.text = "Current HLS State : " + state.name
if (state == HLSState.HLS_STARTED) {
hlsEnabled = true
btnHls!!.text = "Stop HLS"
}
if (state == HLSState.HLS_STOPPED) {
hlsEnabled = false
btnHls!!.text = "Start HLS"
}
}
}
override fun onDestroy() {
mContext = null
mActivity = null
if (room != null) {
room!!.removeAllEventListeners()
room = null
}
super.onDestroy()
}
}
- Next step is to render speaker's view. With
RecyclerView, we will display the list of participants who joined the room as ahost(Mode.SEND_AND_RECV).
- Here the participant's video is displayed using
VideoView. - To know more about
VideoView, please visit here
- Create a new layout for the participant view named
item_remote_peer.xmlin theres/layoutfolder.
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout 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="200dp"
android:background="@color/cardview_dark_background"
tools:layout_height="200dp">
<live.videosdk.rtc.android.VideoView
android:id="@+id/participantView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="gone" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
android:background="#99000000"
android:orientation="horizontal">
<TextView
android:id="@+id/tvName"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="center"
android:padding="4dp"
android:textColor="@color/white" />
</LinearLayout>
</FrameLayout>
- Create a recycler view adapter named
SpeakerAdapterwhich will show the participant list. CreatePeerViewHolderin the adapter which will extendRecyclerView.ViewHolder. The adapter takes aCoroutineScope, becausepin()andunpin()aresuspendfunctions. Both take aPinType. Each holder keeps theParticipantEventListenerit added, so a recycled holder stops following its previous participant.
import live.videosdk.rtc.android.MediaType
import live.videosdk.rtc.android.PinType
import live.videosdk.rtc.android.lib.Mode
import videosdk.org.webrtc.VideoTrack
class SpeakerAdapter(private val room: Room, private val scope: CoroutineScope) :
RecyclerView.Adapter<SpeakerAdapter.PeerViewHolder?>() {
private var participantList: MutableList<Participant> = ArrayList()
init {
updateParticipantList()
// adding Room Event listener to get the participant join/leave event in the room.
room.addEventListener(object : RoomEventListener() {
override fun onParticipantJoined(participant: Participant) {
// check participant join as Host/Speaker or not
if (participant.mode == Mode.SEND_AND_RECV) {
// pin the participant
setPinned(participant, true)
// add participant in participantList
participantList.add(participant)
}
notifyDataSetChanged()
}
override fun onParticipantLeft(participant: Participant, reason: LeaveReason) {
val pos = participantList.indexOfFirst { it.id == participant.id }
if (pos >= 0) {
// remove participant from participantList
participantList.removeAt(pos)
notifyItemRemoved(pos)
}
}
})
}
private fun updateParticipantList() {
// adding the local participant(You) to the list
participantList.add(room.localParticipant)
// adding participants who join as Host/Speaker
for (participant in room.participants.values) {
if (participant.mode == Mode.SEND_AND_RECV) {
// pin the participant
setPinned(participant, true)
// add participant in participantList
participantList.add(participant)
}
}
}
// pin() and unpin() are suspend functions, so they run in a coroutine.
private fun setPinned(participant: Participant, pinned: Boolean) {
scope.launch {
try {
if (pinned) participant.pin(PinType.SHARE_AND_CAM) else participant.unpin(PinType.SHARE_AND_CAM)
} catch (e: VideoSDKException) {
Log.e("SpeakerAdapter", "pin/unpin failed: ${e.message}")
}
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PeerViewHolder {
return PeerViewHolder(
LayoutInflater.from(parent.context).inflate(R.layout.item_remote_peer, parent, false)
)
}
override fun onBindViewHolder(holder: PeerViewHolder, position: Int) {
val participant = participantList[position]
holder.tvName.text = participant.displayName
if (holder.participant === participant) return // already showing this participant
// A recycled holder still listens to its previous participant; detach that first.
holder.unbind()
holder.participantView.visibility = View.GONE
// add Listener to the participant which will update start or stop the video stream of that participant.
// Adding it also calls onStreamEnabled for the streams the participant already has.
val listener = object : ParticipantEventListener() {
override fun onStreamEnabled(stream: RTCStream) {
if (stream.kind == MediaType.VIDEO) {
holder.participantView.visibility = View.VISIBLE
val videoTrack = stream.track as VideoTrack
holder.participantView.addTrack(videoTrack)
}
}
override fun onStreamDisabled(stream: RTCStream) {
if (stream.kind == MediaType.VIDEO) {
holder.participantView.removeTrack()
holder.participantView.visibility = View.GONE
}
}
}
participant.addEventListener(listener)
holder.participant = participant
holder.listener = listener
}
override fun getItemCount(): Int {
return participantList.size
}
class PeerViewHolder(view: View) : RecyclerView.ViewHolder(view) {
// 'VideoView' to show Video Stream
var participantView: VideoView
var tvName: TextView
var participant: Participant? = null
var listener: ParticipantEventListener? = null
init {
tvName = view.findViewById(R.id.tvName)
participantView = view.findViewById(R.id.participantView)
}
fun unbind() {
listener?.let { participant?.removeEventListener(it) }
participant = null
listener = null
participantView.removeTrack()
}
}
}
- Add this adapter to the
SpeakerFragment
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val view = inflater.inflate(R.layout.fragment_speaker, container, false)
//...
if (room != null) {
//...
val rvParticipants = view.findViewById<RecyclerView>(R.id.rvParticipants)
rvParticipants.layoutManager = GridLayoutManager(mContext, 2)
rvParticipants.adapter = SpeakerAdapter(room!!, lifecycleScope)
}
return view
}
Output

Step 4: Implement ViewerView
When the host starts the live stream, viewers can watch it.
The player view uses ExoPlayer to play the HLS stream.
Let's first add dependency into the project.
dependencies {
implementation("com.google.android.exoplayer:exoplayer:2.18.5")
// other app dependencies
}
Create a new Fragment named ViewerFragment
The Viewer Fragment will include :
- TextView for Room ID - The room ID that you joined will be displayed in this text view.
- Leave Button - This button will leave the room.
- waitingLayout - This is textView that will be shown when there is no active HLS.
- StyledPlayerView - This is mediaplayer which will display livestreaming.
In /app/res/layout/fragment_viewer.xml file, replace the content with the following.
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout 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="match_parent"
android:background="@color/black"
tools:context=".ViewerFragment">
<LinearLayout
android:id="@+id/roomLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingHorizontal="12sp"
android:paddingVertical="5sp">
<TextView
android:id="@+id/tvRoomId"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="3"
android:text="Room ID : "
android:textColor="@color/white"
android:textSize="20sp" />
<Button
android:id="@+id/btnLeave"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Leave" />
</LinearLayout>
<TextView
android:id="@+id/waitingLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:text="Waiting for host \n to start the live streaming"
android:textColor="@color/white"
android:textFontWeight="700"
android:textSize="20sp"
android:gravity="center"/>
<com.google.android.exoplayer2.ui.StyledPlayerView
android:id="@+id/player_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="gone"
app:resize_mode="fixed_width"
app:show_buffering="when_playing"
app:show_subtitle_button="false"
app:use_artwork="false"
app:show_next_button="false"
app:show_previous_button="false"
app:use_controller="true"
android:layout_below="@id/roomLayout"/>
</RelativeLayout>
Initialize player and Playing HLS stream
- Initialize player and play the HLS when the room's HLS state is
HLSState.HLS_PLAYABLE, and release it when the HLS state isHLSState.HLS_STOPPED. Whenever the room's HLS state changes, the eventonHlsStateChanged(state, hlsUrl)will be triggered.
- With
HLS_PLAYABLE,hlsUrlcarries the URLs to play;hlsUrl.playbackHlsUrlis the playlist for viewers.hlsUrlisnullwith every other state.
class ViewerFragment : Fragment() {
private var room: Room? = null
private var playerView: StyledPlayerView? = null
private var waitingLayout: TextView? = null
private var player: ExoPlayer? = null
private var dataSourceFactory: DefaultHttpDataSource.Factory? = null
private val startAutoPlay = true
private var playableUrl: String? = ""
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// Inflate the layout for this fragment
val view = inflater.inflate(R.layout.fragment_viewer, container, false)
playerView = view.findViewById(R.id.player_view)
waitingLayout = view.findViewById(R.id.waitingLayout)
if (room != null) {
// set the room ID to TextView
(view.findViewById<View>(R.id.tvRoomId) as TextView).text =
"Room ID : " + room!!.id
// leave the room on btnLeave click
(view.findViewById<View>(R.id.btnLeave) as Button).setOnClickListener {
lifecycleScope.launch {
try {
room?.leave()
} catch (e: VideoSDKException) {
Toast.makeText(mContext, e.message, Toast.LENGTH_SHORT).show()
}
}
}
// add listener to the room
room!!.addEventListener(roomEventListener)
}
return view
}
override fun onAttach(context: Context) {
super.onAttach(context)
mContext = context
if (context is Activity) {
mActivity = context
// get the room object from RoomActivity
room = (mActivity as RoomActivity?)!!.room
}
}
private val roomEventListener: RoomEventListener = object : RoomEventListener() {
override fun onRoomLeft(reason: LeaveReason) {
if (isAdded) {
val intents = Intent(mContext, JoinActivity::class.java)
intents.addFlags(
Intent.FLAG_ACTIVITY_NEW_TASK
or Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_CLEAR_TASK
)
startActivity(intents)
mActivity!!.finish()
}
}
override fun onHlsStateChanged(state: HLSState, hlsUrl: HLSUrl?) {
val url = hlsUrl?.playbackHlsUrl
if (state == HLSState.HLS_PLAYABLE && url != null) {
playableUrl = url
waitingLayout!!.visibility = View.GONE
playerView!!.visibility = View.VISIBLE
// initialize player
initializePlayer()
}
if (state == HLSState.HLS_STOPPED) {
// release the player
releasePlayer()
playableUrl = null
waitingLayout!!.text = "Host has stopped \n the live streaming"
waitingLayout!!.visibility = View.VISIBLE
playerView!!.visibility = View.GONE
}
}
}
private fun initializePlayer() {
if (player == null) {
dataSourceFactory = DefaultHttpDataSource.Factory()
val mediaSource = HlsMediaSource.Factory(dataSourceFactory!!).createMediaSource(
MediaItem.fromUri(Uri.parse(playableUrl))
)
val playerBuilder = ExoPlayer.Builder( /* context = */mContext!!)
player = playerBuilder.build()
// auto play when player is ready
player!!.playWhenReady = startAutoPlay
player!!.setMediaSource(mediaSource)
// if you want display setting for player then remove this line
playerView!!.findViewById<View>(com.google.android.exoplayer2.ui.R.id.exo_settings).visibility =
View.GONE
playerView!!.player = player
}
player!!.prepare()
}
private fun releasePlayer() {
if (player != null) {
player!!.release()
player = null
dataSourceFactory = null
playerView!!.player = null
}
}
override fun onDestroy() {
mContext = null
mActivity = null
playableUrl = null
releasePlayer()
if (room != null) {
room!!.removeAllEventListeners()
room = null
}
super.onDestroy()
}
companion object {
private var mActivity: Activity? = null
private var mContext: Context? = null
}
}
Output


Final Output
You have built an HLS live streaming app in Android with VideoSDK. To explore more features, go through Basic and Advanced features.
Got a Question? Ask us on discord

