davidliu
Committed by GitHub

Prewarm audio to speed up mic publishing (#623)

* Prewarm audio to speed up mic publishing

* update cache action

* spotless and changeset
  1 +---
  2 +"client-sdk-android": minor
  3 +---
  4 +
  5 +Prewarm audio to speed up mic publishing
@@ -35,7 +35,7 @@ jobs: @@ -35,7 +35,7 @@ jobs:
35 java-version: '17' 35 java-version: '17'
36 distribution: 'adopt' 36 distribution: 'adopt'
37 37
38 - - uses: actions/cache@v3.3.2 38 + - uses: actions/cache@v4
39 with: 39 with:
40 path: | 40 path: |
41 ~/.gradle/caches 41 ~/.gradle/caches
1 [versions] 1 [versions]
2 -webrtc = "125.6422.06.1" 2 +webrtc = "125.6422.07"
3 3
4 androidJainSipRi = "1.3.0-91" 4 androidJainSipRi = "1.3.0-91"
5 androidx-activity = "1.9.0" 5 androidx-activity = "1.9.0"
  1 +/*
  2 + * Copyright 2025 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.audio
  18 +
  19 +import livekit.org.webrtc.audio.JavaAudioDeviceModule
  20 +
  21 +/**
  22 + * @suppress
  23 + */
  24 +interface AudioRecordPrewarmer {
  25 + fun prewarm()
  26 + fun stop()
  27 +}
  28 +
  29 +/**
  30 + * @suppress
  31 + */
  32 +class NoAudioRecordPrewarmer : AudioRecordPrewarmer {
  33 + override fun prewarm() {
  34 + // nothing to do.
  35 + }
  36 +
  37 + override fun stop() {
  38 + // nothing to do.
  39 + }
  40 +}
  41 +
  42 +/**
  43 + * @suppress
  44 + */
  45 +class JavaAudioRecordPrewarmer(private val audioDeviceModule: JavaAudioDeviceModule) : AudioRecordPrewarmer {
  46 + override fun prewarm() {
  47 + audioDeviceModule.prewarmRecording()
  48 + }
  49 +
  50 + override fun stop() {
  51 + audioDeviceModule.requestStopRecording()
  52 + }
  53 +}
@@ -27,8 +27,11 @@ import io.livekit.android.LiveKit @@ -27,8 +27,11 @@ import io.livekit.android.LiveKit
27 import io.livekit.android.audio.AudioBufferCallbackDispatcher 27 import io.livekit.android.audio.AudioBufferCallbackDispatcher
28 import io.livekit.android.audio.AudioProcessingController 28 import io.livekit.android.audio.AudioProcessingController
29 import io.livekit.android.audio.AudioProcessorOptions 29 import io.livekit.android.audio.AudioProcessorOptions
  30 +import io.livekit.android.audio.AudioRecordPrewarmer
30 import io.livekit.android.audio.AudioRecordSamplesDispatcher 31 import io.livekit.android.audio.AudioRecordSamplesDispatcher
31 import io.livekit.android.audio.CommunicationWorkaround 32 import io.livekit.android.audio.CommunicationWorkaround
  33 +import io.livekit.android.audio.JavaAudioRecordPrewarmer
  34 +import io.livekit.android.audio.NoAudioRecordPrewarmer
32 import io.livekit.android.memory.CloseableManager 35 import io.livekit.android.memory.CloseableManager
33 import io.livekit.android.util.LKLog 36 import io.livekit.android.util.LKLog
34 import io.livekit.android.util.LoggingLevel 37 import io.livekit.android.util.LoggingLevel
@@ -244,6 +247,15 @@ internal object RTCModule { @@ -244,6 +247,15 @@ internal object RTCModule {
244 } 247 }
245 248
246 @Provides 249 @Provides
  250 + fun audioPrewarmer(audioDeviceModule: AudioDeviceModule): AudioRecordPrewarmer {
  251 + return if (audioDeviceModule is JavaAudioDeviceModule) {
  252 + JavaAudioRecordPrewarmer(audioDeviceModule)
  253 + } else {
  254 + NoAudioRecordPrewarmer()
  255 + }
  256 + }
  257 +
  258 + @Provides
247 @Singleton 259 @Singleton
248 fun eglBase( 260 fun eglBase(
249 @Named(InjectionNames.OVERRIDE_EGL_BASE) 261 @Named(InjectionNames.OVERRIDE_EGL_BASE)
@@ -32,6 +32,7 @@ import io.livekit.android.RoomOptions @@ -32,6 +32,7 @@ import io.livekit.android.RoomOptions
32 import io.livekit.android.Version 32 import io.livekit.android.Version
33 import io.livekit.android.audio.AudioHandler 33 import io.livekit.android.audio.AudioHandler
34 import io.livekit.android.audio.AudioProcessingController 34 import io.livekit.android.audio.AudioProcessingController
  35 +import io.livekit.android.audio.AudioRecordPrewarmer
35 import io.livekit.android.audio.AudioSwitchHandler 36 import io.livekit.android.audio.AudioSwitchHandler
36 import io.livekit.android.audio.AuthedAudioProcessingController 37 import io.livekit.android.audio.AuthedAudioProcessingController
37 import io.livekit.android.audio.CommunicationWorkaround 38 import io.livekit.android.audio.CommunicationWorkaround
@@ -104,6 +105,7 @@ constructor( @@ -104,6 +105,7 @@ constructor(
104 private val audioDeviceModule: AudioDeviceModule, 105 private val audioDeviceModule: AudioDeviceModule,
105 private val regionUrlProviderFactory: RegionUrlProvider.Factory, 106 private val regionUrlProviderFactory: RegionUrlProvider.Factory,
106 private val connectionWarmer: ConnectionWarmer, 107 private val connectionWarmer: ConnectionWarmer,
  108 + private val audioRecordPrewarmer: AudioRecordPrewarmer,
107 ) : RTCEngine.Listener, ParticipantListener { 109 ) : RTCEngine.Listener, ParticipantListener {
108 110
109 private lateinit var coroutineScope: CoroutineScope 111 private lateinit var coroutineScope: CoroutineScope
@@ -177,6 +179,7 @@ constructor( @@ -177,6 +179,7 @@ constructor(
177 State.DISCONNECTED -> { 179 State.DISCONNECTED -> {
178 audioHandler.stop() 180 audioHandler.stop()
179 communicationWorkaround.stop() 181 communicationWorkaround.stop()
  182 + audioRecordPrewarmer.stop()
180 } 183 }
181 184
182 else -> {} 185 else -> {}
@@ -475,6 +478,7 @@ constructor( @@ -475,6 +478,7 @@ constructor(
475 networkCallbackManager.registerCallback() 478 networkCallbackManager.registerCallback()
476 if (options.audio) { 479 if (options.audio) {
477 val audioTrack = localParticipant.createAudioTrack() 480 val audioTrack = localParticipant.createAudioTrack()
  481 + audioTrack.prewarm()
478 localParticipant.publishAudioTrack(audioTrack) 482 localParticipant.publishAudioTrack(audioTrack)
479 } 483 }
480 ensureActive() 484 ensureActive()
@@ -317,6 +317,7 @@ internal constructor( @@ -317,6 +317,7 @@ internal constructor(
317 317
318 Track.Source.MICROPHONE -> { 318 Track.Source.MICROPHONE -> {
319 val track = createAudioTrack() 319 val track = createAudioTrack()
  320 + track.prewarm()
320 publishAudioTrack(track) 321 publishAudioTrack(track)
321 } 322 }
322 323
1 /* 1 /*
2 - * Copyright 2023-2024 LiveKit, Inc. 2 + * Copyright 2023-2025 LiveKit, Inc.
3 * 3 *
4 * Licensed under the Apache License, Version 2.0 (the "License"); 4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License. 5 * you may not use this file except in compliance with the License.
@@ -26,6 +26,7 @@ import dagger.assisted.AssistedInject @@ -26,6 +26,7 @@ import dagger.assisted.AssistedInject
26 import io.livekit.android.audio.AudioBufferCallback 26 import io.livekit.android.audio.AudioBufferCallback
27 import io.livekit.android.audio.AudioBufferCallbackDispatcher 27 import io.livekit.android.audio.AudioBufferCallbackDispatcher
28 import io.livekit.android.audio.AudioProcessingController 28 import io.livekit.android.audio.AudioProcessingController
  29 +import io.livekit.android.audio.AudioRecordPrewarmer
29 import io.livekit.android.audio.AudioRecordSamplesDispatcher 30 import io.livekit.android.audio.AudioRecordSamplesDispatcher
30 import io.livekit.android.audio.MixerAudioBufferCallback 31 import io.livekit.android.audio.MixerAudioBufferCallback
31 import io.livekit.android.dagger.InjectionNames 32 import io.livekit.android.dagger.InjectionNames
@@ -70,6 +71,7 @@ constructor( @@ -70,6 +71,7 @@ constructor(
70 private val audioRecordSamplesDispatcher: AudioRecordSamplesDispatcher, 71 private val audioRecordSamplesDispatcher: AudioRecordSamplesDispatcher,
71 @Named(InjectionNames.LOCAL_AUDIO_BUFFER_CALLBACK_DISPATCHER) 72 @Named(InjectionNames.LOCAL_AUDIO_BUFFER_CALLBACK_DISPATCHER)
72 private val audioBufferCallbackDispatcher: AudioBufferCallbackDispatcher, 73 private val audioBufferCallbackDispatcher: AudioBufferCallbackDispatcher,
  74 + private val audioRecordPrewarmer: AudioRecordPrewarmer,
73 ) : AudioTrack(name, mediaTrack) { 75 ) : AudioTrack(name, mediaTrack) {
74 /** 76 /**
75 * To only be used for flow delegate scoping, and should not be cancelled. 77 * To only be used for flow delegate scoping, and should not be cancelled.
@@ -83,6 +85,17 @@ constructor( @@ -83,6 +85,17 @@ constructor(
83 private val trackSinks = mutableSetOf<AudioTrackSink>() 85 private val trackSinks = mutableSetOf<AudioTrackSink>()
84 86
85 /** 87 /**
  88 + * Prewarms the audio stack if needed by starting the recording regardless of whether it's being published.
  89 + */
  90 + fun prewarm() {
  91 + audioRecordPrewarmer.prewarm()
  92 + }
  93 +
  94 + fun stopPrewarm() {
  95 + audioRecordPrewarmer.stop()
  96 + }
  97 +
  98 + /**
86 * Note: This function relies on us setting 99 * Note: This function relies on us setting
87 * [JavaAudioDeviceModule.Builder.setSamplesReadyCallback]. 100 * [JavaAudioDeviceModule.Builder.setSamplesReadyCallback].
88 * If you provide your own [AudioDeviceModule], or set your own 101 * If you provide your own [AudioDeviceModule], or set your own
1 /* 1 /*
2 - * Copyright 2023-2024 LiveKit, Inc. 2 + * Copyright 2023-2025 LiveKit, Inc.
3 * 3 *
4 * Licensed under the Apache License, Version 2.0 (the "License"); 4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License. 5 * you may not use this file except in compliance with the License.
@@ -22,7 +22,9 @@ import dagger.Module @@ -22,7 +22,9 @@ import dagger.Module
22 import dagger.Provides 22 import dagger.Provides
23 import io.livekit.android.audio.AudioBufferCallbackDispatcher 23 import io.livekit.android.audio.AudioBufferCallbackDispatcher
24 import io.livekit.android.audio.AudioProcessingController 24 import io.livekit.android.audio.AudioProcessingController
  25 +import io.livekit.android.audio.AudioRecordPrewarmer
25 import io.livekit.android.audio.AudioRecordSamplesDispatcher 26 import io.livekit.android.audio.AudioRecordSamplesDispatcher
  27 +import io.livekit.android.audio.NoAudioRecordPrewarmer
26 import io.livekit.android.dagger.CapabilitiesGetter 28 import io.livekit.android.dagger.CapabilitiesGetter
27 import io.livekit.android.dagger.InjectionNames 29 import io.livekit.android.dagger.InjectionNames
28 import io.livekit.android.test.mock.MockAudioDeviceModule 30 import io.livekit.android.test.mock.MockAudioDeviceModule
@@ -75,6 +77,11 @@ object TestRTCModule { @@ -75,6 +77,11 @@ object TestRTCModule {
75 } 77 }
76 78
77 @Provides 79 @Provides
  80 + fun audioPrewarmer(): AudioRecordPrewarmer {
  81 + return NoAudioRecordPrewarmer()
  82 + }
  83 +
  84 + @Provides
78 @Singleton 85 @Singleton
79 fun peerConnectionFactory( 86 fun peerConnectionFactory(
80 appContext: Context, 87 appContext: Context,
1 /* 1 /*
2 - * Copyright 2024 LiveKit, Inc. 2 + * Copyright 2024-2025 LiveKit, Inc.
3 * 3 *
4 * Licensed under the Apache License, Version 2.0 (the "License"); 4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License. 5 * you may not use this file except in compliance with the License.
@@ -18,7 +18,9 @@ package io.livekit.android.test.mock.room.track @@ -18,7 +18,9 @@ package io.livekit.android.test.mock.room.track
18 18
19 import io.livekit.android.audio.AudioBufferCallbackDispatcher 19 import io.livekit.android.audio.AudioBufferCallbackDispatcher
20 import io.livekit.android.audio.AudioProcessingController 20 import io.livekit.android.audio.AudioProcessingController
  21 +import io.livekit.android.audio.AudioRecordPrewarmer
21 import io.livekit.android.audio.AudioRecordSamplesDispatcher 22 import io.livekit.android.audio.AudioRecordSamplesDispatcher
  23 +import io.livekit.android.audio.NoAudioRecordPrewarmer
22 import io.livekit.android.room.track.LocalAudioTrack 24 import io.livekit.android.room.track.LocalAudioTrack
23 import io.livekit.android.room.track.LocalAudioTrackOptions 25 import io.livekit.android.room.track.LocalAudioTrackOptions
24 import io.livekit.android.test.MockE2ETest 26 import io.livekit.android.test.MockE2ETest
@@ -38,6 +40,7 @@ fun MockE2ETest.createMockLocalAudioTrack( @@ -38,6 +40,7 @@ fun MockE2ETest.createMockLocalAudioTrack(
38 dispatcher: CoroutineDispatcher = coroutineRule.dispatcher, 40 dispatcher: CoroutineDispatcher = coroutineRule.dispatcher,
39 audioRecordSamplesDispatcher: AudioRecordSamplesDispatcher = AudioRecordSamplesDispatcher(), 41 audioRecordSamplesDispatcher: AudioRecordSamplesDispatcher = AudioRecordSamplesDispatcher(),
40 audioBufferCallbackDispatcher: AudioBufferCallbackDispatcher = AudioBufferCallbackDispatcher(), 42 audioBufferCallbackDispatcher: AudioBufferCallbackDispatcher = AudioBufferCallbackDispatcher(),
  43 + audioRecordPrewarmer: AudioRecordPrewarmer = NoAudioRecordPrewarmer(),
41 ): LocalAudioTrack { 44 ): LocalAudioTrack {
42 return LocalAudioTrack( 45 return LocalAudioTrack(
43 name = name, 46 name = name,
@@ -47,5 +50,6 @@ fun MockE2ETest.createMockLocalAudioTrack( @@ -47,5 +50,6 @@ fun MockE2ETest.createMockLocalAudioTrack(
47 dispatcher = dispatcher, 50 dispatcher = dispatcher,
48 audioRecordSamplesDispatcher = audioRecordSamplesDispatcher, 51 audioRecordSamplesDispatcher = audioRecordSamplesDispatcher,
49 audioBufferCallbackDispatcher = audioBufferCallbackDispatcher, 52 audioBufferCallbackDispatcher = audioBufferCallbackDispatcher,
  53 + audioRecordPrewarmer = audioRecordPrewarmer,
50 ) 54 )
51 } 55 }
1 /* 1 /*
2 - * Copyright 2023-2024 LiveKit, Inc. 2 + * Copyright 2023-2025 LiveKit, Inc.
3 * 3 *
4 * Licensed under the Apache License, Version 2.0 (the "License"); 4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License. 5 * you may not use this file except in compliance with the License.
@@ -21,6 +21,7 @@ import android.net.ConnectivityManager.NetworkCallback @@ -21,6 +21,7 @@ import android.net.ConnectivityManager.NetworkCallback
21 import android.net.Network 21 import android.net.Network
22 import androidx.test.core.app.ApplicationProvider 22 import androidx.test.core.app.ApplicationProvider
23 import io.livekit.android.audio.NoAudioHandler 23 import io.livekit.android.audio.NoAudioHandler
  24 +import io.livekit.android.audio.NoAudioRecordPrewarmer
24 import io.livekit.android.audio.NoopCommunicationWorkaround 25 import io.livekit.android.audio.NoopCommunicationWorkaround
25 import io.livekit.android.e2ee.E2EEManager 26 import io.livekit.android.e2ee.E2EEManager
26 import io.livekit.android.events.DisconnectReason 27 import io.livekit.android.events.DisconnectReason
@@ -131,6 +132,7 @@ class RoomTest { @@ -131,6 +132,7 @@ class RoomTest {
131 audioDeviceModule = MockAudioDeviceModule(), 132 audioDeviceModule = MockAudioDeviceModule(),
132 regionUrlProviderFactory = regionUrlProviderFactory, 133 regionUrlProviderFactory = regionUrlProviderFactory,
133 connectionWarmer = MockConnectionWarmer(), 134 connectionWarmer = MockConnectionWarmer(),
  135 + audioRecordPrewarmer = NoAudioRecordPrewarmer(),
134 ) 136 )
135 } 137 }
136 138