Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
573e887
Organize imports
hiroshihorie Jul 21, 2025
4655b0b
Update options.dart
hiroshihorie Jul 21, 2025
808c18b
Impl
hiroshihorie Jul 22, 2025
d5b8422
Buffer
hiroshihorie Jul 22, 2025
2440c37
start stop
hiroshihorie Jul 22, 2025
05cae43
Merge branch 'main' into hiroshi/audio-buffer
hiroshihorie Aug 12, 2025
4cb6c43
Merge branch 'main' into hiroshi/audio-buffer
hiroshihorie Aug 12, 2025
ae64244
pre-connect impl
hiroshihorie Aug 12, 2025
8236f39
Improve completer
hiroshihorie Aug 17, 2025
189a2d7
Fix bytes to read
hiroshihorie Aug 26, 2025
9ce8c0d
Logging
hiroshihorie Aug 17, 2025
8ffc398
Patch
hiroshihorie Aug 26, 2025
6423664
audio converter
hiroshihorie Aug 26, 2025
90d22ab
Merge branch 'main' into hiroshi/audio-buffer
hiroshihorie Sep 1, 2025
f2ca756
Update pubspec.lock
hiroshihorie Sep 1, 2025
0d5b67e
Fix buffer data
hiroshihorie Sep 1, 2025
f1ec7c7
Remove check
hiroshihorie Sep 2, 2025
36f15a0
Fix buffer format
hiroshihorie Sep 2, 2025
58b6b6d
Use only full frames
hiroshihorie Sep 2, 2025
d1a77d3
Minor adjustments
hiroshihorie Sep 2, 2025
8f7d984
Format AudioRenderer
hiroshihorie Sep 2, 2025
79f59d1
Merge branch 'main' into hiroshi/audio-buffer
hiroshihorie Sep 5, 2025
fe2f6bd
Pre-connect logic for room
hiroshihorie Sep 5, 2025
15d85fb
Silence concurrency warnings
hiroshihorie Sep 5, 2025
c5fe16f
Start local audio on task
hiroshihorie Sep 5, 2025
afb7244
Merge branch 'main' into hiroshi/audio-buffer
hiroshihorie Sep 8, 2025
c4a9175
Merge branch 'hiroshi/audio-buffer' of https://github.com/livekit/cli…
hiroshihorie Sep 8, 2025
4c5e1ad
Don't publish option audio if pre-audio is used
hiroshihorie Sep 8, 2025
c5db552
Merge branch 'main' into hiroshi/audio-buffer
hiroshihorie Sep 8, 2025
2f3a59b
Merge branch 'hiroshi/audio-buffer' of https://github.com/livekit/cli…
hiroshihorie Sep 8, 2025
4872682
Local track publish listener
hiroshihorie Sep 8, 2025
060d776
Simplify
hiroshihorie Sep 8, 2025
841c54a
Events
hiroshihorie Sep 8, 2025
708c511
F1
hiroshihorie Sep 8, 2025
c4205a4
Dispose buffer
hiroshihorie Sep 8, 2025
1a8edb7
Fix stop audio renderer
hiroshihorie Sep 8, 2025
e9e676d
fix clean up
hiroshihorie Sep 8, 2025
9d5f93d
Android
hiroshihorie Sep 8, 2025
7ec5c4a
Move start loc recording to rtc
hiroshihorie Sep 8, 2025
3136206
Use rendered sample rate
hiroshihorie Sep 9, 2025
bd3c511
Clean up android logic
hiroshihorie Sep 9, 2025
4413fe5
Update headers
hiroshihorie Sep 11, 2025
f890929
Stop native audio on error
hiroshihorie Sep 11, 2025
6d91923
Completer manager tests
hiroshihorie Sep 11, 2025
ad93665
Merge branch 'main' into hiroshi/audio-buffer
hiroshihorie Sep 22, 2025
3343ec0
Merge branch 'main' into hiroshi/audio-buffer
hiroshihorie Oct 1, 2025
403fa70
Update pubspec.lock
hiroshihorie Oct 1, 2025
b4efd4f
Merge branch 'hiroshi/audio-buffer' of https://github.com/livekit/cli…
hiroshihorie Oct 1, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions android/src/main/kotlin/io/livekit/plugin/AudioProcessors.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/*
* Copyright 2024 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.plugin

/**
* Container for managing audio processors (renderers and visualizers) for a specific audio track
* Similar to iOS AudioProcessors implementation
*/
class AudioProcessors(
val track: LKAudioTrack
) {
val renderers = mutableMapOf<String, AudioRenderer>()
val visualizers = mutableMapOf<String, Visualizer>()

/**
* Clean up all processors and release resources
*/
fun cleanup() {
renderers.values.forEach { it.detach() }
renderers.clear()

visualizers.values.forEach { it.stop() }
visualizers.clear()
}
}
299 changes: 299 additions & 0 deletions android/src/main/kotlin/io/livekit/plugin/AudioRenderer.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,299 @@
/*
* Copyright 2024 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.plugin

import android.os.Handler
import android.os.Looper
import io.flutter.plugin.common.BinaryMessenger
import io.flutter.plugin.common.EventChannel
import org.webrtc.AudioTrackSink
import java.nio.ByteBuffer
import java.nio.ByteOrder

/**
* AudioRenderer for capturing audio data from WebRTC tracks and streaming to Flutter
* Similar to iOS AudioRenderer implementation
*/
class AudioRenderer(
private val audioTrack: LKAudioTrack,
private val binaryMessenger: BinaryMessenger,
private val rendererId: String,
private val targetFormat: RendererAudioFormat
) : EventChannel.StreamHandler, AudioTrackSink {

private var eventChannel: EventChannel? = null
private var eventSink: EventChannel.EventSink? = null
private var isAttached = false

private val handler: Handler by lazy {
Handler(Looper.getMainLooper())
}

init {
val channelName = "io.livekit.audio.renderer/channel-$rendererId"
eventChannel = EventChannel(binaryMessenger, channelName)
eventChannel?.setStreamHandler(this)

// Attach to the audio track
audioTrack.addSink(this)
isAttached = true
}

fun detach() {
if (isAttached) {
audioTrack.removeSink(this)
isAttached = false
}
eventChannel?.setStreamHandler(null)
eventSink = null
}

override fun onListen(arguments: Any?, events: EventChannel.EventSink?) {
eventSink = events
}

override fun onCancel(arguments: Any?) {
eventSink = null
}

override fun onData(
audioData: ByteBuffer,
bitsPerSample: Int,
sampleRate: Int,
numberOfChannels: Int,
numberOfFrames: Int,
absoluteCaptureTimestampMs: Long
) {
eventSink?.let { sink ->
try {
// Convert audio data to the target format
val convertedData = convertAudioData(
audioData,
bitsPerSample,
sampleRate,
numberOfChannels,
numberOfFrames
)

// Send to Flutter on the main thread
handler.post {
sink.success(convertedData)
}
} catch (e: Exception) {
handler.post {
sink.error(
"AUDIO_CONVERSION_ERROR",
"Failed to convert audio data: ${e.message}",
null
)
}
}
}
}

private fun convertAudioData(
audioData: ByteBuffer,
bitsPerSample: Int,
sampleRate: Int,
numberOfChannels: Int,
numberOfFrames: Int
): Map<String, Any> {
// Create result similar to iOS implementation
val result = mutableMapOf<String, Any>(
"sampleRate" to sampleRate,
"channels" to numberOfChannels,
"frameLength" to numberOfFrames
)

// Convert based on target format
when (targetFormat.commonFormat) {
"int16" -> {
result["commonFormat"] = "int16"
result["data"] =
convertToInt16(audioData, bitsPerSample, numberOfChannels, numberOfFrames)
}

"float32" -> {
result["commonFormat"] = "float32"
result["data"] =
convertToFloat32(audioData, bitsPerSample, numberOfChannels, numberOfFrames)
}

else -> {
result["commonFormat"] = "int16" // Default fallback
result["data"] =
convertToInt16(audioData, bitsPerSample, numberOfChannels, numberOfFrames)
}
}

return result
}

private fun convertToInt16(
audioData: ByteBuffer,
bitsPerSample: Int,
numberOfChannels: Int,
numberOfFrames: Int
): List<List<Int>> {
val channelsData = mutableListOf<List<Int>>()

// Prepare buffer for reading
val buffer = audioData.duplicate()
buffer.order(ByteOrder.LITTLE_ENDIAN)
buffer.rewind()

when (bitsPerSample) {
16 -> {
// Already 16-bit, just reformat by channels
for (channel in 0 until numberOfChannels) {
val channelData = mutableListOf<Int>()
buffer.position(0) // Start from beginning for each channel

for (frame in 0 until numberOfFrames) {
val sampleIndex = frame * numberOfChannels + channel
val byteIndex = sampleIndex * 2

if (byteIndex + 1 < buffer.capacity()) {
buffer.position(byteIndex)
val sample = buffer.short.toInt()
channelData.add(sample)
}
}
channelsData.add(channelData)
}
}

32 -> {
// Convert from 32-bit to 16-bit
for (channel in 0 until numberOfChannels) {
val channelData = mutableListOf<Int>()
buffer.position(0)

for (frame in 0 until numberOfFrames) {
val sampleIndex = frame * numberOfChannels + channel
val byteIndex = sampleIndex * 4

if (byteIndex + 3 < buffer.capacity()) {
buffer.position(byteIndex)
val sample32 = buffer.int
// Convert 32-bit to 16-bit by right-shifting
val sample16 = (sample32 shr 16).toShort().toInt()
channelData.add(sample16)
}
}
channelsData.add(channelData)
}
}

else -> {
// Unsupported format, return empty data
repeat(numberOfChannels) {
channelsData.add(emptyList())
}
}
}

return channelsData
}

private fun convertToFloat32(
audioData: ByteBuffer,
bitsPerSample: Int,
numberOfChannels: Int,
numberOfFrames: Int
): List<List<Float>> {
val channelsData = mutableListOf<List<Float>>()

val buffer = audioData.duplicate()
buffer.order(ByteOrder.LITTLE_ENDIAN)
buffer.rewind()

when (bitsPerSample) {
16 -> {
// Convert from 16-bit to float32
for (channel in 0 until numberOfChannels) {
val channelData = mutableListOf<Float>()
buffer.position(0)

for (frame in 0 until numberOfFrames) {
val sampleIndex = frame * numberOfChannels + channel
val byteIndex = sampleIndex * 2

if (byteIndex + 1 < buffer.capacity()) {
buffer.position(byteIndex)
val sample16 = buffer.short
// Convert to float (-1.0 to 1.0)
val sampleFloat = sample16.toFloat() / Short.MAX_VALUE
channelData.add(sampleFloat)
}
}
channelsData.add(channelData)
}
}

32 -> {
// Assume 32-bit float input
for (channel in 0 until numberOfChannels) {
val channelData = mutableListOf<Float>()
buffer.position(0)

for (frame in 0 until numberOfFrames) {
val sampleIndex = frame * numberOfChannels + channel
val byteIndex = sampleIndex * 4

if (byteIndex + 3 < buffer.capacity()) {
buffer.position(byteIndex)
val sampleFloat = buffer.float
channelData.add(sampleFloat)
}
}
channelsData.add(channelData)
}
}

else -> {
// Unsupported format
repeat(numberOfChannels) {
channelsData.add(emptyList())
}
}
}

return channelsData
}
}

/**
* Audio format specification for the renderer
*/
data class RendererAudioFormat(
val bitsPerSample: Int,
val sampleRate: Int,
val numberOfChannels: Int,
val commonFormat: String = "int16"
) {
companion object {
fun fromMap(formatMap: Map<String, Any?>): RendererAudioFormat? {
val bitsPerSample = formatMap["bitsPerSample"] as? Int ?: 16
val sampleRate = formatMap["sampleRate"] as? Int ?: 48000
val numberOfChannels = formatMap["channels"] as? Int ?: 1
val commonFormat = formatMap["commonFormat"] as? String ?: "int16"

return RendererAudioFormat(bitsPerSample, sampleRate, numberOfChannels, commonFormat)
}
}
}
Loading
Loading