David Liu

publish local tracks upon connecting

@@ -2,5 +2,7 @@ package io.livekit.android @@ -2,5 +2,7 @@ package io.livekit.android
2 2
3 3
4 data class ConnectOptions( 4 data class ConnectOptions(
5 - val isSecure: Boolean = true 5 + val isSecure: Boolean = true,
  6 + val sendAudio: Boolean = true,
  7 + val sendVideo: Boolean = true,
6 ) 8 )
@@ -3,6 +3,11 @@ package io.livekit.android @@ -3,6 +3,11 @@ 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.track.LocalAudioTrack
  7 +import io.livekit.android.room.track.LocalVideoTrack
  8 +import org.webrtc.EglBase
  9 +import org.webrtc.MediaConstraints
  10 +import org.webrtc.PeerConnectionFactory
6 11
7 class LiveKit { 12 class LiveKit {
8 companion object { 13 companion object {
@@ -23,7 +28,42 @@ class LiveKit { @@ -23,7 +28,42 @@ class LiveKit {
23 room.listener = listener 28 room.listener = listener
24 room.connect(url, token, options.isSecure) 29 room.connect(url, token, options.isSecure)
25 30
  31 + val localParticipant = room.localParticipant
  32 + if (localParticipant != null) {
  33 + val factory = component.peerConnectionFactory()
  34 + if (options.sendAudio) {
  35 + localParticipant.publishAudioTrack(createLocalAudioTrack(factory))
  36 + }
  37 + if (options.sendVideo) {
  38 + localParticipant.publishVideoTrack(
  39 + createLocalVideoTrack(
  40 + factory,
  41 + appContext,
  42 + component.eglBase()
  43 + )
  44 + )
  45 + }
  46 + }
26 return room 47 return room
27 } 48 }
  49 +
  50 + private fun createLocalVideoTrack(
  51 + peerConnectionFactory: PeerConnectionFactory,
  52 + context: Context,
  53 + rootEglBase: EglBase,
  54 + ): LocalVideoTrack {
  55 + return LocalVideoTrack.track(
  56 + peerConnectionFactory,
  57 + context,
  58 + true,
  59 + "LiveKit Video",
  60 + rootEglBase
  61 + )
  62 + }
  63 +
  64 + private fun createLocalAudioTrack(factory: PeerConnectionFactory): LocalAudioTrack {
  65 + val audioConstraints = MediaConstraints()
  66 + return LocalAudioTrack.createTrack(factory, audioConstraints)
  67 + }
28 } 68 }
29 } 69 }
@@ -4,6 +4,8 @@ import android.content.Context @@ -4,6 +4,8 @@ import android.content.Context
4 import dagger.BindsInstance 4 import dagger.BindsInstance
5 import dagger.Component 5 import dagger.Component
6 import io.livekit.android.room.Room 6 import io.livekit.android.room.Room
  7 +import org.webrtc.EglBase
  8 +import org.webrtc.PeerConnectionFactory
