正在显示
10 个修改的文件
包含
315 行增加
和
11 行删除
| 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 | +} |
-
请 注册 或 登录 后发表评论