David Zhao

also fire events off of the Participant

@@ -3,6 +3,7 @@ package io.livekit.android @@ -3,6 +3,7 @@ package io.livekit.android
3 import android.content.Context 3 import android.content.Context
4 import io.livekit.android.dagger.DaggerLiveKitComponent 4 import io.livekit.android.dagger.DaggerLiveKitComponent
5 import io.livekit.android.room.Room 5 import io.livekit.android.room.Room
  6 +import io.livekit.android.room.RoomListener
6 import io.livekit.android.room.track.LocalAudioTrack 7 import io.livekit.android.room.track.LocalAudioTrack
7 import io.livekit.android.room.track.LocalVideoTrack 8 import io.livekit.android.room.track.LocalVideoTrack
8 import org.webrtc.EglBase 9 import org.webrtc.EglBase
@@ -21,7 +22,7 @@ class LiveKit { @@ -21,7 +22,7 @@ class LiveKit {
21 url: String, 22 url: String,
22 token: String, 23 token: String,
23 options: ConnectOptions, 24 options: ConnectOptions,
24 - listener: Room.Listener? 25 + listener: RoomListener?
25 ): Room { 26 ): Room {
26 27
27 val component = DaggerLiveKitComponent 28 val component = DaggerLiveKitComponent
@@ -26,7 +26,6 @@ import kotlin.coroutines.suspendCoroutine @@ -26,7 +26,6 @@ import kotlin.coroutines.suspendCoroutine
26 class RTCEngine 26 class RTCEngine
27 @Inject 27 @Inject
28 constructor( 28 constructor(
29 - private val appContext: Context,  
30 val client: RTCClient, 29 val client: RTCClient,
31 pctFactory: PeerConnectionTransport.Factory, 30 pctFactory: PeerConnectionTransport.Factory,
32 @Named(InjectionNames.DISPATCHER_IO) ioDispatcher: CoroutineDispatcher, 31 @Named(InjectionNames.DISPATCHER_IO) ioDispatcher: CoroutineDispatcher,
@@ -190,6 +189,7 @@ constructor( @@ -190,6 +189,7 @@ constructor(
190 override fun onAnswer(sessionDescription: SessionDescription) { 189 override fun onAnswer(sessionDescription: SessionDescription) {
191 Timber.v { "received server answer: ${sessionDescription.type}, ${publisher.peerConnection.signalingState()}" } 190 Timber.v { "received server answer: ${sessionDescription.type}, ${publisher.peerConnection.signalingState()}" }
192 coroutineScope.launch { 191 coroutineScope.launch {
  192 + Timber.i { sessionDescription.toString() }
193 when (val outcome = publisher.peerConnection.setRemoteDescription(sessionDescription)) { 193 when (val outcome = publisher.peerConnection.setRemoteDescription(sessionDescription)) {
194 is Either.Left -> { 194 is Either.Left -> {
195 if (!rtcConnected) { 195 if (!rtcConnected) {
1 package io.livekit.android.room 1 package io.livekit.android.room
2 2
3 import com.github.ajalt.timberkt.Timber 3 import com.github.ajalt.timberkt.Timber
4 -import com.vdurmont.semver4j.Semver  
5 import dagger.assisted.Assisted 4 import dagger.assisted.Assisted
6 import dagger.assisted.AssistedFactory 5 import dagger.assisted.AssistedFactory
7 import dagger.assisted.AssistedInject 6 import dagger.assisted.AssistedInject
8 import io.livekit.android.ConnectOptions 7 import io.livekit.android.ConnectOptions
9 import io.livekit.android.room.participant.LocalParticipant 8 import io.livekit.android.room.participant.LocalParticipant
10 import io.livekit.android.room.participant.Participant 9 import io.livekit.android.room.participant.Participant
  10 +import io.livekit.android.room.participant.ParticipantListener
11 import io.livekit.android.room.participant.RemoteParticipant 11 import io.livekit.android.room.participant.RemoteParticipant
12 import io.livekit.android.room.track.DataTrack 12 import io.livekit.android.room.track.DataTrack
13 import io.livekit.android.room.track.Track 13 import io.livekit.android.room.track.Track
@@ -27,7 +27,7 @@ constructor( @@ -27,7 +27,7 @@ constructor(
27 @Assisted private val connectOptions: ConnectOptions, 27 @Assisted private val connectOptions: ConnectOptions,
28 private val engine: RTCEngine, 28 private val engine: RTCEngine,
29 private val eglBase: EglBase, 29 private val eglBase: EglBase,
30 -) : RTCEngine.Listener, RemoteParticipant.Listener { 30 +) : RTCEngine.Listener, ParticipantListener {
31 init { 31 init {
32 engine.listener = this 32 engine.listener = this
33 } 33 }
@@ -41,7 +41,7 @@ constructor( @@ -41,7 +41,7 @@ constructor(
41 41
42 inline class Sid(val sid: String) 42 inline class Sid(val sid: String)
43 43
44 - var listener: Listener? = null 44 + var listener: RoomListener? = null
45 45
46 var sid: Sid? = null 46 var sid: Sid? = null
47 private set 47 private set
@@ -95,7 +95,7 @@ constructor( @@ -95,7 +95,7 @@ constructor(
95 } else { 95 } else {
96 RemoteParticipant(sid, null) 96 RemoteParticipant(sid, null)
97 } 97 }
98 - participant.listener = this 98 + participant.internalListener = this
99 mutableRemoteParticipants[sid] = participant 99 mutableRemoteParticipants[sid] = participant
100 return participant 100 return participant
101 } 101 }
@@ -108,13 +108,15 @@ constructor( @@ -108,13 +108,15 @@ constructor(
108 val speakerSid = speakerInfo.sid!! 108 val speakerSid = speakerInfo.sid!!
109 seenSids.add(speakerSid) 109 seenSids.add(speakerSid)
110 110
111 - if (speakerSid == localParticipant?.sid) { 111 + if (speakerSid == localParticipant.sid) {
112 localParticipant.audioLevel = speakerInfo.level 112 localParticipant.audioLevel = speakerInfo.level
  113 + localParticipant.isSpeaking = true
113 speakers.add(localParticipant) 114 speakers.add(localParticipant)
114 } else { 115 } else {
115 val participant = remoteParticipants[speakerSid] 116 val participant = remoteParticipants[speakerSid]
116 if (participant != null) { 117 if (participant != null) {
117 participant.audioLevel = speakerInfo.level 118 participant.audioLevel = speakerInfo.level
  119 + participant.isSpeaking = true
118 speakers.add(participant) 120 speakers.add(participant)
119 } 121 }
120 } 122 }
@@ -122,10 +124,14 @@ constructor( @@ -122,10 +124,14 @@ constructor(
122 124
123 if (!seenSids.contains(localParticipant.sid)) { 125 if (!seenSids.contains(localParticipant.sid)) {
124 localParticipant.audioLevel = 0.0f 126 localParticipant.audioLevel = 0.0f
  127 + localParticipant.isSpeaking = false
125 } 128 }
126 remoteParticipants.values 129 remoteParticipants.values
127 .filterNot { seenSids.contains(it.sid) } 130 .filterNot { seenSids.contains(it.sid) }
128 - .forEach { it.audioLevel = 0.0f } 131 + .forEach {
  132 + it.audioLevel = 0.0f
  133 + it.isSpeaking = false
  134 + }
129 135
130 mutableActiveSpeakers.clear() 136 mutableActiveSpeakers.clear()
131 mutableActiveSpeakers.addAll(speakers) 137 mutableActiveSpeakers.addAll(speakers)
@@ -296,86 +302,86 @@ constructor( @@ -296,86 +302,86 @@ constructor(
296 viewRenderer.setScalingType(RendererCommon.ScalingType.SCALE_ASPECT_FIT) 302 viewRenderer.setScalingType(RendererCommon.ScalingType.SCALE_ASPECT_FIT)
297 viewRenderer.setEnableHardwareScaler(false /* enabled */); 303 viewRenderer.setEnableHardwareScaler(false /* enabled */);
298 } 304 }
  305 +}
  306 +
  307 +/**
  308 + * Room Listener, this class provides callbacks that clients should override.
  309 + *
  310 + */
  311 +interface RoomListener {
  312 + /**
  313 + * Disconnected from room
  314 + */
  315 + fun onDisconnect(room: Room, error: Exception?) {}
  316 +
  317 + /**
  318 + * When a [RemoteParticipant] joins after the local participant. It will not emit events
  319 + * for participants that are already in the room
  320 + */
  321 + fun onParticipantConnected(room: Room, participant: RemoteParticipant) {}
  322 +
  323 + /**
  324 + * When a [RemoteParticipant] leaves after the local participant has joined.
  325 + */
  326 + fun onParticipantDisconnected(room: Room, participant: RemoteParticipant) {}
299 327
300 /** 328 /**
301 - * Room Listener, this class provides callbacks that clients should override.  
302 - * 329 + * Could not connect to the room
303 */ 330 */
304 - interface Listener {  
305 - /**  
306 - * Disconnected from room  
307 - */  
308 - fun onDisconnect(room: Room, error: Exception?) {}  
309 -  
310 - /**  
311 - * When a [RemoteParticipant] joins after the local participant. It will not emit events  
312 - * for participants that are already in the room  
313 - */  
314 - fun onParticipantConnected(room: Room, participant: RemoteParticipant) {}  
315 -  
316 - /**  
317 - * When a [RemoteParticipant] leaves after the local participant has joined.  
318 - */  
319 - fun onParticipantDisconnected(room: Room, participant: RemoteParticipant) {}  
320 -  
321 - /**  
322 - * Could not connect to the room  
323 - */  
324 - fun onFailedToConnect(room: Room, error: Exception) {} 331 + fun onFailedToConnect(room: Room, error: Exception) {}
325 // fun onReconnecting(room: Room, error: Exception) {} 332 // fun onReconnecting(room: Room, error: Exception) {}
326 // fun onReconnect(room: Room) {} 333 // fun onReconnect(room: Room) {}
327 334
328 - /**  
329 - * Active speakers changed. List of speakers are ordered by their audio level. loudest  
330 - * speakers first. This will include the [LocalParticipant] too.  
331 - */  
332 - fun onActiveSpeakersChanged(speakers: List<Participant>, room: Room) {}  
333 -  
334 - // Participant callbacks  
335 - /**  
336 - * Participant metadata is a simple way for app-specific state to be pushed to all users.  
337 - * When RoomService.UpdateParticipantMetadata is called to change a participant's state,  
338 - * this event will be fired for all clients in the room.  
339 - */  
340 - fun onMetadataChanged(Participant: Participant, prevMetadata: String?, room: Room) {}  
341 -  
342 - /**  
343 - * When a new track is published to room after the local participant has joined. It will  
344 - * not fire for tracks that are already published  
345 - */  
346 - fun onTrackPublished(publication: TrackPublication, participant: RemoteParticipant, room: Room) {}  
347 -  
348 - /**  
349 - * A [RemoteParticipant] has unpublished a track  
350 - */  
351 - fun onTrackUnpublished(publication: TrackPublication, participant: RemoteParticipant, room: Room) {}  
352 -  
353 - /**  
354 - * The [LocalParticipant] has subscribed to a new track. This event will always fire as  
355 - * long as new tracks are ready for use.  
356 - */  
357 - fun onTrackSubscribed(track: Track, publication: TrackPublication, participant: RemoteParticipant, room: Room) {}  
358 -  
359 - /**  
360 - * Could not subscribe to a track  
361 - */  
362 - fun onTrackSubscriptionFailed(sid: String, exception: Exception, participant: RemoteParticipant, room: Room) {}  
363 -  
364 - /**  
365 - * A subscribed track is no longer available. Clients should listen to this event and ensure  
366 - * the track removes all renderers  
367 - */  
368 - fun onTrackUnsubscribed(track: Track, publications: TrackPublication, participant: RemoteParticipant, room: Room) {}  
369 -  
370 - /**  
371 - * Message received over a [DataTrack]  
372 - */  
373 - fun onDataReceived(data: ByteBuffer, dataTrack: DataTrack, participant: RemoteParticipant, room: Room) {}  
374 - } 335 + /**
  336 + * Active speakers changed. List of speakers are ordered by their audio level. loudest
  337 + * speakers first. This will include the [LocalParticipant] too.
  338 + */
  339 + fun onActiveSpeakersChanged(speakers: List<Participant>, room: Room) {}
  340 +
  341 + // Participant callbacks
  342 + /**
  343 + * Participant metadata is a simple way for app-specific state to be pushed to all users.
  344 + * When RoomService.UpdateParticipantMetadata is called to change a participant's state,
  345 + * this event will be fired for all clients in the room.
  346 + */
  347 + fun onMetadataChanged(participant: Participant, prevMetadata: String?, room: Room) {}
  348 +
  349 + /**
  350 + * When a new track is published to room after the local participant has joined. It will
  351 + * not fire for tracks that are already published
  352 + */
  353 + fun onTrackPublished(publication: TrackPublication, participant: RemoteParticipant, room: Room) {}
  354 +
  355 + /**
  356 + * A [RemoteParticipant] has unpublished a track
  357 + */
  358 + fun onTrackUnpublished(publication: TrackPublication, participant: RemoteParticipant, room: Room) {}
  359 +
  360 + /**
  361 + * The [LocalParticipant] has subscribed to a new track. This event will always fire as
  362 + * long as new tracks are ready for use.
  363 + */
  364 + fun onTrackSubscribed(track: Track, publication: TrackPublication, participant: RemoteParticipant, room: Room) {}
  365 +
  366 + /**
  367 + * Could not subscribe to a track
  368 + */
  369 + fun onTrackSubscriptionFailed(sid: String, exception: Exception, participant: RemoteParticipant, room: Room) {}
  370 +
  371 + /**
  372 + * A subscribed track is no longer available. Clients should listen to this event and ensure
  373 + * the track removes all renderers
  374 + */
  375 + fun onTrackUnsubscribed(track: Track, publications: TrackPublication, participant: RemoteParticipant, room: Room) {}
  376 +
  377 + /**
  378 + * Message received over a [DataTrack]
  379 + */
  380 + fun onDataReceived(data: ByteBuffer, dataTrack: DataTrack, participant: RemoteParticipant, room: Room) {}
375 } 381 }
376 382
377 sealed class RoomException(message: String? = null, cause: Throwable? = null) : 383 sealed class RoomException(message: String? = null, cause: Throwable? = null) :
378 Exception(message, cause) { 384 Exception(message, cause) {
379 class ConnectException(message: String? = null, cause: Throwable? = null) : 385 class ConnectException(message: String? = null, cause: Throwable? = null) :
380 RoomException(message, cause) 386 RoomException(message, cause)
381 -}  
  387 +}
@@ -15,15 +15,9 @@ class LocalParticipant(info: LivekitModels.ParticipantInfo, private val engine: @@ -15,15 +15,9 @@ class LocalParticipant(info: LivekitModels.ParticipantInfo, private val engine:
15 updateFromInfo(info) 15 updateFromInfo(info)
16 } 16 }
17 17
18 - val localTrackPublications 18 + private val localTrackPublications
19 get() = tracks.values.toList() 19 get() = tracks.values.toList()
20 20
21 - var listener: Listener? = null  
22 - set(v) {  
23 - field = v  
24 - participantListener = v  
25 - }  
26 -  
27 suspend fun publishAudioTrack( 21 suspend fun publishAudioTrack(
28 track: LocalAudioTrack, 22 track: LocalAudioTrack,
29 publishListener: PublishListener? = null 23 publishListener: PublishListener? = null
@@ -2,6 +2,7 @@ package io.livekit.android.room.participant @@ -2,6 +2,7 @@ package io.livekit.android.room.participant
2 2
3 import io.livekit.android.room.track.* 3 import io.livekit.android.room.track.*
4 import livekit.LivekitModels 4 import livekit.LivekitModels
  5 +import java.nio.ByteBuffer
5 6
6 open class Participant(var sid: String, identity: String? = null) { 7 open class Participant(var sid: String, identity: String? = null) {
7 var participantInfo: LivekitModels.ParticipantInfo? = null 8 var participantInfo: LivekitModels.ParticipantInfo? = null
@@ -10,8 +11,27 @@ open class Participant(var sid: String, identity: String? = null) { @@ -10,8 +11,27 @@ open class Participant(var sid: String, identity: String? = null) {
10 internal set 11 internal set
11 var audioLevel: Float = 0f 12 var audioLevel: Float = 0f
12 internal set 13 internal set
  14 + var isSpeaking: Boolean = false
  15 + internal set(v) {
  16 + val changed = v == field
  17 + field = v
  18 + if (changed) {
  19 + listener?.onSpeakingChanged(this)
  20 + internalListener?.onSpeakingChanged(this)
  21 + }
  22 + }
13 var metadata: String? = null 23 var metadata: String? = null
14 - var participantListener: Listener? = null 24 +
  25 + /**
  26 + * Listener for when participant properties change
  27 + */
  28 + var listener: ParticipantListener? = null
  29 +
  30 + /**
  31 + * @suppress
  32 + */
  33 + internal var internalListener: ParticipantListener? = null
  34 +
15 val hasInfo 35 val hasInfo
16 get() = participantInfo != null 36 get() = participantInfo != null
17 37
@@ -49,7 +69,8 @@ open class Participant(var sid: String, identity: String? = null) { @@ -49,7 +69,8 @@ open class Participant(var sid: String, identity: String? = null) {
49 metadata = info.metadata 69 metadata = info.metadata
50 70
51 if (prevMetadata != metadata) { 71 if (prevMetadata != metadata) {
52 - participantListener?.onMetadataChanged(this, prevMetadata) 72 + listener?.onMetadataChanged(this, prevMetadata)
  73 + internalListener?.onMetadataChanged(this, prevMetadata)
53 } 74 }
54 } 75 }
55 76
@@ -67,8 +88,48 @@ open class Participant(var sid: String, identity: String? = null) { @@ -67,8 +88,48 @@ open class Participant(var sid: String, identity: String? = null) {
67 override fun hashCode(): Int { 88 override fun hashCode(): Int {
68 return sid.hashCode() 89 return sid.hashCode()
69 } 90 }
  91 +}
70 92
71 - interface Listener {  
72 - fun onMetadataChanged(participant: Participant, prevMetadata: String?) {} 93 +
  94 +interface ParticipantListener {
  95 + /**
  96 + * When a participant's metadata is updated, fired for all participants
  97 + */
  98 + fun onMetadataChanged(participant: Participant, prevMetadata: String?) {}
  99 +
  100 + /**
  101 + * Fired when the current participant's isSpeaking property changes. (including LocalParticipant)
  102 + */
  103 + fun onSpeakingChanged(participant: Participant) {}
  104 +
  105 + fun onTrackPublished(publication: TrackPublication, participant: RemoteParticipant) {}
  106 + fun onTrackUnpublished(publication: TrackPublication, participant: RemoteParticipant) {}
  107 +
  108 + fun onEnable(publication: TrackPublication, participant: RemoteParticipant) {}
  109 + fun onDisable(publication: TrackPublication, participant: RemoteParticipant) {}
  110 +
  111 + fun onTrackSubscribed(track: Track, publication: TrackPublication, participant: RemoteParticipant) {}
  112 + fun onTrackSubscriptionFailed(
  113 + sid: String,
  114 + exception: Exception,
  115 + participant: RemoteParticipant
  116 + ) {
73 } 117 }
  118 +
  119 + fun onTrackUnsubscribed(
  120 + track: Track,
  121 + publication: TrackPublication,
  122 + participant: RemoteParticipant
  123 + ) {
  124 + }
  125 +
  126 + fun onDataReceived(
  127 + data: ByteBuffer,
  128 + dataTrack: DataTrack,
  129 + participant: RemoteParticipant
  130 + ) {
  131 + }
  132 +
  133 + fun switchedOffVideo(track: VideoTrack, publication: TrackPublication, participant: RemoteParticipant) {}
  134 + fun switchedOnVideo(track: VideoTrack, publication: TrackPublication, participant: RemoteParticipant) {}
74 } 135 }
@@ -23,12 +23,6 @@ class RemoteParticipant( @@ -23,12 +23,6 @@ class RemoteParticipant(
23 updateFromInfo(info) 23 updateFromInfo(info)
24 } 24 }
25 25
26 - var listener: Listener? = null  
27 - set(v) {  
28 - field = v  
29 - participantListener = v  
30 - }  
31 -  
32 private val coroutineScope = CloseableCoroutineScope(SupervisorJob()) 26 private val coroutineScope = CloseableCoroutineScope(SupervisorJob())
33 27
34 fun getTrackPublication(sid: String): TrackPublication? = tracks[sid] 28 fun getTrackPublication(sid: String): TrackPublication? = tracks[sid]
@@ -61,6 +55,7 @@ class RemoteParticipant( @@ -61,6 +55,7 @@ class RemoteParticipant(
61 55
62 if (hadInfo) { 56 if (hadInfo) {
63 for (publication in newTrackPublications.values) { 57 for (publication in newTrackPublications.values) {
  58 + internalListener?.onTrackPublished(publication, this)
64 listener?.onTrackPublished(publication, this) 59 listener?.onTrackPublished(publication, this)
65 } 60 }
66 } 61 }
@@ -89,6 +84,7 @@ class RemoteParticipant( @@ -89,6 +84,7 @@ class RemoteParticipant(
89 val exception = TrackException.InvalidTrackStateException(message) 84 val exception = TrackException.InvalidTrackStateException(message)
90 Timber.e { "remote participant ${this.sid} --- $message" } 85 Timber.e { "remote participant ${this.sid} --- $message" }
91 86
  87 + internalListener?.onTrackSubscriptionFailed(sid, exception, this)
92 listener?.onTrackSubscriptionFailed(sid, exception, this) 88 listener?.onTrackSubscriptionFailed(sid, exception, this)
93 } else { 89 } else {
94 coroutineScope.launch { 90 coroutineScope.launch {
@@ -106,6 +102,7 @@ class RemoteParticipant( @@ -106,6 +102,7 @@ class RemoteParticipant(
106 102
107 // TODO: how does mediatrack send ended event? 103 // TODO: how does mediatrack send ended event?
108 104
  105 + internalListener?.onTrackSubscribed(track, publication, this)
109 listener?.onTrackSubscribed(track, publication, this) 106 listener?.onTrackSubscribed(track, publication, this)
110 } 107 }
111 108
@@ -125,6 +122,7 @@ class RemoteParticipant( @@ -125,6 +122,7 @@ class RemoteParticipant(
125 publication = TrackPublication(info = trackInfo) 122 publication = TrackPublication(info = trackInfo)
126 addTrackPublication(publication) 123 addTrackPublication(publication)
127 if (hasInfo) { 124 if (hasInfo) {
  125 + internalListener?.onTrackPublished(publication, this)
128 listener?.onTrackPublished(publication, this) 126 listener?.onTrackPublished(publication, this)
129 } 127 }
130 } 128 }
@@ -137,15 +135,18 @@ class RemoteParticipant( @@ -137,15 +135,18 @@ class RemoteParticipant(
137 val newState = dataChannel.state() 135 val newState = dataChannel.state()
138 if (newState == DataChannel.State.CLOSED) { 136 if (newState == DataChannel.State.CLOSED) {
139 publication.track = null 137 publication.track = null
  138 + internalListener?.onTrackUnsubscribed(track, publication, this@RemoteParticipant)
140 listener?.onTrackUnsubscribed(track, publication, this@RemoteParticipant) 139 listener?.onTrackUnsubscribed(track, publication, this@RemoteParticipant)
141 } 140 }
142 } 141 }
143 142
144 override fun onMessage(buffer: DataChannel.Buffer) { 143 override fun onMessage(buffer: DataChannel.Buffer) {
  144 + internalListener?.onDataReceived(buffer.data, track, this@RemoteParticipant)
145 listener?.onDataReceived(buffer.data, track, this@RemoteParticipant) 145 listener?.onDataReceived(buffer.data, track, this@RemoteParticipant)
146 } 146 }
147 }) 147 })
148 - listener?.onTrackSubscribed(track, publication, participant = this) 148 + internalListener?.onTrackSubscribed(track, publication, participant = this)
  149 + listener?.onTrackSubscribed(track, publication, this)
149 } 150 }
150 151
151 fun unpublishTrack(trackSid: String, sendUnpublish: Boolean = false) { 152 fun unpublishTrack(trackSid: String, sendUnpublish: Boolean = false) {
@@ -160,9 +161,11 @@ class RemoteParticipant( @@ -160,9 +161,11 @@ class RemoteParticipant(
160 val track = publication.track 161 val track = publication.track
161 if (track != null) { 162 if (track != null) {
162 track.stop() 163 track.stop()
  164 + internalListener?.onTrackUnsubscribed(track, publication, this)
163 listener?.onTrackUnsubscribed(track, publication, this) 165 listener?.onTrackUnsubscribed(track, publication, this)
164 } 166 }
165 if (sendUnpublish) { 167 if (sendUnpublish) {
  168 + internalListener?.onTrackUnpublished(publication, this)
166 listener?.onTrackUnpublished(publication, this) 169 listener?.onTrackUnpublished(publication, this)
167 } 170 }
168 } 171 }
@@ -171,38 +174,4 @@ class RemoteParticipant( @@ -171,38 +174,4 @@ class RemoteParticipant(
171 private const val KIND_AUDIO = "audio" 174 private const val KIND_AUDIO = "audio"
172 private const val KIND_VIDEO = "video" 175 private const val KIND_VIDEO = "video"
173 } 176 }
174 -  
175 - interface Listener: Participant.Listener {  
176 - fun onTrackPublished(publication: TrackPublication, participant: RemoteParticipant) {}  
177 - fun onTrackUnpublished(publication: TrackPublication, participant: RemoteParticipant) {}  
178 -  
179 - fun onEnable(publication: TrackPublication, participant: RemoteParticipant) {}  
180 - fun onDisable(publication: TrackPublication, participant: RemoteParticipant) {}  
181 -  
182 - fun onTrackSubscribed(track: Track, publication: TrackPublication, participant: RemoteParticipant) {}  
183 - fun onTrackSubscriptionFailed(  
184 - sid: String,  
185 - exception: Exception,  
186 - participant: RemoteParticipant  
187 - ) {  
188 - }  
189 -  
190 - fun onTrackUnsubscribed(  
191 - track: Track,  
192 - publication: TrackPublication,  
193 - participant: RemoteParticipant  
194 - ) {  
195 - }  
196 -  
197 - fun onDataReceived(  
198 - data: ByteBuffer,  
199 - dataTrack: DataTrack,  
200 - participant: RemoteParticipant  
201 - ) {  
202 - }  
203 -  
204 - fun switchedOffVideo(track: VideoTrack, publication: TrackPublication, participant: RemoteParticipant) {}  
205 - fun switchedOnVideo(track: VideoTrack, publication: TrackPublication, participant: RemoteParticipant) {}  
206 - }  
207 -  
208 -}  
  177 +}
@@ -41,7 +41,6 @@ class CallActivity : AppCompatActivity() { @@ -41,7 +41,6 @@ class CallActivity : AppCompatActivity() {
41 viewModel.remoteParticipants 41 viewModel.remoteParticipants
42 ) { room, participants -> room to participants } 42 ) { room, participants -> room to participants }
43 .observe(this) { 43 .observe(this) {
44 -  
45 tabLayoutMediator?.detach() 44 tabLayoutMediator?.detach()
46 tabLayoutMediator = null 45 tabLayoutMediator = null
47 46
@@ -58,13 +57,10 @@ class CallActivity : AppCompatActivity() { @@ -58,13 +57,10 @@ class CallActivity : AppCompatActivity() {
58 57
59 viewModel.room.observe(this) { room -> 58 viewModel.room.observe(this) { room ->
60 room.initVideoRenderer(binding.pipVideoView) 59 room.initVideoRenderer(binding.pipVideoView)
61 - val localParticipant = room.localParticipant  
62 - if (localParticipant != null) {  
63 - val videoTrack = localParticipant.videoTracks.values  
64 - .firstOrNull()  
65 - ?.track as? LocalVideoTrack  
66 - videoTrack?.addRenderer(binding.pipVideoView)  
67 - } 60 + val videoTrack = room.localParticipant.videoTracks.values
  61 + .firstOrNull()
  62 + ?.track as? LocalVideoTrack
  63 + videoTrack?.addRenderer(binding.pipVideoView)
68 } 64 }
69 val audioManager = getSystemService(AUDIO_SERVICE) as AudioManager 65 val audioManager = getSystemService(AUDIO_SERVICE) as AudioManager
70 with(audioManager) { 66 with(audioManager) {
@@ -5,9 +5,11 @@ import androidx.lifecycle.AndroidViewModel @@ -5,9 +5,11 @@ import androidx.lifecycle.AndroidViewModel
5 import androidx.lifecycle.LiveData 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 io.livekit.android.ConnectOptions 9 import io.livekit.android.ConnectOptions
9 import io.livekit.android.LiveKit 10 import io.livekit.android.LiveKit
10 import io.livekit.android.room.Room 11 import io.livekit.android.room.Room
  12 +import io.livekit.android.room.RoomListener
11 import io.livekit.android.room.participant.Participant 13 import io.livekit.android.room.participant.Participant
12 import io.livekit.android.room.participant.RemoteParticipant 14 import io.livekit.android.room.participant.RemoteParticipant
13 import kotlinx.coroutines.launch 15 import kotlinx.coroutines.launch
@@ -16,57 +18,34 @@ class CallViewModel( @@ -16,57 +18,34 @@ class CallViewModel(
16 val url: String, 18 val url: String,
17 val token: String, 19 val token: String,
18 application: Application 20 application: Application
19 -) : AndroidViewModel(application) {  
20 -  
21 - 21 +) : AndroidViewModel(application), RoomListener {
22 private val mutableRoom = MutableLiveData<Room>() 22 private val mutableRoom = MutableLiveData<Room>()
23 val room: LiveData<Room> = mutableRoom 23 val room: LiveData<Room> = mutableRoom
24 private val mutableRemoteParticipants = MutableLiveData<List<RemoteParticipant>>() 24 private val mutableRemoteParticipants = MutableLiveData<List<RemoteParticipant>>()
25 val remoteParticipants: LiveData<List<RemoteParticipant>> = mutableRemoteParticipants 25 val remoteParticipants: LiveData<List<RemoteParticipant>> = mutableRemoteParticipants
  26 + private val participants = HashMap<String, LiveData<RemoteParticipant>>()
26 27
27 init { 28 init {
28 -  
29 viewModelScope.launch { 29 viewModelScope.launch {
30 -  
31 - mutableRoom.value = LiveKit.connect( 30 + val room = LiveKit.connect(
32 application, 31 application,
33 url, 32 url,
34 token, 33 token,
35 ConnectOptions(false), 34 ConnectOptions(false),
36 - object : Room.Listener {  
37 - override fun onDisconnect(room: Room, error: Exception?) {  
38 - }  
39 -  
40 - override fun onParticipantConnected(  
41 - room: Room,  
42 - participant: RemoteParticipant  
43 - ) {  
44 - updateParticipants(room)  
45 - }  
46 -  
47 - override fun onParticipantDisconnected(  
48 - room: Room,  
49 - participant: RemoteParticipant  
50 - ) {  
51 - updateParticipants(room)  
52 - }  
53 -  
54 - override fun onFailedToConnect(room: Room, error: Exception) {  
55 - }  
56 -  
57 - override fun onActiveSpeakersChanged(speakers: List<Participant>, room: Room) {  
58 - }  
59 -  
60 - override fun onMetadataChanged(Participant: Participant, prevMetadata: String?, room: Room) {  
61 -  
62 - }  
63 - } 35 + this@CallViewModel
64 ) 36 )
65 - updateParticipants(mutableRoom.value!!) 37 + updateParticipants(room)
  38 + for (p in room.remoteParticipants.values) {
  39 + if (p.identity == null) {
  40 + continue
  41 + }
  42 + participants[p.identity!!] = MutableLiveData(p)
  43 + }
  44 + mutableRoom.value = room
66 } 45 }
67 } 46 }
68 47
69 - fun updateParticipants(room: Room) { 48 + private fun updateParticipants(room: Room) {
70 mutableRemoteParticipants.postValue( 49 mutableRemoteParticipants.postValue(
71 room.remoteParticipants 50 room.remoteParticipants
72 .keys 51 .keys
@@ -75,9 +54,43 @@ class CallViewModel( @@ -75,9 +54,43 @@ class CallViewModel(
75 ) 54 )
76 } 55 }
77 56
  57 + fun getParticipantObservable(identity: String): LiveData<RemoteParticipant>? {
  58 + return participants[identity]
  59 + }
  60 +
78 override fun onCleared() { 61 override fun onCleared() {
79 super.onCleared() 62 super.onCleared()
80 mutableRoom.value?.disconnect() 63 mutableRoom.value?.disconnect()
81 mutableRoom.value = null 64 mutableRoom.value = null
82 } 65 }
  66 +
  67 + override fun onDisconnect(room: Room, error: Exception?) {
  68 + }
  69 +
  70 + override fun onParticipantConnected(
  71 + room: Room,
  72 + participant: RemoteParticipant
  73 + ) {
  74 + updateParticipants(room)
  75 + participants[participant.identity!!] = MutableLiveData(participant)
  76 +
  77 + }
  78 +
  79 + override fun onParticipantDisconnected(
  80 + room: Room,
  81 + participant: RemoteParticipant
  82 + ) {
  83 + updateParticipants(room)
  84 + participants.remove(participant.identity!!)
  85 + }
  86 +
  87 + override fun onFailedToConnect(room: Room, error: Exception) {
  88 + }
  89 +
  90 + override fun onActiveSpeakersChanged(speakers: List<Participant>, room: Room) {
  91 + }
  92 +
  93 + override fun onMetadataChanged(participant: Participant, prevMetadata: String?, room: Room) {
  94 + Timber.i { "Participant metadata changed: ${participant.identity}" }
  95 + }
83 } 96 }
1 package io.livekit.android.sample 1 package io.livekit.android.sample
2 2
3 -import android.provider.MediaStore  
4 import android.view.View 3 import android.view.View
5 import com.github.ajalt.timberkt.Timber 4 import com.github.ajalt.timberkt.Timber
6 import com.xwray.groupie.viewbinding.BindableItem 5 import com.xwray.groupie.viewbinding.BindableItem
7 import com.xwray.groupie.viewbinding.GroupieViewHolder 6 import com.xwray.groupie.viewbinding.GroupieViewHolder
8 import io.livekit.android.room.Room 7 import io.livekit.android.room.Room
  8 +import io.livekit.android.room.participant.ParticipantListener
9 import io.livekit.android.room.participant.RemoteParticipant 9 import io.livekit.android.room.participant.RemoteParticipant
10 import io.livekit.android.room.track.* 10 import io.livekit.android.room.track.*
11 import io.livekit.android.sample.databinding.ParticipantItemBinding 11 import io.livekit.android.sample.databinding.ParticipantItemBinding
@@ -27,7 +27,7 @@ class ParticipantItem( @@ -27,7 +27,7 @@ class ParticipantItem(
27 override fun bind(viewBinding: ParticipantItemBinding, position: Int) { 27 override fun bind(viewBinding: ParticipantItemBinding, position: Int) {
28 viewBinding.run { 28 viewBinding.run {
29 29
30 - remoteParticipant.listener = object : RemoteParticipant.Listener { 30 + remoteParticipant.listener = object : ParticipantListener {
31 override fun onTrackSubscribed( 31 override fun onTrackSubscribed(
32 track: Track, 32 track: Track,
33 publication: TrackPublication, 33 publication: TrackPublication,
@@ -51,7 +51,7 @@ class ParticipantItem( @@ -51,7 +51,7 @@ class ParticipantItem(
51 .firstOrNull()?.track as? VideoTrack 51 .firstOrNull()?.track as? VideoTrack
52 } 52 }
53 53
54 - private fun setupVideoIfNeeded(videoTrack: VideoTrack, viewBinding: ParticipantItemBinding) { 54 + internal fun setupVideoIfNeeded(videoTrack: VideoTrack, viewBinding: ParticipantItemBinding) {
55 if (videoBound) { 55 if (videoBound) {
56 return 56 return
57 } 57 }