AudioSwitchHandler.kt
8.0 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
/*
* Copyright 2023 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.audio
import android.content.Context
import android.media.AudioAttributes
import android.media.AudioManager
import android.os.Build
import android.os.Handler
import android.os.Looper
import com.twilio.audioswitch.*
import javax.inject.Inject
import javax.inject.Singleton
/**
* An [AudioHandler] built on top of [AudioSwitch].
*
* The various settings should be set before connecting to a [Room] and [start] is called.
*/
@Singleton
class AudioSwitchHandler
@Inject
constructor(private val context: Context) : AudioHandler {
/**
* Toggle whether logging is enabled for [AudioSwitch]. By default, this is set to false.
*/
var loggingEnabled = false
/**
* Listen to changes in the available and active audio devices.
*
* @see AudioDeviceChangeListener
*/
var audioDeviceChangeListener: AudioDeviceChangeListener? = null
/**
* Listen to changes in audio focus.
*
* @see AudioManager.OnAudioFocusChangeListener
*/
var onAudioFocusChangeListener: AudioManager.OnAudioFocusChangeListener? = null
/**
* The preferred priority of audio devices to use. The first available audio device will be used.
*
* By default, the preferred order is set to:
* 1. BluetoothHeadset
* 2. WiredHeadset
* 3. Earpiece
* 4. Speakerphone
*/
var preferredDeviceList: List<Class<out AudioDevice>>? = null
/**
* When true, AudioSwitchHandler will request audio focus on start and abandon on stop.
*
* Defaults to true.
*/
var manageAudioFocus = true
/**
* The audio mode to use when requesting audio focus.
*
* Defaults to [AudioManager.MODE_IN_COMMUNICATION].
*
* Note: Manual audio routing may not work appropriately when using non-default values.
*/
var audioMode: Int = AudioManager.MODE_IN_COMMUNICATION
/**
* The audio focus mode to use while started.
*
* Defaults to [AudioManager.AUDIOFOCUS_GAIN].
*/
var focusMode: Int = AudioManager.AUDIOFOCUS_GAIN
/**
* The audio stream type to use when requesting audio focus on pre-O devices.
*
* Defaults to [AudioManager.STREAM_VOICE_CALL].
*
* Refer to this [compatibility table](https://source.android.com/docs/core/audio/attributes#compatibility)
* to ensure that your values match between android versions.
*
* Note: Manual audio routing may not work appropriately when using non-default values.
*/
var audioStreamType: Int = AudioManager.STREAM_VOICE_CALL
/**
* The audio attribute usage type to use when requesting audio focus on devices O and beyond.
*
* Defaults to [AudioAttributes.USAGE_VOICE_COMMUNICATION].
*
* Refer to this [compatibility table](https://source.android.com/docs/core/audio/attributes#compatibility)
* to ensure that your values match between android versions.
*
* Note: Manual audio routing may not work appropriately when using non-default values.
*/
var audioAttributeUsageType: Int = AudioAttributes.USAGE_VOICE_COMMUNICATION
/**
* The audio attribute content type to use when requesting audio focus on devices O and beyond.
*
* Defaults to [AudioAttributes.CONTENT_TYPE_SPEECH].
*
* Refer to this [compatibility table](https://source.android.com/docs/core/audio/attributes#compatibility)
* to ensure that your values match between android versions.
*
* Note: Manual audio routing may not work appropriately when using non-default values.
*/
var audioAttributeContentType: Int = AudioAttributes.CONTENT_TYPE_SPEECH
/**
* On certain Android devices, audio routing does not function properly and bluetooth microphones will not work
* unless audio mode is set to [AudioManager.MODE_IN_COMMUNICATION] or [AudioManager.MODE_IN_CALL].
*
* AudioSwitchHandler by default will not handle audio routing in those cases to avoid audio issues.
*
* If this set to true, AudioSwitchHandler will attempt to do audio routing, though behavior is undefined.
*/
var forceHandleAudioRouting = false
private var audioSwitch: AbstractAudioSwitch? = null
// AudioSwitch is not threadsafe, so all calls should be done on the main thread.
private val handler = Handler(Looper.getMainLooper())
override fun start() {
if (audioSwitch == null) {
handler.removeCallbacksAndMessages(null)
handler.postAtFrontOfQueue {
val switch =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
AudioSwitch(
context = context,
loggingEnabled = loggingEnabled,
audioFocusChangeListener = onAudioFocusChangeListener ?: defaultOnAudioFocusChangeListener,
preferredDeviceList = preferredDeviceList ?: defaultPreferredDeviceList,
)
} else {
LegacyAudioSwitch(
context = context,
loggingEnabled = loggingEnabled,
audioFocusChangeListener = onAudioFocusChangeListener ?: defaultOnAudioFocusChangeListener,
preferredDeviceList = preferredDeviceList ?: defaultPreferredDeviceList,
)
}
switch.manageAudioFocus = manageAudioFocus
switch.audioMode = audioMode
switch.focusMode = focusMode
switch.audioStreamType = audioStreamType
switch.audioAttributeUsageType = audioAttributeUsageType
switch.audioAttributeContentType = audioAttributeContentType
switch.forceHandleAudioRouting = forceHandleAudioRouting
audioSwitch = switch
switch.start(audioDeviceChangeListener ?: defaultAudioDeviceChangeListener)
switch.activate()
}
}
}
override fun stop() {
handler.removeCallbacksAndMessages(null)
handler.postAtFrontOfQueue {
audioSwitch?.stop()
audioSwitch = null
}
}
val selectedAudioDevice: AudioDevice?
get() = audioSwitch?.selectedAudioDevice
val availableAudioDevices: List<AudioDevice>
get() = audioSwitch?.availableAudioDevices ?: listOf()
fun selectDevice(audioDevice: AudioDevice?) {
if (Looper.myLooper() == Looper.getMainLooper()) {
audioSwitch?.selectDevice(audioDevice)
} else {
handler.post {
audioSwitch?.selectDevice(audioDevice)
}
}
}
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,
AudioDevice.WiredHeadset::class.java,
AudioDevice.Earpiece::class.java,
AudioDevice.Speakerphone::class.java,
)
}
}
}