David Liu

display identities and no video indicator

@@ -117,6 +117,7 @@ @@ -117,6 +117,7 @@
117 </codeStyleSettings> 117 </codeStyleSettings>
118 <codeStyleSettings language="kotlin"> 118 <codeStyleSettings language="kotlin">
119 <option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" /> 119 <option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
  120 + <option name="RIGHT_MARGIN" value="120" />
120 </codeStyleSettings> 121 </codeStyleSettings>
121 </code_scheme> 122 </code_scheme>
122 </component> 123 </component>
@@ -5,6 +5,7 @@ import android.media.AudioManager @@ -5,6 +5,7 @@ import android.media.AudioManager
5 import android.media.projection.MediaProjectionManager 5 import android.media.projection.MediaProjectionManager
6 import android.os.Bundle 6 import android.os.Bundle
7 import android.os.Parcelable 7 import android.os.Parcelable
  8 +import android.view.View
8 import androidx.activity.result.contract.ActivityResultContracts 9 import androidx.activity.result.contract.ActivityResultContracts
9 import androidx.appcompat.app.AppCompatActivity 10 import androidx.appcompat.app.AppCompatActivity
10 import androidx.recyclerview.widget.LinearLayoutManager 11 import androidx.recyclerview.widget.LinearLayoutManager
@@ -72,7 +73,7 @@ class CallActivity : AppCompatActivity() { @@ -72,7 +73,7 @@ class CallActivity : AppCompatActivity() {
72 73
73 // speaker view setup 74 // speaker view setup
74 viewModel.room.take(1).observe(this) { room -> 75 viewModel.room.take(1).observe(this) { room ->
75 - room.initVideoRenderer(binding.speakerView) 76 + room.initVideoRenderer(binding.speakerVideoView)
76 viewModel.activeSpeaker 77 viewModel.activeSpeaker
77 .scan(Pair<Participant?, Participant?>(null, null)) { pair, participant -> 78 .scan(Pair<Participant?, Participant?>(null, null)) { pair, participant ->
78 // old participant is first 79 // old participant is first
@@ -83,13 +84,19 @@ class CallActivity : AppCompatActivity() { @@ -83,13 +84,19 @@ class CallActivity : AppCompatActivity() {
83 oldSpeaker?.videoTracks 84 oldSpeaker?.videoTracks
84 ?.values 85 ?.values
85 ?.forEach { trackPublication -> 86 ?.forEach { trackPublication ->
86 - (trackPublication.track as? VideoTrack)?.removeRenderer(binding.speakerView) 87 + (trackPublication.track as? VideoTrack)?.removeRenderer(binding.speakerVideoView)
87 } 88 }
88 89
  90 + binding.identityText.text = newSpeaker?.identity
89 val videoTrack = newSpeaker?.videoTracks?.values 91 val videoTrack = newSpeaker?.videoTracks?.values
90 ?.firstOrNull() 92 ?.firstOrNull()
91 ?.track as? VideoTrack 93 ?.track as? VideoTrack
92 - videoTrack?.addRenderer(binding.speakerView) 94 + if (videoTrack != null) {
  95 + videoTrack.addRenderer(binding.speakerVideoView)
  96 + binding.speakerVideoView.visibility = View.VISIBLE
  97 + } else {
  98 + binding.speakerVideoView.visibility = View.INVISIBLE
  99 + }
93 } 100 }
94 } 101 }
95 102
@@ -156,7 +163,7 @@ class CallActivity : AppCompatActivity() { @@ -156,7 +163,7 @@ class CallActivity : AppCompatActivity() {
156 super.onDestroy() 163 super.onDestroy()
157 164
158 // Release video views 165 // Release video views
159 - binding.speakerView.release() 166 + binding.speakerVideoView.release()
160 167
161 // Undo audio mode changes 168 // Undo audio mode changes
162 val audioManager = getSystemService(AUDIO_SERVICE) as AudioManager 169 val audioManager = getSystemService(AUDIO_SERVICE) as AudioManager
@@ -14,10 +14,9 @@ import io.livekit.android.sample.databinding.ParticipantItemBinding @@ -14,10 +14,9 @@ import io.livekit.android.sample.databinding.ParticipantItemBinding
14 class ParticipantItem( 14 class ParticipantItem(
15 val room: Room, 15 val room: Room,
16 val participant: Participant 16 val participant: Participant
17 -) :  
18 - BindableItem<ParticipantItemBinding>() { 17 +) : BindableItem<ParticipantItemBinding>() {
19 18
20 - private var videoBound = false 19 + private var boundVideoTrack: VideoTrack? = null
21 20
22 override fun initializeViewBinding(view: View): ParticipantItemBinding { 21 override fun initializeViewBinding(view: View): ParticipantItemBinding {
23 val binding = ParticipantItemBinding.bind(view) 22 val binding = ParticipantItemBinding.bind(view)
@@ -26,33 +25,31 @@ class ParticipantItem( @@ -26,33 +25,31 @@ class ParticipantItem(
26 } 25 }
27 26
28 override fun bind(viewBinding: ParticipantItemBinding, position: Int) { 27 override fun bind(viewBinding: ParticipantItemBinding, position: Int) {
29 - viewBinding.run {  
30 -  
31 - participant.listener = object : ParticipantListener {  
32 - override fun onTrackSubscribed(  
33 - track: Track,  
34 - publication: RemoteTrackPublication,  
35 - participant: RemoteParticipant  
36 - ) {  
37 - if (track !is VideoTrack) return  
38 - if (publication.source == Track.Source.CAMERA) {  
39 - setupVideoIfNeeded(track, viewBinding)  
40 - }  
41 - }  
42 -  
43 - override fun onTrackUnpublished(  
44 - publication: RemoteTrackPublication,  
45 - participant: RemoteParticipant  
46 - ) {  
47 - super.onTrackUnpublished(publication, participant)  
48 - Timber.e { "Track unpublished" } 28 + viewBinding.identityText.text = participant.identity
  29 + participant.listener = object : ParticipantListener {
  30 + override fun onTrackSubscribed(
  31 + track: Track,
  32 + publication: RemoteTrackPublication,
  33 + participant: RemoteParticipant
  34 + ) {
  35 + if (track !is VideoTrack) return
  36 + if (publication.source == Track.Source.CAMERA) {
  37 + setupVideoIfNeeded(track, viewBinding)
49 } 38 }
50 } 39 }
51 - val existingTrack = getVideoTrack()  
52 - if (existingTrack != null) {  
53 - setupVideoIfNeeded(existingTrack, viewBinding) 40 +
  41 + override fun onTrackUnpublished(
  42 + publication: RemoteTrackPublication,
  43 + participant: RemoteParticipant
  44 + ) {
  45 + super.onTrackUnpublished(publication, participant)
  46 + Timber.e { "Track unpublished" }
54 } 47 }
55 } 48 }
  49 + val existingTrack = getVideoTrack()
  50 + if (existingTrack != null) {
  51 + setupVideoIfNeeded(existingTrack, viewBinding)
  52 + }
56 } 53 }
57 54
58 private fun getVideoTrack(): VideoTrack? { 55 private fun getVideoTrack(): VideoTrack? {
@@ -60,18 +57,19 @@ class ParticipantItem( @@ -60,18 +57,19 @@ class ParticipantItem(
60 } 57 }
61 58
62 internal fun setupVideoIfNeeded(videoTrack: VideoTrack, viewBinding: ParticipantItemBinding) { 59 internal fun setupVideoIfNeeded(videoTrack: VideoTrack, viewBinding: ParticipantItemBinding) {
63 - if (videoBound) { 60 + if (boundVideoTrack != null) {
64 return 61 return
65 } 62 }
66 63
67 - videoBound = true 64 + boundVideoTrack = videoTrack
68 Timber.v { "adding renderer to $videoTrack" } 65 Timber.v { "adding renderer to $videoTrack" }
69 videoTrack.addRenderer(viewBinding.renderer) 66 videoTrack.addRenderer(viewBinding.renderer)
70 } 67 }
71 68
72 override fun unbind(viewHolder: GroupieViewHolder<ParticipantItemBinding>) { 69 override fun unbind(viewHolder: GroupieViewHolder<ParticipantItemBinding>) {
73 super.unbind(viewHolder) 70 super.unbind(viewHolder)
74 - videoBound = false 71 + boundVideoTrack?.removeRenderer(viewHolder.binding.renderer)
  72 + boundVideoTrack = null
75 } 73 }
76 74
77 override fun getLayout(): Int = R.layout.participant_item 75 override fun getLayout(): Int = R.layout.participant_item
@@ -4,15 +4,64 @@ @@ -4,15 +4,64 @@
4 android:layout_height="match_parent" 4 android:layout_height="match_parent"
5 android:keepScreenOn="true"> 5 android:keepScreenOn="true">
6 6
7 - <io.livekit.android.renderer.TextureViewRenderer 7 + <FrameLayout
8 android:id="@+id/speaker_view" 8 android:id="@+id/speaker_view"
9 android:layout_width="0dp" 9 android:layout_width="0dp"
10 android:layout_height="0dp" 10 android:layout_height="0dp"
  11 + android:background="@color/no_video_background"
11 app:layout_constraintBottom_toTopOf="@id/audience_row" 12 app:layout_constraintBottom_toTopOf="@id/audience_row"
12 app:layout_constraintTop_toTopOf="parent" 13 app:layout_constraintTop_toTopOf="parent"
13 app:layout_constraintEnd_toEndOf="parent" 14 app:layout_constraintEnd_toEndOf="parent"
  15 + app:layout_constraintStart_toStartOf="parent">
  16 +
  17 + <ImageView
  18 + android:layout_width="120dp"
  19 + android:layout_height="120dp"
  20 + android:layout_gravity="center"
  21 + android:src="@drawable/outline_videocam_off_24"
  22 + app:tint="@color/no_video_participant" />
  23 +
  24 + <io.livekit.android.renderer.TextureViewRenderer
  25 + android:id="@+id/speaker_video_view"
  26 + android:layout_width="0dp"
  27 + android:layout_height="0dp"
  28 + app:layout_constraintBottom_toTopOf="@id/audience_row"
  29 + app:layout_constraintEnd_toEndOf="parent"
  30 + app:layout_constraintStart_toStartOf="parent"
  31 + app:layout_constraintTop_toTopOf="parent" />
  32 + </FrameLayout>
  33 +
  34 + <FrameLayout
  35 + android:id="@+id/identity_bar"
  36 + android:layout_width="0dp"
  37 + android:layout_height="30dp"
  38 + android:background="#80000000"
  39 + app:layout_constraintBottom_toBottomOf="@id/speaker_view"
  40 + app:layout_constraintEnd_toEndOf="parent"
14 app:layout_constraintStart_toStartOf="parent" /> 41 app:layout_constraintStart_toStartOf="parent" />
15 42
  43 + <ImageView
  44 + android:id="@+id/mute_indicator"
  45 + android:layout_width="24dp"
  46 + android:layout_height="24dp"
  47 + android:layout_marginEnd="@dimen/identity_bar_padding"
  48 + android:src="@drawable/outline_mic_off_24"
  49 + app:layout_constraintBottom_toBottomOf="@id/identity_bar"
  50 + app:layout_constraintEnd_toEndOf="@id/identity_bar"
  51 + app:layout_constraintTop_toTopOf="@id/identity_bar"
  52 + app:tint="#BB0000" />
  53 +
  54 + <TextView
  55 + android:id="@+id/identity_text"
  56 + android:layout_width="0dp"
  57 + android:layout_height="wrap_content"
  58 + android:layout_marginStart="@dimen/identity_bar_padding"
  59 + android:ellipsize="end"
  60 + app:layout_constraintBottom_toBottomOf="@id/identity_bar"
  61 + app:layout_constraintEnd_toStartOf="@id/mute_indicator"
  62 + app:layout_constraintStart_toStartOf="@id/identity_bar"
  63 + app:layout_constraintTop_toTopOf="@id/identity_bar" />
  64 +
16 <androidx.recyclerview.widget.RecyclerView 65 <androidx.recyclerview.widget.RecyclerView
17 android:id="@+id/audience_row" 66 android:id="@+id/audience_row"
18 android:layout_width="match_parent" 67 android:layout_width="match_parent"
@@ -2,13 +2,60 @@ @@ -2,13 +2,60 @@
2 <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" 2 <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
3 xmlns:app="http://schemas.android.com/apk/res-auto" 3 xmlns:app="http://schemas.android.com/apk/res-auto"
4 android:layout_width="wrap_content" 4 android:layout_width="wrap_content"
5 - android:layout_height="match_parent"> 5 + android:layout_height="match_parent"
  6 + android:background="@color/no_video_background">
6 7
7 - <io.livekit.android.renderer.TextureViewRenderer  
8 - android:id="@+id/renderer" 8 + <FrameLayout
9 android:layout_width="0dp" 9 android:layout_width="0dp"
10 android:layout_height="match_parent" 10 android:layout_height="match_parent"
11 app:layout_constraintDimensionRatio="1:1" 11 app:layout_constraintDimensionRatio="1:1"
12 app:layout_constraintStart_toStartOf="parent" 12 app:layout_constraintStart_toStartOf="parent"
13 - app:layout_constraintTop_toTopOf="parent" /> 13 + android:background="@color/no_video_background"
  14 + app:layout_constraintTop_toTopOf="parent">
  15 +
  16 + <ImageView
  17 + android:layout_width="60dp"
  18 + android:layout_height="60dp"
  19 + android:layout_gravity="center"
  20 + android:src="@drawable/outline_videocam_off_24"
  21 + app:tint="@color/no_video_participant" />
  22 +
  23 + <io.livekit.android.renderer.TextureViewRenderer
  24 + android:id="@+id/renderer"
  25 + android:layout_width="match_parent"
  26 + android:layout_height="match_parent" />
  27 + </FrameLayout>
  28 +
  29 + <FrameLayout
  30 + android:id="@+id/identity_bar"
  31 + android:layout_width="0dp"
  32 + android:layout_height="30dp"
  33 + android:background="#80000000"
  34 + app:layout_constraintBottom_toBottomOf="parent"
  35 + app:layout_constraintEnd_toEndOf="parent"
  36 + app:layout_constraintStart_toStartOf="parent" />
  37 +
  38 + <ImageView
  39 + android:id="@+id/mute_indicator"
  40 + android:layout_width="24dp"
  41 + android:layout_height="24dp"
  42 + android:layout_marginEnd="@dimen/identity_bar_padding"
  43 + android:src="@drawable/outline_mic_off_24"
  44 + app:layout_constraintBottom_toBottomOf="@id/identity_bar"
  45 + app:layout_constraintEnd_toEndOf="@id/identity_bar"
  46 + app:layout_constraintTop_toTopOf="@id/identity_bar"
  47 + app:tint="#BB0000"
  48 + android:visibility="gone"
  49 + />
  50 +
  51 + <TextView
  52 + android:id="@+id/identity_text"
  53 + android:layout_width="0dp"
  54 + android:layout_height="wrap_content"
  55 + android:layout_marginStart="@dimen/identity_bar_padding"
  56 + android:ellipsize="end"
  57 + app:layout_constraintBottom_toBottomOf="@id/identity_bar"
  58 + app:layout_constraintEnd_toStartOf="@id/mute_indicator"
  59 + app:layout_constraintStart_toStartOf="@id/identity_bar"
  60 + app:layout_constraintTop_toTopOf="@id/identity_bar" />
14 </androidx.constraintlayout.widget.ConstraintLayout> 61 </androidx.constraintlayout.widget.ConstraintLayout>
@@ -3,4 +3,6 @@ @@ -3,4 +3,6 @@
3 <color name="colorPrimary">#007DFF</color> 3 <color name="colorPrimary">#007DFF</color>
4 <color name="colorPrimaryDark">#0058b3</color> 4 <color name="colorPrimaryDark">#0058b3</color>
5 <color name="colorAccent">#66b1ff</color> 5 <color name="colorAccent">#66b1ff</color>
  6 + <color name="no_video_participant">#5A8BFF</color>
  7 + <color name="no_video_background">#00153C</color>
6 </resources> 8 </resources>
@@ -2,4 +2,5 @@ @@ -2,4 +2,5 @@
2 <resources> 2 <resources>
3 <dimen name="control_size">40dp</dimen> 3 <dimen name="control_size">40dp</dimen>
4 <dimen name="control_padding">4dp</dimen> 4 <dimen name="control_padding">4dp</dimen>
  5 + <dimen name="identity_bar_padding">4dp</dimen>
5 </resources> 6 </resources>