David Liu

DI and more fleshing out audio

1 apply plugin: 'com.android.library' 1 apply plugin: 'com.android.library'
2 apply plugin: 'kotlin-android' 2 apply plugin: 'kotlin-android'
  3 +apply plugin: 'kotlin-kapt'
3 apply plugin: 'kotlinx-serialization' 4 apply plugin: 'kotlinx-serialization'
4 apply plugin: 'com.google.protobuf' 5 apply plugin: 'com.google.protobuf'
5 6
@@ -34,6 +35,9 @@ android { @@ -34,6 +35,9 @@ android {
34 sourceCompatibility java_version 35 sourceCompatibility java_version
35 targetCompatibility java_version 36 targetCompatibility java_version
36 } 37 }
  38 + kotlinOptions {
  39 + freeCompilerArgs = ["-Xinline-classes"]
  40 + }
37 } 41 }
38 42
39 protobuf { 43 protobuf {
@@ -60,8 +64,8 @@ dependencies { @@ -60,8 +64,8 @@ dependencies {
60 implementation "com.google.protobuf:protobuf-java:${versions.protobuf}" 64 implementation "com.google.protobuf:protobuf-java:${versions.protobuf}"
61 implementation "com.google.protobuf:protobuf-java-util:${versions.protobuf}" 65 implementation "com.google.protobuf:protobuf-java-util:${versions.protobuf}"
62 66
63 - implementation 'com.google.dagger:dagger:2.32'  
64 - annotationProcessor 'com.google.dagger:dagger-compiler:2.32' 67 + implementation 'com.google.dagger:dagger:2.33'
  68 + kapt 'com.google.dagger:dagger-compiler:2.33'
65 69
66 implementation 'com.github.ajalt:timberkt:1.5.1' 70 implementation 'com.github.ajalt:timberkt:1.5.1'
67 71
1 package io.livekit.android 1 package io.livekit.android
2 2
  3 +import android.content.Context
  4 +import io.livekit.android.dagger.DaggerLiveKitComponent
  5 +
3 class LiveKit { 6 class LiveKit {
  7 + companion object {
4 suspend fun connect( 8 suspend fun connect(
  9 + appContext: Context,
5 url: String, 10 url: String,
6 token: String, 11 token: String,
7 - options: ConnectOptions?){ 12 + options: ConnectOptions
  13 + ) {
  14 +
  15 + val component = DaggerLiveKitComponent
  16 + .factory()
  17 + .create(appContext)
8 18
  19 + val room = component.roomFactory()
  20 + .create(options)
  21 + room.connect(url, token, false)
  22 + }
9 } 23 }
10 } 24 }
  1 +package io.livekit.android.dagger
  2 +
  3 +import com.google.protobuf.util.JsonFormat
  4 +import dagger.Module
  5 +import dagger.Provides
  6 +
  7 +@Module
  8 +class JsonFormatModule {
  9 + companion object {
  10 + @Provides
  11 + fun jsonFormatParser(): JsonFormat.Parser {
  12 + return JsonFormat.parser()
  13 + }
  14 +
  15 + @Provides
  16 + fun jsonFormatPrinter(): JsonFormat.Printer {
  17 + return JsonFormat.printer()
  18 + }
  19 + }
  20 +}
  1 +package io.livekit.android.dagger
  2 +
  3 +import android.content.Context
  4 +import dagger.BindsInstance
  5 +import dagger.Component
  6 +import io.livekit.android.room.Room
  7 +import javax.inject.Singleton
  8 +
  9 +@Singleton
  10 +@Component(
  11 + modules = [
  12 + RTCModule::class,
  13 + WebModule::class,
  14 + JsonFormatModule::class,
  15 + ]
  16 +)
  17 +interface LiveKitComponent {
  18 +
  19 + fun roomFactory(): Room.Factory
  20 +
  21 + @Component.Factory
  22 + interface Factory {
  23 + fun create(@BindsInstance appContext: Context): LiveKitComponent
  24 + }
  25 +}
  1 +package io.livekit.android.dagger
  2 +
  3 +import android.content.Context
  4 +import com.github.ajalt.timberkt.Timber
  5 +import dagger.Module
  6 +import dagger.Provides
  7 +import org.webrtc.PeerConnectionFactory
  8 +import org.webrtc.audio.AudioDeviceModule
  9 +import org.webrtc.audio.JavaAudioDeviceModule
  10 +import javax.inject.Singleton
  11 +
  12 +@Module
  13 +class RTCModule {
  14 + companion object {
  15 + @Provides
  16 + @Singleton
  17 + fun audioModule(appContext: Context): AudioDeviceModule {
  18 +
  19 + // Set audio record error callbacks.
  20 + val audioRecordErrorCallback = object : JavaAudioDeviceModule.AudioRecordErrorCallback {
  21 + override fun onWebRtcAudioRecordInitError(errorMessage: String?) {
  22 + Timber.e { "onWebRtcAudioRecordInitError: $errorMessage" }
  23 + }
  24 +
  25 + override fun onWebRtcAudioRecordStartError(
  26 + errorCode: JavaAudioDeviceModule.AudioRecordStartErrorCode?,
  27 + errorMessage: String?
  28 + ) {
  29 + Timber.e { "onWebRtcAudioRecordStartError: $errorCode. $errorMessage" }
  30 + }
  31 +
  32 + override fun onWebRtcAudioRecordError(errorMessage: String?) {
  33 + Timber.e { "onWebRtcAudioRecordError: $errorMessage" }
  34 + }
  35 + }
  36 +
  37 + val audioTrackErrorCallback = object : JavaAudioDeviceModule.AudioTrackErrorCallback {
  38 + override fun onWebRtcAudioTrackInitError(errorMessage: String?) {
  39 + Timber.e { "onWebRtcAudioTrackInitError: $errorMessage" }
  40 + }
  41 +
  42 + override fun onWebRtcAudioTrackStartError(
  43 + errorCode: JavaAudioDeviceModule.AudioTrackStartErrorCode?,
  44 + errorMessage: String?
  45 + ) {
  46 + Timber.e { "onWebRtcAudioTrackStartError: $errorCode. $errorMessage" }
  47 + }
  48 +
  49 + override fun onWebRtcAudioTrackError(errorMessage: String?) {
  50 + Timber.e { "onWebRtcAudioTrackError: $errorMessage" }
  51 + }
  52 +
  53 + }
  54 + val audioRecordStateCallback: JavaAudioDeviceModule.AudioRecordStateCallback = object :
  55 + JavaAudioDeviceModule.AudioRecordStateCallback {
  56 + override fun onWebRtcAudioRecordStart() {
  57 + Timber.i { "Audio recording starts" }
  58 + }
  59 +
  60 + override fun onWebRtcAudioRecordStop() {
  61 + Timber.i { "Audio recording stops" }
  62 + }
  63 + }
  64 +
  65 + // Set audio track state callbacks.
  66 + val audioTrackStateCallback: JavaAudioDeviceModule.AudioTrackStateCallback = object :
  67 + JavaAudioDeviceModule.AudioTrackStateCallback {
  68 + override fun onWebRtcAudioTrackStart() {
  69 + Timber.i { "Audio playout starts" }
  70 + }
  71 +
  72 + override fun onWebRtcAudioTrackStop() {
  73 + Timber.i { "Audio playout stops" }
  74 + }
  75 + }
  76 +
  77 + return JavaAudioDeviceModule.builder(appContext)
  78 + .setUseHardwareAcousticEchoCanceler(true)
  79 + .setUseHardwareNoiseSuppressor(true)
  80 + .setAudioRecordErrorCallback(audioRecordErrorCallback)
  81 + .setAudioTrackErrorCallback(audioTrackErrorCallback)
  82 + .setAudioRecordStateCallback(audioRecordStateCallback)
  83 + .setAudioTrackStateCallback(audioTrackStateCallback)
  84 + .createAudioDeviceModule()
  85 + }
  86 +
  87 + @Provides
  88 + @Singleton
  89 + fun peerConnectionFactory(
  90 + appContext: Context,
  91 + audioDeviceModule: AudioDeviceModule
  92 + ): PeerConnectionFactory {
  93 + PeerConnectionFactory.initialize(
  94 + PeerConnectionFactory.InitializationOptions.builder(appContext)
  95 + .createInitializationOptions()
  96 + )
  97 +
  98 + return PeerConnectionFactory.builder()
  99 + .setAudioDeviceModule(audioDeviceModule)
  100 + .createPeerConnectionFactory()
  101 + }
  102 + }
  103 +}
  1 +package io.livekit.android.dagger
  2 +
  3 +import dagger.Module
  4 +import dagger.Provides
  5 +import okhttp3.OkHttpClient
  6 +import okhttp3.WebSocket
  7 +import javax.inject.Singleton
  8 +
  9 +@Module
  10 +class WebModule {
  11 + companion object {
  12 + @Provides
  13 + @Singleton
  14 + fun okHttpClient(): OkHttpClient {
  15 + return OkHttpClient()
  16 + }
  17 +
  18 + @Provides
  19 + fun websocketFactory(okHttpClient: OkHttpClient): WebSocket.Factory {
  20 + return okHttpClient
  21 + }
  22 + }
  23 +}
@@ -7,8 +7,8 @@ class PublisherTransportObserver( @@ -7,8 +7,8 @@ class PublisherTransportObserver(
7 private val engine: RTCEngine 7 private val engine: RTCEngine
8 ) : PeerConnection.Observer { 8 ) : PeerConnection.Observer {
9 9
10 - override fun onIceCandidate(candidate: IceCandidate?) {  
11 - val candidate = candidate ?: return 10 + override fun onIceCandidate(iceCandidate: IceCandidate?) {
  11 + val candidate = iceCandidate ?: return
12 if (engine.rtcConnected) { 12 if (engine.rtcConnected) {
13 engine.client.sendCandidate(candidate, target = Rtc.SignalTarget.PUBLISHER) 13 engine.client.sendCandidate(candidate, target = Rtc.SignalTarget.PUBLISHER)
14 } else { 14 } else {
@@ -21,10 +21,10 @@ class PublisherTransportObserver( @@ -21,10 +21,10 @@ class PublisherTransportObserver(
21 } 21 }
22 22
23 override fun onIceConnectionChange(newState: PeerConnection.IceConnectionState?) { 23 override fun onIceConnectionChange(newState: PeerConnection.IceConnectionState?) {
24 - val newState = newState ?: throw NullPointerException("unexpected null new state, what do?")  
25 - if (newState == PeerConnection.IceConnectionState.CONNECTED && !engine.iceConnected) { 24 + val state = newState ?: throw NullPointerException("unexpected null new state, what do?")
  25 + if (state == PeerConnection.IceConnectionState.CONNECTED && !engine.iceConnected) {
26 engine.iceConnected = true 26 engine.iceConnected = true
27 - } else if (newState == PeerConnection.IceConnectionState.DISCONNECTED) { 27 + } else if (state == PeerConnection.IceConnectionState.DISCONNECTED) {
28 engine.iceConnected = false 28 engine.iceConnected = false
29 engine.listener?.onDisconnect("Peer connection disconnected") 29 engine.listener?.onDisconnect("Peer connection disconnected")
30 } 30 }
1 package io.livekit.android.room 1 package io.livekit.android.room
2 2
  3 +import android.content.Context
  4 +import io.livekit.android.room.track.Track
3 import livekit.Model 5 import livekit.Model
4 import livekit.Rtc 6 import livekit.Rtc
5 import org.webrtc.* 7 import org.webrtc.*
6 import javax.inject.Inject 8 import javax.inject.Inject
  9 +import kotlin.coroutines.Continuation
  10 +
7 11
8 class RTCEngine 12 class RTCEngine
9 @Inject 13 @Inject
10 constructor( 14 constructor(
  15 + private val appContext: Context,
11 val client: RTCClient, 16 val client: RTCClient,
12 pctFactory: PeerConnectionTransport.Factory, 17 pctFactory: PeerConnectionTransport.Factory,
13 -) { 18 +) : RTCClient.Listener {
14 19
15 var listener: Listener? = null 20 var listener: Listener? = null
16 var rtcConnected: Boolean = false 21 var rtcConnected: Boolean = false
@@ -24,12 +29,16 @@ constructor( @@ -24,12 +29,16 @@ constructor(
24 } 29 }
25 } 30 }
26 val pendingCandidates = mutableListOf<IceCandidate>() 31 val pendingCandidates = mutableListOf<IceCandidate>()
  32 + private val pendingTrackResolvers: MutableMap<Track.Cid, Continuation<Model.TrackInfo>> =
  33 + mutableMapOf()
27 34
28 private val publisherObserver = PublisherTransportObserver(this) 35 private val publisherObserver = PublisherTransportObserver(this)
29 private val subscriberObserver = SubscriberTransportObserver(this) 36 private val subscriberObserver = SubscriberTransportObserver(this)
30 private val publisher: PeerConnectionTransport 37 private val publisher: PeerConnectionTransport
31 private val subscriber: PeerConnectionTransport 38 private val subscriber: PeerConnectionTransport
32 39
  40 + private var privateDataChannel: DataChannel
  41 +
33 init { 42 init {
34 val rtcConfig = PeerConnection.RTCConfiguration(RTCClient.DEFAULT_ICE_SERVERS).apply { 43 val rtcConfig = PeerConnection.RTCConfiguration(RTCClient.DEFAULT_ICE_SERVERS).apply {
35 sdpSemantics = PeerConnection.SdpSemantics.UNIFIED_PLAN 44 sdpSemantics = PeerConnection.SdpSemantics.UNIFIED_PLAN
@@ -38,7 +47,12 @@ constructor( @@ -38,7 +47,12 @@ constructor(
38 47
39 publisher = pctFactory.create(rtcConfig, publisherObserver) 48 publisher = pctFactory.create(rtcConfig, publisherObserver)
40 subscriber = pctFactory.create(rtcConfig, subscriberObserver) 49 subscriber = pctFactory.create(rtcConfig, subscriberObserver)
  50 + client.listener = this
41 51
  52 + privateDataChannel = publisher.peerConnection.createDataChannel(
  53 + PRIVATE_DATA_CHANNEL_LABEL,
  54 + DataChannel.Init()
  55 + )
42 } 56 }
43 57
44 suspend fun join(url: String, token: String, isSecure: Boolean) { 58 suspend fun join(url: String, token: String, isSecure: Boolean) {
@@ -59,4 +73,40 @@ constructor( @@ -59,4 +73,40 @@ constructor(
59 fun onDisconnect(reason: String) 73 fun onDisconnect(reason: String)
60 fun onFailToConnect(error: Error) 74 fun onFailToConnect(error: Error)
61 } 75 }
  76 +
  77 + companion object {
  78 + private const val PRIVATE_DATA_CHANNEL_LABEL = "_private"
  79 + }
  80 +
  81 + override fun onJoin(info: Rtc.JoinResponse) {
  82 + TODO("Not yet implemented")
  83 + }
  84 +
  85 + override fun onAnswer(sessionDescription: SessionDescription) {
  86 + TODO("Not yet implemented")
  87 + }
  88 +
  89 + override fun onOffer(sessionDescription: SessionDescription) {
  90 + TODO("Not yet implemented")
  91 + }
  92 +
  93 + override fun onTrickle(candidate: IceCandidate, target: Rtc.SignalTarget) {
  94 + TODO("Not yet implemented")
  95 + }
  96 +
  97 + override fun onLocalTrackPublished(trackPublished: Rtc.TrackPublishedResponse) {
  98 + TODO("Not yet implemented")
  99 + }
  100 +
  101 + override fun onParticipantUpdate(updates: List<Model.ParticipantInfo>) {
  102 + TODO("Not yet implemented")
  103 + }
  104 +
  105 + override fun onClose(reason: String, code: Int) {
  106 + TODO("Not yet implemented")
  107 + }
  108 +
  109 + override fun onError(error: Error) {
  110 + TODO("Not yet implemented")
  111 + }
62 } 112 }
1 package io.livekit.android.room 1 package io.livekit.android.room
2 2
3 import dagger.assisted.Assisted 3 import dagger.assisted.Assisted
  4 +import dagger.assisted.AssistedFactory
4 import dagger.assisted.AssistedInject 5 import dagger.assisted.AssistedInject
5 import io.livekit.android.ConnectOptions 6 import io.livekit.android.ConnectOptions
6 7
7 class Room 8 class Room
8 @AssistedInject 9 @AssistedInject
9 constructor( 10 constructor(
10 - private val connectOptions: ConnectOptions,  
11 - @Assisted private val engine: RTCEngine, 11 + @Assisted private val connectOptions: ConnectOptions,
  12 + private val engine: RTCEngine,
12 ) { 13 ) {
13 14
14 suspend fun connect(url: String, token: String, isSecure: Boolean) { 15 suspend fun connect(url: String, token: String, isSecure: Boolean) {
15 engine.join(url, token, isSecure) 16 engine.join(url, token, isSecure)
16 } 17 }
  18 +
  19 + @AssistedFactory
  20 + interface Factory {
  21 + fun create(connectOptions: ConnectOptions): Room
  22 + }
17 } 23 }
  1 +package io.livekit.android.room.track
  2 +
  3 +import org.webrtc.DataChannel
  4 +import org.webrtc.MediaStreamTrack
  5 +
  6 +class Track(name: String, state: State) {
  7 +
  8 + var name = name
  9 + internal set
  10 + var state = state
  11 + internal set
  12 +
  13 + inline class Sid(val sid: String)
  14 + inline class Cid(val cid: String)
  15 +
  16 + enum class Priority {
  17 + STANDARD, HIGH, LOW;
  18 + }
  19 +
  20 + enum class State {
  21 + ENDED, LIVE, NONE;
  22 + }
  23 +
  24 + companion object {
  25 + fun stateFromRTCMediaTrackState(trackState: MediaStreamTrack.State): State {
  26 + return when (trackState) {
  27 + MediaStreamTrack.State.ENDED -> State.ENDED
  28 + MediaStreamTrack.State.LIVE -> State.LIVE
  29 + }
  30 + }
  31 +
  32 + fun stateFromRTCDataChannelState(dataChannelState: DataChannel.State): State {
  33 + return when (dataChannelState) {
  34 + DataChannel.State.CONNECTING,
  35 + DataChannel.State.OPEN -> {
  36 + State.LIVE
  37 + }
  38 + DataChannel.State.CLOSING,
  39 + DataChannel.State.CLOSED -> {
  40 + State.ENDED
  41 + }
  42 + }
  43 + }
  44 + }
  45 +}
  46 +
  47 +sealed class TrackException(message: String?, cause: Throwable?) : Exception(message, cause) {
  48 + class InvalidTrackTypeException(message: String?, cause: Throwable?) :
  49 + TrackException(message, cause)
  50 +
  51 + class DuplicateTrackException(message: String?, cause: Throwable?) :
  52 + TrackException(message, cause)
  53 +
  54 + class InvalidTrackStateException(message: String?, cause: Throwable?) :
  55 + TrackException(message, cause)
  56 +
  57 + class MediaException(message: String?, cause: Throwable?) : TrackException(message, cause)
  58 + class PublishException(message: String?, cause: Throwable?) : TrackException(message, cause)
  59 +}