Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .changeset/sample-apps-token-source.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
---
---

Use the TokenSource implementations in the sample apps, with a development token server option. Sample-app-only change; no SDK release needed.
7 changes: 7 additions & 0 deletions sample-app-common/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,13 @@ def getDefaultToken() {
return hasProperty('livekitSampleToken') ? livekitSampleToken : ""
}

def getDefaultTokenServerId() {
return hasProperty('livekitSampleTokenServerId') ? livekitSampleTokenServerId : ""
}

final url = getDefaultUrl()
final token = getDefaultToken()
final tokenServerId = getDefaultTokenServerId()

android {
namespace "io.livekit.android.sample.common"
Expand All @@ -34,11 +39,13 @@ android {

buildConfigField "String", "DEFAULT_URL", "\"$url\""
buildConfigField "String", "DEFAULT_TOKEN", "\"$token\""
buildConfigField "String", "DEFAULT_TOKEN_SERVER_ID", "\"$tokenServerId\""
}

debug {
buildConfigField "String", "DEFAULT_URL", "\"$url\""
buildConfigField "String", "DEFAULT_TOKEN", "\"$token\""
buildConfigField "String", "DEFAULT_TOKEN_SERVER_ID", "\"$tokenServerId\""
}
}
buildFeatures {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,14 @@ import io.livekit.android.room.track.screencapture.ScreenCaptureParams
import io.livekit.android.room.track.video.CameraCapturerUtils
import io.livekit.android.rpc.RpcError
import io.livekit.android.sample.model.StressTest
import io.livekit.android.sample.model.TokenSourceArgs
import io.livekit.android.sample.service.ForegroundService
import io.livekit.android.token.ConfigurableTokenSource
import io.livekit.android.token.FixedTokenSource
import io.livekit.android.token.TokenRequestOptions
import io.livekit.android.token.TokenSource
import io.livekit.android.token.TokenSourceResponse
import io.livekit.android.token.cached
import io.livekit.android.util.LKLog
import io.livekit.android.util.flow
import kotlinx.coroutines.Dispatchers
Expand All @@ -68,8 +75,7 @@ import livekit.org.webrtc.CameraXHelper

@OptIn(ExperimentalCamera2Interop::class)
class CallViewModel(
val url: String,
val token: String,
val tokenSourceArgs: TokenSourceArgs,
application: Application,
val e2ee: Boolean = false,
val e2eeKey: String? = "",
Expand Down Expand Up @@ -149,6 +155,20 @@ class CallViewModel(
private val mutableHandlers = MutableStateFlow<List<RpcHandlerState>>(emptyList())
val handlers: StateFlow<List<RpcHandlerState>> = mutableHandlers

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

init {

CameraXHelper.createCameraProvider(ProcessLifecycleOwner.get()).let {
Expand Down Expand Up @@ -255,12 +275,28 @@ class CallViewModel(
}
}

/**
* Fetches the connection details used to connect to the room from [tokenSource].
*/
private suspend fun fetchConnectionDetails(): Result<TokenSourceResponse> = when (tokenSourceArgs) {
is TokenSourceArgs.Literal -> (tokenSource as FixedTokenSource).fetch()
is TokenSourceArgs.DevTokenServer -> (tokenSource as ConfigurableTokenSource).fetch(
TokenRequestOptions(
roomName = tokenSourceArgs.roomName,
participantName = tokenSourceArgs.participantName,
participantIdentity = tokenSourceArgs.participantIdentity,
agentName = tokenSourceArgs.agentName,
),
)
}

private suspend fun connectToRoom() {
try {
room.e2eeOptions = getE2EEOptions()
val connectionDetails = fetchConnectionDetails().getOrThrow()
room.connect(
url = url,
token = token,
url = connectionDetails.serverUrl,
token = connectionDetails.participantToken,
)

mutableEnhancedNsEnabled.postValue(room.audioProcessorIsEnabled)
Expand Down Expand Up @@ -495,6 +531,8 @@ class CallViewModel(

private suspend fun quickConnectToRoom(token: String) {
try {
// The stress test is only reachable with literal credentials.
val url = (tokenSourceArgs as TokenSourceArgs.Literal).url
room.connect(
url = url,
token = token,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,19 @@
/*
* Copyright 2026 LiveKit, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package io.livekit.android.sample

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

fun getSavedUrl() = preferences.getString(PREFERENCES_KEY_URL, URL) as String
fun getSavedToken() = preferences.getString(PREFERENCES_KEY_TOKEN, TOKEN) as String
fun getSavedTokenServerId() = preferences.getString(PREFERENCES_KEY_TOKEN_SERVER_ID, TOKEN_SERVER_ID) as String
fun getE2EEOptionsOn() = preferences.getBoolean(PREFERENCES_KEY_E2EE_ON, false)
fun getSavedE2EEKey() = preferences.getString(PREFERENCES_KEY_E2EE_KEY, E2EE_KEY) as String

Expand All @@ -27,6 +44,12 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
}
}

fun setSavedTokenServerId(tokenServerId: String) {
preferences.edit {
putString(PREFERENCES_KEY_TOKEN_SERVER_ID, tokenServerId)
}
}

fun setSavedE2EEOn(yesno: Boolean) {
preferences.edit {
putBoolean(PREFERENCES_KEY_E2EE_ON, yesno)
Expand All @@ -46,11 +69,13 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
companion object {
private const val PREFERENCES_KEY_URL = "url"
private const val PREFERENCES_KEY_TOKEN = "token"
private const val PREFERENCES_KEY_TOKEN_SERVER_ID = "token_server_id"
private const val PREFERENCES_KEY_E2EE_ON = "enable_e2ee"
private const val PREFERENCES_KEY_E2EE_KEY = "e2ee_key"

const val URL = BuildConfig.DEFAULT_URL
const val TOKEN = BuildConfig.DEFAULT_TOKEN
const val TOKEN_SERVER_ID = BuildConfig.DEFAULT_TOKEN_SERVER_ID
const val E2EE_KEY = "12345678"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/*
* Copyright 2026 LiveKit, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package io.livekit.android.sample.model

import android.os.Parcelable
import kotlinx.parcelize.Parcelize

/**
* Describes which token source to connect with, in a parcelable form
* so it can be passed through activity intents.
*/
sealed class TokenSourceArgs : Parcelable {
Comment thread
MaxHeimbrock marked this conversation as resolved.

@Parcelize
data class Literal(
val url: String,
val token: String,
) : TokenSourceArgs()

@Parcelize
data class DevTokenServer(
val tokenServerId: String,
val roomName: String? = null,
val participantName: String? = null,
val participantIdentity: String? = null,
val agentName: String? = null,
) : TokenSourceArgs()
}
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ import io.livekit.android.room.participant.Participant
import io.livekit.android.sample.CallViewModel
import io.livekit.android.sample.common.R
import io.livekit.android.sample.model.StressTest
import io.livekit.android.sample.model.TokenSourceArgs
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.parcelize.Parcelize
Expand All @@ -86,8 +87,7 @@ class CallActivity : AppCompatActivity() {
val args = intent.getParcelableExtra<BundleArgs>(KEY_ARGS)
?: throw NullPointerException("args is null!")
CallViewModel(
url = args.url,
token = args.token,
tokenSourceArgs = args.tokenSourceArgs,
e2ee = args.e2eeOn,
e2eeKey = args.e2eeKey,
stressTest = args.stressTest,
Expand Down Expand Up @@ -511,8 +511,7 @@ class CallActivity : AppCompatActivity() {

@Parcelize
data class BundleArgs(
val url: String,
val token: String,
val tokenSourceArgs: TokenSourceArgs,
val e2eeKey: String,
val e2eeOn: Boolean,
val stressTest: StressTest,
Expand Down
Loading
Loading