7 import javax.inject.Singleton 9 import javax.inject.Singleton
8 10
9 @Singleton 11 @Singleton
@@ -19,6 +21,10 @@ interface LiveKitComponent { @@ -19,6 +21,10 @@ interface LiveKitComponent {
19 21
20 fun roomFactory(): Room.Factory 22 fun roomFactory(): Room.Factory
21 23
  24 + fun peerConnectionFactory(): PeerConnectionFactory
  25 +
  26 + fun eglBase(): EglBase
  27 +
22 @Component.Factory 28 @Component.Factory
23 interface Factory { 29 interface Factory {
24 fun create(@BindsInstance appContext: Context): LiveKitComponent 30 fun create(@BindsInstance appContext: Context): LiveKitComponent
1 package io.livekit.android.room 1 package io.livekit.android.room
2 2
  3 +import com.github.ajalt.timberkt.Timber
3 import livekit.Rtc 4 import livekit.Rtc
4 import org.webrtc.* 5 import org.webrtc.*
5 6
@@ -22,6 +23,7 @@ class PublisherTransportObserver( @@ -22,6 +23,7 @@ class PublisherTransportObserver(
22 23
23 override fun onIceConnectionChange(newState: PeerConnection.IceConnectionState?) { 24 override fun onIceConnectionChange(newState: PeerConnection.IceConnectionState?) {
24 val state = newState ?: throw NullPointerException("unexpected null new state, what do?") 25 val state = newState ?: throw NullPointerException("unexpected null new state, what do?")
  26 + Timber.v { "onIceConnection new state: $newState" }
25 if (state == PeerConnection.IceConnectionState.CONNECTED && !engine.iceConnected) { 27 if (state == PeerConnection.IceConnectionState.CONNECTED && !engine.iceConnected) {
26 engine.iceConnected = true 28 engine.iceConnected = true
27 } else if (state == PeerConnection.IceConnectionState.DISCONNECTED) { 29 } else if (state == PeerConnection.IceConnectionState.DISCONNECTED) {
@@ -36,9 +36,9 @@ constructor( @@ -36,9 +36,9 @@ constructor(
36 var iceConnected: Boolean = false 36 var iceConnected: Boolean = false
37 set(value) { 37 set(value) {
38 field = value 38 field = value
39 - if (field) {  
40 - // TODO get rid of this assertion  
41 - listener?.onJoin(joinResponse!!) 39 + val savedJoinResponse = joinResponse
  40 + if (field && savedJoinResponse != null) {
  41 + listener?.onJoin(savedJoinResponse)
42 joinResponse = null 42 joinResponse = null
43 } 43 }
44 } 44 }
@@ -90,6 +90,7 @@ constructor( @@ -90,6 +90,7 @@ constructor(
90 } 90 }
91 91
92 fun close() { 92 fun close() {
  93 + coroutineScope.close()
93 publisher.close() 94 publisher.close()
94 subscriber.close() 95 subscriber.close()
95 client.close() 96 client.close()
@@ -110,6 +111,7 @@ constructor( @@ -110,6 +111,7 @@ constructor(
110 } 111 }
111 } 112 }
112 113
  114 + Timber.v { "sdp offer = $sdpOffer, description: ${sdpOffer.description}, type: ${sdpOffer.type}" }
113 when (val outcome = publisher.peerConnection.setLocalDescription(sdpOffer)) { 115 when (val outcome = publisher.peerConnection.setLocalDescription(sdpOffer)) {
114 is Either.Right -> { 116 is Either.Right -> {
115 Timber.d { "error setting local description: ${outcome.value}" } 117 Timber.d { "error setting local description: ${outcome.value}" }
@@ -14,6 +14,9 @@ import io.livekit.android.room.util.unpackedTrackLabel @@ -14,6 +14,9 @@ import io.livekit.android.room.util.unpackedTrackLabel
14 import livekit.Model 14 import livekit.Model
15 import livekit.Rtc 15 import livekit.Rtc
16 import org.webrtc.* 16 import org.webrtc.*
  17 +import kotlin.coroutines.Continuation
  18 +import kotlin.coroutines.resume
  19 +import kotlin.coroutines.suspendCoroutine
17 20
18 class Room 21 class Room
19 @AssistedInject 22 @AssistedInject
@@ -53,12 +56,15 @@ constructor( @@ -53,12 +56,15 @@ constructor(
53 val activeSpeakers: List<Participant> 56 val activeSpeakers: List<Participant>
54 get() = mutableActiveSpeakers 57 get() = mutableActiveSpeakers
55 58
  59 + private var connectContinuation: Continuation<Unit>? = null
56 suspend fun connect(url: String, token: String, isSecure: Boolean) { 60 suspend fun connect(url: String, token: String, isSecure: Boolean) {
57 if (localParticipant != null) { 61 if (localParticipant != null) {
58 Timber.d { "Attempting to connect to room when already connected." } 62 Timber.d { "Attempting to connect to room when already connected." }
59 return 63 return
60 } 64 }
61 engine.join(url, token, isSecure) 65 engine.join(url, token, isSecure)
  66 +
  67 + return suspendCoroutine { connectContinuation = it }
62 } 68 }
63 69
64 fun disconnect() { 70 fun disconnect() {
@@ -170,6 +176,8 @@ constructor( @@ -170,6 +176,8 @@ constructor(
170 } 176 }
171 } 177 }
172 178
  179 + connectContinuation?.resume(Unit)
  180 + connectContinuation = null
173 listener?.onConnect(this) 181 listener?.onConnect(this)
174 } 182 }
175 183
@@ -192,7 +200,7 @@ constructor( @@ -192,7 +200,7 @@ constructor(
192 participant.addSubscribedDataTrack(channel, trackSid, name) 200 participant.addSubscribedDataTrack(channel, trackSid, name)
193 } 201 }
194 202
195 - override fun onPublishLocalTrack(cid: String, track: Model.TrackInfo) { 203 + override fun onPublishLocalTrack(cid: Track.Cid, track: Model.TrackInfo) {
196 } 204 }
197 205
198 206
@@ -6,6 +6,7 @@ import io.livekit.android.room.track.* @@ -6,6 +6,7 @@ import io.livekit.android.room.track.*
6 import livekit.Model 6 import livekit.Model
7 import org.webrtc.DataChannel 7 import org.webrtc.DataChannel
8 import org.webrtc.RtpTransceiver 8 import org.webrtc.RtpTransceiver
  9 +import java.util.*
9 10
10 class LocalParticipant(sid: Sid, name: String? = null) : 11 class LocalParticipant(sid: Sid, name: String? = null) :
11 Participant(sid, name) { 12 Participant(sid, name) {
@@ -17,7 +18,7 @@ class LocalParticipant(sid: Sid, name: String? = null) : @@ -17,7 +18,7 @@ class LocalParticipant(sid: Sid, name: String? = null) :
17 this.engine = engine 18 this.engine = engine
18 } 19 }
19 20
20 - private val streamId = "stream" 21 + private val streamId = UUID.randomUUID().toString()
21 22
22 val localAudioTrackPublications 23 val localAudioTrackPublications
23 get() = audioTracks.values.toList() 24 get() = audioTracks.values.toList()
1 package io.livekit.android.room.track 1 package io.livekit.android.room.track
2 2
  3 +import org.webrtc.MediaConstraints
  4 +import org.webrtc.PeerConnectionFactory
  5 +
3 class LocalAudioTrack( 6 class LocalAudioTrack(
4 name: String, 7 name: String,
5 audioOptions: AudioOptions? = null, 8 audioOptions: AudioOptions? = null,
@@ -9,4 +12,20 @@ class LocalAudioTrack( @@ -9,4 +12,20 @@ class LocalAudioTrack(
9 internal set 12 internal set
10 var audioOptions = audioOptions 13 var audioOptions = audioOptions
11 private set 14 private set
  15 +
  16 + companion object {
  17 + fun createTrack(
  18 + factory: PeerConnectionFactory,
  19 + audioConstraints: MediaConstraints,
  20 + name: String = ""
  21 + ): LocalAudioTrack {
  22 +
  23 + val audioSource = factory.createAudioSource(audioConstraints)
  24 + val rtcAudioTrack =
  25 + factory.createAudioTrack("phone_audio_track_id", audioSource)
  26 + rtcAudioTrack.setEnabled(true)
  27 +
  28 + return LocalAudioTrack(name = name, rtcTrack = rtcAudioTrack)
  29 + }
  30 + }
12 } 31 }