Antoine Baché
Committed by GitHub

CommunicationWorkaround crash fix and global references leak (#379)

@@ -27,10 +27,12 @@ import io.livekit.android.dagger.InjectionNames @@ -27,10 +27,12 @@ import io.livekit.android.dagger.InjectionNames
27 import io.livekit.android.util.CloseableCoroutineScope 27 import io.livekit.android.util.CloseableCoroutineScope
28 import kotlinx.coroutines.MainCoroutineDispatcher 28 import kotlinx.coroutines.MainCoroutineDispatcher
29 import kotlinx.coroutines.flow.MutableStateFlow 29 import kotlinx.coroutines.flow.MutableStateFlow
  30 +import kotlinx.coroutines.flow.collectLatest
30 import kotlinx.coroutines.flow.combine 31 import kotlinx.coroutines.flow.combine
31 import kotlinx.coroutines.flow.distinctUntilChanged 32 import kotlinx.coroutines.flow.distinctUntilChanged
32 import kotlinx.coroutines.launch 33 import kotlinx.coroutines.launch
33 import java.nio.ByteBuffer 34 import java.nio.ByteBuffer
  35 +import java.util.concurrent.atomic.AtomicBoolean
34 import javax.inject.Inject 36 import javax.inject.Inject
35 import javax.inject.Named 37 import javax.inject.Named
36 import javax.inject.Singleton 38 import javax.inject.Singleton
@@ -87,11 +89,13 @@ constructor( @@ -87,11 +89,13 @@ constructor(
87 89
88 private var audioTrack: AudioTrack? = null 90 private var audioTrack: AudioTrack? = null
89 91
  92 + private val isAudioTrackStarted = AtomicBoolean(false)
  93 +
90 init { 94 init {
91 coroutineScope.launch { 95 coroutineScope.launch {
92 started.combine(playoutStopped) { a, b -> a to b } 96 started.combine(playoutStopped) { a, b -> a to b }
93 .distinctUntilChanged() 97 .distinctUntilChanged()
94 - .collect { (started, playoutStopped) -> 98 + .collectLatest { (started, playoutStopped) ->
95 onStateChanged(started, playoutStopped) 99 onStateChanged(started, playoutStopped)
96 } 100 }
97 } 101 }
@@ -113,32 +117,32 @@ constructor( @@ -113,32 +117,32 @@ constructor(
113 playoutStopped.value = true 117 playoutStopped.value = true
114 } 118 }
115 119
116 - @SuppressLint("NewApi") 120 + override fun dispose() {
  121 + coroutineScope.close()
  122 +
  123 + stop()
  124 +
  125 + audioTrack?.stop()
  126 + audioTrack?.release()
  127 + }
  128 +
117 private fun onStateChanged(started: Boolean, playoutStopped: Boolean) { 129 private fun onStateChanged(started: Boolean, playoutStopped: Boolean) {
118 if (started && playoutStopped) { 130 if (started && playoutStopped) {
119 - startAudioTrackIfNeeded() 131 + playAudioTrackIfNeeded()
120 } else { 132 } else {
121 - stopAudioTrackIfNeeded() 133 + pauseAudioTrackIfNeeded()
122 } 134 }
123 } 135 }
124 136
125 @SuppressLint("Range") 137 @SuppressLint("Range")
126 - private fun startAudioTrackIfNeeded() {  
127 - if (audioTrack != null) {  
128 - return  
129 - }  
130 -  
131 - val sampleRate = 16000  
132 - val audioFormat = AudioFormat.ENCODING_PCM_16BIT  
133 - val bytesPerFrame = 1 * getBytesPerSample(audioFormat)  
134 - val framesPerBuffer: Int = sampleRate / 100 // 10 ms  
135 - val byteBuffer = ByteBuffer.allocateDirect(bytesPerFrame * framesPerBuffer) 138 + private fun buildAudioTrack(): AudioTrack {
  139 + val audioSample = ByteBuffer.allocateDirect(getBytesPerSample(AUDIO_FORMAT) * AUDIO_FRAME_PER_BUFFER)
136 140
137 - audioTrack = AudioTrack.Builder() 141 + return AudioTrack.Builder()
138 .setAudioFormat( 142 .setAudioFormat(
139 AudioFormat.Builder() 143 AudioFormat.Builder()
140 - .setEncoding(audioFormat)  
141 - .setSampleRate(sampleRate) 144 + .setEncoding(AUDIO_FORMAT)
  145 + .setSampleRate(SAMPLE_RATE)
142 .setChannelMask(AudioFormat.CHANNEL_OUT_MONO) 146 .setChannelMask(AudioFormat.CHANNEL_OUT_MONO)
143 .build(), 147 .build(),
144 ) 148 )
@@ -148,15 +152,35 @@ constructor( @@ -148,15 +152,35 @@ constructor(
148 .setContentType(AudioAttributes.CONTENT_TYPE_SPEECH) 152 .setContentType(AudioAttributes.CONTENT_TYPE_SPEECH)
149 .build(), 153 .build(),
150 ) 154 )
151 - .setBufferSizeInBytes(byteBuffer.capacity()) 155 + .setBufferSizeInBytes(audioSample.capacity())
152 .setTransferMode(AudioTrack.MODE_STATIC) 156 .setTransferMode(AudioTrack.MODE_STATIC)
153 .setSessionId(AudioManager.AUDIO_SESSION_ID_GENERATE) 157 .setSessionId(AudioManager.AUDIO_SESSION_ID_GENERATE)
154 .build() 158 .build()
  159 + .apply {
  160 + write(audioSample, audioSample.remaining(), AudioTrack.WRITE_BLOCKING)
  161 + setLoopPoints(0, AUDIO_FRAME_PER_BUFFER - 1, -1)
  162 + }
  163 + }
155 164
156 - audioTrack?.write(byteBuffer, byteBuffer.remaining(), AudioTrack.WRITE_BLOCKING)  
157 - audioTrack?.setLoopPoints(0, framesPerBuffer - 1, -1) 165 + private fun playAudioTrackIfNeeded() {
  166 + val swapped = isAudioTrackStarted.compareAndSet(false, true)
  167 + if (!swapped) {
  168 + // Already playing, nothing to do
  169 + return
  170 + }
  171 +
  172 + val audioTrack = audioTrack ?: buildAudioTrack().also { audioTrack = it }
  173 + audioTrack.play()
  174 + }
  175 +
  176 + private fun pauseAudioTrackIfNeeded() {
  177 + val swapped = isAudioTrackStarted.compareAndSet(true, false)
  178 + if (!swapped) {
  179 + // Already stopped, nothing to do
  180 + return
  181 + }
158 182
159 - audioTrack?.play() 183 + audioTrack?.pause()
160 } 184 }
161 185
162 // Reference from Android code, AudioFormat.getBytesPerSample. BitPerSample / 8 186 // Reference from Android code, AudioFormat.getBytesPerSample. BitPerSample / 8
@@ -172,15 +196,9 @@ constructor( @@ -172,15 +196,9 @@ constructor(
172 } 196 }
173 } 197 }
174 198
175 - private fun stopAudioTrackIfNeeded() {  
176 - audioTrack?.stop()  
177 - audioTrack?.release()  
178 - audioTrack = null  
179 - }  
180 -  
181 - override fun dispose() {  
182 - coroutineScope.close()  
183 - stop()  
184 - stopAudioTrackIfNeeded() 199 + companion object {
  200 + private const val SAMPLE_RATE = 16000
  201 + private const val AUDIO_FORMAT = AudioFormat.ENCODING_PCM_16BIT
  202 + private const val AUDIO_FRAME_PER_BUFFER = SAMPLE_RATE / 100 // 10 ms
185 } 203 }
186 } 204 }