Skip to content

Commit b733c47

Browse files
MaxHeimbrockclaude
andauthored
Sample apps using the Token Source implementations (#995)
* feat: use TokenSource in sample apps with a development token server option The connect screens of sample-app and sample-app-compose get two tabs: the existing literal URL/token entry and a new development token server mode that takes a token server ID. Both paths now go through the SDK's TokenSource API: the shared CallViewModel receives a parcelable TokenSourceArgs and fetches connection details at connect time via TokenSource.fromLiteral or TokenSource.fromDevelopmentTokenServer before calling room.connect. The token server ID is persisted alongside the existing url/token preferences and can be defaulted via the livekitSampleTokenServerId gradle property. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat: add connection option fields to the development token server tab The dev token server tab in both sample apps now takes optional room name, participant name, participant identity, and agent name inputs. They travel through TokenSourceArgs.DevTokenServer and are passed to the token source fetch as TokenRequestOptions; blank fields are treated as unset so the server defaults apply. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Add empty changeset, make list a scrolling list for token source tab * fix: create TokenSource once and cache it so reconnects reuse credentials Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Apply PR comment --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 parent eeec899 commit b733c47

11 files changed

Lines changed: 467 additions & 111 deletions

File tree

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
---
2+
---
3+
4+
Use the TokenSource implementations in the sample apps, with a development token server option. Sample-app-only change; no SDK release needed.

sample-app-common/build.gradle

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,13 @@ def getDefaultToken() {
1212
return hasProperty('livekitSampleToken') ? livekitSampleToken : ""
1313
}
1414

15+
def getDefaultTokenServerId() {
16+
return hasProperty('livekitSampleTokenServerId') ? livekitSampleTokenServerId : ""
17+
}
18+
1519
final url = getDefaultUrl()
1620
final token = getDefaultToken()
21+
final tokenServerId = getDefaultTokenServerId()
1722

1823
android {
1924
namespace "io.livekit.android.sample.common"
@@ -34,11 +39,13 @@ android {
3439

3540
buildConfigField "String", "DEFAULT_URL", "\"$url\""
3641
buildConfigField "String", "DEFAULT_TOKEN", "\"$token\""
42+
buildConfigField "String", "DEFAULT_TOKEN_SERVER_ID", "\"$tokenServerId\""
3743
}
3844

3945
debug {
4046
buildConfigField "String", "DEFAULT_URL", "\"$url\""
4147
buildConfigField "String", "DEFAULT_TOKEN", "\"$token\""
48+
buildConfigField "String", "DEFAULT_TOKEN_SERVER_ID", "\"$tokenServerId\""
4249
}
4350
}
4451
buildFeatures {

sample-app-common/src/main/java/io/livekit/android/sample/CallViewModel.kt

Lines changed: 42 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,14 @@ import io.livekit.android.room.track.screencapture.ScreenCaptureParams
5151
import io.livekit.android.room.track.video.CameraCapturerUtils
5252
import io.livekit.android.rpc.RpcError
5353
import io.livekit.android.sample.model.StressTest
54+
import io.livekit.android.sample.model.TokenSourceArgs
5455
import io.livekit.android.sample.service.ForegroundService
56+
import io.livekit.android.token.ConfigurableTokenSource
57+
import io.livekit.android.token.FixedTokenSource
58+
import io.livekit.android.token.TokenRequestOptions
59+
import io.livekit.android.token.TokenSource
60+
import io.livekit.android.token.TokenSourceResponse
61+
import io.livekit.android.token.cached
5562
import io.livekit.android.util.LKLog
5663
import io.livekit.android.util.flow
5764
import kotlinx.coroutines.Dispatchers
@@ -68,8 +75,7 @@ import livekit.org.webrtc.CameraXHelper
6875

6976
@OptIn(ExperimentalCamera2Interop::class)
7077
class CallViewModel(
71-
val url: String,
72-
val token: String,
78+
val tokenSourceArgs: TokenSourceArgs,
7379
application: Application,
7480
val e2ee: Boolean = false,
7581
val e2eeKey: String? = "",
@@ -149,6 +155,20 @@ class CallViewModel(
149155
private val mutableHandlers = MutableStateFlow<List<RpcHandlerState>>(emptyList())
150156
val handlers: StateFlow<List<RpcHandlerState>> = mutableHandlers
151157

158+
/**
159+
* The token source used to fetch connection details for the room.
160+
*
161+
* Created once and (for the development token server) wrapped with [cached], so
162+
* reconnects reuse the same credentials until the token expires. Otherwise the
163+
* development token server would issue new random values on every fetch when no
164+
* explicit room name/participant identity is set, causing reconnects to join a
165+
* different room.
166+
*/
167+
private val tokenSource: TokenSource = when (tokenSourceArgs) {
168+
is TokenSourceArgs.Literal -> TokenSource.fromLiteral(tokenSourceArgs.url, tokenSourceArgs.token)
169+
is TokenSourceArgs.DevTokenServer -> TokenSource.fromDevelopmentTokenServer(tokenSourceArgs.tokenServerId).cached()
170+
}
171+
152172
init {
153173

154174
CameraXHelper.createCameraProvider(ProcessLifecycleOwner.get()).let {
@@ -255,12 +275,28 @@ class CallViewModel(
255275
}
256276
}
257277

278+
/**
279+
* Fetches the connection details used to connect to the room from [tokenSource].
280+
*/
281+
private suspend fun fetchConnectionDetails(): Result<TokenSourceResponse> = when (tokenSourceArgs) {
282+
is TokenSourceArgs.Literal -> (tokenSource as FixedTokenSource).fetch()
283+
is TokenSourceArgs.DevTokenServer -> (tokenSource as ConfigurableTokenSource).fetch(
284+
TokenRequestOptions(
285+
roomName = tokenSourceArgs.roomName,
286+
participantName = tokenSourceArgs.participantName,
287+
participantIdentity = tokenSourceArgs.participantIdentity,
288+
agentName = tokenSourceArgs.agentName,
289+
),
290+
)
291+
}
292+
258293
private suspend fun connectToRoom() {
259294
try {
260295
room.e2eeOptions = getE2EEOptions()
296+
val connectionDetails = fetchConnectionDetails().getOrThrow()
261297
room.connect(
262-
url = url,
263-
token = token,
298+
url = connectionDetails.serverUrl,
299+
token = connectionDetails.participantToken,
264300
)
265301

266302
mutableEnhancedNsEnabled.postValue(room.audioProcessorIsEnabled)
@@ -495,6 +531,8 @@ class CallViewModel(
495531

496532
private suspend fun quickConnectToRoom(token: String) {
497533
try {
534+
// The stress test is only reachable with literal credentials.
535+
val url = (tokenSourceArgs as TokenSourceArgs.Literal).url
498536
room.connect(
499537
url = url,
500538
token = token,

sample-app-common/src/main/java/io/livekit/android/sample/MainViewModel.kt

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,19 @@
1+
/*
2+
* Copyright 2026 LiveKit, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
117
package io.livekit.android.sample
218

319
import android.app.Application
@@ -12,6 +28,7 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
1228

1329
fun getSavedUrl() = preferences.getString(PREFERENCES_KEY_URL, URL) as String
1430
fun getSavedToken() = preferences.getString(PREFERENCES_KEY_TOKEN, TOKEN) as String
31+
fun getSavedTokenServerId() = preferences.getString(PREFERENCES_KEY_TOKEN_SERVER_ID, TOKEN_SERVER_ID) as String
1532
fun getE2EEOptionsOn() = preferences.getBoolean(PREFERENCES_KEY_E2EE_ON, false)
1633
fun getSavedE2EEKey() = preferences.getString(PREFERENCES_KEY_E2EE_KEY, E2EE_KEY) as String
1734

@@ -27,6 +44,12 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
2744
}
2845
}
2946

47+
fun setSavedTokenServerId(tokenServerId: String) {
48+
preferences.edit {
49+
putString(PREFERENCES_KEY_TOKEN_SERVER_ID, tokenServerId)
50+
}
51+
}
52+
3053
fun setSavedE2EEOn(yesno: Boolean) {
3154
preferences.edit {
3255
putBoolean(PREFERENCES_KEY_E2EE_ON, yesno)
@@ -46,11 +69,13 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
4669
companion object {
4770
private const val PREFERENCES_KEY_URL = "url"
4871
private const val PREFERENCES_KEY_TOKEN = "token"
72+
private const val PREFERENCES_KEY_TOKEN_SERVER_ID = "token_server_id"
4973
private const val PREFERENCES_KEY_E2EE_ON = "enable_e2ee"
5074
private const val PREFERENCES_KEY_E2EE_KEY = "e2ee_key"
5175

5276
const val URL = BuildConfig.DEFAULT_URL
5377
const val TOKEN = BuildConfig.DEFAULT_TOKEN
78+
const val TOKEN_SERVER_ID = BuildConfig.DEFAULT_TOKEN_SERVER_ID
5479
const val E2EE_KEY = "12345678"
5580
}
5681
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/*
2+
* Copyright 2026 LiveKit, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package io.livekit.android.sample.model
18+
19+
import android.os.Parcelable
20+
import kotlinx.parcelize.Parcelize
21+
22+
/**
23+
* Describes which token source to connect with, in a parcelable form
24+
* so it can be passed through activity intents.
25+
*/
26+
sealed class TokenSourceArgs : Parcelable {
27+
28+
@Parcelize
29+
data class Literal(
30+
val url: String,
31+
val token: String,
32+
) : TokenSourceArgs()
33+
34+
@Parcelize
35+
data class DevTokenServer(
36+
val tokenServerId: String,
37+
val roomName: String? = null,
38+
val participantName: String? = null,
39+
val participantIdentity: String? = null,
40+
val agentName: String? = null,
41+
) : TokenSourceArgs()
42+
}

sample-app-compose/src/main/java/io/livekit/android/composesample/CallActivity.kt

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ import io.livekit.android.room.participant.Participant
7676
import io.livekit.android.sample.CallViewModel
7777
import io.livekit.android.sample.common.R
7878
import io.livekit.android.sample.model.StressTest
79+
import io.livekit.android.sample.model.TokenSourceArgs
7980
import kotlinx.coroutines.Dispatchers
8081
import kotlinx.coroutines.launch
8182
import kotlinx.parcelize.Parcelize
@@ -86,8 +87,7 @@ class CallActivity : AppCompatActivity() {
8687
val args = intent.getParcelableExtra<BundleArgs>(KEY_ARGS)
8788
?: throw NullPointerException("args is null!")
8889
CallViewModel(
89-
url = args.url,
90-
token = args.token,
90+
tokenSourceArgs = args.tokenSourceArgs,
9191
e2ee = args.e2eeOn,
9292
e2eeKey = args.e2eeKey,
9393
stressTest = args.stressTest,
@@ -511,8 +511,7 @@ class CallActivity : AppCompatActivity() {
511511

512512
@Parcelize
513513
data class BundleArgs(
514-
val url: String,
515-
val token: String,
514+
val tokenSourceArgs: TokenSourceArgs,
516515
val e2eeKey: String,
517516
val e2eeOn: Boolean,
518517
val stressTest: StressTest,

0 commit comments

Comments
 (0)