David Liu

Sample app

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,5 +2,5 @@ package io.livekit.android @@ -2,5 +2,5 @@ package io.livekit.android
2 2
3 3
4 data class ConnectOptions( 4 data class ConnectOptions(
5 - val isSecure: Boolean 5 + val isSecure: Boolean = true
6 ) 6 )
@@ -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 +package io.livekit.android.sample
  2 +
  3 +import android.app.Application
  4 +import timber.log.Timber
  5 +
  6 +class SampleApplication : Application() {
  7 +
  8 + override fun onCreate() {
  9 + super.onCreate()
  10 + Timber.plant(Timber.DebugTree())
  11 + }
  12 +}
  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>