Skip to content

Commit 8a1900b

Browse files
authored
Pre-connect audio buffer (#830)
Implemented for Darwin & Android Requires flutter-webrtc/flutter-webrtc@a192ea9
1 parent f3563a1 commit 8a1900b

34 files changed

+1635
-143
lines changed

.changes/audio-buffer

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
patch type="added" "Pre-connect audio buffering"
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
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.plugin
18+
19+
/**
20+
* Container for managing audio processors (renderers and visualizers) for a specific audio track
21+
* Similar to iOS AudioProcessors implementation
22+
*/
23+
class AudioProcessors(
24+
val track: LKAudioTrack
25+
) {
26+
val renderers = mutableMapOf<String, AudioRenderer>()
27+
val visualizers = mutableMapOf<String, Visualizer>()
28+
29+
/**
30+
* Clean up all processors and release resources
31+
*/
32+
fun cleanup() {
33+
renderers.values.forEach { it.detach() }
34+
renderers.clear()
35+
36+
visualizers.values.forEach { it.stop() }
37+
visualizers.clear()
38+
}
39+
}
Lines changed: 299 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,299 @@
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.plugin
18+
19+
import android.os.Handler
20+
import android.os.Looper
21+
import io.flutter.plugin.common.BinaryMessenger
22+
import io.flutter.plugin.common.EventChannel
23+
import org.webrtc.AudioTrackSink
24+
import java.nio.ByteBuffer
25+
import java.nio.ByteOrder
26+
27+
/**
28+
* AudioRenderer for capturing audio data from WebRTC tracks and streaming to Flutter
29+
* Similar to iOS AudioRenderer implementation
30+
*/
31+
class AudioRenderer(
32+
private val audioTrack: LKAudioTrack,
33+
private val binaryMessenger: BinaryMessenger,
34+
private val rendererId: String,
35+
private val targetFormat: RendererAudioFormat
36+
) : EventChannel.StreamHandler, AudioTrackSink {
37+
38+
private var eventChannel: EventChannel? = null
39+
private var eventSink: EventChannel.EventSink? = null
40+
private var isAttached = false
41+
42+
private val handler: Handler by lazy {
43+
Handler(Looper.getMainLooper())
44+
}
45+
46+
init {
47+
val channelName = "io.livekit.audio.renderer/channel-$rendererId"
48+
eventChannel = EventChannel(binaryMessenger, channelName)
49+
eventChannel?.setStreamHandler(this)
50+
51+
// Attach to the audio track
52+
audioTrack.addSink(this)
53+
isAttached = true
54+
}
55+
56+
fun detach() {
57+
if (isAttached) {
58+
audioTrack.removeSink(this)
59+
isAttached = false
60+
}
61+
eventChannel?.setStreamHandler(null)
62+
eventSink = null
63+
}
64+
65+
override fun onListen(arguments: Any?, events: EventChannel.EventSink?) {
66+
eventSink = events
67+
}
68+
69+
override fun onCancel(arguments: Any?) {
70+
eventSink = null
71+
}
72+
73+
override fun onData(
74+
audioData: ByteBuffer,
75+
bitsPerSample: Int,
76+
sampleRate: Int,
77+
numberOfChannels: Int,
78+
numberOfFrames: Int,
79+
absoluteCaptureTimestampMs: Long
80+
) {
81+
eventSink?.let { sink ->
82+
try {
83+
// Convert audio data to the target format
84+
val convertedData = convertAudioData(
85+
audioData,
86+
bitsPerSample,
87+
sampleRate,
88+
numberOfChannels,
89+
numberOfFrames
90+
)
91+
92+
// Send to Flutter on the main thread
93+
handler.post {
94+
sink.success(convertedData)
95+
}
96+
} 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+
}
104+
}
105+
}
106+
}
107+
108+
private fun convertAudioData(
109+
audioData: ByteBuffer,
110+
bitsPerSample: Int,
111+
sampleRate: Int,
112+
numberOfChannels: Int,
113+
numberOfFrames: Int
114+
): Map<String, Any> {
115+
// Create result similar to iOS implementation
116+
val result = mutableMapOf<String, Any>(
117+
"sampleRate" to sampleRate,
118+
"channels" to numberOfChannels,
119+
"frameLength" to numberOfFrames
120+
)
121+
122+
// Convert based on target format
123+
when (targetFormat.commonFormat) {
124+
"int16" -> {
125+
result["commonFormat"] = "int16"
126+
result["data"] =
127+
convertToInt16(audioData, bitsPerSample, numberOfChannels, numberOfFrames)
128+
}
129+
130+
"float32" -> {
131+
result["commonFormat"] = "float32"
132+
result["data"] =
133+
convertToFloat32(audioData, bitsPerSample, numberOfChannels, numberOfFrames)
134+
}
135+
136+
else -> {
137+
result["commonFormat"] = "int16" // Default fallback
138+
result["data"] =
139+
convertToInt16(audioData, bitsPerSample, numberOfChannels, numberOfFrames)
140+
}
141+
}
142+
143+
return result
144+
}
145+
146+
private fun convertToInt16(
147+
audioData: ByteBuffer,
148+
bitsPerSample: Int,
149+
numberOfChannels: Int,
150+
numberOfFrames: Int
151+
): List<List<Int>> {
152+
val channelsData = mutableListOf<List<Int>>()
153+
154+
// Prepare buffer for reading
155+
val buffer = audioData.duplicate()
156+
buffer.order(ByteOrder.LITTLE_ENDIAN)
157+
buffer.rewind()
158+
159+
when (bitsPerSample) {
160+
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+
170+
if (byteIndex + 1 < buffer.capacity()) {
171+
buffer.position(byteIndex)
172+
val sample = buffer.short.toInt()
173+
channelData.add(sample)
174+
}
175+
}
176+
channelsData.add(channelData)
177+
}
178+
}
179+
180+
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+
190+
if (byteIndex + 3 < buffer.capacity()) {
191+
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)
196+
}
197+
}
198+
channelsData.add(channelData)
199+
}
200+
}
201+
202+
else -> {
203+
// Unsupported format, return empty data
204+
repeat(numberOfChannels) {
205+
channelsData.add(emptyList())
206+
}
207+
}
208+
}
209+
210+
return channelsData
211+
}
212+
213+
private fun convertToFloat32(
214+
audioData: ByteBuffer,
215+
bitsPerSample: Int,
216+
numberOfChannels: Int,
217+
numberOfFrames: Int
218+
): List<List<Float>> {
219+
val channelsData = mutableListOf<List<Float>>()
220+
221+
val buffer = audioData.duplicate()
222+
buffer.order(ByteOrder.LITTLE_ENDIAN)
223+
buffer.rewind()
224+
225+
when (bitsPerSample) {
226+
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+
236+
if (byteIndex + 1 < buffer.capacity()) {
237+
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)
242+
}
243+
}
244+
channelsData.add(channelData)
245+
}
246+
}
247+
248+
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+
258+
if (byteIndex + 3 < buffer.capacity()) {
259+
buffer.position(byteIndex)
260+
val sampleFloat = buffer.float
261+
channelData.add(sampleFloat)
262+
}
263+
}
264+
channelsData.add(channelData)
265+
}
266+
}
267+
268+
else -> {
269+
// Unsupported format
270+
repeat(numberOfChannels) {
271+
channelsData.add(emptyList())
272+
}
273+
}
274+
}
275+
276+
return channelsData
277+
}
278+
}
279+
280+
/**
281+
* Audio format specification for the renderer
282+
*/
283+
data class RendererAudioFormat(
284+
val bitsPerSample: Int,
285+
val sampleRate: Int,
286+
val numberOfChannels: Int,
287+
val commonFormat: String = "int16"
288+
) {
289+
companion object {
290+
fun fromMap(formatMap: Map<String, Any?>): RendererAudioFormat? {
291+
val bitsPerSample = formatMap["bitsPerSample"] as? Int ?: 16
292+
val sampleRate = formatMap["sampleRate"] as? Int ?: 48000
293+
val numberOfChannels = formatMap["channels"] as? Int ?: 1
294+
val commonFormat = formatMap["commonFormat"] as? String ?: "int16"
295+
296+
return RendererAudioFormat(bitsPerSample, sampleRate, numberOfChannels, commonFormat)
297+
}
298+
}
299+
}

0 commit comments

Comments
 (0)