David Liu

revamp sample app to support multiple remote participants

... ... @@ -39,9 +39,11 @@ ext {
minVersion : 21,
]
versions = [
androidx_core: "1.2.0",
dagger : "2.27",
protobuf : "3.15.1",
androidx_core : "1.2.0",
androidx_lifecycle: "2.3.0",
dagger : "2.27",
groupie : "2.9.0",
protobuf : "3.15.1",
]
generated = [
protoSrc: "$projectDir/../protocol",
... ...
... ... @@ -236,7 +236,9 @@ constructor(
Rtc.SignalResponse.MessageCase.TRACK_PUBLISHED -> {
listener?.onLocalTrackPublished(response.trackPublished)
}
Rtc.SignalResponse.MessageCase.SPEAKER -> TODO()
Rtc.SignalResponse.MessageCase.SPEAKER -> {
listener?.onActiveSpeakersChanged(response.speaker.speakersList)
}
Rtc.SignalResponse.MessageCase.MESSAGE_NOT_SET -> TODO()
else -> {
Timber.v { "unhandled response type: ${response.messageCase.name}" }
... ...
... ... @@ -225,7 +225,7 @@ constructor(
listener?.onFailedToConnect(this, error)
}
fun setupVideo(viewRenderer: SurfaceViewRenderer) {
fun initVideoRenderer(viewRenderer: SurfaceViewRenderer) {
viewRenderer.init(eglBase.eglBaseContext, null)
viewRenderer.setScalingType(RendererCommon.ScalingType.SCALE_ASPECT_FIT)
viewRenderer.setEnableHardwareScaler(false /* enabled */);
... ...
... ... @@ -28,7 +28,7 @@ class RemoteParticipant(
val remoteDataTracks
get() = dataTracks.values.toList()
val listener: Listener? = null
var listener: Listener? = null
var participantInfo: Model.ParticipantInfo? = null
... ... @@ -237,53 +237,65 @@ class RemoteParticipant(
}
interface Listener {
fun onPublish(audioTrack: RemoteAudioTrackPublication, participant: RemoteParticipant)
fun onUnpublish(audioTrack: RemoteAudioTrackPublication, participant: RemoteParticipant)
fun onPublish(videoTrack: RemoteVideoTrackPublication, participant: RemoteParticipant)
fun onUnpublish(videoTrack: RemoteVideoTrackPublication, participant: RemoteParticipant)
fun onPublish(dataTrack: RemoteDataTrackPublication, participant: RemoteParticipant)
fun onUnpublish(dataTrack: RemoteDataTrackPublication, participant: RemoteParticipant)
fun onEnable(audioTrack: RemoteAudioTrackPublication, participant: RemoteParticipant)
fun onDisable(audioTrack: RemoteAudioTrackPublication, participant: RemoteParticipant)
fun onEnable(videoTrack: RemoteVideoTrackPublication, participant: RemoteParticipant)
fun onDisable(videoTrack: RemoteVideoTrackPublication, participant: RemoteParticipant)
fun onSubscribe(audioTrack: RemoteAudioTrackPublication, participant: RemoteParticipant)
fun onPublish(audioTrack: RemoteAudioTrackPublication, participant: RemoteParticipant) {}
fun onUnpublish(audioTrack: RemoteAudioTrackPublication, participant: RemoteParticipant) {}
fun onPublish(videoTrack: RemoteVideoTrackPublication, participant: RemoteParticipant) {}
fun onUnpublish(videoTrack: RemoteVideoTrackPublication, participant: RemoteParticipant) {}
fun onPublish(dataTrack: RemoteDataTrackPublication, participant: RemoteParticipant) {}
fun onUnpublish(dataTrack: RemoteDataTrackPublication, participant: RemoteParticipant) {}
fun onEnable(audioTrack: RemoteAudioTrackPublication, participant: RemoteParticipant) {}
fun onDisable(audioTrack: RemoteAudioTrackPublication, participant: RemoteParticipant) {}
fun onEnable(videoTrack: RemoteVideoTrackPublication, participant: RemoteParticipant) {}
fun onDisable(videoTrack: RemoteVideoTrackPublication, participant: RemoteParticipant) {}
fun onSubscribe(audioTrack: RemoteAudioTrackPublication, participant: RemoteParticipant) {}
fun onFailToSubscribe(
audioTrack: RemoteAudioTrack,
exception: Exception,
participant: RemoteParticipant
)
) {
}
fun onUnsubscribe(audioTrack: RemoteAudioTrackPublication, participant: RemoteParticipant)
fun onUnsubscribe(
audioTrack: RemoteAudioTrackPublication,
participant: RemoteParticipant
) {
}
fun onSubscribe(videoTrack: RemoteVideoTrackPublication, participant: RemoteParticipant)
fun onSubscribe(videoTrack: RemoteVideoTrackPublication, participant: RemoteParticipant) {}
fun onFailToSubscribe(
videoTrack: RemoteVideoTrack,
exception: Exception,
participant: RemoteParticipant
)
) {
}
fun onUnsubscribe(videoTrack: RemoteVideoTrackPublication, participant: RemoteParticipant)
fun onUnsubscribe(
videoTrack: RemoteVideoTrackPublication,
participant: RemoteParticipant
) {
}
fun onSubscribe(dataTrack: RemoteDataTrackPublication, participant: RemoteParticipant)
fun onSubscribe(dataTrack: RemoteDataTrackPublication, participant: RemoteParticipant) {}
fun onFailToSubscribe(
dataTrack: RemoteDataTrackPublication,
exception: Exception,
participant: RemoteParticipant
)
) {
}
fun onUnsubscribe(dataTrack: RemoteDataTrackPublication, participant: RemoteParticipant)
fun onUnsubscribe(dataTrack: RemoteDataTrackPublication, participant: RemoteParticipant) {}
fun onReceive(
data: ByteBuffer,
dataTrack: RemoteDataTrackPublication,
participant: RemoteParticipant
)
) {
}
//fun networkQualityDidChange(networkQualityLevel: NetworkQualityLevel, participant: remoteParticipant)
fun switchedOffVideo(track: RemoteVideoTrack, participant: RemoteParticipant)
fun switchedOnVideo(track: RemoteVideoTrack, participant: RemoteParticipant)
fun switchedOffVideo(track: RemoteVideoTrack, participant: RemoteParticipant) {}
fun switchedOnVideo(track: RemoteVideoTrack, participant: RemoteParticipant) {}
// fun onChangePublishPriority(videoTrack: RemoteVideoTrackPublication, priority: PublishPriority, participant: RemoteParticipant)
// fun onChangePublishPriority(audioTrack: RemoteAudioTrackPublication, priority: PublishPriority, participant: RemoteParticipant)
// fun onChangePublishPriority(dataTrack: RemoteDataTrackPublication, priority: PublishPriority, participant: RemoteParticipant)
... ...
... ... @@ -24,6 +24,9 @@ android {
sourceCompatibility java_version
targetCompatibility java_version
}
kotlinOptions {
jvmTarget = java_version
}
buildFeatures {
viewBinding = true
}
... ... @@ -36,7 +39,14 @@ dependencies {
implementation 'com.google.android.material:material:1.3.0'
implementation 'androidx.appcompat:appcompat:1.2.0'
implementation 'androidx.core:core-ktx:1.3.2'
implementation "androidx.activity:activity-ktx:1.2.1"
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.3.0'
implementation "androidx.viewpager2:viewpager2:1.0.0"
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:${versions.androidx_lifecycle}"
implementation "androidx.lifecycle:lifecycle-common-java8:${versions.androidx_lifecycle}"
implementation "com.xwray:groupie:${versions.groupie}"
implementation "com.xwray:groupie-viewbinding:${versions.groupie}"
implementation 'com.snakydesign.livedataextensions:lives:1.3.0'
implementation 'com.github.ajalt:timberkt:1.5.1'
implementation project(":livekit-android-sdk")
testImplementation 'junit:junit:4.12'
... ...
... ... @@ -3,20 +3,21 @@ package io.livekit.android.sample
import android.os.Bundle
import android.os.Parcelable
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope
import io.livekit.android.ConnectOptions
import io.livekit.android.LiveKit
import io.livekit.android.room.Room
import io.livekit.android.room.participant.Participant
import io.livekit.android.room.participant.RemoteParticipant
import io.livekit.android.room.track.VideoTrack
import com.snakydesign.livedataextensions.combineLatest
import com.xwray.groupie.GroupieAdapter
import io.livekit.android.sample.databinding.CallActivityBinding
import kotlinx.coroutines.launch
import kotlinx.parcelize.Parcelize
class CallActivity : AppCompatActivity() {
val viewModel: CallViewModel by viewModelByFactory {
val args = intent.getParcelableExtra<BundleArgs>(KEY_ARGS)
?: throw NullPointerException("args is null!")
CallViewModel(args.url, args.token, application)
}
lateinit var binding: CallActivityBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
... ... @@ -24,69 +25,20 @@ class CallActivity : AppCompatActivity() {
setContentView(binding.root)
val args = intent.getParcelableExtra<BundleArgs>(KEY_ARGS)
if (args == null) {
finish()
return
val adapter = GroupieAdapter()
binding.viewPager.apply {
this.adapter = adapter
}
lifecycleScope.launch {
val room = LiveKit.connect(
applicationContext,
args.url,
args.token,
ConnectOptions(false),
object : Room.Listener {
var loadedParticipant = false
override fun onConnect(room: Room) {
}
override fun onDisconnect(room: Room, error: Exception?) {
}
override fun onParticipantConnected(
room: Room,
participant: RemoteParticipant
) {
if (!loadedParticipant) {
room.setupVideo(binding.fullscreenVideoView)
participant.remoteVideoTracks
.first()
.track
.let { it as? VideoTrack }
?.addRenderer(binding.fullscreenVideoView)
}
}
override fun onParticipantDisconnected(
room: Room,
participant: RemoteParticipant
) {
}
override fun onFailedToConnect(room: Room, error: Exception) {
}
override fun onReconnecting(room: Room, error: Exception) {
}
override fun onReconnect(room: Room) {
}
override fun onStartRecording(room: Room) {
}
override fun onStopRecording(room: Room) {
}
override fun onActiveSpeakersChanged(speakers: List<Participant>, room: Room) {
}
}
)
}
combineLatest(
viewModel.room,
viewModel.remoteParticipants
) { room, participants -> room to participants }
.observe(this) {
val (room, participants) = it
val items = participants.map { participant -> ParticipantItem(room, participant) }
adapter.update(items)
}
}
... ...
package io.livekit.android.sample
import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.viewModelScope
import io.livekit.android.ConnectOptions
import io.livekit.android.LiveKit
import io.livekit.android.room.Room
import io.livekit.android.room.participant.Participant
import io.livekit.android.room.participant.RemoteParticipant
import kotlinx.coroutines.launch
class CallViewModel(
val url: String,
val token: String,
application: Application
) : AndroidViewModel(application) {
private val mutableRoom = MutableLiveData<Room>()
val room: LiveData<Room> = mutableRoom
private val mutableRemoteParticipants = MutableLiveData<List<RemoteParticipant>>()
val remoteParticipants: LiveData<List<RemoteParticipant>> = mutableRemoteParticipants
init {
viewModelScope.launch {
mutableRoom.value = LiveKit.connect(
application,
url,
token,
ConnectOptions(false),
object : Room.Listener {
override fun onConnect(room: Room) {
}
override fun onDisconnect(room: Room, error: Exception?) {
}
override fun onParticipantConnected(
room: Room,
participant: RemoteParticipant
) {
updateParticipants(room)
}
override fun onParticipantDisconnected(
room: Room,
participant: RemoteParticipant
) {
updateParticipants(room)
}
override fun onFailedToConnect(room: Room, error: Exception) {
}
override fun onReconnecting(room: Room, error: Exception) {
}
override fun onReconnect(room: Room) {
}
override fun onStartRecording(room: Room) {
}
override fun onStopRecording(room: Room) {
}
override fun onActiveSpeakersChanged(speakers: List<Participant>, room: Room) {
}
}
)
}
}
fun updateParticipants(room: Room) {
mutableRemoteParticipants.value = room.remoteParticipants
.keys
.sortedBy { it.sid }
.mapNotNull { room.remoteParticipants[it] }
}
}
... ...
... ... @@ -35,6 +35,6 @@ class MainActivity : AppCompatActivity() {
companion object {
val URL = SpannableStringBuilder("192.168.11.2:7880")
val TOKEN =
SpannableStringBuilder("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2MTg1NzgxNzMsImlzcyI6IkFQSXdMZWFoN2c0ZnVMWURZQUplYUtzU0UiLCJqdGkiOiJwaG9uZSIsIm5iZiI6MTYxNTk4NjE3MywidmlkZW8iOnsicm9vbSI6Im15cm9vbSIsInJvb21Kb2luIjp0cnVlfX0.O3UedhM9lwdPxsZJQoTfVk0qXc-0ukjV6oZCBIaRTck")
SpannableStringBuilder("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2MTg2MjY0NDAsImlzcyI6IkFQSXdMZWFoN2c0ZnVMWURZQUplYUtzU0UiLCJqdGkiOiJwaG9uZSIsIm5iZiI6MTYxNjAzNDQ0MCwidmlkZW8iOnsicm9vbSI6Im15cm9vbSIsInJvb21Kb2luIjp0cnVlfX0.QWN0B_DO8eSP2sJivnr_QzBud_sdIgJeWDQGQz67DvY")
}
}
\ No newline at end of file
... ...
package io.livekit.android.sample
import android.view.View
import com.xwray.groupie.viewbinding.BindableItem
import com.xwray.groupie.viewbinding.GroupieViewHolder
import io.livekit.android.room.Room
import io.livekit.android.room.participant.RemoteParticipant
import io.livekit.android.room.track.RemoteVideoTrackPublication
import io.livekit.android.room.track.VideoTrack
import io.livekit.android.room.track.VideoTrackPublication
import io.livekit.android.sample.databinding.ParticipantItemBinding
class ParticipantItem(
val room: Room,
val remoteParticipant: RemoteParticipant
) :
BindableItem<ParticipantItemBinding>() {
private var videoBound = false
override fun initializeViewBinding(view: View): ParticipantItemBinding {
return ParticipantItemBinding.bind(view)
}
override fun bind(viewBinding: ParticipantItemBinding, position: Int) {
viewBinding.run {
room.initVideoRenderer(renderer)
val existingTrack = getVideoTrack()
if (existingTrack != null) {
setupVideoIfNeeded(existingTrack, viewBinding)
} else {
remoteParticipant.listener = object : RemoteParticipant.Listener {
override fun onSubscribe(
videoTrack: RemoteVideoTrackPublication,
participant: RemoteParticipant
) {
val track = videoTrack.videoTrack
if (track != null) {
setupVideoIfNeeded(track, viewBinding)
}
}
}
}
}
}
private fun getVideoTrack(): VideoTrack? {
return remoteParticipant
.remoteVideoTracks
.firstOrNull()
.let { it as? VideoTrackPublication }
?.videoTrack
}
private fun setupVideoIfNeeded(videoTrack: VideoTrack, viewBinding: ParticipantItemBinding) {
if (videoBound) {
return
}
videoTrack.addRenderer(viewBinding.renderer)
}
override fun unbind(viewHolder: GroupieViewHolder<ParticipantItemBinding>) {
super.unbind(viewHolder)
videoBound = false
}
override fun getLayout(): Int = R.layout.participant_item
}
\ No newline at end of file
... ...
package io.livekit.android.sample
import androidx.activity.viewModels
import androidx.fragment.app.FragmentActivity
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
typealias CreateViewModel<VM> = () -> VM
inline fun <reified VM : ViewModel> FragmentActivity.viewModelByFactory(
noinline create: CreateViewModel<VM>
): Lazy<VM> {
return viewModels {
createViewModelFactoryFactory(create)
}
}
fun <VM> createViewModelFactoryFactory(
create: CreateViewModel<VM>
): ViewModelProvider.Factory {
return object : ViewModelProvider.Factory {
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
@Suppress("UNCHECKED_CAST")
return create() as? T
?: throw IllegalArgumentException("Unknown viewmodel class!")
}
}
}
\ No newline at end of file
... ...
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<org.webrtc.SurfaceViewRenderer
android:id="@+id/fullscreen_video_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center" />
<androidx.viewpager2.widget.ViewPager2
android:id="@+id/view_pager"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<org.webrtc.SurfaceViewRenderer
android:id="@+id/pip_video_view"
android:layout_height="144dp"
android:layout_width="wrap_content"
android:layout_gravity="bottom|end"
android:layout_margin="16dp"/>
<FrameLayout
android:id="@+id/call_fragment_container"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<FrameLayout
android:id="@+id/hud_fragment_container"
android:layout_width="match_parent"
android:layout_height="match_parent" />
android:layout_margin="16dp" />
</FrameLayout>
... ...
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<org.webrtc.SurfaceViewRenderer
android:id="@+id/renderer"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</FrameLayout>
\ No newline at end of file
... ...