David Liu

revamp sample app to support multiple remote participants

@@ -39,9 +39,11 @@ ext { @@ -39,9 +39,11 @@ ext {
39 minVersion : 21, 39 minVersion : 21,
40 ] 40 ]
41 versions = [ 41 versions = [
42 - androidx_core: "1.2.0",  
43 - dagger : "2.27",  
44 - protobuf : "3.15.1", 42 + androidx_core : "1.2.0",
  43 + androidx_lifecycle: "2.3.0",
  44 + dagger : "2.27",
  45 + groupie : "2.9.0",
  46 + protobuf : "3.15.1",
45 ] 47 ]
46 generated = [ 48 generated = [
47 protoSrc: "$projectDir/../protocol", 49 protoSrc: "$projectDir/../protocol",
@@ -236,7 +236,9 @@ constructor( @@ -236,7 +236,9 @@ constructor(
236 Rtc.SignalResponse.MessageCase.TRACK_PUBLISHED -> { 236 Rtc.SignalResponse.MessageCase.TRACK_PUBLISHED -> {
237 listener?.onLocalTrackPublished(response.trackPublished) 237 listener?.onLocalTrackPublished(response.trackPublished)
238 } 238 }
239 - Rtc.SignalResponse.MessageCase.SPEAKER -> TODO() 239 + Rtc.SignalResponse.MessageCase.SPEAKER -> {
  240 + listener?.onActiveSpeakersChanged(response.speaker.speakersList)
  241 + }
240 Rtc.SignalResponse.MessageCase.MESSAGE_NOT_SET -> TODO() 242 Rtc.SignalResponse.MessageCase.MESSAGE_NOT_SET -> TODO()
241 else -> { 243 else -> {
242 Timber.v { "unhandled response type: ${response.messageCase.name}" } 244 Timber.v { "unhandled response type: ${response.messageCase.name}" }
@@ -225,7 +225,7 @@ constructor( @@ -225,7 +225,7 @@ constructor(
225 listener?.onFailedToConnect(this, error) 225 listener?.onFailedToConnect(this, error)
226 } 226 }
227 227
228 - fun setupVideo(viewRenderer: SurfaceViewRenderer) { 228 + fun initVideoRenderer(viewRenderer: SurfaceViewRenderer) {
229 viewRenderer.init(eglBase.eglBaseContext, null) 229 viewRenderer.init(eglBase.eglBaseContext, null)
230 viewRenderer.setScalingType(RendererCommon.ScalingType.SCALE_ASPECT_FIT) 230 viewRenderer.setScalingType(RendererCommon.ScalingType.SCALE_ASPECT_FIT)
231 viewRenderer.setEnableHardwareScaler(false /* enabled */); 231 viewRenderer.setEnableHardwareScaler(false /* enabled */);
@@ -28,7 +28,7 @@ class RemoteParticipant( @@ -28,7 +28,7 @@ class RemoteParticipant(
28 val remoteDataTracks 28 val remoteDataTracks
29 get() = dataTracks.values.toList() 29 get() = dataTracks.values.toList()
30 30
31 - val listener: Listener? = null 31 + var listener: Listener? = null
32 32
33 var participantInfo: Model.ParticipantInfo? = null 33 var participantInfo: Model.ParticipantInfo? = null
34 34
@@ -237,53 +237,65 @@ class RemoteParticipant( @@ -237,53 +237,65 @@ class RemoteParticipant(
237 } 237 }
238 238
239 interface Listener { 239 interface Listener {
240 - fun onPublish(audioTrack: RemoteAudioTrackPublication, participant: RemoteParticipant)  
241 - fun onUnpublish(audioTrack: RemoteAudioTrackPublication, participant: RemoteParticipant)  
242 - fun onPublish(videoTrack: RemoteVideoTrackPublication, participant: RemoteParticipant)  
243 - fun onUnpublish(videoTrack: RemoteVideoTrackPublication, participant: RemoteParticipant)  
244 - fun onPublish(dataTrack: RemoteDataTrackPublication, participant: RemoteParticipant)  
245 - fun onUnpublish(dataTrack: RemoteDataTrackPublication, participant: RemoteParticipant)  
246 -  
247 - fun onEnable(audioTrack: RemoteAudioTrackPublication, participant: RemoteParticipant)  
248 - fun onDisable(audioTrack: RemoteAudioTrackPublication, participant: RemoteParticipant)  
249 - fun onEnable(videoTrack: RemoteVideoTrackPublication, participant: RemoteParticipant)  
250 - fun onDisable(videoTrack: RemoteVideoTrackPublication, participant: RemoteParticipant)  
251 -  
252 - fun onSubscribe(audioTrack: RemoteAudioTrackPublication, participant: RemoteParticipant) 240 + fun onPublish(audioTrack: RemoteAudioTrackPublication, participant: RemoteParticipant) {}
  241 + fun onUnpublish(audioTrack: RemoteAudioTrackPublication, participant: RemoteParticipant) {}
  242 + fun onPublish(videoTrack: RemoteVideoTrackPublication, participant: RemoteParticipant) {}
  243 + fun onUnpublish(videoTrack: RemoteVideoTrackPublication, participant: RemoteParticipant) {}
  244 + fun onPublish(dataTrack: RemoteDataTrackPublication, participant: RemoteParticipant) {}
  245 + fun onUnpublish(dataTrack: RemoteDataTrackPublication, participant: RemoteParticipant) {}
  246 +
  247 + fun onEnable(audioTrack: RemoteAudioTrackPublication, participant: RemoteParticipant) {}
  248 + fun onDisable(audioTrack: RemoteAudioTrackPublication, participant: RemoteParticipant) {}
  249 + fun onEnable(videoTrack: RemoteVideoTrackPublication, participant: RemoteParticipant) {}
  250 + fun onDisable(videoTrack: RemoteVideoTrackPublication, participant: RemoteParticipant) {}
  251 +
  252 + fun onSubscribe(audioTrack: RemoteAudioTrackPublication, participant: RemoteParticipant) {}
253 fun onFailToSubscribe( 253 fun onFailToSubscribe(
254 audioTrack: RemoteAudioTrack, 254 audioTrack: RemoteAudioTrack,
255 exception: Exception, 255 exception: Exception,
256 participant: RemoteParticipant 256 participant: RemoteParticipant
257 - ) 257 + ) {
  258 + }
258 259
259 - fun onUnsubscribe(audioTrack: RemoteAudioTrackPublication, participant: RemoteParticipant) 260 + fun onUnsubscribe(
  261 + audioTrack: RemoteAudioTrackPublication,
  262 + participant: RemoteParticipant
  263 + ) {
  264 + }
260 265
261 - fun onSubscribe(videoTrack: RemoteVideoTrackPublication, participant: RemoteParticipant) 266 + fun onSubscribe(videoTrack: RemoteVideoTrackPublication, participant: RemoteParticipant) {}
262 fun onFailToSubscribe( 267 fun onFailToSubscribe(
263 videoTrack: RemoteVideoTrack, 268 videoTrack: RemoteVideoTrack,
264 exception: Exception, 269 exception: Exception,
265 participant: RemoteParticipant 270 participant: RemoteParticipant
266 - ) 271 + ) {
  272 + }
267 273
268 - fun onUnsubscribe(videoTrack: RemoteVideoTrackPublication, participant: RemoteParticipant) 274 + fun onUnsubscribe(
  275 + videoTrack: RemoteVideoTrackPublication,
  276 + participant: RemoteParticipant
  277 + ) {
  278 + }
269 279
270 - fun onSubscribe(dataTrack: RemoteDataTrackPublication, participant: RemoteParticipant) 280 + fun onSubscribe(dataTrack: RemoteDataTrackPublication, participant: RemoteParticipant) {}
271 fun onFailToSubscribe( 281 fun onFailToSubscribe(
272 dataTrack: RemoteDataTrackPublication, 282 dataTrack: RemoteDataTrackPublication,
273 exception: Exception, 283 exception: Exception,
274 participant: RemoteParticipant 284 participant: RemoteParticipant
275 - ) 285 + ) {
  286 + }
276 287
277 - fun onUnsubscribe(dataTrack: RemoteDataTrackPublication, participant: RemoteParticipant) 288 + fun onUnsubscribe(dataTrack: RemoteDataTrackPublication, participant: RemoteParticipant) {}
278 fun onReceive( 289 fun onReceive(
279 data: ByteBuffer, 290 data: ByteBuffer,
280 dataTrack: RemoteDataTrackPublication, 291 dataTrack: RemoteDataTrackPublication,
281 participant: RemoteParticipant 292 participant: RemoteParticipant
282 - ) 293 + ) {
  294 + }
283 295
284 //fun networkQualityDidChange(networkQualityLevel: NetworkQualityLevel, participant: remoteParticipant) 296 //fun networkQualityDidChange(networkQualityLevel: NetworkQualityLevel, participant: remoteParticipant)
285 - fun switchedOffVideo(track: RemoteVideoTrack, participant: RemoteParticipant)  
286 - fun switchedOnVideo(track: RemoteVideoTrack, participant: RemoteParticipant) 297 + fun switchedOffVideo(track: RemoteVideoTrack, participant: RemoteParticipant) {}
  298 + fun switchedOnVideo(track: RemoteVideoTrack, participant: RemoteParticipant) {}
287 // fun onChangePublishPriority(videoTrack: RemoteVideoTrackPublication, priority: PublishPriority, participant: RemoteParticipant) 299 // fun onChangePublishPriority(videoTrack: RemoteVideoTrackPublication, priority: PublishPriority, participant: RemoteParticipant)
288 // fun onChangePublishPriority(audioTrack: RemoteAudioTrackPublication, priority: PublishPriority, participant: RemoteParticipant) 300 // fun onChangePublishPriority(audioTrack: RemoteAudioTrackPublication, priority: PublishPriority, participant: RemoteParticipant)
289 // fun onChangePublishPriority(dataTrack: RemoteDataTrackPublication, priority: PublishPriority, participant: RemoteParticipant) 301 // fun onChangePublishPriority(dataTrack: RemoteDataTrackPublication, priority: PublishPriority, participant: RemoteParticipant)
@@ -24,6 +24,9 @@ android { @@ -24,6 +24,9 @@ android {
24 sourceCompatibility java_version 24 sourceCompatibility java_version
25 targetCompatibility java_version 25 targetCompatibility java_version
26 } 26 }
  27 + kotlinOptions {
  28 + jvmTarget = java_version
  29 + }
27 buildFeatures { 30 buildFeatures {
28 viewBinding = true 31 viewBinding = true
29 } 32 }
@@ -36,7 +39,14 @@ dependencies { @@ -36,7 +39,14 @@ dependencies {
36 implementation 'com.google.android.material:material:1.3.0' 39 implementation 'com.google.android.material:material:1.3.0'
37 implementation 'androidx.appcompat:appcompat:1.2.0' 40 implementation 'androidx.appcompat:appcompat:1.2.0'
38 implementation 'androidx.core:core-ktx:1.3.2' 41 implementation 'androidx.core:core-ktx:1.3.2'
  42 + implementation "androidx.activity:activity-ktx:1.2.1"
39 implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.3.0' 43 implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.3.0'
  44 + implementation "androidx.viewpager2:viewpager2:1.0.0"
  45 + implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:${versions.androidx_lifecycle}"
  46 + implementation "androidx.lifecycle:lifecycle-common-java8:${versions.androidx_lifecycle}"
  47 + implementation "com.xwray:groupie:${versions.groupie}"
  48 + implementation "com.xwray:groupie-viewbinding:${versions.groupie}"
  49 + implementation 'com.snakydesign.livedataextensions:lives:1.3.0'
40 implementation 'com.github.ajalt:timberkt:1.5.1' 50 implementation 'com.github.ajalt:timberkt:1.5.1'
41 implementation project(":livekit-android-sdk") 51 implementation project(":livekit-android-sdk")
42 testImplementation 'junit:junit:4.12' 52 testImplementation 'junit:junit:4.12'
@@ -3,20 +3,21 @@ package io.livekit.android.sample @@ -3,20 +3,21 @@ package io.livekit.android.sample
3 import android.os.Bundle 3 import android.os.Bundle
4 import android.os.Parcelable 4 import android.os.Parcelable
5 import androidx.appcompat.app.AppCompatActivity 5 import androidx.appcompat.app.AppCompatActivity
6 -import androidx.lifecycle.lifecycleScope  
7 -import io.livekit.android.ConnectOptions  
8 -import io.livekit.android.LiveKit  
9 -import io.livekit.android.room.Room  
10 -import io.livekit.android.room.participant.Participant  
11 -import io.livekit.android.room.participant.RemoteParticipant  
12 -import io.livekit.android.room.track.VideoTrack 6 +import com.snakydesign.livedataextensions.combineLatest
  7 +import com.xwray.groupie.GroupieAdapter
13 import io.livekit.android.sample.databinding.CallActivityBinding 8 import io.livekit.android.sample.databinding.CallActivityBinding
14 -import kotlinx.coroutines.launch  
15 import kotlinx.parcelize.Parcelize 9 import kotlinx.parcelize.Parcelize
16 10
17 class CallActivity : AppCompatActivity() { 11 class CallActivity : AppCompatActivity() {
18 12
  13 + val viewModel: CallViewModel by viewModelByFactory {
  14 +
  15 + val args = intent.getParcelableExtra<BundleArgs>(KEY_ARGS)
  16 + ?: throw NullPointerException("args is null!")
  17 + CallViewModel(args.url, args.token, application)
  18 + }
19 lateinit var binding: CallActivityBinding 19 lateinit var binding: CallActivityBinding
  20 +
20 override fun onCreate(savedInstanceState: Bundle?) { 21 override fun onCreate(savedInstanceState: Bundle?) {
21 super.onCreate(savedInstanceState) 22 super.onCreate(savedInstanceState)
22 23
@@ -24,69 +25,20 @@ class CallActivity : AppCompatActivity() { @@ -24,69 +25,20 @@ class CallActivity : AppCompatActivity() {
24 25
25 setContentView(binding.root) 26 setContentView(binding.root)
26 27
27 - val args = intent.getParcelableExtra<BundleArgs>(KEY_ARGS)  
28 - if (args == null) {  
29 - finish()  
30 - return 28 + val adapter = GroupieAdapter()
  29 + binding.viewPager.apply {
  30 + this.adapter = adapter
31 } 31 }
32 32
33 - lifecycleScope.launch {  
34 -  
35 - val room = LiveKit.connect(  
36 - applicationContext,  
37 - args.url,  
38 - args.token,  
39 - ConnectOptions(false),  
40 - object : Room.Listener {  
41 -  
42 - var loadedParticipant = false  
43 - override fun onConnect(room: Room) {  
44 - }  
45 -  
46 - override fun onDisconnect(room: Room, error: Exception?) {  
47 - }  
48 -  
49 - override fun onParticipantConnected(  
50 - room: Room,  
51 - participant: RemoteParticipant  
52 - ) {  
53 - if (!loadedParticipant) {  
54 - room.setupVideo(binding.fullscreenVideoView)  
55 - participant.remoteVideoTracks  
56 - .first()  
57 - .track  
58 - .let { it as? VideoTrack }  
59 - ?.addRenderer(binding.fullscreenVideoView)  
60 - }  
61 - }  
62 -  
63 - override fun onParticipantDisconnected(  
64 - room: Room,  
65 - participant: RemoteParticipant  
66 - ) {  
67 - }  
68 -  
69 - override fun onFailedToConnect(room: Room, error: Exception) {  
70 - }  
71 -  
72 - override fun onReconnecting(room: Room, error: Exception) {  
73 - }  
74 -  
75 - override fun onReconnect(room: Room) {  
76 - }  
77 -  
78 - override fun onStartRecording(room: Room) {  
79 - }  
80 -  
81 - override fun onStopRecording(room: Room) {  
82 - }  
83 -  
84 - override fun onActiveSpeakersChanged(speakers: List<Participant>, room: Room) {  
85 - }  
86 -  
87 - }  
88 - )  
89 - } 33 + combineLatest(
  34 + viewModel.room,
  35 + viewModel.remoteParticipants
  36 + ) { room, participants -> room to participants }
  37 + .observe(this) {
  38 + val (room, participants) = it
  39 + val items = participants.map { participant -> ParticipantItem(room, participant) }
  40 + adapter.update(items)
  41 + }
90 } 42 }
91 43
92 44
  1 +package io.livekit.android.sample
  2 +
  3 +import android.app.Application
  4 +import androidx.lifecycle.AndroidViewModel
  5 +import androidx.lifecycle.LiveData
  6 +import androidx.lifecycle.MutableLiveData
  7 +import androidx.lifecycle.viewModelScope
  8 +import io.livekit.android.ConnectOptions
  9 +import io.livekit.android.LiveKit
  10 +import io.livekit.android.room.Room
  11 +import io.livekit.android.room.participant.Participant
  12 +import io.livekit.android.room.participant.RemoteParticipant
  13 +import kotlinx.coroutines.launch
  14 +
  15 +class CallViewModel(
  16 + val url: String,
  17 + val token: String,
  18 + application: Application
  19 +) : AndroidViewModel(application) {
  20 +
  21 +
  22 + private val mutableRoom = MutableLiveData<Room>()
  23 + val room: LiveData<Room> = mutableRoom
  24 + private val mutableRemoteParticipants = MutableLiveData<List<RemoteParticipant>>()
  25 + val remoteParticipants: LiveData<List<RemoteParticipant>> = mutableRemoteParticipants
  26 +
  27 + init {
  28 +
  29 + viewModelScope.launch {
  30 +
  31 + mutableRoom.value = LiveKit.connect(
  32 + application,
  33 + url,
  34 + token,
  35 + ConnectOptions(false),
  36 + object : Room.Listener {
  37 + override fun onConnect(room: Room) {
  38 + }
  39 +
  40 + override fun onDisconnect(room: Room, error: Exception?) {
  41 + }
  42 +
  43 + override fun onParticipantConnected(
  44 + room: Room,
  45 + participant: RemoteParticipant
  46 + ) {
  47 + updateParticipants(room)
  48 + }
  49 +
  50 + override fun onParticipantDisconnected(
  51 + room: Room,
  52 + participant: RemoteParticipant
  53 + ) {
  54 + updateParticipants(room)
  55 + }
  56 +
  57 + override fun onFailedToConnect(room: Room, error: Exception) {
  58 + }
  59 +
  60 + override fun onReconnecting(room: Room, error: Exception) {
  61 + }
  62 +
  63 + override fun onReconnect(room: Room) {
  64 + }
  65 +
  66 + override fun onStartRecording(room: Room) {
  67 + }
  68 +
  69 + override fun onStopRecording(room: Room) {
  70 + }
  71 +
  72 + override fun onActiveSpeakersChanged(speakers: List<Participant>, room: Room) {
  73 + }
  74 +
  75 + }
  76 + )
  77 + }
  78 + }
  79 +
  80 + fun updateParticipants(room: Room) {
  81 + mutableRemoteParticipants.value = room.remoteParticipants
  82 + .keys
  83 + .sortedBy { it.sid }
  84 + .mapNotNull { room.remoteParticipants[it] }
  85 + }
  86 +
  87 +}
@@ -35,6 +35,6 @@ class MainActivity : AppCompatActivity() { @@ -35,6 +35,6 @@ class MainActivity : AppCompatActivity() {
35 companion object { 35 companion object {
36 val URL = SpannableStringBuilder("192.168.11.2:7880") 36 val URL = SpannableStringBuilder("192.168.11.2:7880")
37 val TOKEN = 37 val TOKEN =
38 - SpannableStringBuilder("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2MTg1NzgxNzMsImlzcyI6IkFQSXdMZWFoN2c0ZnVMWURZQUplYUtzU0UiLCJqdGkiOiJwaG9uZSIsIm5iZiI6MTYxNTk4NjE3MywidmlkZW8iOnsicm9vbSI6Im15cm9vbSIsInJvb21Kb2luIjp0cnVlfX0.O3UedhM9lwdPxsZJQoTfVk0qXc-0ukjV6oZCBIaRTck") 38 + SpannableStringBuilder("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2MTg2MjY0NDAsImlzcyI6IkFQSXdMZWFoN2c0ZnVMWURZQUplYUtzU0UiLCJqdGkiOiJwaG9uZSIsIm5iZiI6MTYxNjAzNDQ0MCwidmlkZW8iOnsicm9vbSI6Im15cm9vbSIsInJvb21Kb2luIjp0cnVlfX0.QWN0B_DO8eSP2sJivnr_QzBud_sdIgJeWDQGQz67DvY")
39 } 39 }
40 } 40 }
  1 +package io.livekit.android.sample
  2 +
  3 +import android.view.View
  4 +import com.xwray.groupie.viewbinding.BindableItem
  5 +import com.xwray.groupie.viewbinding.GroupieViewHolder
  6 +import io.livekit.android.room.Room
  7 +import io.livekit.android.room.participant.RemoteParticipant
  8 +import io.livekit.android.room.track.RemoteVideoTrackPublication
  9 +import io.livekit.android.room.track.VideoTrack
  10 +import io.livekit.android.room.track.VideoTrackPublication
  11 +import io.livekit.android.sample.databinding.ParticipantItemBinding
  12 +
  13 +class ParticipantItem(
  14 + val room: Room,
  15 + val remoteParticipant: RemoteParticipant
  16 +) :
  17 + BindableItem<ParticipantItemBinding>() {
  18 +
  19 + private var videoBound = false
  20 +
  21 + override fun initializeViewBinding(view: View): ParticipantItemBinding {
  22 + return ParticipantItemBinding.bind(view)
  23 + }
  24 +
  25 + override fun bind(viewBinding: ParticipantItemBinding, position: Int) {
  26 + viewBinding.run {
  27 + room.initVideoRenderer(renderer)
  28 +
  29 + val existingTrack = getVideoTrack()
  30 + if (existingTrack != null) {
  31 + setupVideoIfNeeded(existingTrack, viewBinding)
  32 + } else {
  33 + remoteParticipant.listener = object : RemoteParticipant.Listener {
  34 + override fun onSubscribe(
  35 + videoTrack: RemoteVideoTrackPublication,
  36 + participant: RemoteParticipant
  37 + ) {
  38 + val track = videoTrack.videoTrack
  39 + if (track != null) {
  40 + setupVideoIfNeeded(track, viewBinding)
  41 + }
  42 + }
  43 + }
  44 + }
  45 + }
  46 + }
  47 +
  48 + private fun getVideoTrack(): VideoTrack? {
  49 + return remoteParticipant
  50 + .remoteVideoTracks
  51 + .firstOrNull()
  52 + .let { it as? VideoTrackPublication }
  53 + ?.videoTrack
  54 + }
  55 +
  56 + private fun setupVideoIfNeeded(videoTrack: VideoTrack, viewBinding: ParticipantItemBinding) {
  57 + if (videoBound) {
  58 + return
  59 + }
  60 +
  61 + videoTrack.addRenderer(viewBinding.renderer)
  62 + }
  63 +
  64 + override fun unbind(viewHolder: GroupieViewHolder<ParticipantItemBinding>) {
  65 + super.unbind(viewHolder)
  66 + videoBound = false
  67 + }
  68 +
  69 + override fun getLayout(): Int = R.layout.participant_item
  70 +}
  1 +package io.livekit.android.sample
  2 +
  3 +import androidx.activity.viewModels
  4 +import androidx.fragment.app.FragmentActivity
  5 +import androidx.lifecycle.ViewModel
  6 +import androidx.lifecycle.ViewModelProvider
  7 +
  8 +typealias CreateViewModel<VM> = () -> VM
  9 +
  10 +inline fun <reified VM : ViewModel> FragmentActivity.viewModelByFactory(
  11 + noinline create: CreateViewModel<VM>
  12 +): Lazy<VM> {
  13 + return viewModels {
  14 + createViewModelFactoryFactory(create)
  15 + }
  16 +}
  17 +
  18 +fun <VM> createViewModelFactoryFactory(
  19 + create: CreateViewModel<VM>
  20 +): ViewModelProvider.Factory {
  21 + return object : ViewModelProvider.Factory {
  22 + override fun <T : ViewModel?> create(modelClass: Class<T>): T {
  23 + @Suppress("UNCHECKED_CAST")
  24 + return create() as? T
  25 + ?: throw IllegalArgumentException("Unknown viewmodel class!")
  26 + }
  27 + }
  28 +}
1 -  
2 -<FrameLayout  
3 - xmlns:android="http://schemas.android.com/apk/res/android"  
4 - xmlns:tools="http://schemas.android.com/tools" 1 +<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
5 android:layout_width="match_parent" 2 android:layout_width="match_parent"
6 android:layout_height="match_parent"> 3 android:layout_height="match_parent">
7 4
8 - <org.webrtc.SurfaceViewRenderer  
9 - android:id="@+id/fullscreen_video_view"  
10 - android:layout_width="wrap_content"  
11 - android:layout_height="wrap_content"  
12 - android:layout_gravity="center" /> 5 + <androidx.viewpager2.widget.ViewPager2
  6 + android:id="@+id/view_pager"
  7 + android:layout_width="match_parent"
  8 + android:layout_height="match_parent" />
13 9
14 <org.webrtc.SurfaceViewRenderer 10 <org.webrtc.SurfaceViewRenderer
15 android:id="@+id/pip_video_view" 11 android:id="@+id/pip_video_view"
16 android:layout_height="144dp" 12 android:layout_height="144dp"
17 android:layout_width="wrap_content" 13 android:layout_width="wrap_content"
18 android:layout_gravity="bottom|end" 14 android:layout_gravity="bottom|end"
19 - android:layout_margin="16dp"/>  
20 -  
21 - <FrameLayout  
22 - android:id="@+id/call_fragment_container"  
23 - android:layout_width="match_parent"  
24 - android:layout_height="match_parent" />  
25 - <FrameLayout  
26 - android:id="@+id/hud_fragment_container"  
27 - android:layout_width="match_parent"  
28 - android:layout_height="match_parent" /> 15 + android:layout_margin="16dp" />
29 16
30 </FrameLayout> 17 </FrameLayout>
  1 +<?xml version="1.0" encoding="utf-8"?>
  2 +<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
  3 + android:layout_width="match_parent"
  4 + android:layout_height="match_parent">
  5 +
  6 + <org.webrtc.SurfaceViewRenderer
  7 + android:id="@+id/renderer"
  8 + android:layout_width="match_parent"
  9 + android:layout_height="match_parent" />
  10 +</FrameLayout>