@@ -18,6 +18,7 @@ package io.livekit.plugin
1818
1919import android.os.Handler
2020import android.os.Looper
21+ import android.util.Log
2122import io.flutter.plugin.common.BinaryMessenger
2223import io.flutter.plugin.common.EventChannel
2324import org.webrtc.AudioTrackSink
@@ -34,10 +35,14 @@ class AudioRenderer(
3435 private val rendererId : String ,
3536 private val targetFormat : RendererAudioFormat
3637) : EventChannel.StreamHandler, AudioTrackSink {
38+ companion object {
39+ private const val TAG = " LKAudioRenderer"
40+ }
3741
3842 private var eventChannel: EventChannel ? = null
3943 private var eventSink: EventChannel .EventSink ? = null
4044 private var isAttached = false
45+ private var droppedFrameCount = 0L
4146
4247 private val handler: Handler by lazy {
4348 Handler (Looper .getMainLooper())
@@ -79,201 +84,221 @@ class AudioRenderer(
7984 absoluteCaptureTimestampMs : Long
8085 ) {
8186 eventSink?.let { sink ->
82- try {
87+ val convertedData = try {
8388 // Convert audio data to the target format
84- val convertedData = convertAudioData(
89+ convertAudioData(
8590 audioData,
8691 bitsPerSample,
8792 sampleRate,
8893 numberOfChannels,
8994 numberOfFrames
9095 )
91-
92- // Send to Flutter on the main thread
93- handler.post {
94- sink.success(convertedData)
95- }
9696 } catch (e: Exception ) {
97- handler.post {
98- sink.error(
99- " AUDIO_CONVERSION_ERROR" ,
100- " Failed to convert audio data: ${e.message} " ,
101- null
102- )
103- }
97+ logDroppedFrame(" Audio conversion exception: ${e.message} " )
98+ null
99+ }
100+
101+ if (convertedData == null ) {
102+ return @let
103+ }
104+
105+ // Send to Flutter on the main thread
106+ handler.post {
107+ sink.success(convertedData)
104108 }
105109 }
106110 }
107111
112+ /* *
113+ * Converts audio data to raw interleaved bytes.
114+ *
115+ * If source and target channel counts match, data is copied directly.
116+ * If target requests fewer channels, the first channels are kept and interleaved.
117+ *
118+ * Sends raw byte arrays instead of boxed sample lists.
119+ */
108120 private fun convertAudioData (
109121 audioData : ByteBuffer ,
110122 bitsPerSample : Int ,
111123 sampleRate : Int ,
112124 numberOfChannels : Int ,
113125 numberOfFrames : Int
114- ): Map <String , Any > {
115- // Create result similar to iOS implementation
126+ ): Map <String , Any >? {
127+ if (bitsPerSample != 16 && bitsPerSample != 32 ) {
128+ logDroppedFrame(" Unsupported bitsPerSample: $bitsPerSample " )
129+ return null
130+ }
131+ if (numberOfChannels <= 0 ) {
132+ logDroppedFrame(" Invalid numberOfChannels: $numberOfChannels " )
133+ return null
134+ }
135+ if (numberOfFrames <= 0 ) {
136+ logDroppedFrame(" Invalid numberOfFrames: $numberOfFrames " )
137+ return null
138+ }
139+
140+ val bytesPerSample = bitsPerSample / 8
141+ val bytesPerFrame = numberOfChannels * bytesPerSample
142+ if (bytesPerFrame <= 0 ) {
143+ logDroppedFrame(" Invalid bytesPerFrame: $bytesPerFrame " )
144+ return null
145+ }
146+
147+ val requestedChannels = targetFormat.numberOfChannels.coerceAtLeast(1 )
148+ val outChannels = requestedChannels.coerceAtMost(numberOfChannels)
149+
150+ val buffer = audioData.duplicate()
151+ buffer.order(ByteOrder .LITTLE_ENDIAN )
152+ buffer.rewind()
153+
154+ val availableBytes = buffer.remaining()
155+ if (availableBytes <= 0 ) {
156+ logDroppedFrame(" No audio payload bytes available" )
157+ return null
158+ }
159+
160+ val expectedBytes = numberOfFrames.toLong() * bytesPerFrame.toLong()
161+ val frameLength = if (expectedBytes <= availableBytes.toLong()) {
162+ numberOfFrames
163+ } else {
164+ val availableFrames = availableBytes / bytesPerFrame
165+ if (availableFrames <= 0 ) {
166+ logDroppedFrame(
167+ " Insufficient bytes for one frame (available=$availableBytes , bytesPerFrame=$bytesPerFrame )"
168+ )
169+ return null
170+ }
171+ logDroppedFrame(" Short audio payload; truncating frames from $numberOfFrames to $availableFrames " )
172+ availableFrames
173+ }
174+
116175 val result = mutableMapOf<String , Any >(
117176 " sampleRate" to sampleRate,
118- " channels" to numberOfChannels ,
119- " frameLength" to numberOfFrames
177+ " channels" to outChannels ,
178+ " frameLength" to frameLength,
120179 )
121180
122- // Convert based on target format
123181 when (targetFormat.commonFormat) {
124182 " int16" -> {
125183 result[" commonFormat" ] = " int16"
126- result[" data" ] =
127- convertToInt16(audioData, bitsPerSample, numberOfChannels, numberOfFrames)
184+ result[" data" ] = extractAsInt16Bytes(buffer, bitsPerSample, numberOfChannels, outChannels, frameLength)
128185 }
129-
130186 " float32" -> {
131187 result[" commonFormat" ] = " float32"
132- result[" data" ] =
133- convertToFloat32(audioData, bitsPerSample, numberOfChannels, numberOfFrames)
188+ result[" data" ] = extractAsFloat32Bytes(buffer, bitsPerSample, numberOfChannels, outChannels, frameLength)
134189 }
135-
136190 else -> {
137- result[" commonFormat" ] = " int16" // Default fallback
138- result[" data" ] =
139- convertToInt16(audioData, bitsPerSample, numberOfChannels, numberOfFrames)
191+ result[" commonFormat" ] = " int16"
192+ result[" data" ] = extractAsInt16Bytes(buffer, bitsPerSample, numberOfChannels, outChannels, frameLength)
140193 }
141194 }
142195
143196 return result
144197 }
145198
146- private fun convertToInt16 (
147- audioData : ByteBuffer ,
199+ private fun logDroppedFrame (reason : String ) {
200+ droppedFrameCount + = 1
201+ if (droppedFrameCount <= 5 || droppedFrameCount % 100 == 0L ) {
202+ Log .w(TAG , " Dropping audio frame #$droppedFrameCount for rendererId=$rendererId : $reason " )
203+ }
204+ }
205+
206+ private fun extractAsInt16Bytes (
207+ buffer : ByteBuffer ,
148208 bitsPerSample : Int ,
149- numberOfChannels : Int ,
209+ srcChannels : Int ,
210+ outChannels : Int ,
150211 numberOfFrames : Int
151- ): List <List <Int >> {
152- val channelsData = mutableListOf<List <Int >>()
212+ ): ByteArray {
213+ // Fast path for int16 with matching channel count.
214+ if (bitsPerSample == 16 && srcChannels == outChannels) {
215+ val totalBytes = numberOfFrames * outChannels * 2
216+ val out = ByteArray (totalBytes)
217+ buffer.get(out , 0 , totalBytes.coerceAtMost(buffer.remaining()))
218+ return out
219+ }
153220
154- // Prepare buffer for reading
155- val buffer = audioData.duplicate()
156- buffer.order(ByteOrder .LITTLE_ENDIAN )
157- buffer.rewind()
221+ val out = ByteArray (numberOfFrames * outChannels * 2 )
222+ val outBuf = ByteBuffer .wrap(out ).order(ByteOrder .LITTLE_ENDIAN )
158223
159224 when (bitsPerSample) {
160225 16 -> {
161- // Already 16-bit, just reformat by channels
162- for (channel in 0 until numberOfChannels) {
163- val channelData = mutableListOf<Int >()
164- buffer.position(0 ) // Start from beginning for each channel
165-
166- for (frame in 0 until numberOfFrames) {
167- val sampleIndex = frame * numberOfChannels + channel
168- val byteIndex = sampleIndex * 2
169-
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
170230 if (byteIndex + 1 < buffer.capacity()) {
171231 buffer.position(byteIndex)
172- val sample = buffer.short.toInt()
173- channelData.add(sample)
232+ outBuf.putShort((frame * outChannels + ch) * 2 , buffer.short)
174233 }
175234 }
176- channelsData.add(channelData)
177235 }
178236 }
179-
180237 32 -> {
181- // Convert from 32-bit to 16-bit
182- for (channel in 0 until numberOfChannels) {
183- val channelData = mutableListOf<Int >()
184- buffer.position(0 )
185-
186- for (frame in 0 until numberOfFrames) {
187- val sampleIndex = frame * numberOfChannels + channel
188- val byteIndex = sampleIndex * 4
189-
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
190242 if (byteIndex + 3 < buffer.capacity()) {
191243 buffer.position(byteIndex)
192- val sample32 = buffer.int
193- // Convert 32-bit to 16-bit by right-shifting
194- val sample16 = (sample32 shr 16 ).toShort().toInt()
195- channelData.add(sample16)
244+ val sample16 = (buffer.int shr 16 ).toShort()
245+ outBuf.putShort((frame * outChannels + ch) * 2 , sample16)
196246 }
197247 }
198- channelsData.add(channelData)
199- }
200- }
201-
202- else -> {
203- // Unsupported format, return empty data
204- repeat(numberOfChannels) {
205- channelsData.add(emptyList())
206248 }
207249 }
208250 }
209251
210- return channelsData
252+ return out
211253 }
212254
213- private fun convertToFloat32 (
214- audioData : ByteBuffer ,
255+ private fun extractAsFloat32Bytes (
256+ buffer : ByteBuffer ,
215257 bitsPerSample : Int ,
216- numberOfChannels : Int ,
258+ srcChannels : Int ,
259+ outChannels : Int ,
217260 numberOfFrames : Int
218- ): List <List <Float >> {
219- val channelsData = mutableListOf<List <Float >>()
261+ ): 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+ }
220269
221- val buffer = audioData.duplicate()
222- buffer.order(ByteOrder .LITTLE_ENDIAN )
223- buffer.rewind()
270+ val out = ByteArray (numberOfFrames * outChannels * 4 )
271+ val outBuf = ByteBuffer .wrap(out ).order(ByteOrder .LITTLE_ENDIAN )
224272
225273 when (bitsPerSample) {
226274 16 -> {
227- // Convert from 16-bit to float32
228- for (channel in 0 until numberOfChannels) {
229- val channelData = mutableListOf<Float >()
230- buffer.position(0 )
231-
232- for (frame in 0 until numberOfFrames) {
233- val sampleIndex = frame * numberOfChannels + channel
234- val byteIndex = sampleIndex * 2
235-
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
236279 if (byteIndex + 1 < buffer.capacity()) {
237280 buffer.position(byteIndex)
238- val sample16 = buffer.short
239- // Convert to float (-1.0 to 1.0)
240- val sampleFloat = sample16.toFloat() / Short .MAX_VALUE
241- channelData.add(sampleFloat)
281+ val sampleFloat = buffer.short.toFloat() / Short .MAX_VALUE
282+ outBuf.putFloat((frame * outChannels + ch) * 4 , sampleFloat)
242283 }
243284 }
244- channelsData.add(channelData)
245285 }
246286 }
247-
248287 32 -> {
249- // Assume 32-bit float input
250- for (channel in 0 until numberOfChannels) {
251- val channelData = mutableListOf<Float >()
252- buffer.position(0 )
253-
254- for (frame in 0 until numberOfFrames) {
255- val sampleIndex = frame * numberOfChannels + channel
256- val byteIndex = sampleIndex * 4
257-
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
258292 if (byteIndex + 3 < buffer.capacity()) {
259293 buffer.position(byteIndex)
260- val sampleFloat = buffer.float
261- channelData.add(sampleFloat)
294+ outBuf.putFloat((frame * outChannels + ch) * 4 , buffer.float)
262295 }
263296 }
264- channelsData.add(channelData)
265- }
266- }
267-
268- else -> {
269- // Unsupported format
270- repeat(numberOfChannels) {
271- channelsData.add(emptyList())
272297 }
273298 }
274299 }
275300
276- return channelsData
301+ return out
277302 }
278303}
279304
0 commit comments