Room.kt
8.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
package io.livekit.android.room
import com.github.ajalt.timberkt.Timber
import com.vdurmont.semver4j.Semver
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import io.livekit.android.ConnectOptions
import io.livekit.android.room.participant.LocalParticipant
import io.livekit.android.room.participant.Participant
import io.livekit.android.room.participant.RemoteParticipant
import io.livekit.android.room.track.Track
import io.livekit.android.room.util.unpackedTrackLabel
import livekit.Model
import livekit.Rtc
import org.webrtc.*
class Room
@AssistedInject
constructor(
@Assisted private val connectOptions: ConnectOptions,
private val engine: RTCEngine,
private val eglBase: EglBase,
) : RTCEngine.Listener {
init {
engine.listener = this
}
enum class State {
CONNECTING,
CONNECTED,
DISCONNECTED,
RECONNECTING;
}
inline class Sid(val sid: String)
var listener: Listener? = null
var sid: Sid? = null
private set
var name: String? = null
private set
var state: State = State.DISCONNECTED
private set
var localParticipant: LocalParticipant? = null
private set
private val mutableRemoteParticipants = mutableMapOf<Participant.Sid, RemoteParticipant>()
val remoteParticipants: Map<Participant.Sid, RemoteParticipant>
get() = mutableRemoteParticipants
private val mutableActiveSpeakers = mutableListOf<Participant>()
val activeSpeakers: List<Participant>
get() = mutableActiveSpeakers
suspend fun connect(url: String, token: String, isSecure: Boolean) {
if (localParticipant != null) {
Timber.d { "Attempting to connect to room when already connected." }
return
}
engine.join(url, token, isSecure)
}
fun disconnect() {
engine.close()
state = State.DISCONNECTED
listener?.onDisconnect(this, null)
}
private fun handleParticipantDisconnect(sid: Participant.Sid, participant: RemoteParticipant) {
val removedParticipant = mutableRemoteParticipants.remove(sid) ?: return
removedParticipant.tracks.values.forEach { publication ->
removedParticipant.unpublishTrack(publication.trackSid)
}
listener?.onParticipantDisconnected(this, removedParticipant)
}
private fun getOrCreateRemoteParticipant(
sid: Participant.Sid,
info: Model.ParticipantInfo? = null
): RemoteParticipant {
var participant = remoteParticipants[sid]
if (participant != null) {
return participant
}
participant = if (info != null) {
RemoteParticipant(info)
} else {
RemoteParticipant(sid, null)
}
mutableRemoteParticipants[sid] = participant
return participant
}
private fun handleSpeakerUpdate(speakerInfos: List<Rtc.SpeakerInfo>) {
val speakers = mutableListOf<Participant>()
val seenSids = mutableSetOf<Participant.Sid>()
val localParticipant = localParticipant
speakerInfos.forEach { speakerInfo ->
val speakerSid = Participant.Sid(speakerInfo.sid)
seenSids.add(speakerSid)
if (speakerSid == localParticipant?.sid) {
localParticipant.audioLevel = speakerInfo.level
speakers.add(localParticipant)
} else {
val participant = remoteParticipants[speakerSid]
if (participant != null) {
participant.audioLevel = speakerInfo.level
speakers.add(participant)
}
}
}
if (localParticipant != null && seenSids.contains(localParticipant.sid)) {
localParticipant.audioLevel = 0.0f
}
remoteParticipants.values
.filterNot { seenSids.contains(it.sid) }
.forEach { it.audioLevel = 0.0f }
mutableActiveSpeakers.clear()
mutableActiveSpeakers.addAll(speakers)
listener?.onActiveSpeakersChanged(speakers, this)
}
@AssistedFactory
interface Factory {
fun create(connectOptions: ConnectOptions): Room
}
interface Listener {
fun onConnect(room: Room) {}
fun onDisconnect(room: Room, error: Exception?) {}
fun onParticipantConnected(room: Room, participant: RemoteParticipant) {}
fun onParticipantDisconnected(room: Room, participant: RemoteParticipant) {}
fun onFailedToConnect(room: Room, error: Exception) {}
fun onReconnecting(room: Room, error: Exception) {}
fun onReconnect(room: Room) {}
fun onStartRecording(room: Room) {}
fun onStopRecording(room: Room) {}
fun onActiveSpeakersChanged(speakers: List<Participant>, room: Room) {}
}
override fun onJoin(response: Rtc.JoinResponse) {
Timber.v { "engine did join, version: ${response.serverVersion}" }
try {
val serverVersion = Semver(response.serverVersion)
if (serverVersion.major == 0 && serverVersion.minor < 5) {
Timber.e { "This version of livekit requires server version >= 0.5.x" }
return
}
} catch (e: Exception) {
Timber.e { "Unable to parse server version!" }
return
}
state = State.CONNECTED
sid = Sid(response.room.sid)
name = response.room.name
if (response.hasParticipant()) {
localParticipant = LocalParticipant(response.participant, engine)
}
if (response.otherParticipantsList.isNotEmpty()) {
response.otherParticipantsList.forEach {
getOrCreateRemoteParticipant(Participant.Sid(it.sid), it)
}
}
listener?.onConnect(this)
}
override fun onAddTrack(track: MediaStreamTrack, streams: Array<out MediaStream>) {
if (streams.count() < 0) {
Timber.i { "add track with empty streams?" }
return
}
val participantSid = Participant.Sid(streams.first().id)
val trackSid = Track.Sid(track.id())
val participant = getOrCreateRemoteParticipant(participantSid)
participant.addSubscribedMediaTrack(track, trackSid)
}
override fun onAddDataChannel(channel: DataChannel) {
val unpackedTrackLabel = channel.unpackedTrackLabel()
val (participantSid, trackSid, name) = unpackedTrackLabel
val participant = getOrCreateRemoteParticipant(participantSid)
participant.addSubscribedDataTrack(channel, trackSid, name)
}
override fun onPublishLocalTrack(cid: String, track: Model.TrackInfo) {
}
override fun onUpdateParticipants(updates: List<Model.ParticipantInfo>) {
for (info in updates) {
val participantSid = Participant.Sid(info.sid)
if(localParticipant?.sid == participantSid) {
localParticipant?.updateFromInfo(info)
}
val isNewParticipant = remoteParticipants.contains(participantSid)
val participant = getOrCreateRemoteParticipant(participantSid, info)
if (info.state == Model.ParticipantInfo.State.DISCONNECTED) {
handleParticipantDisconnect(participantSid, participant)
} else if (isNewParticipant) {
listener?.onParticipantConnected(this, participant)
} else {
participant.updateFromInfo(info)
}
}
}
override fun onUpdateSpeakers(speakers: List<Rtc.SpeakerInfo>) {
handleSpeakerUpdate(speakers)
}
override fun onDisconnect(reason: String) {
Timber.v { "engine did disconnect: $reason" }
listener?.onDisconnect(this, null)
}
override fun onFailToConnect(error: Exception) {
listener?.onFailedToConnect(this, error)
}
fun initVideoRenderer(viewRenderer: SurfaceViewRenderer) {
viewRenderer.init(eglBase.eglBaseContext, null)
viewRenderer.setScalingType(RendererCommon.ScalingType.SCALE_ASPECT_FIT)
viewRenderer.setEnableHardwareScaler(false /* enabled */);
}
}