David Liu

sample app redesign

正在显示 20 个修改的文件 包含 244 行增加145 行删除
@@ -108,16 +108,24 @@ internal constructor( @@ -108,16 +108,24 @@ internal constructor(
108 return super.getTrackPublicationByName(name) as? LocalTrackPublication 108 return super.getTrackPublicationByName(name) as? LocalTrackPublication
109 } 109 }
110 110
111 - suspend fun setCameraEnabled(enabled: Boolean){ 111 + suspend fun setCameraEnabled(enabled: Boolean) {
112 setTrackEnabled(Track.Source.CAMERA, enabled) 112 setTrackEnabled(Track.Source.CAMERA, enabled)
113 } 113 }
114 114
115 - suspend fun setMicrophoneEnabled(enabled: Boolean){ 115 + suspend fun setMicrophoneEnabled(enabled: Boolean) {
116 setTrackEnabled(Track.Source.MICROPHONE, enabled) 116 setTrackEnabled(Track.Source.MICROPHONE, enabled)
117 } 117 }
118 118
119 - suspend fun setScreenShareEnabled(enabled: Boolean) {  
120 - setTrackEnabled(Track.Source.SCREEN_SHARE, enabled) 119 + /**
  120 + * @param mediaProjectionPermissionResultData The resultData returned from launching
  121 + * [MediaProjectionManager.createScreenCaptureIntent()](https://developer.android.com/reference/android/media/projection/MediaProjectionManager#createScreenCaptureIntent()).
  122 + * @throws IllegalArgumentException if attempting to enable screenshare without [mediaProjectionPermissionResultData]
  123 + */
  124 + suspend fun setScreenShareEnabled(
  125 + enabled: Boolean,
  126 + mediaProjectionPermissionResultData: Intent? = null
  127 + ) {
  128 + setTrackEnabled(Track.Source.SCREEN_SHARE, enabled, mediaProjectionPermissionResultData)
121 } 129 }
122 130
123 private suspend fun setTrackEnabled( 131 private suspend fun setTrackEnabled(
  1 +package io.livekit.android.sample.util
  2 +
  3 +import androidx.lifecycle.LiveData
  4 +import androidx.lifecycle.MutableLiveData
  5 +
  6 +fun <T> MutableLiveData<T>.hide(): LiveData<T> = this
1 package io.livekit.android.sample 1 package io.livekit.android.sample
2 2
  3 +import android.app.Activity
3 import android.media.AudioManager 4 import android.media.AudioManager
  5 +import android.media.projection.MediaProjectionManager
4 import android.os.Bundle 6 import android.os.Bundle
5 import android.os.Parcelable 7 import android.os.Parcelable
  8 +import androidx.activity.result.contract.ActivityResultContracts
6 import androidx.appcompat.app.AppCompatActivity 9 import androidx.appcompat.app.AppCompatActivity
  10 +import androidx.recyclerview.widget.LinearLayoutManager
7 import com.github.ajalt.timberkt.Timber 11 import com.github.ajalt.timberkt.Timber
8 -import com.google.android.material.tabs.TabLayoutMediator  
9 import com.snakydesign.livedataextensions.combineLatest 12 import com.snakydesign.livedataextensions.combineLatest
10 import com.xwray.groupie.GroupieAdapter 13 import com.xwray.groupie.GroupieAdapter
11 import io.livekit.android.room.track.LocalVideoTrack 14 import io.livekit.android.room.track.LocalVideoTrack
@@ -20,12 +23,23 @@ class CallActivity : AppCompatActivity() { @@ -20,12 +23,23 @@ class CallActivity : AppCompatActivity() {
20 CallViewModel(args.url, args.token, application) 23 CallViewModel(args.url, args.token, application)
21 } 24 }
22 lateinit var binding: CallActivityBinding 25 lateinit var binding: CallActivityBinding
23 - var tabLayoutMediator: TabLayoutMediator? = null  
24 val focusChangeListener = AudioManager.OnAudioFocusChangeListener {} 26 val focusChangeListener = AudioManager.OnAudioFocusChangeListener {}
25 27
26 private var previousSpeakerphoneOn = true 28 private var previousSpeakerphoneOn = true
27 private var previousMicrophoneMute = false 29 private var previousMicrophoneMute = false
28 30
  31 + private val screenCaptureIntentLauncher =
  32 + registerForActivityResult(
  33 + ActivityResultContracts.StartActivityForResult()
  34 + ) { result ->
  35 + val resultCode = result.resultCode
  36 + val data = result.data
  37 + if (resultCode != Activity.RESULT_OK || data == null) {
  38 + return@registerForActivityResult
  39 + }
  40 + viewModel.setScreenshare(true, data)
  41 + }
  42 +
29 override fun onCreate(savedInstanceState: Bundle?) { 43 override fun onCreate(savedInstanceState: Bundle?) {
30 super.onCreate(savedInstanceState) 44 super.onCreate(savedInstanceState)
31 45
@@ -33,44 +47,71 @@ class CallActivity : AppCompatActivity() { @@ -33,44 +47,71 @@ class CallActivity : AppCompatActivity() {
33 47
34 setContentView(binding.root) 48 setContentView(binding.root)
35 49
36 - // Viewpager setup 50 + // Audience row setup
  51 + binding.audienceRow.layoutManager =
  52 + LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false)
37 val adapter = GroupieAdapter() 53 val adapter = GroupieAdapter()
38 54
39 - binding.viewPager.apply { 55 + binding.audienceRow.apply {
40 this.adapter = adapter 56 this.adapter = adapter
41 } 57 }
42 58
43 combineLatest( 59 combineLatest(
44 viewModel.room, 60 viewModel.room,
45 - viewModel.remoteParticipants 61 + viewModel.participants
46 ) { room, participants -> room to participants } 62 ) { room, participants -> room to participants }
47 .observe(this) { 63 .observe(this) {
48 - tabLayoutMediator?.detach()  
49 - tabLayoutMediator = null  
50 64
51 val (room, participants) = it 65 val (room, participants) = it
52 val items = participants.map { participant -> ParticipantItem(room, participant) } 66 val items = participants.map { participant -> ParticipantItem(room, participant) }
53 adapter.update(items) 67 adapter.update(items)
54 -  
55 - tabLayoutMediator =  
56 - TabLayoutMediator(binding.tabs, binding.viewPager) { tab, position ->  
57 - tab.text = participants[position].identity  
58 - }  
59 - tabLayoutMediator?.attach()  
60 } 68 }
61 69
  70 + // speaker view setup
62 viewModel.room.observe(this) { room -> 71 viewModel.room.observe(this) { room ->
63 - room.initVideoRenderer(binding.pipVideoView) 72 + room.initVideoRenderer(binding.speakerView)
64 val videoTrack = room.localParticipant.videoTracks.values 73 val videoTrack = room.localParticipant.videoTracks.values
65 .firstOrNull() 74 .firstOrNull()
66 ?.track as? LocalVideoTrack 75 ?.track as? LocalVideoTrack
67 76
68 videoTrack?.let { 77 videoTrack?.let {
69 - it.addRenderer(binding.pipVideoView) 78 + it.addRenderer(binding.speakerView)
  79 + }
70 } 80 }
71 81
  82 + // Controls setup
  83 + viewModel.videoEnabled.observe(this) { enabled ->
  84 + binding.camera.setOnClickListener { viewModel.setCameraEnabled(!enabled) }
  85 + binding.camera.setImageResource(
  86 + if (enabled) R.drawable.outline_videocam_24
  87 + else R.drawable.outline_videocam_off_24
  88 + )
  89 + binding.flipCamera.isEnabled = enabled
  90 + }
  91 + viewModel.micEnabled.observe(this) { enabled ->
  92 + binding.mic.setOnClickListener { viewModel.setMicEnabled(!enabled) }
  93 + binding.mic.setImageResource(
  94 + if (enabled) R.drawable.outline_mic_24
  95 + else R.drawable.outline_mic_off_24
  96 + )
  97 + }
72 98
  99 + binding.flipCamera.setOnClickListener { viewModel.flipCamera() }
  100 + viewModel.screenshareEnabled.observe(this) { enabled ->
  101 + binding.screenShare.setOnClickListener {
  102 + if (enabled) {
  103 + viewModel.setScreenshare(!enabled)
  104 + } else {
  105 + requestMediaProjection()
  106 + }
  107 + }
  108 + binding.screenShare.setImageResource(
  109 + if (enabled) R.drawable.baseline_cast_connected_24
  110 + else R.drawable.baseline_cast_24
  111 + )
73 } 112 }
  113 +
  114 + // Grab audio focus for video call
74 val audioManager = getSystemService(AUDIO_SERVICE) as AudioManager 115 val audioManager = getSystemService(AUDIO_SERVICE) as AudioManager
75 with(audioManager) { 116 with(audioManager) {
76 previousSpeakerphoneOn = isSpeakerphoneOn 117 previousSpeakerphoneOn = isSpeakerphoneOn
@@ -91,10 +132,19 @@ class CallActivity : AppCompatActivity() { @@ -91,10 +132,19 @@ class CallActivity : AppCompatActivity() {
91 } 132 }
92 } 133 }
93 134
  135 + private fun requestMediaProjection() {
  136 + val mediaProjectionManager =
  137 + getSystemService(MEDIA_PROJECTION_SERVICE) as MediaProjectionManager
  138 + screenCaptureIntentLauncher.launch(mediaProjectionManager.createScreenCaptureIntent())
  139 + }
  140 +
94 override fun onDestroy() { 141 override fun onDestroy() {
95 super.onDestroy() 142 super.onDestroy()
96 143
97 - binding.pipVideoView.release() 144 + // Release video views
  145 + binding.speakerView.release()
  146 +
  147 + // Undo audio mode changes
98 val audioManager = getSystemService(AUDIO_SERVICE) as AudioManager 148 val audioManager = getSystemService(AUDIO_SERVICE) as AudioManager
99 with(audioManager) { 149 with(audioManager) {
100 isSpeakerphoneOn = previousSpeakerphoneOn 150 isSpeakerphoneOn = previousSpeakerphoneOn
1 package io.livekit.android.sample 1 package io.livekit.android.sample
2 2
3 import android.app.Application 3 import android.app.Application
  4 +import android.content.Intent
4 import androidx.lifecycle.AndroidViewModel 5 import androidx.lifecycle.AndroidViewModel
5 -import androidx.lifecycle.LiveData  
6 import androidx.lifecycle.MutableLiveData 6 import androidx.lifecycle.MutableLiveData
7 import androidx.lifecycle.viewModelScope 7 import androidx.lifecycle.viewModelScope
8 -import com.github.ajalt.timberkt.Timber 8 +import com.snakydesign.livedataextensions.distinctUntilChanged
9 import io.livekit.android.ConnectOptions 9 import io.livekit.android.ConnectOptions
10 import io.livekit.android.LiveKit 10 import io.livekit.android.LiveKit
  11 +import io.livekit.android.events.RoomEvent
  12 +import io.livekit.android.events.collect
11 import io.livekit.android.room.Room 13 import io.livekit.android.room.Room
12 -import io.livekit.android.room.RoomListener  
13 import io.livekit.android.room.participant.Participant 14 import io.livekit.android.room.participant.Participant
14 -import io.livekit.android.room.participant.RemoteParticipant 15 +import io.livekit.android.room.track.CameraPosition
  16 +import io.livekit.android.room.track.LocalVideoTrack
  17 +import io.livekit.android.room.track.Track
  18 +import io.livekit.android.sample.util.hide
15 import kotlinx.coroutines.launch 19 import kotlinx.coroutines.launch
16 20
17 class CallViewModel( 21 class CallViewModel(
18 val url: String, 22 val url: String,
19 val token: String, 23 val token: String,
20 application: Application 24 application: Application
21 -) : AndroidViewModel(application), RoomListener { 25 +) : AndroidViewModel(application) {
22 private val mutableRoom = MutableLiveData<Room>() 26 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 27 + val room = mutableRoom.hide()
  28 + private val mutableParticipants = MutableLiveData<List<Participant>>()
  29 + val participants = mutableParticipants.hide()
  30 + private val mutableActiveSpeaker = MutableLiveData<Participant>()
  31 + val activeSpeaker = mutableActiveSpeaker.hide().distinctUntilChanged()
  32 +
  33 + private val mutableVideoEnabled = MutableLiveData<Boolean>()
  34 + val videoEnabled = mutableVideoEnabled.hide().distinctUntilChanged()
  35 + private val mutableMicEnabled = MutableLiveData<Boolean>()
  36 + val micEnabled = mutableMicEnabled.hide().distinctUntilChanged()
  37 + private val mutableScreenshareEnabled = MutableLiveData<Boolean>()
  38 + val screenshareEnabled = mutableScreenshareEnabled.hide().distinctUntilChanged()
26 39
27 init { 40 init {
28 viewModelScope.launch { 41 viewModelScope.launch {
@@ -31,9 +44,15 @@ class CallViewModel( @@ -31,9 +44,15 @@ class CallViewModel(
31 url, 44 url,
32 token, 45 token,
33 ConnectOptions(), 46 ConnectOptions(),
34 - this@CallViewModel 47 + null
35 ) 48 )
36 49
  50 + launch {
  51 + room.events.collect {
  52 + handleRoomEvent(it)
  53 + }
  54 + }
  55 +
37 val localParticipant = room.localParticipant 56 val localParticipant = room.localParticipant
38 val audioTrack = localParticipant.createAudioTrack() 57 val audioTrack = localParticipant.createAudioTrack()
39 localParticipant.publishAudioTrack(audioTrack) 58 localParticipant.publishAudioTrack(audioTrack)
@@ -42,12 +61,28 @@ class CallViewModel( @@ -42,12 +61,28 @@ class CallViewModel(
42 videoTrack.startCapture() 61 videoTrack.startCapture()
43 62
44 updateParticipants(room) 63 updateParticipants(room)
  64 + mutableActiveSpeaker.value = localParticipant
45 mutableRoom.value = room 65 mutableRoom.value = room
  66 +
  67 + mutableVideoEnabled.value =
  68 + !(localParticipant.getTrackPublication(Track.Source.CAMERA)?.muted ?: false)
  69 + mutableMicEnabled.value =
  70 + !(localParticipant.getTrackPublication(Track.Source.MICROPHONE)?.muted ?: false)
  71 + mutableScreenshareEnabled.value = false
  72 + }
  73 + }
  74 +
  75 + private fun handleRoomEvent(event: RoomEvent) {
  76 + when (event) {
  77 + is RoomEvent.ParticipantConnected -> updateParticipants(event.room)
  78 + is RoomEvent.ParticipantDisconnected -> updateParticipants(event.room)
  79 + is RoomEvent.ActiveSpeakersChanged -> handleActiveSpeakersChanged(event.speakers)
46 } 80 }
47 } 81 }
48 82
49 private fun updateParticipants(room: Room) { 83 private fun updateParticipants(room: Room) {
50 - mutableRemoteParticipants.postValue( 84 + mutableParticipants.postValue(
  85 + listOf(room.localParticipant) +
51 room.remoteParticipants 86 room.remoteParticipants
52 .keys 87 .keys
53 .sortedBy { it } 88 .sortedBy { it }
@@ -55,36 +90,66 @@ class CallViewModel( @@ -55,36 +90,66 @@ class CallViewModel(
55 ) 90 )
56 } 91 }
57 92
  93 + fun handleActiveSpeakersChanged(speakers: List<Participant>) {
  94 + // If old active speaker is still active, don't change.
  95 + if (speakers.isEmpty() || speakers.contains(mutableActiveSpeaker.value)) {
  96 + return
  97 + }
  98 + val newSpeaker = speakers.firstOrNull() ?: return
  99 + mutableActiveSpeaker.postValue(newSpeaker)
  100 + }
  101 +
58 override fun onCleared() { 102 override fun onCleared() {
59 super.onCleared() 103 super.onCleared()
60 mutableRoom.value?.disconnect() 104 mutableRoom.value?.disconnect()
61 } 105 }
62 106
63 - override fun onDisconnect(room: Room, error: Exception?) { 107 + fun setCameraEnabled(enabled: Boolean) {
  108 + val localParticipant = room.value?.localParticipant ?: return
  109 +
  110 + viewModelScope.launch {
  111 + localParticipant.setCameraEnabled(enabled)
  112 + mutableVideoEnabled.postValue(enabled)
  113 + }
64 } 114 }
65 115
66 - override fun onParticipantConnected(  
67 - room: Room,  
68 - participant: RemoteParticipant  
69 - ) {  
70 - updateParticipants(room) 116 + fun setMicEnabled(enabled: Boolean) {
  117 + val localParticipant = room.value?.localParticipant ?: return
  118 +
  119 + viewModelScope.launch {
  120 + localParticipant.setMicrophoneEnabled(enabled)
  121 + mutableMicEnabled.postValue(enabled)
  122 + }
71 } 123 }
72 124
73 - override fun onParticipantDisconnected(  
74 - room: Room,  
75 - participant: RemoteParticipant 125 + fun setScreenshare(
  126 + enabled: Boolean,
  127 + mediaProjectionPermissionResultData: Intent? = null
76 ) { 128 ) {
77 - updateParticipants(room)  
78 - } 129 + val localParticipant = room.value?.localParticipant ?: return
79 130
80 - override fun onFailedToConnect(room: Room, error: Exception) { 131 + viewModelScope.launch {
  132 + localParticipant.setScreenShareEnabled(enabled, mediaProjectionPermissionResultData)
  133 + mutableScreenshareEnabled.postValue(enabled)
81 } 134 }
  135 + }
  136 +
  137 + fun flipCamera() {
  138 + val localParticipant = room.value?.localParticipant ?: return
  139 + val localVideoTrack = localParticipant
  140 + .getTrackPublication(Track.Source.CAMERA)
  141 + ?.track as? LocalVideoTrack
  142 + ?: return
82 143
83 - override fun onActiveSpeakersChanged(speakers: List<Participant>, room: Room) {  
84 - Timber.i { "active speakers changed ${speakers.count()}" } 144 + val currentOptions = localVideoTrack.options
  145 + val newPosition = when (currentOptions.position) {
  146 + CameraPosition.FRONT -> CameraPosition.BACK
  147 + CameraPosition.BACK -> CameraPosition.FRONT
  148 + null -> null
85 } 149 }
86 150
87 - override fun onMetadataChanged(participant: Participant, prevMetadata: String?, room: Room) {  
88 - Timber.i { "Participant metadata changed: ${participant.identity}" } 151 + if (newPosition != null) {
  152 + localVideoTrack.restartTrack(options = currentOptions.copy(position = newPosition))
  153 + }
89 } 154 }
90 } 155 }
@@ -5,6 +5,7 @@ import com.github.ajalt.timberkt.Timber @@ -5,6 +5,7 @@ import com.github.ajalt.timberkt.Timber
5 import com.xwray.groupie.viewbinding.BindableItem 5 import com.xwray.groupie.viewbinding.BindableItem
6 import com.xwray.groupie.viewbinding.GroupieViewHolder 6 import com.xwray.groupie.viewbinding.GroupieViewHolder
7 import io.livekit.android.room.Room 7 import io.livekit.android.room.Room
  8 +import io.livekit.android.room.participant.Participant
8 import io.livekit.android.room.participant.ParticipantListener 9 import io.livekit.android.room.participant.ParticipantListener
9 import io.livekit.android.room.participant.RemoteParticipant 10 import io.livekit.android.room.participant.RemoteParticipant
10 import io.livekit.android.room.track.* 11 import io.livekit.android.room.track.*
@@ -12,7 +13,7 @@ import io.livekit.android.sample.databinding.ParticipantItemBinding @@ -12,7 +13,7 @@ import io.livekit.android.sample.databinding.ParticipantItemBinding
12 13
13 class ParticipantItem( 14 class ParticipantItem(
14 val room: Room, 15 val room: Room,
15 - val remoteParticipant: RemoteParticipant 16 + val participant: Participant
16 ) : 17 ) :
17 BindableItem<ParticipantItemBinding>() { 18 BindableItem<ParticipantItemBinding>() {
18 19
@@ -27,13 +28,14 @@ class ParticipantItem( @@ -27,13 +28,14 @@ class ParticipantItem(
27 override fun bind(viewBinding: ParticipantItemBinding, position: Int) { 28 override fun bind(viewBinding: ParticipantItemBinding, position: Int) {
28 viewBinding.run { 29 viewBinding.run {
29 30
30 - remoteParticipant.listener = object : ParticipantListener { 31 + participant.listener = object : ParticipantListener {
31 override fun onTrackSubscribed( 32 override fun onTrackSubscribed(
32 track: Track, 33 track: Track,
33 publication: RemoteTrackPublication, 34 publication: RemoteTrackPublication,
34 participant: RemoteParticipant 35 participant: RemoteParticipant
35 ) { 36 ) {
36 - if (track is VideoTrack) { 37 + if (track !is VideoTrack) return
  38 + if (publication.source == Track.Source.CAMERA) {
37 setupVideoIfNeeded(track, viewBinding) 39 setupVideoIfNeeded(track, viewBinding)
38 } 40 }
39 } 41 }
@@ -54,9 +56,7 @@ class ParticipantItem( @@ -54,9 +56,7 @@ class ParticipantItem(
54 } 56 }
55 57
56 private fun getVideoTrack(): VideoTrack? { 58 private fun getVideoTrack(): VideoTrack? {
57 - return remoteParticipant  
58 - .videoTracks.values  
59 - .firstOrNull()?.track as? VideoTrack 59 + return participant.getTrackPublication(Track.Source.CAMERA)?.track as? VideoTrack
60 } 60 }
61 61
62 internal fun setupVideoIfNeeded(videoTrack: VideoTrack, viewBinding: ParticipantItemBinding) { 62 internal fun setupVideoIfNeeded(videoTrack: VideoTrack, viewBinding: ParticipantItemBinding) {
1 -<vector xmlns:android="http://schemas.android.com/apk/res/android"  
2 - android:width="24dp"  
3 - android:height="24dp"  
4 - android:viewportWidth="24"  
5 - android:viewportHeight="24"  
6 - android:tint="?attr/colorControlNormal">  
7 - <path  
8 - android:fillColor="@android:color/white"  
9 - android:pathData="M12,14c1.66,0 3,-1.34 3,-3L15,5c0,-1.66 -1.34,-3 -3,-3S9,3.34 9,5v6c0,1.66 1.34,3 3,3zM17.91,11c-0.49,0 -0.9,0.36 -0.98,0.85C16.52,14.2 14.47,16 12,16s-4.52,-1.8 -4.93,-4.15c-0.08,-0.49 -0.49,-0.85 -0.98,-0.85 -0.61,0 -1.09,0.54 -1,1.14 0.49,3 2.89,5.35 5.91,5.78L11,20c0,0.55 0.45,1 1,1s1,-0.45 1,-1v-2.08c3.02,-0.43 5.42,-2.78 5.91,-5.78 0.1,-0.6 -0.39,-1.14 -1,-1.14z"/>  
10 -</vector>  
1 -<vector xmlns:android="http://schemas.android.com/apk/res/android"  
2 - android:width="24dp"  
3 - android:height="24dp"  
4 - android:viewportWidth="24"  
5 - android:viewportHeight="24"  
6 - android:tint="?attr/colorControlNormal">  
7 - <path  
8 - android:fillColor="@android:color/white"  
9 - android:pathData="M15,10.6L15,5c0,-1.66 -1.34,-3 -3,-3 -1.54,0 -2.79,1.16 -2.96,2.65L15,10.6zM18.08,11c-0.41,0 -0.77,0.3 -0.83,0.71 -0.05,0.32 -0.12,0.64 -0.22,0.93l1.27,1.27c0.3,-0.6 0.52,-1.25 0.63,-1.94 0.07,-0.51 -0.33,-0.97 -0.85,-0.97zM3.71,3.56c-0.39,0.39 -0.39,1.02 0,1.41L9,10.27v0.43c0,1.19 0.6,2.32 1.63,2.91 0.75,0.43 1.41,0.44 2.02,0.31l1.66,1.66c-0.71,0.33 -1.5,0.52 -2.31,0.52 -2.54,0 -4.88,-1.77 -5.25,-4.39 -0.06,-0.41 -0.42,-0.71 -0.83,-0.71 -0.52,0 -0.92,0.46 -0.85,0.97 0.46,2.96 2.96,5.3 5.93,5.75L11,20c0,0.55 0.45,1 1,1s1,-0.45 1,-1v-2.28c0.91,-0.13 1.77,-0.45 2.55,-0.9l3.49,3.49c0.39,0.39 1.02,0.39 1.41,0 0.39,-0.39 0.39,-1.02 0,-1.41L5.12,3.56c-0.39,-0.39 -1.02,-0.39 -1.41,0z"/>  
10 -</vector>  
1 -<vector xmlns:android="http://schemas.android.com/apk/res/android"  
2 - android:width="24dp"  
3 - android:height="24dp"  
4 - android:viewportWidth="24"  
5 - android:viewportHeight="24"  
6 - android:tint="?attr/colorControlNormal">  
7 - <path  
8 - android:fillColor="@android:color/white"  
9 - android:pathData="M17,10.5V7c0,-0.55 -0.45,-1 -1,-1H4c-0.55,0 -1,0.45 -1,1v10c0,0.55 0.45,1 1,1h12c0.55,0 1,-0.45 1,-1v-3.5l2.29,2.29c0.63,0.63 1.71,0.18 1.71,-0.71V8.91c0,-0.89 -1.08,-1.34 -1.71,-0.71L17,10.5z"/>  
10 -</vector>  
1 -<vector xmlns:android="http://schemas.android.com/apk/res/android"  
2 - android:width="24dp"  
3 - android:height="24dp"  
4 - android:viewportWidth="24"  
5 - android:viewportHeight="24"  
6 - android:tint="?attr/colorControlNormal">  
7 - <path  
8 - android:fillColor="@android:color/white"  
9 - android:pathData="M21,14.2V8.91c0,-0.89 -1.08,-1.34 -1.71,-0.71L17,10.5V7c0,-0.55 -0.45,-1 -1,-1h-5.61l8.91,8.91c0.62,0.63 1.7,0.18 1.7,-0.71zM2.71,2.56c-0.39,0.39 -0.39,1.02 0,1.41L4.73,6H4c-0.55,0 -1,0.45 -1,1v10c0,0.55 0.45,1 1,1h12c0.21,0 0.39,-0.08 0.55,-0.18l2.48,2.48c0.39,0.39 1.02,0.39 1.41,0 0.39,-0.39 0.39,-1.02 0,-1.41L4.12,2.56c-0.39,-0.39 -1.02,-0.39 -1.41,0z"/>  
10 -</vector>  
@@ -4,58 +4,59 @@ @@ -4,58 +4,59 @@
4 android:layout_height="match_parent" 4 android:layout_height="match_parent"
5 android:keepScreenOn="true"> 5 android:keepScreenOn="true">
6 6
7 - <com.google.android.material.tabs.TabLayout  
8 - android:id="@+id/tabs"  
9 - android:layout_width="match_parent"  
10 - android:layout_height="48dp" 7 + <io.livekit.android.renderer.TextureViewRenderer
  8 + android:id="@+id/speaker_view"
  9 + android:layout_width="0dp"
  10 + android:layout_height="0dp"
  11 + app:layout_constraintBottom_toTopOf="@id/audience_row"
11 app:layout_constraintTop_toTopOf="parent" 12 app:layout_constraintTop_toTopOf="parent"
12 - app:tabMode="auto" /> 13 + app:layout_constraintEnd_toEndOf="parent"
  14 + app:layout_constraintStart_toStartOf="parent" />
13 15
14 - <androidx.viewpager2.widget.ViewPager2  
15 - android:id="@+id/view_pager" 16 + <androidx.recyclerview.widget.RecyclerView
  17 + android:id="@+id/audience_row"
16 android:layout_width="match_parent" 18 android:layout_width="match_parent"
17 - android:layout_height="0dp"  
18 - app:layout_constraintBottom_toBottomOf="parent"  
19 - app:layout_constraintTop_toBottomOf="@id/tabs" />  
20 -  
21 - <androidx.constraintlayout.widget.ConstraintLayout  
22 - android:layout_width="200dp"  
23 - android:layout_height="200dp"  
24 - app:layout_constraintBottom_toBottomOf="parent"  
25 - app:layout_constraintEnd_toEndOf="parent">  
26 -  
27 - <io.livekit.android.renderer.TextureViewRenderer  
28 - android:id="@+id/pip_video_view"  
29 - android:layout_width="match_parent"  
30 - android:layout_height="match_parent"  
31 - android:layout_margin="16dp" /> 19 + android:layout_height="120dp"
  20 + app:layout_constraintBottom_toTopOf="@id/controls_box" />
32 21
33 <LinearLayout 22 <LinearLayout
34 - android:layout_width="wrap_content"  
35 - android:layout_height="wrap_content"  
36 - android:layout_margin="30dp" 23 + android:id="@+id/controls_box"
  24 + android:layout_width="match_parent"
  25 + android:layout_height="60dp"
  26 + android:gravity="center"
37 android:orientation="horizontal" 27 android:orientation="horizontal"
38 - app:layout_constraintBottom_toBottomOf="parent"  
39 - app:layout_constraintEnd_toEndOf="parent"  
40 - app:layout_constraintStart_toStartOf="parent">  
41 -  
42 - <ImageButton  
43 - android:id="@+id/mic_button"  
44 - android:layout_width="48dp"  
45 - android:layout_height="48dp"  
46 - android:src="@drawable/ic_round_mic_24" />  
47 -  
48 - <Space  
49 - android:layout_width="20dp"  
50 - android:layout_height="1dp" />  
51 -  
52 - <ImageButton  
53 - android:id="@+id/video_button"  
54 - android:layout_width="48dp"  
55 - android:layout_height="48dp"  
56 - android:src="@drawable/ic_round_videocam_24" />  
57 - 28 + app:layout_constraintBottom_toBottomOf="parent">
  29 +
  30 + <ImageView
  31 + android:id="@+id/camera"
  32 + android:layout_width="@dimen/control_size"
  33 + android:layout_height="@dimen/control_size"
  34 + android:background="?android:attr/selectableItemBackground"
  35 + android:padding="@dimen/control_padding"
  36 + android:src="@drawable/outline_videocam_24" />
  37 +
  38 + <ImageView
  39 + android:id="@+id/mic"
  40 + android:layout_width="@dimen/control_size"
  41 + android:layout_height="@dimen/control_size"
  42 + android:background="?android:attr/selectableItemBackground"
  43 + android:padding="@dimen/control_padding"
  44 + android:src="@drawable/outline_mic_24" />
  45 +
  46 + <ImageView
  47 + android:id="@+id/flip_camera"
  48 + android:layout_width="@dimen/control_size"
  49 + android:layout_height="@dimen/control_size"
  50 + android:background="?android:attr/selectableItemBackground"
  51 + android:padding="@dimen/control_padding"
  52 + android:src="@drawable/outline_flip_camera_android_24" />
  53 +
  54 + <ImageView
  55 + android:id="@+id/screen_share"
  56 + android:layout_width="@dimen/control_size"
  57 + android:layout_height="@dimen/control_size"
  58 + android:background="?android:attr/selectableItemBackground"
  59 + android:padding="@dimen/control_padding"
  60 + android:src="@drawable/baseline_cast_24" />
58 </LinearLayout> 61 </LinearLayout>
59 -  
60 - </androidx.constraintlayout.widget.ConstraintLayout>  
61 </androidx.constraintlayout.widget.ConstraintLayout> 62 </androidx.constraintlayout.widget.ConstraintLayout>
1 <?xml version="1.0" encoding="utf-8"?> 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" 2 +<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
  3 + xmlns:app="http://schemas.android.com/apk/res-auto"
  4 + android:layout_width="wrap_content"
4 android:layout_height="match_parent"> 5 android:layout_height="match_parent">
5 6
6 <io.livekit.android.renderer.TextureViewRenderer 7 <io.livekit.android.renderer.TextureViewRenderer
7 android:id="@+id/renderer" 8 android:id="@+id/renderer"
8 - android:layout_width="match_parent"  
9 - android:layout_height="match_parent" />  
10 -</FrameLayout>  
  9 + android:layout_width="0dp"
  10 + android:layout_height="match_parent"
  11 + app:layout_constraintDimensionRatio="1:1"
  12 + app:layout_constraintStart_toStartOf="parent"
  13 + app:layout_constraintTop_toTopOf="parent" />
  14 +</androidx.constraintlayout.widget.ConstraintLayout>
  1 +<?xml version="1.0" encoding="utf-8"?>
  2 +<resources>
  3 + <dimen name="control_size">40dp</dimen>
  4 + <dimen name="control_padding">4dp</dimen>
  5 +</resources>
@@ -6,7 +6,7 @@ @@ -6,7 +6,7 @@
6 <item name="colorPrimary">@color/colorPrimary</item> 6 <item name="colorPrimary">@color/colorPrimary</item>
7 <item name="colorPrimaryDark">@color/colorPrimaryDark</item> 7 <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
8 <item name="colorAccent">@color/colorAccent</item> 8 <item name="colorAccent">@color/colorAccent</item>
9 - <item name="android:windowBackground">#000000</item> 9 + <item name="android:windowBackground">@android:color/black</item>
10 </style> 10 </style>
11 11
12 </resources> 12 </resources>