Skip to content

Commit 4f33259

Browse files
authored
Fix native audio renderer cleanup and simplify Android path (#1001)
- Fix iOS `AudioRenderer.detach()` not clearing stream handler, channel, and event sink - Remove Android 32-bit audio path (WebRTC always delivers 16-bit)
1 parent 6b896eb commit 4f33259

3 files changed

Lines changed: 41 additions & 66 deletions

File tree

.changes/harden-audio-renderers

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
patch type="fixed" "Fix iOS audio renderer resource leak and remove Android 32-bit dead code"

android/src/main/kotlin/io/livekit/plugin/AudioRenderer.kt

Lines changed: 37 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -124,8 +124,9 @@ class AudioRenderer(
124124
numberOfChannels: Int,
125125
numberOfFrames: Int
126126
): Map<String, Any>? {
127-
if (bitsPerSample != 16 && bitsPerSample != 32) {
128-
logDroppedFrame("Unsupported bitsPerSample: $bitsPerSample")
127+
// WebRTC AudioTrackSink always delivers 16-bit signed int16 PCM.
128+
if (bitsPerSample != 16) {
129+
logDroppedFrame("Unsupported bitsPerSample: $bitsPerSample (expected 16)")
129130
return null
130131
}
131132
if (numberOfChannels <= 0) {
@@ -137,7 +138,7 @@ class AudioRenderer(
137138
return null
138139
}
139140

140-
val bytesPerSample = bitsPerSample / 8
141+
val bytesPerSample = 2 // 16-bit
141142
val bytesPerFrame = numberOfChannels * bytesPerSample
142143
if (bytesPerFrame <= 0) {
143144
logDroppedFrame("Invalid bytesPerFrame: $bytesPerFrame")
@@ -181,15 +182,15 @@ class AudioRenderer(
181182
when (targetFormat.commonFormat) {
182183
"int16" -> {
183184
result["commonFormat"] = "int16"
184-
result["data"] = extractAsInt16Bytes(buffer, bitsPerSample, numberOfChannels, outChannels, frameLength)
185+
result["data"] = extractAsInt16Bytes(buffer, numberOfChannels, outChannels, frameLength)
185186
}
186187
"float32" -> {
187188
result["commonFormat"] = "float32"
188-
result["data"] = extractAsFloat32Bytes(buffer, bitsPerSample, numberOfChannels, outChannels, frameLength)
189+
result["data"] = extractAsFloat32Bytes(buffer, numberOfChannels, outChannels, frameLength)
189190
}
190191
else -> {
191192
result["commonFormat"] = "int16"
192-
result["data"] = extractAsInt16Bytes(buffer, bitsPerSample, numberOfChannels, outChannels, frameLength)
193+
result["data"] = extractAsInt16Bytes(buffer, numberOfChannels, outChannels, frameLength)
193194
}
194195
}
195196

@@ -203,97 +204,67 @@ class AudioRenderer(
203204
}
204205
}
205206

207+
/**
208+
* Extracts int16 PCM bytes from an int16 source buffer.
209+
*
210+
* Fast path when channel counts match (direct copy).
211+
* Otherwise keeps only the first [outChannels] channels, interleaved.
212+
*/
206213
private fun extractAsInt16Bytes(
207214
buffer: ByteBuffer,
208-
bitsPerSample: Int,
209215
srcChannels: Int,
210216
outChannels: Int,
211217
numberOfFrames: Int
212218
): ByteArray {
213-
// Fast path for int16 with matching channel count.
214-
if (bitsPerSample == 16 && srcChannels == outChannels) {
219+
// Fast path: matching channel count — bulk copy.
220+
if (srcChannels == outChannels) {
215221
val totalBytes = numberOfFrames * outChannels * 2
216222
val out = ByteArray(totalBytes)
217223
buffer.get(out, 0, totalBytes.coerceAtMost(buffer.remaining()))
218224
return out
219225
}
220226

227+
// Channel reduction: keep first outChannels.
221228
val out = ByteArray(numberOfFrames * outChannels * 2)
222229
val outBuf = ByteBuffer.wrap(out).order(ByteOrder.LITTLE_ENDIAN)
223230

224-
when (bitsPerSample) {
225-
16 -> {
226-
for (frame in 0 until numberOfFrames) {
227-
val srcOffset = frame * srcChannels * 2
228-
for (ch in 0 until outChannels) {
229-
val byteIndex = srcOffset + ch * 2
230-
if (byteIndex + 1 < buffer.capacity()) {
231-
buffer.position(byteIndex)
232-
outBuf.putShort((frame * outChannels + ch) * 2, buffer.short)
233-
}
234-
}
235-
}
236-
}
237-
32 -> {
238-
for (frame in 0 until numberOfFrames) {
239-
val srcOffset = frame * srcChannels * 4
240-
for (ch in 0 until outChannels) {
241-
val byteIndex = srcOffset + ch * 4
242-
if (byteIndex + 3 < buffer.capacity()) {
243-
buffer.position(byteIndex)
244-
val sample16 = (buffer.int shr 16).toShort()
245-
outBuf.putShort((frame * outChannels + ch) * 2, sample16)
246-
}
247-
}
231+
for (frame in 0 until numberOfFrames) {
232+
val srcOffset = frame * srcChannels * 2
233+
for (ch in 0 until outChannels) {
234+
val byteIndex = srcOffset + ch * 2
235+
if (byteIndex + 1 < buffer.capacity()) {
236+
buffer.position(byteIndex)
237+
outBuf.putShort((frame * outChannels + ch) * 2, buffer.short)
248238
}
249239
}
250240
}
251241

252242
return out
253243
}
254244

245+
/**
246+
* Converts int16 PCM source to float32 bytes.
247+
*
248+
* Each int16 sample is scaled to the [-1.0, 1.0] range.
249+
* Only the first [outChannels] channels are kept.
250+
*/
255251
private fun extractAsFloat32Bytes(
256252
buffer: ByteBuffer,
257-
bitsPerSample: Int,
258253
srcChannels: Int,
259254
outChannels: Int,
260255
numberOfFrames: Int
261256
): ByteArray {
262-
// Fast path for float32 with matching channel count.
263-
if (bitsPerSample == 32 && srcChannels == outChannels) {
264-
val totalBytes = numberOfFrames * outChannels * 4
265-
val out = ByteArray(totalBytes)
266-
buffer.get(out, 0, totalBytes.coerceAtMost(buffer.remaining()))
267-
return out
268-
}
269-
270257
val out = ByteArray(numberOfFrames * outChannels * 4)
271258
val outBuf = ByteBuffer.wrap(out).order(ByteOrder.LITTLE_ENDIAN)
272259

273-
when (bitsPerSample) {
274-
16 -> {
275-
for (frame in 0 until numberOfFrames) {
276-
val srcOffset = frame * srcChannels * 2
277-
for (ch in 0 until outChannels) {
278-
val byteIndex = srcOffset + ch * 2
279-
if (byteIndex + 1 < buffer.capacity()) {
280-
buffer.position(byteIndex)
281-
val sampleFloat = buffer.short.toFloat() / Short.MAX_VALUE
282-
outBuf.putFloat((frame * outChannels + ch) * 4, sampleFloat)
283-
}
284-
}
285-
}
286-
}
287-
32 -> {
288-
for (frame in 0 until numberOfFrames) {
289-
val srcOffset = frame * srcChannels * 4
290-
for (ch in 0 until outChannels) {
291-
val byteIndex = srcOffset + ch * 4
292-
if (byteIndex + 3 < buffer.capacity()) {
293-
buffer.position(byteIndex)
294-
outBuf.putFloat((frame * outChannels + ch) * 4, buffer.float)
295-
}
296-
}
260+
for (frame in 0 until numberOfFrames) {
261+
val srcOffset = frame * srcChannels * 2
262+
for (ch in 0 until outChannels) {
263+
val byteIndex = srcOffset + ch * 2
264+
if (byteIndex + 1 < buffer.capacity()) {
265+
buffer.position(byteIndex)
266+
val sampleFloat = buffer.short.toFloat() / Short.MAX_VALUE
267+
outBuf.putFloat((frame * outChannels + ch) * 4, sampleFloat)
297268
}
298269
}
299270
}

shared_swift/AudioRenderer.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,9 @@ public class AudioRenderer: NSObject {
5656

5757
func detach() {
5858
_track?.remove(audioRenderer: self)
59+
channel?.setStreamHandler(nil)
60+
channel = nil
61+
eventSink = nil
5962
}
6063

6164
deinit {

0 commit comments

Comments
 (0)