正在显示
19 个修改的文件
包含
279 行增加
和
43 行删除
| 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. | 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. |
| 2 | 2 | ||
| 3 | buildscript { | 3 | buildscript { |
| 4 | - ext.kotlin_version = '1.4.30' | 4 | + ext.kotlin_version = '1.4.31' |
| 5 | ext.java_version = JavaVersion.VERSION_1_8 | 5 | ext.java_version = JavaVersion.VERSION_1_8 |
| 6 | repositories { | 6 | repositories { |
| 7 | google() | 7 | google() |
| @@ -60,8 +60,8 @@ dependencies { | @@ -60,8 +60,8 @@ dependencies { | ||
| 60 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" | 60 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" |
| 61 | implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.4.2' | 61 | implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.4.2' |
| 62 | implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.1.0' | 62 | implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.1.0' |
| 63 | - implementation 'org.webrtc:google-webrtc:1.0.32006' | ||
| 64 | - implementation "com.squareup.okhttp3:okhttp:4.9.0" | 63 | + api 'org.webrtc:google-webrtc:1.0.32006' |
| 64 | + api "com.squareup.okhttp3:okhttp:4.9.0" | ||
| 65 | implementation "com.google.protobuf:protobuf-java:${versions.protobuf}" | 65 | implementation "com.google.protobuf:protobuf-java:${versions.protobuf}" |
| 66 | implementation "com.google.protobuf:protobuf-java-util:${versions.protobuf}" | 66 | implementation "com.google.protobuf:protobuf-java-util:${versions.protobuf}" |
| 67 | 67 |
| @@ -2,6 +2,7 @@ package io.livekit.android | @@ -2,6 +2,7 @@ package io.livekit.android | ||
| 2 | 2 | ||
| 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 | 6 | ||
| 6 | class LiveKit { | 7 | class LiveKit { |
| 7 | companion object { | 8 | companion object { |
| @@ -9,8 +10,9 @@ class LiveKit { | @@ -9,8 +10,9 @@ class LiveKit { | ||
| 9 | appContext: Context, | 10 | appContext: Context, |
| 10 | url: String, | 11 | url: String, |
| 11 | token: String, | 12 | token: String, |
| 12 | - options: ConnectOptions | ||
| 13 | - ) { | 13 | + options: ConnectOptions, |
| 14 | + listener: Room.Listener? | ||
| 15 | + ): Room { | ||
| 14 | 16 | ||
| 15 | val component = DaggerLiveKitComponent | 17 | val component = DaggerLiveKitComponent |
| 16 | .factory() | 18 | .factory() |
| @@ -18,7 +20,10 @@ class LiveKit { | @@ -18,7 +20,10 @@ class LiveKit { | ||
| 18 | 20 | ||
| 19 | val room = component.roomFactory() | 21 | val room = component.roomFactory() |
| 20 | .create(options) | 22 | .create(options) |
| 21 | - room.connect(url, token, false) | 23 | + room.listener = listener |
| 24 | + room.connect(url, token, options.isSecure) | ||
| 25 | + | ||
| 26 | + return room | ||
| 22 | } | 27 | } |
| 23 | } | 28 | } |
| 24 | } | 29 | } |
| @@ -3,22 +3,31 @@ package io.livekit.android.dagger | @@ -3,22 +3,31 @@ package io.livekit.android.dagger | ||
| 3 | import com.google.protobuf.util.JsonFormat | 3 | import com.google.protobuf.util.JsonFormat |
| 4 | import dagger.Module | 4 | import dagger.Module |
| 5 | import dagger.Provides | 5 | import dagger.Provides |
| 6 | +import dagger.Reusable | ||
| 7 | +import kotlinx.serialization.json.Json | ||
| 6 | import javax.inject.Named | 8 | import javax.inject.Named |
| 7 | 9 | ||
| 8 | @Module | 10 | @Module |
| 9 | class JsonFormatModule { | 11 | class JsonFormatModule { |
| 10 | companion object { | 12 | companion object { |
| 11 | @Provides | 13 | @Provides |
| 12 | - fun jsonFormatParser(): JsonFormat.Parser { | 14 | + fun protobufJsonFormatParser(): JsonFormat.Parser { |
| 13 | return JsonFormat.parser() | 15 | return JsonFormat.parser() |
| 14 | } | 16 | } |
| 15 | 17 | ||
| 16 | @Provides | 18 | @Provides |
| 17 | - fun jsonFormatPrinter(): JsonFormat.Printer { | 19 | + fun protobufJsonFormatPrinter(): JsonFormat.Printer { |
| 18 | return JsonFormat.printer() | 20 | return JsonFormat.printer() |
| 19 | } | 21 | } |
| 20 | 22 | ||
| 21 | @Provides | 23 | @Provides |
| 24 | + @Reusable | ||
| 25 | + fun kotlinSerializationJson(): Json = | ||
| 26 | + Json { | ||
| 27 | + ignoreUnknownKeys = true | ||
| 28 | + } | ||
| 29 | + | ||
| 30 | + @Provides | ||
| 22 | @Named(InjectionNames.SIGNAL_JSON_ENABLED) | 31 | @Named(InjectionNames.SIGNAL_JSON_ENABLED) |
| 23 | fun signalJsonEnabled(): Boolean = false | 32 | fun signalJsonEnabled(): Boolean = false |
| 24 | } | 33 | } |
| @@ -4,6 +4,7 @@ import android.content.Context | @@ -4,6 +4,7 @@ import android.content.Context | ||
| 4 | import com.github.ajalt.timberkt.Timber | 4 | import com.github.ajalt.timberkt.Timber |
| 5 | import dagger.Module | 5 | import dagger.Module |
| 6 | import dagger.Provides | 6 | import dagger.Provides |
| 7 | +import org.webrtc.EglBase | ||
| 7 | import org.webrtc.PeerConnectionFactory | 8 | import org.webrtc.PeerConnectionFactory |
| 8 | import org.webrtc.audio.AudioDeviceModule | 9 | import org.webrtc.audio.AudioDeviceModule |
| 9 | import org.webrtc.audio.JavaAudioDeviceModule | 10 | import org.webrtc.audio.JavaAudioDeviceModule |
| @@ -99,5 +100,12 @@ class RTCModule { | @@ -99,5 +100,12 @@ class RTCModule { | ||
| 99 | .setAudioDeviceModule(audioDeviceModule) | 100 | .setAudioDeviceModule(audioDeviceModule) |
| 100 | .createPeerConnectionFactory() | 101 | .createPeerConnectionFactory() |
| 101 | } | 102 | } |
| 103 | + | ||
| 104 | + | ||
| 105 | + @Provides | ||
| 106 | + @Singleton | ||
| 107 | + fun eglBase(): EglBase { | ||
| 108 | + return EglBase.create() | ||
| 109 | + } | ||
| 102 | } | 110 | } |
| 103 | } | 111 | } |
| @@ -3,4 +3,4 @@ package io.livekit.android.room | @@ -3,4 +3,4 @@ package io.livekit.android.room | ||
| 3 | import kotlinx.serialization.Serializable | 3 | import kotlinx.serialization.Serializable |
| 4 | 4 | ||
| 5 | @Serializable | 5 | @Serializable |
| 6 | -data class IceCandidateJSON(val sdp: String, val sdpMLineIndex: Int, val sdpMid: String?) | ||
| 6 | +data class IceCandidateJSON(val candidate: String, val sdpMLineIndex: Int, val sdpMid: String?) |
| @@ -25,13 +25,15 @@ class RTCClient | @@ -25,13 +25,15 @@ class RTCClient | ||
| 25 | @Inject | 25 | @Inject |
| 26 | constructor( | 26 | constructor( |
| 27 | private val websocketFactory: WebSocket.Factory, | 27 | private val websocketFactory: WebSocket.Factory, |
| 28 | - private val fromJson: JsonFormat.Parser, | ||
| 29 | - private val toJson: JsonFormat.Printer, | 28 | + private val fromJsonProtobuf: JsonFormat.Parser, |
| 29 | + private val toJsonProtobuf: JsonFormat.Printer, | ||
| 30 | + private val json: Json, | ||
| 30 | @Named(InjectionNames.SIGNAL_JSON_ENABLED) | 31 | @Named(InjectionNames.SIGNAL_JSON_ENABLED) |
| 31 | private val useJson: Boolean, | 32 | private val useJson: Boolean, |
| 32 | ) : WebSocketListener() { | 33 | ) : WebSocketListener() { |
| 33 | 34 | ||
| 34 | - private var isConnected = false | 35 | + var isConnected = false |
| 36 | + private set | ||
| 35 | private var currentWs: WebSocket? = null | 37 | private var currentWs: WebSocket? = null |
| 36 | var listener: Listener? = null | 38 | var listener: Listener? = null |
| 37 | 39 | ||
| @@ -52,12 +54,14 @@ constructor( | @@ -52,12 +54,14 @@ constructor( | ||
| 52 | } | 54 | } |
| 53 | 55 | ||
| 54 | override fun onOpen(webSocket: WebSocket, response: Response) { | 56 | override fun onOpen(webSocket: WebSocket, response: Response) { |
| 57 | + Timber.v { response.message } | ||
| 55 | super.onOpen(webSocket, response) | 58 | super.onOpen(webSocket, response) |
| 56 | } | 59 | } |
| 57 | 60 | ||
| 58 | override fun onMessage(webSocket: WebSocket, text: String) { | 61 | override fun onMessage(webSocket: WebSocket, text: String) { |
| 62 | + Timber.v { text } | ||
| 59 | val signalResponseBuilder = Rtc.SignalResponse.newBuilder() | 63 | val signalResponseBuilder = Rtc.SignalResponse.newBuilder() |
| 60 | - fromJson.merge(text, signalResponseBuilder) | 64 | + fromJsonProtobuf.merge(text, signalResponseBuilder) |
| 61 | val response = signalResponseBuilder.build() | 65 | val response = signalResponseBuilder.build() |
| 62 | 66 | ||
| 63 | handleSignalResponse(response) | 67 | handleSignalResponse(response) |
| @@ -73,14 +77,18 @@ constructor( | @@ -73,14 +77,18 @@ constructor( | ||
| 73 | } | 77 | } |
| 74 | 78 | ||
| 75 | override fun onClosed(webSocket: WebSocket, code: Int, reason: String) { | 79 | override fun onClosed(webSocket: WebSocket, code: Int, reason: String) { |
| 80 | + Timber.v { "websocket closed" } | ||
| 76 | super.onClosed(webSocket, code, reason) | 81 | super.onClosed(webSocket, code, reason) |
| 77 | } | 82 | } |
| 78 | 83 | ||
| 79 | override fun onClosing(webSocket: WebSocket, code: Int, reason: String) { | 84 | override fun onClosing(webSocket: WebSocket, code: Int, reason: String) { |
| 85 | + Timber.v { "websocket closing" } | ||
| 80 | super.onClosing(webSocket, code, reason) | 86 | super.onClosing(webSocket, code, reason) |
| 81 | } | 87 | } |
| 82 | 88 | ||
| 83 | override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) { | 89 | override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) { |
| 90 | + Timber.v(t) { "websocket failure: ${response}" } | ||
| 91 | + | ||
| 84 | super.onFailure(webSocket, t, response) | 92 | super.onFailure(webSocket, t, response) |
| 85 | } | 93 | } |
| 86 | 94 | ||
| @@ -128,13 +136,13 @@ constructor( | @@ -128,13 +136,13 @@ constructor( | ||
| 128 | 136 | ||
| 129 | fun sendCandidate(candidate: IceCandidate, target: Rtc.SignalTarget){ | 137 | fun sendCandidate(candidate: IceCandidate, target: Rtc.SignalTarget){ |
| 130 | val iceCandidateJSON = IceCandidateJSON( | 138 | val iceCandidateJSON = IceCandidateJSON( |
| 131 | - sdp = candidate.sdp, | 139 | + candidate = candidate.sdp, |
| 132 | sdpMid = candidate.sdpMid, | 140 | sdpMid = candidate.sdpMid, |
| 133 | sdpMLineIndex = candidate.sdpMLineIndex | 141 | sdpMLineIndex = candidate.sdpMLineIndex |
| 134 | ) | 142 | ) |
| 135 | 143 | ||
| 136 | val trickleRequest = Rtc.TrickleRequest.newBuilder() | 144 | val trickleRequest = Rtc.TrickleRequest.newBuilder() |
| 137 | - .setCandidateInit(Json.encodeToString(iceCandidateJSON)) | 145 | + .setCandidateInit(json.encodeToString(iceCandidateJSON)) |
| 138 | .setTarget(target) | 146 | .setTarget(target) |
| 139 | .build() | 147 | .build() |
| 140 | 148 | ||
| @@ -174,12 +182,12 @@ constructor( | @@ -174,12 +182,12 @@ constructor( | ||
| 174 | 182 | ||
| 175 | fun sendRequest(request: Rtc.SignalRequest) { | 183 | fun sendRequest(request: Rtc.SignalRequest) { |
| 176 | Timber.v { "sending request: $request" } | 184 | Timber.v { "sending request: $request" } |
| 177 | - if (!isConnected || currentWs != null) { | 185 | + if (!isConnected || currentWs == null) { |
| 178 | throw IllegalStateException("not connected!") | 186 | throw IllegalStateException("not connected!") |
| 179 | } | 187 | } |
| 180 | val sent: Boolean | 188 | val sent: Boolean |
| 181 | if (useJson) { | 189 | if (useJson) { |
| 182 | - val message = toJson.print(request) | 190 | + val message = toJsonProtobuf.print(request) |
| 183 | sent = currentWs?.send(message) ?: false | 191 | sent = currentWs?.send(message) ?: false |
| 184 | } else { | 192 | } else { |
| 185 | val message = request.toByteArray().toByteString() | 193 | val message = request.toByteArray().toByteString() |
| @@ -199,7 +207,7 @@ constructor( | @@ -199,7 +207,7 @@ constructor( | ||
| 199 | isConnected = true | 207 | isConnected = true |
| 200 | listener?.onJoin(response.join) | 208 | listener?.onJoin(response.join) |
| 201 | } else { | 209 | } else { |
| 202 | - Timber.e { "Received response while not connected. ${toJson.print(response)}" } | 210 | + Timber.e { "Received response while not connected. ${toJsonProtobuf.print(response)}" } |
| 203 | } | 211 | } |
| 204 | return | 212 | return |
| 205 | } | 213 | } |
| @@ -214,11 +222,11 @@ constructor( | @@ -214,11 +222,11 @@ constructor( | ||
| 214 | } | 222 | } |
| 215 | Rtc.SignalResponse.MessageCase.TRICKLE -> { | 223 | Rtc.SignalResponse.MessageCase.TRICKLE -> { |
| 216 | val iceCandidateJson = | 224 | val iceCandidateJson = |
| 217 | - Json.decodeFromString<IceCandidateJSON>(response.trickle.candidateInit) | 225 | + json.decodeFromString<IceCandidateJSON>(response.trickle.candidateInit) |
| 218 | val iceCandidate = IceCandidate( | 226 | val iceCandidate = IceCandidate( |
| 219 | iceCandidateJson.sdpMid, | 227 | iceCandidateJson.sdpMid, |
| 220 | iceCandidateJson.sdpMLineIndex, | 228 | iceCandidateJson.sdpMLineIndex, |
| 221 | - iceCandidateJson.sdp | 229 | + iceCandidateJson.candidate |
| 222 | ) | 230 | ) |
| 223 | listener?.onTrickle(iceCandidate, response.trickle.target) | 231 | listener?.onTrickle(iceCandidate, response.trickle.target) |
| 224 | } | 232 | } |
| @@ -96,24 +96,26 @@ constructor( | @@ -96,24 +96,26 @@ constructor( | ||
| 96 | } | 96 | } |
| 97 | 97 | ||
| 98 | fun negotiate() { | 98 | fun negotiate() { |
| 99 | + | ||
| 100 | + if (!client.isConnected) { | ||
| 101 | + return | ||
| 102 | + } | ||
| 99 | coroutineScope.launch { | 103 | coroutineScope.launch { |
| 100 | val offerObserver = CoroutineSdpObserver() | 104 | val offerObserver = CoroutineSdpObserver() |
| 101 | publisher.peerConnection.createOffer(offerObserver, OFFER_CONSTRAINTS) | 105 | publisher.peerConnection.createOffer(offerObserver, OFFER_CONSTRAINTS) |
| 102 | - val offerOutcome = offerObserver.awaitCreate() | ||
| 103 | - val sdpOffer = when (offerOutcome) { | ||
| 104 | - is Either.Left -> offerOutcome.value | 106 | + val sdpOffer = when (val outcome = offerObserver.awaitCreate()) { |
| 107 | + is Either.Left -> outcome.value | ||
| 105 | is Either.Right -> { | 108 | is Either.Right -> { |
| 106 | - Timber.d { "error creating offer: ${offerOutcome.value}" } | 109 | + Timber.d { "error creating offer: ${outcome.value}" } |
| 107 | return@launch | 110 | return@launch |
| 108 | } | 111 | } |
| 109 | } | 112 | } |
| 110 | 113 | ||
| 111 | val setObserver = CoroutineSdpObserver() | 114 | val setObserver = CoroutineSdpObserver() |
| 112 | publisher.peerConnection.setLocalDescription(setObserver, sdpOffer) | 115 | publisher.peerConnection.setLocalDescription(setObserver, sdpOffer) |
| 113 | - val setOutcome = setObserver.awaitSet() | ||
| 114 | - when (setOutcome) { | 116 | + when (val outcome = setObserver.awaitSet()) { |
| 115 | is Either.Left -> client.sendOffer(sdpOffer) | 117 | is Either.Left -> client.sendOffer(sdpOffer) |
| 116 | - is Either.Right -> Timber.d { "error setting local description: ${setOutcome.value}" } | 118 | + is Either.Right -> Timber.d { "error setting local description: ${outcome.value}" } |
| 117 | } | 119 | } |
| 118 | } | 120 | } |
| 119 | } | 121 | } |
| @@ -13,15 +13,14 @@ import io.livekit.android.room.track.Track | @@ -13,15 +13,14 @@ import io.livekit.android.room.track.Track | ||
| 13 | import io.livekit.android.room.util.unpackedTrackLabel | 13 | 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.DataChannel | ||
| 17 | -import org.webrtc.MediaStream | ||
| 18 | -import org.webrtc.MediaStreamTrack | 16 | +import org.webrtc.* |
| 19 | 17 | ||
| 20 | class Room | 18 | class Room |
| 21 | @AssistedInject | 19 | @AssistedInject |
| 22 | constructor( | 20 | constructor( |
| 23 | @Assisted private val connectOptions: ConnectOptions, | 21 | @Assisted private val connectOptions: ConnectOptions, |
| 24 | private val engine: RTCEngine, | 22 | private val engine: RTCEngine, |
| 23 | + private val eglBase: EglBase, | ||
| 25 | ) : RTCEngine.Listener { | 24 | ) : RTCEngine.Listener { |
| 26 | init { | 25 | init { |
| 27 | engine.listener = this | 26 | engine.listener = this |
| @@ -74,7 +73,7 @@ constructor( | @@ -74,7 +73,7 @@ constructor( | ||
| 74 | removedParticipant.unpublishTrack(publication.trackSid) | 73 | removedParticipant.unpublishTrack(publication.trackSid) |
| 75 | } | 74 | } |
| 76 | 75 | ||
| 77 | - listener?.onparticipantDisconnected(this, removedParticipant) | 76 | + listener?.onParticipantDisconnected(this, removedParticipant) |
| 78 | } | 77 | } |
| 79 | 78 | ||
| 80 | private fun getOrCreateRemoteParticipant( | 79 | private fun getOrCreateRemoteParticipant( |
| @@ -136,7 +135,7 @@ constructor( | @@ -136,7 +135,7 @@ constructor( | ||
| 136 | fun onConnect(room: Room) | 135 | fun onConnect(room: Room) |
| 137 | fun onDisconnect(room: Room, error: Exception?) | 136 | fun onDisconnect(room: Room, error: Exception?) |
| 138 | fun onParticipantConnected(room: Room, participant: RemoteParticipant) | 137 | fun onParticipantConnected(room: Room, participant: RemoteParticipant) |
| 139 | - fun onparticipantDisconnected(room: Room, participant: RemoteParticipant) | 138 | + fun onParticipantDisconnected(room: Room, participant: RemoteParticipant) |
| 140 | fun onFailedToConnect(room: Room, error: Exception) | 139 | fun onFailedToConnect(room: Room, error: Exception) |
| 141 | fun onReconnecting(room: Room, error: Exception) | 140 | fun onReconnecting(room: Room, error: Exception) |
| 142 | fun onReconnect(room: Room) | 141 | fun onReconnect(room: Room) |
| @@ -225,4 +224,10 @@ constructor( | @@ -225,4 +224,10 @@ constructor( | ||
| 225 | override fun onFailToConnect(error: Exception) { | 224 | override fun onFailToConnect(error: Exception) { |
| 226 | listener?.onFailedToConnect(this, error) | 225 | listener?.onFailedToConnect(this, error) |
| 227 | } | 226 | } |
| 227 | + | ||
| 228 | + fun setupVideo(viewRenderer: SurfaceViewRenderer) { | ||
| 229 | + viewRenderer.init(eglBase.eglBaseContext, null) | ||
| 230 | + viewRenderer.setScalingType(RendererCommon.ScalingType.SCALE_ASPECT_FIT) | ||
| 231 | + viewRenderer.setEnableHardwareScaler(false /* enabled */); | ||
| 232 | + } | ||
| 228 | } | 233 | } |
| @@ -19,11 +19,11 @@ class LocalVideoTrack( | @@ -19,11 +19,11 @@ class LocalVideoTrack( | ||
| 19 | peerConnectionFactory: PeerConnectionFactory, | 19 | peerConnectionFactory: PeerConnectionFactory, |
| 20 | context: Context, | 20 | context: Context, |
| 21 | enabled: Boolean, | 21 | enabled: Boolean, |
| 22 | - name: String | 22 | + name: String, |
| 23 | + rootEglBase: EglBase, | ||
| 23 | ): LocalVideoTrack { | 24 | ): LocalVideoTrack { |
| 24 | val source = peerConnectionFactory.createVideoSource(false) | 25 | val source = peerConnectionFactory.createVideoSource(false) |
| 25 | val capturer = createVideoCapturer(context) ?: TODO() | 26 | val capturer = createVideoCapturer(context) ?: TODO() |
| 26 | - val rootEglBase = EglBase.create() | ||
| 27 | capturer.initialize( | 27 | capturer.initialize( |
| 28 | SurfaceTextureHelper.create("CaptureThread", rootEglBase.eglBaseContext), | 28 | SurfaceTextureHelper.create("CaptureThread", rootEglBase.eglBaseContext), |
| 29 | context, | 29 | context, |
| 1 | apply plugin: 'com.android.application' | 1 | apply plugin: 'com.android.application' |
| 2 | 2 | ||
| 3 | apply plugin: 'kotlin-android' | 3 | apply plugin: 'kotlin-android' |
| 4 | +apply plugin: 'kotlin-parcelize' | ||
| 4 | 5 | ||
| 5 | android { | 6 | android { |
| 6 | compileSdkVersion 29 | 7 | compileSdkVersion 29 |
| @@ -19,20 +20,24 @@ android { | @@ -19,20 +20,24 @@ android { | ||
| 19 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' | 20 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' |
| 20 | } | 21 | } |
| 21 | } | 22 | } |
| 22 | - android { | ||
| 23 | - compileOptions { | ||
| 24 | - sourceCompatibility java_version | ||
| 25 | - targetCompatibility java_version | ||
| 26 | - } | 23 | + compileOptions { |
| 24 | + sourceCompatibility java_version | ||
| 25 | + targetCompatibility java_version | ||
| 26 | + } | ||
| 27 | + buildFeatures { | ||
| 28 | + viewBinding = true | ||
| 27 | } | 29 | } |
| 28 | } | 30 | } |
| 29 | 31 | ||
| 30 | dependencies { | 32 | dependencies { |
| 31 | implementation fileTree(dir: 'libs', include: ['*.jar']) | 33 | implementation fileTree(dir: 'libs', include: ['*.jar']) |
| 32 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" | 34 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" |
| 35 | + implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.4.2' | ||
| 33 | implementation 'com.google.android.material:material:1.3.0' | 36 | implementation 'com.google.android.material:material:1.3.0' |
| 34 | implementation 'androidx.appcompat:appcompat:1.2.0' | 37 | implementation 'androidx.appcompat:appcompat:1.2.0' |
| 35 | implementation 'androidx.core:core-ktx:1.3.2' | 38 | implementation 'androidx.core:core-ktx:1.3.2' |
| 39 | + implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.3.0' | ||
| 40 | + implementation 'com.github.ajalt:timberkt:1.5.1' | ||
| 36 | implementation project(":livekit-android-sdk") | 41 | implementation project(":livekit-android-sdk") |
| 37 | testImplementation 'junit:junit:4.12' | 42 | testImplementation 'junit:junit:4.12' |
| 38 | androidTestImplementation 'androidx.test.ext:junit:1.1.2' | 43 | androidTestImplementation 'androidx.test.ext:junit:1.1.2' |
| 1 | <manifest xmlns:android="http://schemas.android.com/apk/res/android" | 1 | <manifest xmlns:android="http://schemas.android.com/apk/res/android" |
| 2 | - package="io.livekit.android"> | 2 | + package="io.livekit.android.sample"> |
| 3 | + | ||
| 4 | + <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> | ||
| 5 | + <uses-permission android:name="android.permission.INTERNET" /> | ||
| 3 | 6 | ||
| 4 | <application | 7 | <application |
| 8 | + android:name=".SampleApplication" | ||
| 9 | + android:networkSecurityConfig="@xml/network_security_config" | ||
| 5 | android:allowBackup="true" | 10 | android:allowBackup="true" |
| 6 | android:icon="@mipmap/ic_launcher" | 11 | android:icon="@mipmap/ic_launcher" |
| 7 | android:label="@string/app_name" | 12 | android:label="@string/app_name" |
| 8 | android:roundIcon="@mipmap/ic_launcher_round" | 13 | android:roundIcon="@mipmap/ic_launcher_round" |
| 9 | android:supportsRtl="true" | 14 | android:supportsRtl="true" |
| 10 | - android:theme="@style/AppTheme" > | ||
| 11 | - <activity android:name=".sample.MainActivity" /> | 15 | + android:theme="@style/AppTheme"> |
| 16 | + <activity android:name=".MainActivity"> | ||
| 17 | + <intent-filter> | ||
| 18 | + <action android:name="android.intent.action.MAIN" /> | ||
| 19 | + <category android:name="android.intent.category.LAUNCHER" /> | ||
| 20 | + </intent-filter> | ||
| 21 | + </activity> | ||
| 22 | + <activity android:name=".CallActivity" /> | ||
| 12 | </application> | 23 | </application> |
| 13 | </manifest> | 24 | </manifest> |
| 1 | +package io.livekit.android.sample | ||
| 2 | + | ||
| 3 | +import android.os.Bundle | ||
| 4 | +import android.os.Parcelable | ||
| 5 | +import androidx.appcompat.app.AppCompatActivity | ||
| 6 | +import androidx.lifecycle.lifecycleScope | ||
| 7 | +import io.livekit.android.ConnectOptions | ||
| 8 | +import io.livekit.android.LiveKit | ||
| 9 | +import io.livekit.android.room.Room | ||
| 10 | +import io.livekit.android.room.participant.Participant | ||
| 11 | +import io.livekit.android.room.participant.RemoteParticipant | ||
| 12 | +import io.livekit.android.room.track.VideoTrack | ||
| 13 | +import io.livekit.android.sample.databinding.CallActivityBinding | ||
| 14 | +import kotlinx.coroutines.launch | ||
| 15 | +import kotlinx.parcelize.Parcelize | ||
| 16 | + | ||
| 17 | +class CallActivity : AppCompatActivity() { | ||
| 18 | + | ||
| 19 | + lateinit var binding: CallActivityBinding | ||
| 20 | + override fun onCreate(savedInstanceState: Bundle?) { | ||
| 21 | + super.onCreate(savedInstanceState) | ||
| 22 | + | ||
| 23 | + binding = CallActivityBinding.inflate(layoutInflater) | ||
| 24 | + | ||
| 25 | + setContentView(binding.root) | ||
| 26 | + | ||
| 27 | + val args = intent.getParcelableExtra<BundleArgs>(KEY_ARGS) | ||
| 28 | + if (args == null) { | ||
| 29 | + finish() | ||
| 30 | + return | ||
| 31 | + } | ||
| 32 | + | ||
| 33 | + lifecycleScope.launch { | ||
| 34 | + | ||
| 35 | + val room = LiveKit.connect( | ||
| 36 | + applicationContext, | ||
| 37 | + args.url, | ||
| 38 | + args.token, | ||
| 39 | + ConnectOptions(false), | ||
| 40 | + object : Room.Listener { | ||
| 41 | + | ||
| 42 | + var loadedParticipant = false | ||
| 43 | + override fun onConnect(room: Room) { | ||
| 44 | + } | ||
| 45 | + | ||
| 46 | + override fun onDisconnect(room: Room, error: Exception?) { | ||
| 47 | + } | ||
| 48 | + | ||
| 49 | + override fun onParticipantConnected( | ||
| 50 | + room: Room, | ||
| 51 | + participant: RemoteParticipant | ||
| 52 | + ) { | ||
| 53 | + if (!loadedParticipant) { | ||
| 54 | + room.setupVideo(binding.fullscreenVideoView) | ||
| 55 | + participant.remoteVideoTracks | ||
| 56 | + .first() | ||
| 57 | + .track | ||
| 58 | + .let { it as? VideoTrack } | ||
| 59 | + ?.addRenderer(binding.fullscreenVideoView) | ||
| 60 | + } | ||
| 61 | + } | ||
| 62 | + | ||
| 63 | + override fun onParticipantDisconnected( | ||
| 64 | + room: Room, | ||
| 65 | + participant: RemoteParticipant | ||
| 66 | + ) { | ||
| 67 | + } | ||
| 68 | + | ||
| 69 | + override fun onFailedToConnect(room: Room, error: Exception) { | ||
| 70 | + } | ||
| 71 | + | ||
| 72 | + override fun onReconnecting(room: Room, error: Exception) { | ||
| 73 | + } | ||
| 74 | + | ||
| 75 | + override fun onReconnect(room: Room) { | ||
| 76 | + } | ||
| 77 | + | ||
| 78 | + override fun onStartRecording(room: Room) { | ||
| 79 | + } | ||
| 80 | + | ||
| 81 | + override fun onStopRecording(room: Room) { | ||
| 82 | + } | ||
| 83 | + | ||
| 84 | + override fun onActiveSpeakersChanged(speakers: List<Participant>, room: Room) { | ||
| 85 | + } | ||
| 86 | + | ||
| 87 | + } | ||
| 88 | + ) | ||
| 89 | + } | ||
| 90 | + } | ||
| 91 | + | ||
| 92 | + | ||
| 93 | + companion object { | ||
| 94 | + const val KEY_ARGS = "args" | ||
| 95 | + } | ||
| 96 | + | ||
| 97 | + @Parcelize | ||
| 98 | + data class BundleArgs(val url: String, val token: String) : Parcelable | ||
| 99 | +} |
| 1 | package io.livekit.android.sample | 1 | package io.livekit.android.sample |
| 2 | 2 | ||
| 3 | +import android.content.Intent | ||
| 3 | import android.os.Bundle | 4 | import android.os.Bundle |
| 5 | +import android.text.SpannableStringBuilder | ||
| 4 | import androidx.appcompat.app.AppCompatActivity | 6 | import androidx.appcompat.app.AppCompatActivity |
| 7 | +import io.livekit.android.sample.databinding.MainActivityBinding | ||
| 5 | 8 | ||
| 6 | class MainActivity : AppCompatActivity() { | 9 | class MainActivity : AppCompatActivity() { |
| 7 | override fun onCreate(savedInstanceState: Bundle?) { | 10 | override fun onCreate(savedInstanceState: Bundle?) { |
| 8 | super.onCreate(savedInstanceState) | 11 | super.onCreate(savedInstanceState) |
| 12 | + | ||
| 13 | + val binding = MainActivityBinding.inflate(layoutInflater) | ||
| 14 | + binding.run { | ||
| 15 | + url.editText?.text = URL | ||
| 16 | + token.editText?.text = TOKEN | ||
| 17 | + connectButton.setOnClickListener { | ||
| 18 | + val intent = Intent(this@MainActivity, CallActivity::class.java).apply { | ||
| 19 | + putExtra( | ||
| 20 | + CallActivity.KEY_ARGS, | ||
| 21 | + CallActivity.BundleArgs( | ||
| 22 | + url.editText?.text.toString(), | ||
| 23 | + token.editText?.text.toString() | ||
| 24 | + ) | ||
| 25 | + ) | ||
| 26 | + } | ||
| 27 | + | ||
| 28 | + startActivity(intent) | ||
| 29 | + } | ||
| 30 | + } | ||
| 31 | + | ||
| 32 | + setContentView(binding.root) | ||
| 33 | + } | ||
| 34 | + | ||
| 35 | + companion object { | ||
| 36 | + val URL = SpannableStringBuilder("192.168.11.2:7880") | ||
| 37 | + val TOKEN = | ||
| 38 | + SpannableStringBuilder("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2MTg1NzgxNzMsImlzcyI6IkFQSXdMZWFoN2c0ZnVMWURZQUplYUtzU0UiLCJqdGkiOiJwaG9uZSIsIm5iZiI6MTYxNTk4NjE3MywidmlkZW8iOnsicm9vbSI6Im15cm9vbSIsInJvb21Kb2luIjp0cnVlfX0.O3UedhM9lwdPxsZJQoTfVk0qXc-0ukjV6oZCBIaRTck") | ||
| 9 | } | 39 | } |
| 10 | } | 40 | } |
| 1 | + | ||
| 2 | +<FrameLayout | ||
| 3 | + xmlns:android="http://schemas.android.com/apk/res/android" | ||
| 4 | + xmlns:tools="http://schemas.android.com/tools" | ||
| 5 | + android:layout_width="match_parent" | ||
| 6 | + android:layout_height="match_parent"> | ||
| 7 | + | ||
| 8 | + <org.webrtc.SurfaceViewRenderer | ||
| 9 | + android:id="@+id/fullscreen_video_view" | ||
| 10 | + android:layout_width="wrap_content" | ||
| 11 | + android:layout_height="wrap_content" | ||
| 12 | + android:layout_gravity="center" /> | ||
| 13 | + | ||
| 14 | + <org.webrtc.SurfaceViewRenderer | ||
| 15 | + android:id="@+id/pip_video_view" | ||
| 16 | + android:layout_height="144dp" | ||
| 17 | + android:layout_width="wrap_content" | ||
| 18 | + android:layout_gravity="bottom|end" | ||
| 19 | + android:layout_margin="16dp"/> | ||
| 20 | + | ||
| 21 | + <FrameLayout | ||
| 22 | + android:id="@+id/call_fragment_container" | ||
| 23 | + android:layout_width="match_parent" | ||
| 24 | + android:layout_height="match_parent" /> | ||
| 25 | + <FrameLayout | ||
| 26 | + android:id="@+id/hud_fragment_container" | ||
| 27 | + android:layout_width="match_parent" | ||
| 28 | + android:layout_height="match_parent" /> | ||
| 29 | + | ||
| 30 | +</FrameLayout> |
| 1 | +<?xml version="1.0" encoding="utf-8"?> | ||
| 2 | +<network-security-config> | ||
| 3 | + <domain-config cleartextTrafficPermitted="true"> | ||
| 4 | + <domain includeSubdomains="true">example.com</domain> | ||
| 5 | + </domain-config> | ||
| 6 | + | ||
| 7 | + <base-config cleartextTrafficPermitted="true"> | ||
| 8 | + <trust-anchors> | ||
| 9 | + <certificates src="system" /> | ||
| 10 | + </trust-anchors> | ||
| 11 | + </base-config> | ||
| 12 | +</network-security-config> |
-
请 注册 或 登录 后发表评论