David Liu

DI and more fleshing out audio

apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-kapt'
apply plugin: 'kotlinx-serialization'
apply plugin: 'com.google.protobuf'
... ... @@ -34,6 +35,9 @@ android {
sourceCompatibility java_version
targetCompatibility java_version
}
kotlinOptions {
freeCompilerArgs = ["-Xinline-classes"]
}
}
protobuf {
... ... @@ -60,12 +64,12 @@ dependencies {
implementation "com.google.protobuf:protobuf-java:${versions.protobuf}"
implementation "com.google.protobuf:protobuf-java-util:${versions.protobuf}"
implementation 'com.google.dagger:dagger:2.32'
annotationProcessor 'com.google.dagger:dagger-compiler:2.32'
implementation 'com.google.dagger:dagger:2.33'
kapt 'com.google.dagger:dagger-compiler:2.33'
implementation 'com.github.ajalt:timberkt:1.5.1'
testImplementation 'junit:junit:4.12'
androidTestImplementation 'androidx.test.ext:junit:1.1.2'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
}
}
\ No newline at end of file
... ...
package io.livekit.android
import android.content.Context
import io.livekit.android.dagger.DaggerLiveKitComponent
class LiveKit {
suspend fun connect(
url: String,
token: String,
options: ConnectOptions?){
companion object {
suspend fun connect(
appContext: Context,
url: String,
token: String,
options: ConnectOptions
) {
val component = DaggerLiveKitComponent
.factory()
.create(appContext)
val room = component.roomFactory()
.create(options)
room.connect(url, token, false)
}
}
}
... ...
package io.livekit.android.dagger
import com.google.protobuf.util.JsonFormat
import dagger.Module
import dagger.Provides
@Module
class JsonFormatModule {
companion object {
@Provides
fun jsonFormatParser(): JsonFormat.Parser {
return JsonFormat.parser()
}
@Provides
fun jsonFormatPrinter(): JsonFormat.Printer {
return JsonFormat.printer()
}
}
}
\ No newline at end of file
... ...
package io.livekit.android.dagger
import android.content.Context
import dagger.BindsInstance
import dagger.Component
import io.livekit.android.room.Room
import javax.inject.Singleton
@Singleton
@Component(
modules = [
RTCModule::class,
WebModule::class,
JsonFormatModule::class,
]
)
interface LiveKitComponent {
fun roomFactory(): Room.Factory
@Component.Factory
interface Factory {
fun create(@BindsInstance appContext: Context): LiveKitComponent
}
}
\ No newline at end of file
... ...
package io.livekit.android.dagger
import android.content.Context
import com.github.ajalt.timberkt.Timber
import dagger.Module
import dagger.Provides
import org.webrtc.PeerConnectionFactory
import org.webrtc.audio.AudioDeviceModule
import org.webrtc.audio.JavaAudioDeviceModule
import javax.inject.Singleton
@Module
class RTCModule {
companion object {
@Provides
@Singleton
fun audioModule(appContext: Context): AudioDeviceModule {
// Set audio record error callbacks.
val audioRecordErrorCallback = object : JavaAudioDeviceModule.AudioRecordErrorCallback {
override fun onWebRtcAudioRecordInitError(errorMessage: String?) {
Timber.e { "onWebRtcAudioRecordInitError: $errorMessage" }
}
override fun onWebRtcAudioRecordStartError(
errorCode: JavaAudioDeviceModule.AudioRecordStartErrorCode?,
errorMessage: String?
) {
Timber.e { "onWebRtcAudioRecordStartError: $errorCode. $errorMessage" }
}
override fun onWebRtcAudioRecordError(errorMessage: String?) {
Timber.e { "onWebRtcAudioRecordError: $errorMessage" }
}
}
val audioTrackErrorCallback = object : JavaAudioDeviceModule.AudioTrackErrorCallback {
override fun onWebRtcAudioTrackInitError(errorMessage: String?) {
Timber.e { "onWebRtcAudioTrackInitError: $errorMessage" }
}
override fun onWebRtcAudioTrackStartError(
errorCode: JavaAudioDeviceModule.AudioTrackStartErrorCode?,
errorMessage: String?
) {
Timber.e { "onWebRtcAudioTrackStartError: $errorCode. $errorMessage" }
}
override fun onWebRtcAudioTrackError(errorMessage: String?) {
Timber.e { "onWebRtcAudioTrackError: $errorMessage" }
}
}
val audioRecordStateCallback: JavaAudioDeviceModule.AudioRecordStateCallback = object :
JavaAudioDeviceModule.AudioRecordStateCallback {
override fun onWebRtcAudioRecordStart() {
Timber.i { "Audio recording starts" }
}
override fun onWebRtcAudioRecordStop() {
Timber.i { "Audio recording stops" }
}
}
// Set audio track state callbacks.
val audioTrackStateCallback: JavaAudioDeviceModule.AudioTrackStateCallback = object :
JavaAudioDeviceModule.AudioTrackStateCallback {
override fun onWebRtcAudioTrackStart() {
Timber.i { "Audio playout starts" }
}
override fun onWebRtcAudioTrackStop() {
Timber.i { "Audio playout stops" }
}
}
return JavaAudioDeviceModule.builder(appContext)
.setUseHardwareAcousticEchoCanceler(true)
.setUseHardwareNoiseSuppressor(true)
.setAudioRecordErrorCallback(audioRecordErrorCallback)
.setAudioTrackErrorCallback(audioTrackErrorCallback)
.setAudioRecordStateCallback(audioRecordStateCallback)
.setAudioTrackStateCallback(audioTrackStateCallback)
.createAudioDeviceModule()
}
@Provides
@Singleton
fun peerConnectionFactory(
appContext: Context,
audioDeviceModule: AudioDeviceModule
): PeerConnectionFactory {
PeerConnectionFactory.initialize(
PeerConnectionFactory.InitializationOptions.builder(appContext)
.createInitializationOptions()
)
return PeerConnectionFactory.builder()
.setAudioDeviceModule(audioDeviceModule)
.createPeerConnectionFactory()
}
}
}
\ No newline at end of file
... ...
package io.livekit.android.dagger
import dagger.Module
import dagger.Provides
import okhttp3.OkHttpClient
import okhttp3.WebSocket
import javax.inject.Singleton
@Module
class WebModule {
companion object {
@Provides
@Singleton
fun okHttpClient(): OkHttpClient {
return OkHttpClient()
}
@Provides
fun websocketFactory(okHttpClient: OkHttpClient): WebSocket.Factory {
return okHttpClient
}
}
}
\ No newline at end of file
... ...
... ... @@ -7,8 +7,8 @@ class PublisherTransportObserver(
private val engine: RTCEngine
) : PeerConnection.Observer {
override fun onIceCandidate(candidate: IceCandidate?) {
val candidate = candidate ?: return
override fun onIceCandidate(iceCandidate: IceCandidate?) {
val candidate = iceCandidate ?: return
if (engine.rtcConnected) {
engine.client.sendCandidate(candidate, target = Rtc.SignalTarget.PUBLISHER)
} else {
... ... @@ -21,10 +21,10 @@ class PublisherTransportObserver(
}
override fun onIceConnectionChange(newState: PeerConnection.IceConnectionState?) {
val newState = newState ?: throw NullPointerException("unexpected null new state, what do?")
if (newState == PeerConnection.IceConnectionState.CONNECTED && !engine.iceConnected) {
val state = newState ?: throw NullPointerException("unexpected null new state, what do?")
if (state == PeerConnection.IceConnectionState.CONNECTED && !engine.iceConnected) {
engine.iceConnected = true
} else if (newState == PeerConnection.IceConnectionState.DISCONNECTED) {
} else if (state == PeerConnection.IceConnectionState.DISCONNECTED) {
engine.iceConnected = false
engine.listener?.onDisconnect("Peer connection disconnected")
}
... ...
package io.livekit.android.room
import android.content.Context
import io.livekit.android.room.track.Track
import livekit.Model
import livekit.Rtc
import org.webrtc.*
import javax.inject.Inject
import kotlin.coroutines.Continuation
class RTCEngine
@Inject
constructor(
private val appContext: Context,
val client: RTCClient,
pctFactory: PeerConnectionTransport.Factory,
) {
) : RTCClient.Listener {
var listener: Listener? = null
var rtcConnected: Boolean = false
... ... @@ -24,12 +29,16 @@ constructor(
}
}
val pendingCandidates = mutableListOf<IceCandidate>()
private val pendingTrackResolvers: MutableMap<Track.Cid, Continuation<Model.TrackInfo>> =
mutableMapOf()
private val publisherObserver = PublisherTransportObserver(this)
private val subscriberObserver = SubscriberTransportObserver(this)
private val publisher: PeerConnectionTransport
private val subscriber: PeerConnectionTransport
private var privateDataChannel: DataChannel
init {
val rtcConfig = PeerConnection.RTCConfiguration(RTCClient.DEFAULT_ICE_SERVERS).apply {
sdpSemantics = PeerConnection.SdpSemantics.UNIFIED_PLAN
... ... @@ -38,7 +47,12 @@ constructor(
publisher = pctFactory.create(rtcConfig, publisherObserver)
subscriber = pctFactory.create(rtcConfig, subscriberObserver)
client.listener = this
privateDataChannel = publisher.peerConnection.createDataChannel(
PRIVATE_DATA_CHANNEL_LABEL,
DataChannel.Init()
)
}
suspend fun join(url: String, token: String, isSecure: Boolean) {
... ... @@ -59,4 +73,40 @@ constructor(
fun onDisconnect(reason: String)
fun onFailToConnect(error: Error)
}
companion object {
private const val PRIVATE_DATA_CHANNEL_LABEL = "_private"
}
override fun onJoin(info: Rtc.JoinResponse) {
TODO("Not yet implemented")
}
override fun onAnswer(sessionDescription: SessionDescription) {
TODO("Not yet implemented")
}
override fun onOffer(sessionDescription: SessionDescription) {
TODO("Not yet implemented")
}
override fun onTrickle(candidate: IceCandidate, target: Rtc.SignalTarget) {
TODO("Not yet implemented")
}
override fun onLocalTrackPublished(trackPublished: Rtc.TrackPublishedResponse) {
TODO("Not yet implemented")
}
override fun onParticipantUpdate(updates: List<Model.ParticipantInfo>) {
TODO("Not yet implemented")
}
override fun onClose(reason: String, code: Int) {
TODO("Not yet implemented")
}
override fun onError(error: Error) {
TODO("Not yet implemented")
}
}
\ No newline at end of file
... ...
package io.livekit.android.room
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import io.livekit.android.ConnectOptions
class Room
@AssistedInject
constructor(
private val connectOptions: ConnectOptions,
@Assisted private val engine: RTCEngine,
@Assisted private val connectOptions: ConnectOptions,
private val engine: RTCEngine,
) {
suspend fun connect(url: String, token: String, isSecure: Boolean) {
engine.join(url, token, isSecure)
}
@AssistedFactory
interface Factory {
fun create(connectOptions: ConnectOptions): Room
}
}
\ No newline at end of file
... ...
package io.livekit.android.room.track
import org.webrtc.DataChannel
import org.webrtc.MediaStreamTrack
class Track(name: String, state: State) {
var name = name
internal set
var state = state
internal set
inline class Sid(val sid: String)
inline class Cid(val cid: String)
enum class Priority {
STANDARD, HIGH, LOW;
}
enum class State {
ENDED, LIVE, NONE;
}
companion object {
fun stateFromRTCMediaTrackState(trackState: MediaStreamTrack.State): State {
return when (trackState) {
MediaStreamTrack.State.ENDED -> State.ENDED
MediaStreamTrack.State.LIVE -> State.LIVE
}
}
fun stateFromRTCDataChannelState(dataChannelState: DataChannel.State): State {
return when (dataChannelState) {
DataChannel.State.CONNECTING,
DataChannel.State.OPEN -> {
State.LIVE
}
DataChannel.State.CLOSING,
DataChannel.State.CLOSED -> {
State.ENDED
}
}
}
}
}
sealed class TrackException(message: String?, cause: Throwable?) : Exception(message, cause) {
class InvalidTrackTypeException(message: String?, cause: Throwable?) :
TrackException(message, cause)
class DuplicateTrackException(message: String?, cause: Throwable?) :
TrackException(message, cause)
class InvalidTrackStateException(message: String?, cause: Throwable?) :
TrackException(message, cause)
class MediaException(message: String?, cause: Throwable?) : TrackException(message, cause)
class PublishException(message: String?, cause: Throwable?) : TrackException(message, cause)
}
\ No newline at end of file
... ...