davidliu
Committed by GitHub

Support authenticating audio processors (#390)

* Support authenticating audio processors

* spotless

* fix tests

* spotless
@@ -40,3 +40,13 @@ interface AudioProcessingController { @@ -40,3 +40,13 @@ interface AudioProcessingController {
40 */ 40 */
41 fun setBypassForRenderPreProcessing(bypass: Boolean) 41 fun setBypassForRenderPreProcessing(bypass: Boolean)
42 } 42 }
  43 +
  44 +/**
  45 + * @suppress
  46 + */
  47 +interface AuthedAudioProcessingController : AudioProcessingController {
  48 + /**
  49 + * @suppress
  50 + */
  51 + fun authenticate(url: String, token: String)
  52 +}
@@ -34,16 +34,35 @@ interface AudioProcessorInterface { @@ -34,16 +34,35 @@ interface AudioProcessorInterface {
34 34
35 /** 35 /**
36 * Initialize the audio processing. 36 * Initialize the audio processing.
  37 + *
  38 + * Note: audio processing methods will be called regardless of whether
  39 + * [isEnabled] returns true or not.
37 */ 40 */
38 fun initializeAudioProcessing(sampleRateHz: Int, numChannels: Int) 41 fun initializeAudioProcessing(sampleRateHz: Int, numChannels: Int)
39 42
40 /** 43 /**
41 * Called when the sample rate has changed. 44 * Called when the sample rate has changed.
  45 + *
  46 + * Note: audio processing methods will be called regardless of whether
  47 + * [isEnabled] returns true or not.
42 */ 48 */
43 fun resetAudioProcessing(newRate: Int) 49 fun resetAudioProcessing(newRate: Int)
44 50
45 /** 51 /**
46 * Process the audio frame (10ms). 52 * Process the audio frame (10ms).
  53 + *
  54 + * Note: audio processing methods will be called regardless of whether
  55 + * [isEnabled] returns true or not.
47 */ 56 */
48 fun processAudio(numBands: Int, numFrames: Int, buffer: ByteBuffer) 57 fun processAudio(numBands: Int, numFrames: Int, buffer: ByteBuffer)
49 } 58 }
  59 +
  60 +/**
  61 + * @suppress
  62 + */
  63 +interface AuthedAudioProcessorInterface : AudioProcessorInterface {
  64 + /**
  65 + * @suppress
  66 + */
  67 + fun authenticate(url: String, token: String)
  68 +}
@@ -38,3 +38,12 @@ data class AudioProcessorOptions( @@ -38,3 +38,12 @@ data class AudioProcessorOptions(
38 */ 38 */
39 val renderPreBypass: Boolean = false, 39 val renderPreBypass: Boolean = false,
40 ) 40 )
  41 +
  42 +internal fun AudioProcessorOptions.authenticateProcessors(url: String, token: String) {
  43 + if (capturePostProcessor is AuthedAudioProcessorInterface) {
  44 + capturePostProcessor.authenticate(url, token)
  45 + }
  46 + if (renderPreProcessor is AuthedAudioProcessorInterface) {
  47 + renderPreProcessor.authenticate(url, token)
  48 + }
  49 +}
@@ -287,7 +287,7 @@ internal object RTCModule { @@ -287,7 +287,7 @@ internal object RTCModule {
287 @Named(InjectionNames.OVERRIDE_PEER_CONNECTION_FACTORY_OPTIONS) 287 @Named(InjectionNames.OVERRIDE_PEER_CONNECTION_FACTORY_OPTIONS)
288 peerConnectionFactoryOptions: PeerConnectionFactory.Options?, 288 peerConnectionFactoryOptions: PeerConnectionFactory.Options?,
289 memoryManager: CloseableManager, 289 memoryManager: CloseableManager,
290 - audioProcessingFactory: AudioProcessingFactory?, 290 + audioProcessingFactory: AudioProcessingFactory,
291 ): PeerConnectionFactory { 291 ): PeerConnectionFactory {
292 return PeerConnectionFactory.builder() 292 return PeerConnectionFactory.builder()
293 .setAudioDeviceModule(audioDeviceModule) 293 .setAudioDeviceModule(audioDeviceModule)
@@ -31,7 +31,7 @@ import io.livekit.android.RoomOptions @@ -31,7 +31,7 @@ import io.livekit.android.RoomOptions
31 import io.livekit.android.Version 31 import io.livekit.android.Version
32 import io.livekit.android.audio.AudioHandler 32 import io.livekit.android.audio.AudioHandler
33 import io.livekit.android.audio.AudioProcessingController 33 import io.livekit.android.audio.AudioProcessingController
34 -import io.livekit.android.audio.AudioProcessorOptions 34 +import io.livekit.android.audio.AuthedAudioProcessingController
35 import io.livekit.android.audio.CommunicationWorkaround 35 import io.livekit.android.audio.CommunicationWorkaround
36 import io.livekit.android.dagger.InjectionNames 36 import io.livekit.android.dagger.InjectionNames
37 import io.livekit.android.e2ee.E2EEManager 37 import io.livekit.android.e2ee.E2EEManager
@@ -41,6 +41,7 @@ import io.livekit.android.memory.CloseableManager @@ -41,6 +41,7 @@ import io.livekit.android.memory.CloseableManager
41 import io.livekit.android.renderer.TextureViewRenderer 41 import io.livekit.android.renderer.TextureViewRenderer
42 import io.livekit.android.room.network.NetworkCallbackManager 42 import io.livekit.android.room.network.NetworkCallbackManager
43 import io.livekit.android.room.participant.* 43 import io.livekit.android.room.participant.*
  44 +import io.livekit.android.room.provisions.LKObjects
44 import io.livekit.android.room.track.* 45 import io.livekit.android.room.track.*
45 import io.livekit.android.util.FlowObservable 46 import io.livekit.android.util.FlowObservable
46 import io.livekit.android.util.LKLog 47 import io.livekit.android.util.LKLog
@@ -76,6 +77,7 @@ constructor( @@ -76,6 +77,7 @@ constructor(
76 private val e2EEManagerFactory: E2EEManager.Factory, 77 private val e2EEManagerFactory: E2EEManager.Factory,
77 private val communicationWorkaround: CommunicationWorkaround, 78 private val communicationWorkaround: CommunicationWorkaround,
78 val audioProcessingController: AudioProcessingController, 79 val audioProcessingController: AudioProcessingController,
  80 + val lkObjects: LKObjects,
79 ) : RTCEngine.Listener, ParticipantListener { 81 ) : RTCEngine.Listener, ParticipantListener {
80 82
81 private lateinit var coroutineScope: CoroutineScope 83 private lateinit var coroutineScope: CoroutineScope
@@ -211,11 +213,6 @@ constructor( @@ -211,11 +213,6 @@ constructor(
211 var e2eeOptions: E2EEOptions? = null 213 var e2eeOptions: E2EEOptions? = null
212 214
213 /** 215 /**
214 - * @see external audio processing options  
215 - */  
216 - var audioProcessorOptions: AudioProcessorOptions? = null  
217 -  
218 - /**  
219 * Default options to use when creating an audio track. 216 * Default options to use when creating an audio track.
220 */ 217 */
221 var audioTrackCaptureDefaults: LocalAudioTrackOptions by defaultsManager::audioTrackCaptureDefaults 218 var audioTrackCaptureDefaults: LocalAudioTrackOptions by defaultsManager::audioTrackCaptureDefaults
@@ -370,10 +367,12 @@ constructor( @@ -370,10 +367,12 @@ constructor(
370 val connectJob = coroutineScope.launch( 367 val connectJob = coroutineScope.launch(
371 ioDispatcher + emptyCoroutineExceptionHandler, 368 ioDispatcher + emptyCoroutineExceptionHandler,
372 ) { 369 ) {
  370 + if (audioProcessingController is AuthedAudioProcessingController) {
  371 + audioProcessingController.authenticate(url, token)
  372 + }
373 engine.join(url, token, options, roomOptions) 373 engine.join(url, token, options, roomOptions)
374 - networkCallbackManager.registerCallback()  
375 -  
376 ensureActive() 374 ensureActive()
  375 + networkCallbackManager.registerCallback()
377 if (options.audio) { 376 if (options.audio) {
378 val audioTrack = localParticipant.createAudioTrack() 377 val audioTrack = localParticipant.createAudioTrack()
379 localParticipant.publishAudioTrack(audioTrack) 378 localParticipant.publishAudioTrack(audioTrack)
  1 +/*
  2 + * Copyright 2024 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.room.provisions
  18 +
  19 +import livekit.org.webrtc.EglBase
  20 +import javax.inject.Inject
  21 +import javax.inject.Provider
  22 +
  23 +/**
  24 + * Provides access to objects used internally.
  25 + */
  26 +// Note, to avoid accidentally instantiating an unneeded object,
  27 +// only store Providers here.
  28 +//
  29 +// Additionally, the provided objects should only be singletons.
  30 +// Otherwise the created objects may not be the one used internally.
  31 +class LKObjects
  32 +@Inject
  33 +constructor(
  34 + private val eglBaseProvider: Provider<EglBase>,
  35 +) {
  36 + val eglBase: EglBase
  37 + get() = eglBaseProvider.get()
  38 +}
@@ -16,21 +16,21 @@ @@ -16,21 +16,21 @@
16 16
17 package io.livekit.android.webrtc 17 package io.livekit.android.webrtc
18 18
19 -import io.livekit.android.audio.AudioProcessingController  
20 import io.livekit.android.audio.AudioProcessorInterface 19 import io.livekit.android.audio.AudioProcessorInterface
21 import io.livekit.android.audio.AudioProcessorOptions 20 import io.livekit.android.audio.AudioProcessorOptions
  21 +import io.livekit.android.audio.AuthedAudioProcessingController
  22 +import io.livekit.android.audio.authenticateProcessors
22 import livekit.org.webrtc.AudioProcessingFactory 23 import livekit.org.webrtc.AudioProcessingFactory
23 import livekit.org.webrtc.ExternalAudioProcessingFactory 24 import livekit.org.webrtc.ExternalAudioProcessingFactory
24 import java.nio.ByteBuffer 25 import java.nio.ByteBuffer
25 26
26 -class CustomAudioProcessingFactory(private val audioProcessorOptions: AudioProcessorOptions) : AudioProcessingController { 27 +class CustomAudioProcessingFactory(private var audioProcessorOptions: AudioProcessorOptions) : AuthedAudioProcessingController {
27 28
28 private val externalAudioProcessor = ExternalAudioProcessingFactory() 29 private val externalAudioProcessor = ExternalAudioProcessingFactory()
29 30
30 init { 31 init {
31 if (audioProcessorOptions.capturePostProcessor != null) { 32 if (audioProcessorOptions.capturePostProcessor != null) {
32 setCapturePostProcessing(audioProcessorOptions.capturePostProcessor) 33 setCapturePostProcessing(audioProcessorOptions.capturePostProcessor)
33 - setBypassForCapturePostProcessing(audioProcessorOptions.capturePostBypass)  
34 } else { 34 } else {
35 setCapturePostProcessing(null) 35 setCapturePostProcessing(null)
36 setBypassForCapturePostProcessing(false) 36 setBypassForCapturePostProcessing(false)
@@ -48,23 +48,31 @@ class CustomAudioProcessingFactory(private val audioProcessorOptions: AudioProce @@ -48,23 +48,31 @@ class CustomAudioProcessingFactory(private val audioProcessorOptions: AudioProce
48 return externalAudioProcessor 48 return externalAudioProcessor
49 } 49 }
50 50
  51 + override fun authenticate(url: String, token: String) {
  52 + audioProcessorOptions.authenticateProcessors(url, token)
  53 + }
  54 +
51 override fun setCapturePostProcessing(processing: AudioProcessorInterface?) { 55 override fun setCapturePostProcessing(processing: AudioProcessorInterface?) {
  56 + audioProcessorOptions = audioProcessorOptions.copy(capturePostProcessor = processing)
52 externalAudioProcessor.setCapturePostProcessing( 57 externalAudioProcessor.setCapturePostProcessing(
53 processing.toAudioProcessing(), 58 processing.toAudioProcessing(),
54 ) 59 )
55 } 60 }
56 61
57 override fun setBypassForCapturePostProcessing(bypass: Boolean) { 62 override fun setBypassForCapturePostProcessing(bypass: Boolean) {
  63 + audioProcessorOptions = audioProcessorOptions.copy(capturePostBypass = bypass)
58 externalAudioProcessor.setBypassFlagForCapturePost(bypass) 64 externalAudioProcessor.setBypassFlagForCapturePost(bypass)
59 } 65 }
60 66
61 override fun setRenderPreProcessing(processing: AudioProcessorInterface?) { 67 override fun setRenderPreProcessing(processing: AudioProcessorInterface?) {
  68 + audioProcessorOptions = audioProcessorOptions.copy(renderPreProcessor = processing)
62 externalAudioProcessor.setRenderPreProcessing( 69 externalAudioProcessor.setRenderPreProcessing(
63 processing.toAudioProcessing(), 70 processing.toAudioProcessing(),
64 ) 71 )
65 } 72 }
66 73
67 override fun setBypassForRenderPreProcessing(bypass: Boolean) { 74 override fun setBypassForRenderPreProcessing(bypass: Boolean) {
  75 + audioProcessorOptions = audioProcessorOptions.copy(renderPreBypass = bypass)
68 externalAudioProcessor.setBypassFlagForRenderPre(bypass) 76 externalAudioProcessor.setBypassFlagForRenderPre(bypass)
69 } 77 }
70 78
@@ -76,7 +84,9 @@ class CustomAudioProcessingFactory(private val audioProcessorOptions: AudioProce @@ -76,7 +84,9 @@ class CustomAudioProcessingFactory(private val audioProcessorOptions: AudioProce
76 } 84 }
77 85
78 override fun reset(newRate: Int) { 86 override fun reset(newRate: Int) {
79 - audioProcessing?.resetAudioProcessing(newRate) 87 + // bug in webrtc lib causes newRate to be off by a factor of 10
  88 + // TODO: remove divide by 10 when updating libwebrtc
  89 + audioProcessing?.resetAudioProcessing(newRate / 10)
80 } 90 }
81 91
82 override fun process(numBands: Int, numFrames: Int, buffer: ByteBuffer?) { 92 override fun process(numBands: Int, numFrames: Int, buffer: ByteBuffer?) {
  1 +/*
  2 + * Copyright 2024 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.mock
  18 +
  19 +import io.livekit.android.room.provisions.LKObjects
  20 +
  21 +object MockLKObjects {
  22 + fun get(): LKObjects {
  23 + return LKObjects(
  24 + eglBaseProvider = { MockEglBase() },
  25 + )
  26 + }
  27 +}
@@ -102,6 +102,7 @@ class RoomTest { @@ -102,6 +102,7 @@ class RoomTest {
102 e2EEManagerFactory = e2EEManagerFactory, 102 e2EEManagerFactory = e2EEManagerFactory,
103 communicationWorkaround = NoopCommunicationWorkaround(), 103 communicationWorkaround = NoopCommunicationWorkaround(),
104 audioProcessingController = MockAudioProcessingController(), 104 audioProcessingController = MockAudioProcessingController(),
  105 + lkObjects = MockLKObjects.get(),
105 ) 106 )
106 } 107 }
107 108
@@ -227,7 +227,6 @@ class CallViewModel( @@ -227,7 +227,6 @@ class CallViewModel(
227 private suspend fun connectToRoom() { 227 private suspend fun connectToRoom() {
228 try { 228 try {
229 room.e2eeOptions = getE2EEOptions() 229 room.e2eeOptions = getE2EEOptions()
230 - room.audioProcessorOptions = audioProcessorOptions  
231 room.connect( 230 room.connect(
232 url = url, 231 url = url,
233 token = token, 232 token = token,