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
5 changes: 5 additions & 0 deletions .changeset/brown-cheetahs-argue.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"client-sdk-android": minor
---

Change TokenSource.fetch methods to return Result<TokenSourceResponse> to explicitly handle exceptions
5 changes: 5 additions & 0 deletions .changeset/cold-days-speak.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"client-sdk-android": minor
---

Add support for multiple listeners on AudioSwitchHandler
5 changes: 5 additions & 0 deletions .changeset/cool-meals-fetch.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"client-sdk-android": minor
---

Rename AgentState to AgentSdkState
7 changes: 7 additions & 0 deletions .changeset/curvy-otters-move.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"client-sdk-android": minor
---

Deprecate Room.withPreconnectAudio method.

- Set AudioTrackPublishDefaults.preconnect = true on the RoomOptions instead to use the preconnect buffer.
5 changes: 5 additions & 0 deletions .changeset/gorgeous-carrots-share.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"client-sdk-android": patch
---

Fix crash when cleaning up local participant
5 changes: 5 additions & 0 deletions .changeset/spicy-planes-sip.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"client-sdk-android": minor
---

Expose agentAttributes as a value on Participant
5 changes: 5 additions & 0 deletions .changeset/spotty-fishes-hang.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"client-sdk-android": minor
---

Expose the server info of the currently connected server on Room
2 changes: 1 addition & 1 deletion livekit-android-sdk/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@ dependencies {
kapt libs.dagger.compiler

implementation libs.timber
implementation libs.semver4j
api libs.semver4j

lintChecks project(':livekit-lint')
lintPublish project(':livekit-lint')
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import com.twilio.audioswitch.AudioSwitch
import com.twilio.audioswitch.LegacyAudioSwitch
import io.livekit.android.room.Room
import io.livekit.android.util.LKLog
import java.util.Collections
import javax.inject.Inject
import javax.inject.Singleton

Expand All @@ -54,15 +55,47 @@ constructor(private val context: Context) : AudioHandler {
*
* @see AudioDeviceChangeListener
*/
@Deprecated("Use registerAudioDeviceChangeListener.")
var audioDeviceChangeListener: AudioDeviceChangeListener? = null

private val audioDeviceChangeListeners = Collections.synchronizedSet(mutableSetOf<AudioDeviceChangeListener>())

private val audioDeviceChangeDispatcher by lazy(LazyThreadSafetyMode.NONE) {
object : AudioDeviceChangeListener {
override fun invoke(audioDevices: List<AudioDevice>, selectedAudioDevice: AudioDevice?) {
@Suppress("DEPRECATION")
audioDeviceChangeListener?.invoke(audioDevices, selectedAudioDevice)
synchronized(audioDeviceChangeListeners) {
for (listener in audioDeviceChangeListeners) {
listener.invoke(audioDevices, selectedAudioDevice)
}
}
}
}
}

/**
* Listen to changes in audio focus.
*
* @see AudioManager.OnAudioFocusChangeListener
*/
@Deprecated("Use registerOnAudioFocusChangeListener.")
var onAudioFocusChangeListener: AudioManager.OnAudioFocusChangeListener? = null

private val onAudioFocusChangeListeners = Collections.synchronizedSet(mutableSetOf<AudioManager.OnAudioFocusChangeListener>())

private val onAudioFocusChangeDispatcher by lazy(LazyThreadSafetyMode.NONE) {
AudioManager.OnAudioFocusChangeListener { focusChange ->
@Suppress("DEPRECATION")
onAudioFocusChangeListener?.onAudioFocusChange(focusChange)
synchronized(onAudioFocusChangeListeners) {
for (listener in onAudioFocusChangeListeners) {
listener.onAudioFocusChange(focusChange)
}
}
}
}

/**
* The preferred priority of audio devices to use. The first available audio device will be used.
*
Expand Down Expand Up @@ -170,14 +203,14 @@ constructor(private val context: Context) : AudioHandler {
AudioSwitch(
context = context,
loggingEnabled = loggingEnabled,
audioFocusChangeListener = onAudioFocusChangeListener ?: defaultOnAudioFocusChangeListener,
audioFocusChangeListener = onAudioFocusChangeDispatcher,
preferredDeviceList = preferredDeviceList ?: defaultPreferredDeviceList,
)
} else {
LegacyAudioSwitch(
context = context,
loggingEnabled = loggingEnabled,
audioFocusChangeListener = onAudioFocusChangeListener ?: defaultOnAudioFocusChangeListener,
audioFocusChangeListener = onAudioFocusChangeDispatcher,
preferredDeviceList = preferredDeviceList ?: defaultPreferredDeviceList,
)
}
Expand All @@ -190,7 +223,7 @@ constructor(private val context: Context) : AudioHandler {
switch.forceHandleAudioRouting = forceHandleAudioRouting

audioSwitch = switch
switch.start(audioDeviceChangeListener ?: defaultAudioDeviceChangeListener)
switch.start(audioDeviceChangeDispatcher)
switch.activate()
}
}
Expand Down Expand Up @@ -235,16 +268,43 @@ constructor(private val context: Context) : AudioHandler {
}
}

/**
* Listen to changes in the available and active audio devices.
* @see unregisterAudioDeviceChangeListener
*/
fun registerAudioDeviceChangeListener(listener: AudioDeviceChangeListener) {
audioDeviceChangeListeners.add(listener)
}

/**
* Remove a previously registered audio device change listener.
* @see registerAudioDeviceChangeListener
*/
fun unregisterAudioDeviceChangeListener(listener: AudioDeviceChangeListener) {
audioDeviceChangeListeners.remove(listener)
}

/**
* Listen to changes in audio focus.
*
* @see AudioManager.OnAudioFocusChangeListener
* @see unregisterOnAudioFocusChangeListener
*/
fun registerOnAudioFocusChangeListener(listener: AudioManager.OnAudioFocusChangeListener) {
onAudioFocusChangeListeners.add(listener)
}

/**
* Remove a previously registered focus change listener.
*
* @see AudioManager.OnAudioFocusChangeListener
* @see registerOnAudioFocusChangeListener
*/
fun unregisterOnAudioFocusChangeListener(listener: AudioManager.OnAudioFocusChangeListener) {
onAudioFocusChangeListeners.remove(listener)
}

companion object {
private val defaultOnAudioFocusChangeListener by lazy(LazyThreadSafetyMode.NONE) {
AudioManager.OnAudioFocusChangeListener { }
}
private val defaultAudioDeviceChangeListener by lazy(LazyThreadSafetyMode.NONE) {
object : AudioDeviceChangeListener {
override fun invoke(audioDevices: List<AudioDevice>, selectedAudioDevice: AudioDevice?) {
}
}
}
private val defaultPreferredDeviceList by lazy(LazyThreadSafetyMode.NONE) {
listOf(
AudioDevice.BluetoothHeadset::class.java,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,12 @@ import io.livekit.android.room.datastream.StreamBytesOptions
import io.livekit.android.room.participant.Participant
import io.livekit.android.util.LKLog
import io.livekit.android.util.flow
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.cancel
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.ensureActive
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.takeWhile
import kotlinx.coroutines.launch
Expand Down Expand Up @@ -195,8 +198,8 @@ internal constructor(timeout: Duration) : AudioTrackSink {
* use with LiveKit Agents.
* @param onError The error handler to call when an error occurs while sending the audio buffer.
* @param operation The connection lambda to call with the pre-connect audio.
*
*/
@Deprecated("Set AudioTrackPublishDefaults.preconnect = true on the RoomOptions instead.")
suspend fun <T> Room.withPreconnectAudio(
timeout: Duration = TIMEOUT,
topic: String = DEFAULT_TOPIC,
Expand Down Expand Up @@ -298,3 +301,104 @@ suspend fun <T> Room.withPreconnectAudio(

return@coroutineScope retValue
}

internal suspend fun Room.startPreconnectAudioJob(
roomScope: CoroutineScope,
timeout: Duration = TIMEOUT,
topic: String = DEFAULT_TOPIC
): () -> Unit {
isPrerecording = true
val audioTrack = localParticipant.getOrCreateDefaultAudioTrack()
val preconnectAudioBuffer = PreconnectAudioBuffer(timeout)

LKLog.v { "Starting preconnect audio buffer" }
preconnectAudioBuffer.startRecording()
audioTrack.addSink(preconnectAudioBuffer)
audioTrack.prewarm()

val jobs = mutableListOf<Job>()
fun stopRecording() {
if (!isPrerecording) {
return
}

LKLog.v { "Stopping preconnect audio buffer" }
audioTrack.removeSink(preconnectAudioBuffer)
preconnectAudioBuffer.stopRecording()
isPrerecording = false
}

// Clear the preconnect audio buffer after the timeout to free memory.
roomScope.launch {
delay(TIMEOUT)
preconnectAudioBuffer.clear()
}

val sentIdentities = mutableSetOf<Participant.Identity>()
roomScope.launch {
suspend fun handleSendIfNeeded(participant: Participant) {
coroutineScope inner@{
engine::connectionState.flow
.takeWhile { it != ConnectionState.CONNECTED }
.collect()

ensureActive()
val kind = participant.kind
val state = participant.state
val identity = participant.identity
if (sentIdentities.contains(identity) || kind != Participant.Kind.AGENT || state != Participant.State.ACTIVE || identity == null) {
return@inner
}

stopRecording()
launch {
try {
preconnectAudioBuffer.sendAudioData(
room = this@startPreconnectAudioJob,
trackSid = audioTrack.sid,
agentIdentities = listOf(identity),
topic = topic,
)
sentIdentities.add(identity)
} catch (e: Exception) {
LKLog.w(e) { "Error occurred while sending the audio preconnect data." }
}
}
}
}

events.collect { event ->
when (event) {
is RoomEvent.LocalTrackSubscribed -> {
LKLog.i { "Local audio track has been subscribed to, stopping preconnect audio recording." }
stopRecording()
}

is RoomEvent.ParticipantConnected -> {
// agents may connect with ACTIVE state and not trigger a participant state changed.
handleSendIfNeeded(event.participant)
}

is RoomEvent.ParticipantStateChanged -> {
handleSendIfNeeded(event.participant)
}

is RoomEvent.Disconnected -> {
cancel()
}

else -> {
// Intentionally blank.
}
}
}
}

return cancelPrerecord@{
if (!isPrerecording) {
return@cancelPrerecord
}
jobs.forEach { it.cancel() }
preconnectAudioBuffer.clear()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,9 @@ internal constructor(
internal val serverVersion: Semver?
get() = client.serverVersion

internal val serverInfo: ServerInfo?
get() = client.serverInfo

private val publisherObserver = PublisherTransportObserver(this, client, rtcThreadToken)
private val subscriberObserver = SubscriberTransportObserver(this, client, rtcThreadToken)

Expand Down Expand Up @@ -1284,10 +1287,6 @@ internal constructor(
-> {
LKLog.v { "invalid value for data packet" }
}

LivekitModels.DataPacket.ValueCase.ENCRYPTED_PACKET -> {
// TODO
}
}
}

Expand Down
10 changes: 10 additions & 0 deletions livekit-android-sdk/src/main/java/io/livekit/android/room/Room.kt
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import io.livekit.android.audio.AudioRecordPrewarmer
import io.livekit.android.audio.AudioSwitchHandler
import io.livekit.android.audio.AuthedAudioProcessingController
import io.livekit.android.audio.CommunicationWorkaround
import io.livekit.android.audio.startPreconnectAudioJob
import io.livekit.android.dagger.InjectionNames
import io.livekit.android.e2ee.E2EEManager
import io.livekit.android.e2ee.E2EEOptions
Expand Down Expand Up @@ -335,6 +336,9 @@ constructor(
val audioSwitchHandler: AudioSwitchHandler?
get() = audioHandler as? AudioSwitchHandler

val serverInfo: ServerInfo?
get() = engine.serverInfo

private var sidToIdentity = mutableMapOf<Participant.Sid, Participant.Identity>()

private var mutableActiveSpeakers by flowDelegate(emptyList<Participant>())
Expand Down Expand Up @@ -520,9 +524,15 @@ constructor(
if (options.audio) {
val audioTrack = localParticipant.getOrCreateDefaultAudioTrack()
audioTrack.prewarm()
var cancelPreconnect: (() -> Unit)? = null

if (audioTrackPublishDefaults.preconnect) {
cancelPreconnect = startPreconnectAudioJob(roomScope = coroutineScope)
}
if (!localParticipant.publishAudioTrack(audioTrack)) {
audioTrack.stop()
audioTrack.stopPrewarm()
cancelPreconnect?.invoke()
}
}
ensureActive()
Expand Down
Loading
Loading