David Liu

some coroutines stuff

package io.livekit.android.dagger
import dagger.Module
import dagger.Provides
import kotlinx.coroutines.Dispatchers
import javax.inject.Named
@Module
class CoroutinesModule {
companion object {
@Provides
@Named(InjectionNames.DISPATCHER_DEFAULT)
fun defaultDispatcher() = Dispatchers.Default
@Provides
@Named(InjectionNames.DISPATCHER_IO)
fun ioDispatcher() = Dispatchers.IO
@Provides
@Named(InjectionNames.DISPATCHER_MAIN)
fun mainDispatcher() = Dispatchers.Main
@Provides
@Named(InjectionNames.DISPATCHER_UNCONFINED)
fun unconfinedDispatcher() = Dispatchers.Unconfined
}
}
\ No newline at end of file
... ...
package io.livekit.android.dagger
class InjectionNames {
companion object {
const val DISPATCHER_DEFAULT = "dispatcher_default"
const val DISPATCHER_IO = "dispatcher_io";
const val DISPATCHER_MAIN = "dispatcher_main"
const val DISPATCHER_UNCONFINED = "dispatcher_unconfined"
}
}
\ No newline at end of file
... ...
... ... @@ -2,6 +2,7 @@ package io.livekit.android.room
import com.github.ajalt.timberkt.Timber
import com.google.protobuf.util.JsonFormat
import io.livekit.android.room.track.Track
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
... ... @@ -144,9 +145,9 @@ constructor(
sendRequest(request)
}
fun sendMuteTrack(trackSid: String, muted: Boolean) {
fun sendMuteTrack(trackSid: Track.Sid, muted: Boolean) {
val muteRequest = Rtc.MuteTrackRequest.newBuilder()
.setSid(trackSid)
.setSid(trackSid.sid)
.setMuted(muted)
.build()
... ... @@ -157,9 +158,9 @@ constructor(
sendRequest(request)
}
fun sendAddTrack(cid: String, name: String, type: Model.TrackType) {
fun sendAddTrack(cid: Track.Cid, name: String, type: Model.TrackType) {
val addTrackRequest = Rtc.AddTrackRequest.newBuilder()
.setCid(cid)
.setCid(cid.cid)
.setName(name)
.setType(type)
.build()
... ... @@ -183,7 +184,6 @@ constructor(
Timber.d { "error sending request: $request" }
throw IllegalStateException()
}
}
fun handleSignalResponse(response: Rtc.SignalResponse) {
... ... @@ -224,15 +224,18 @@ constructor(
}
}
fun close() {
TODO("Not yet implemented")
}
interface Listener {
fun onJoin(info: Rtc.JoinResponse)
fun onAnswer(sessionDescription: SessionDescription)
fun onOffer(sessionDescription: SessionDescription)
fun onTrickle(candidate: IceCandidate, target: Rtc.SignalTarget)
fun onLocalTrackPublished(trackPublished: Rtc.TrackPublishedResponse)
fun onLocalTrackPublished(response: Rtc.TrackPublishedResponse)
fun onParticipantUpdate(updates: List<Model.ParticipantInfo>)
fun onActiveSpeakersChanged(speakers: List<Rtc.SpeakerInfo>)
fun onClose(reason: String, code: Int)
fun onError(error: Error)
}
... ...
package io.livekit.android.room
import android.content.Context
import com.github.ajalt.timberkt.Timber
import io.livekit.android.dagger.InjectionNames
import io.livekit.android.room.track.Track
import io.livekit.android.room.track.TrackException
import io.livekit.android.room.util.CoroutineSdpObserver
import io.livekit.android.util.CloseableCoroutineScope
import io.livekit.android.util.Either
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch
import livekit.Model
import livekit.Rtc
import org.webrtc.*
import javax.inject.Inject
import javax.inject.Named
import kotlin.coroutines.Continuation
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
class RTCEngine
... ... @@ -15,6 +27,7 @@ constructor(
private val appContext: Context,
val client: RTCClient,
pctFactory: PeerConnectionTransport.Factory,
@Named(InjectionNames.DISPATCHER_IO) ioDispatcher: CoroutineDispatcher,
) : RTCClient.Listener {
var listener: Listener? = null
... ... @@ -39,6 +52,7 @@ constructor(
private var privateDataChannel: DataChannel
private val coroutineScope = CloseableCoroutineScope(SupervisorJob() + ioDispatcher)
init {
val rtcConfig = PeerConnection.RTCConfiguration(RTCClient.DEFAULT_ICE_SERVERS).apply {
sdpSemantics = PeerConnection.SdpSemantics.UNIFIED_PLAN
... ... @@ -55,12 +69,66 @@ constructor(
)
}
suspend fun join(url: String, token: String, isSecure: Boolean) {
fun join(url: String, token: String, isSecure: Boolean) {
client.join(url, token, isSecure)
}
suspend fun addTrack(cid: Track.Cid, name: String, kind: Model.TrackType): Model.TrackInfo {
if (pendingTrackResolvers[cid] != null) {
throw TrackException.DuplicateTrackException("Track with same ID $cid has already been published!")
}
return suspendCoroutine { cont ->
pendingTrackResolvers[cid] = cont
client.sendAddTrack(cid, name, kind)
}
}
fun updateMuteStatus(sid: Track.Sid, muted: Boolean) {
client.sendMuteTrack(sid, muted)
}
fun close() {
publisher.close()
subscriber.close()
client.close()
}
fun negotiate() {
TODO("Not yet implemented")
coroutineScope.launch {
val offerObserver = CoroutineSdpObserver()
publisher.peerConnection.createOffer(offerObserver, OFFER_CONSTRAINTS)
val offerOutcome = offerObserver.awaitCreate()
val sdpOffer = when (offerOutcome) {
is Either.Left -> offerOutcome.value
is Either.Right -> {
Timber.d { "error creating offer: ${offerOutcome.value}" }
return@launch
}
}
if (sdpOffer == null) {
Timber.d { "sdp is missing during negotiation?" }
return@launch
}
val setObserver = CoroutineSdpObserver()
publisher.peerConnection.setLocalDescription(setObserver, sdpOffer)
val setOutcome = setObserver.awaitSet()
when (setOutcome) {
is Either.Left -> client.sendOffer(sdpOffer)
is Either.Right -> Timber.d { "error setting local description: ${setOutcome.value}" }
}
}
}
private fun onRTCConnected() {
Timber.v { "RTC Connected" }
rtcConnected = true
pendingCandidates.forEach { candidate ->
client.sendCandidate(candidate, Rtc.SignalTarget.PUBLISHER)
}
pendingCandidates.clear()
}
interface Listener {
... ... @@ -69,17 +137,58 @@ constructor(
fun onPublishLocalTrack(cid: String, track: Model.TrackInfo)
fun onAddDataChannel(channel: DataChannel)
fun onUpdateParticipants(updates: Array<out Model.ParticipantInfo>)
fun onUpdateSpeakers(speakers: Array<out Rtc.SpeakerInfo>)
fun onUpdateSpeakers(speakers: List<Rtc.SpeakerInfo>)
fun onDisconnect(reason: String)
fun onFailToConnect(error: Error)
}
companion object {
private const val PRIVATE_DATA_CHANNEL_LABEL = "_private"
private val OFFER_CONSTRAINTS = MediaConstraints().apply {
with(mandatory) {
add(MediaConstraints.KeyValuePair("OfferToReceiveAudio", "false"))
add(MediaConstraints.KeyValuePair("OfferToReceiveVideo", "false"))
}
}
private val MEDIA_CONSTRAINTS = MediaConstraints()
private val CONN_CONSTRAINTS = MediaConstraints().apply {
with(optional) {
add(MediaConstraints.KeyValuePair("DtlsSrtpKeyAgreement", "true"))
}
}
}
override fun onJoin(info: Rtc.JoinResponse) {
TODO("Not yet implemented")
joinResponse = info
coroutineScope.launch {
val offerObserver = CoroutineSdpObserver()
publisher.peerConnection.createOffer(offerObserver, OFFER_CONSTRAINTS)
val offerOutcome = offerObserver.awaitCreate()
val sdpOffer = when (offerOutcome) {
is Either.Left -> offerOutcome.value
is Either.Right -> {
Timber.d { "error creating offer: ${offerOutcome.value}" }
return@launch
}
}
if (sdpOffer == null) {
Timber.d { "sdp is missing during negotiation?" }
return@launch
}
val setObserver = CoroutineSdpObserver()
publisher.peerConnection.setLocalDescription(setObserver, sdpOffer)
val setOutcome = setObserver.awaitSet()
when (setOutcome) {
is Either.Left -> client.sendOffer(sdpOffer)
is Either.Right -> Timber.d { "error setting local description: ${setOutcome.value}" }
}
}
}
override fun onAnswer(sessionDescription: SessionDescription) {
... ... @@ -94,14 +203,36 @@ constructor(
TODO("Not yet implemented")
}
override fun onLocalTrackPublished(trackPublished: Rtc.TrackPublishedResponse) {
TODO("Not yet implemented")
override fun onLocalTrackPublished(response: Rtc.TrackPublishedResponse) {
val cid = response.cid ?: run {
Timber.e { "local track published with null cid?" }
return
}
val track = response.track
if (track == null) {
Timber.d { "local track published with null track info?" }
}
Timber.v { "local track published $cid" }
val cont = pendingTrackResolvers.remove(cid)
if (cont == null) {
Timber.d { "missing track resolver for: $cid" }
return
}
cont.resume(response.track)
listener?.onPublishLocalTrack(cid, track)
}
override fun onParticipantUpdate(updates: List<Model.ParticipantInfo>) {
TODO("Not yet implemented")
}
override fun onActiveSpeakersChanged(speakers: List<Rtc.SpeakerInfo>) {
listener?.onUpdateSpeakers(speakers)
}
override fun onClose(reason: String, code: Int) {
TODO("Not yet implemented")
}
... ...
... ... @@ -44,16 +44,20 @@ class Track(name: String, state: State) {
}
}
sealed class TrackException(message: String?, cause: Throwable?) : Exception(message, cause) {
class InvalidTrackTypeException(message: String?, cause: Throwable?) :
sealed class TrackException(message: String? = null, cause: Throwable? = null) :
Exception(message, cause) {
class InvalidTrackTypeException(message: String? = null, cause: Throwable? = null) :
TrackException(message, cause)
class DuplicateTrackException(message: String?, cause: Throwable?) :
class DuplicateTrackException(message: String? = null, cause: Throwable? = null) :
TrackException(message, cause)
class InvalidTrackStateException(message: String?, cause: Throwable?) :
class InvalidTrackStateException(message: String? = null, cause: Throwable? = null) :
TrackException(message, cause)
class MediaException(message: String?, cause: Throwable?) : TrackException(message, cause)
class PublishException(message: String?, cause: Throwable?) : TrackException(message, cause)
class MediaException(message: String? = null, cause: Throwable? = null) :
TrackException(message, cause)
class PublishException(message: String? = null, cause: Throwable? = null) :
TrackException(message, cause)
}
\ No newline at end of file
... ...
package io.livekit.android.room.util
import io.livekit.android.util.Either
import org.webrtc.SdpObserver
import org.webrtc.SessionDescription
import kotlin.coroutines.Continuation
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
class CoroutineSdpObserver : SdpObserver {
private var createOutcome: Either<SessionDescription?, String?>? = null
set(value) {
field = value
if (value != null) {
val conts = pendingCreate.toList()
pendingCreate.clear()
conts.forEach {
it.resume(value)
}
}
}
private var pendingCreate = mutableListOf<Continuation<Either<SessionDescription?, String?>>>()
private var setOutcome: Either<Unit, String?>? = null
set(value) {
field = value
if (value != null) {
val conts = pendingSets.toList()
pendingSets.clear()
conts.forEach {
it.resume(value)
}
}
}
private var pendingSets = mutableListOf<Continuation<Either<Unit, String?>>>()
override fun onCreateSuccess(sdp: SessionDescription?) {
createOutcome = Either.Left(sdp)
}
override fun onSetSuccess() {
setOutcome = Either.Left(Unit)
}
override fun onCreateFailure(message: String?) {
createOutcome = Either.Right(message)
}
override fun onSetFailure(message: String?) {
setOutcome = Either.Right(message)
}
suspend fun awaitCreate() = suspendCoroutine<Either<SessionDescription?, String?>> { cont ->
val curOutcome = createOutcome
if (curOutcome != null) {
cont.resume(curOutcome)
} else {
pendingCreate.add(cont)
}
}
suspend fun awaitSet() = suspendCoroutine<Either<Unit, String?>> { cont ->
val curOutcome = setOutcome
if (curOutcome != null) {
cont.resume(curOutcome)
} else {
pendingSets.add(cont)
}
}
}
\ No newline at end of file
... ...
package io.livekit.android.util
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.cancel
import java.io.Closeable
import kotlin.coroutines.CoroutineContext
internal class CloseableCoroutineScope(context: CoroutineContext) : Closeable, CoroutineScope {
override val coroutineContext: CoroutineContext = context
override fun close() {
coroutineContext.cancel()
}
}
\ No newline at end of file
... ...
package io.livekit.android.util
sealed class Either<out A, out B> {
class Left<A>(val value: A) : Either<A, Nothing>()
class Right<B>(val value: B) : Either<Nothing, B>()
}
\ No newline at end of file
... ...