Skip to content

Commit

Permalink
fix: Properly close container when recording is stopped.
Browse files Browse the repository at this point in the history
feat: Add device listing/selection (selection is not guaranteed but preferred only...).
feat: Add bluetooth SCO auto linking (i.e. for telephony device like headset/earbuds).
  * Recording quality is limited.
  * You must add "MODIFY_AUDIO_SETTINGS" permission to allow recording from those devices. See README.md.

close #253, #84
  • Loading branch information
llfbandit committed Mar 7, 2024
1 parent 8e06a9e commit a754890
Show file tree
Hide file tree
Showing 15 changed files with 363 additions and 45 deletions.
2 changes: 1 addition & 1 deletion pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,4 @@ environment:
sdk: ">=2.17.0 <4.0.0"

dev_dependencies:
melos: ^3.4.0
melos: ^4.1.0
6 changes: 4 additions & 2 deletions record/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,12 @@ External dependencies:
| amplitude(dBFS) | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ |
| permission check | ✔️ | ✔️ | ✔️ | | ✔️ |
| num of channels | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | ✔️
| device selection | | (auto BT/mic) | ✔️ | ✔️ | ✔️ | ✔️
| device selection | ✔️ * | (auto BT/mic) | ✔️ | ✔️ | ✔️ | ✔️
| auto gain | ✔️ |(always active?)| ✔️ | | |
| echo cancel | ✔️ | | ✔️ | | |
| noise suppresion | ✔️ | | ✔️ | | |

Bluetooth is not supported on Android at this time.
* min SDK: 23. Bluetooth telephony device link (SCO) is automatically done but there's no phone call management.

## File
| Encoder | Android | iOS | web | Windows | macOS | linux
Expand Down Expand Up @@ -80,6 +80,8 @@ record.dispose(); // As always, don't forget this one.
### Android
```xml
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<!-- Optional: Add this permission if you want to use bluetooth telephony device like headset/earbuds (min SDK: 23) -->
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
```
- min SDK: 21 (amrNb/amrWb: 26, Opus: 29)

Expand Down
2 changes: 2 additions & 0 deletions record/example/android/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.llfbandit.record_example">

<uses-permission android:name="android.permission.RECORD_AUDIO" />
<!-- Permissions for bluetooth SCO recording -->
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />

<application
android:name="${applicationName}"
Expand Down
7 changes: 6 additions & 1 deletion record_android/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
## 1.0.5
## 1.1.0
* fix: Properly close container when recording is stopped.
* fix: num channels & sample rate are not applied in AAC format.
* feat: Add device listing/selection (selection is not guaranteed but preferred only...).
* feat: Add bluetooth SCO auto linking (i.e. for telephony device like headset/earbuds).
* Recording quality is limited.
* You must add "MODIFY_AUDIO_SETTINGS" permission to allow recording from those devices. See README.md.

## 1.0.4
* fix: AAC duration can not be obtained.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.embedding.engine.plugins.FlutterPlugin.FlutterPluginBinding
import io.flutter.embedding.engine.plugins.activity.ActivityAware
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding
import io.flutter.plugin.common.BinaryMessenger
import io.flutter.plugin.common.MethodChannel

/**
Expand All @@ -24,7 +23,7 @@ class RecordPlugin : FlutterPlugin, ActivityAware {
/////////////////////////////////////////////////////////////////////////////
/// FlutterPlugin
override fun onAttachedToEngine(binding: FlutterPluginBinding) {
startPlugin(binding.binaryMessenger)
startPlugin(binding)
}

override fun onDetachedFromEngine(binding: FlutterPluginBinding) {
Expand Down Expand Up @@ -69,10 +68,10 @@ class RecordPlugin : FlutterPlugin, ActivityAware {
/// END ActivityAware
/////////////////////////////////////////////////////////////////////////////

private fun startPlugin(messenger: BinaryMessenger) {
private fun startPlugin(binding: FlutterPluginBinding) {
permissionManager = PermissionManager()
callHandler = MethodCallHandlerImpl(permissionManager!!, messenger)
methodChannel = MethodChannel(messenger, MESSAGES_CHANNEL)
callHandler = MethodCallHandlerImpl(permissionManager!!, binding.binaryMessenger, binding.applicationContext)
methodChannel = MethodChannel(binding.binaryMessenger, MESSAGES_CHANNEL)
methodChannel?.setMethodCallHandler(callHandler)
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
package com.llfbandit.record.methodcall

import android.app.Activity
import android.content.Context
import android.os.Build
import com.llfbandit.record.Utils
import com.llfbandit.record.permission.PermissionManager
import com.llfbandit.record.record.RecordConfig
import com.llfbandit.record.record.bluetooth.BluetoothReceiver
import com.llfbandit.record.record.device.DeviceUtils
import com.llfbandit.record.record.format.AudioFormats
import io.flutter.plugin.common.BinaryMessenger
import io.flutter.plugin.common.MethodCall
Expand All @@ -15,14 +19,18 @@ import java.util.concurrent.ConcurrentHashMap

class MethodCallHandlerImpl(
private val permissionManager: PermissionManager,
private val messenger: BinaryMessenger
private val messenger: BinaryMessenger,
private val appContext: Context
) : MethodCallHandler {
private var activity: Activity? = null
private val recorders = ConcurrentHashMap<String, RecorderWrapper>()
private val bluetoothReceiver = BluetoothReceiver(appContext)

fun dispose() {
for (recorder in recorders.values) {
recorder.dispose()
for (entry in recorders.entries) {
disposeRecorder(entry.value, entry.key)
}

recorders.clear()
}

Expand Down Expand Up @@ -61,18 +69,22 @@ class MethodCallHandlerImpl(
}

when (call.method) {
"start" -> try {
val config = getRecordConfig(call)
recorder.startRecordingToFile(config, result)
} catch (e: IOException) {
result.error("record", "Cannot create recording configuration.", e.message)
"start" -> {
try {
val config = getRecordConfig(call)
recorder.startRecordingToFile(config, result)
} catch (e: IOException) {
result.error("record", "Cannot create recording configuration.", e.message)
}
}

"startStream" -> try {
val config = getRecordConfig(call)
recorder.startRecordingToStream(config, result)
} catch (e: IOException) {
result.error("record", "Cannot create recording configuration.", e.message)
"startStream" -> {
try {
val config = getRecordConfig(call)
recorder.startRecordingToStream(config, result)
} catch (e: IOException) {
result.error("record", "Cannot create recording configuration.", e.message)
}
}

"stop" -> recorder.stop(result)
Expand All @@ -83,10 +95,10 @@ class MethodCallHandlerImpl(
"cancel" -> recorder.cancel(result)
"hasPermission" -> permissionManager.hasPermission(result::success)
"getAmplitude" -> recorder.getAmplitude(result)
"listInputDevices" -> result.success(null)
"listInputDevices" -> result.success(DeviceUtils.listInputDevicesAsMap(appContext))

"dispose" -> {
recorder.dispose()
recorders.remove(recorderId)
disposeRecorder(recorder, recorderId)
result.success(null)
}

Expand All @@ -106,17 +118,37 @@ class MethodCallHandlerImpl(
val recorder = RecorderWrapper(recorderId, messenger)
recorder.setActivity(activity)
recorders[recorderId] = recorder

if (!bluetoothReceiver.hasListeners()) {
bluetoothReceiver.register()
}
bluetoothReceiver.addListener(recorder)
}

private fun disposeRecorder(recorder: RecorderWrapper, recorderId: String) {
recorder.dispose()
recorders.remove(recorderId)

bluetoothReceiver.removeListener(recorder)
if (!bluetoothReceiver.hasListeners()) {
bluetoothReceiver.unregister()
}
}

@Throws(IOException::class)
private fun getRecordConfig(call: MethodCall): RecordConfig {
val device = if (Build.VERSION.SDK_INT >= 23) {
DeviceUtils.deviceInfoFromMap(appContext, call.argument("device"))
} else {
null
}

return RecordConfig(
call.argument("path"),
Utils.firstNonNull(call.argument("encoder"), "aacLc"),
Utils.firstNonNull(call.argument("bitRate"), 128000),
Utils.firstNonNull(call.argument("sampleRate"), 44100),
Utils.firstNonNull(call.argument("numChannels"), 2),
//call.argument("device"),
device,
Utils.firstNonNull(call.argument("autoGain"), false),
Utils.firstNonNull(call.argument("echoCancel"), false),
Utils.firstNonNull(call.argument("noiseSuppress"), false)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,19 @@ package com.llfbandit.record.methodcall
import android.app.Activity
import com.llfbandit.record.record.AudioRecorder
import com.llfbandit.record.record.RecordConfig
import com.llfbandit.record.record.bluetooth.BluetoothScoListener
import com.llfbandit.record.record.stream.RecorderRecordStreamHandler
import com.llfbandit.record.record.stream.RecorderStateStreamHandler
import io.flutter.plugin.common.BinaryMessenger
import io.flutter.plugin.common.EventChannel
import io.flutter.plugin.common.MethodChannel

internal class RecorderWrapper(recorderId: String, messenger: BinaryMessenger) {
internal class RecorderWrapper(recorderId: String, messenger: BinaryMessenger): BluetoothScoListener {
companion object {
const val EVENTS_STATE_CHANNEL = "com.llfbandit.record/events/"
const val EVENTS_RECORD_CHANNEL = "com.llfbandit.record/eventsRecord/"
}

private var eventChannel: EventChannel?
private val recorderStateStreamHandler = RecorderStateStreamHandler()
private var eventRecordChannel: EventChannel?
Expand Down Expand Up @@ -137,8 +143,12 @@ internal class RecorderWrapper(recorderId: String, messenger: BinaryMessenger) {
result.success(null)
}

companion object {
const val EVENTS_STATE_CHANNEL = "com.llfbandit.record/events/"
const val EVENTS_RECORD_CHANNEL = "com.llfbandit.record/eventsRecord/"
///////////////////////////////////////////////////////////
// BluetoothScoListener
///////////////////////////////////////////////////////////
override fun onBlScoConnected() {
}

override fun onBlScoDisconnected() {
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import android.media.MediaRecorder
import android.media.audiofx.AcousticEchoCanceler
import android.media.audiofx.AutomaticGainControl
import android.media.audiofx.NoiseSuppressor
import android.os.Build
import android.util.Log
import java.nio.ByteBuffer
import java.nio.ByteOrder
import kotlin.math.abs
Expand All @@ -17,6 +19,10 @@ class PCMReader(
private val config: RecordConfig,
private val mediaFormat: MediaFormat,
) {
companion object {
private val TAG = PCMReader::class.java.simpleName
}

// Recorder & features
private val reader: AudioRecord = createReader()
private var automaticGainControl: AutomaticGainControl? = null
Expand Down Expand Up @@ -99,6 +105,12 @@ class PCMReader(
throw Exception("PCM reader failed to initialize.")
}

if (Build.VERSION.SDK_INT >= 23 && config.device != null) {
if (!reader.setPreferredDevice(config.device)) {
Log.w(TAG, "Unable to set device ${config.device.productName}")
}
}

return reader
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
package com.llfbandit.record.record

import android.media.AudioDeviceInfo

class RecordConfig(
val path: String?,
val encoder: String,
val bitRate: Int,
val sampleRate: Int,
numChannels: Int,
//val device: Map<String, Any>?,
val device: AudioDeviceInfo?,
val autoGain: Boolean = false,
val echoCancel: Boolean = false,
val noiseSuppress: Boolean = false
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,6 @@ class RecordThread(
override fun onEncoderDataSize(): Int = reader?.bufferSize ?: 0

override fun onEncoderDataNeeded(byteBuffer: ByteBuffer): Int {
if (isPaused()) return 0

return reader?.read(byteBuffer) ?: 0
}

Expand All @@ -52,6 +50,7 @@ class RecordThread(

reader?.stop()
reader?.release()
reader = null

if (hasBeenCanceled) {
deleteFile()
Expand All @@ -72,12 +71,14 @@ class RecordThread(

fun pauseRecording() {
if (isRecording()) {
audioEncoder?.pause()
updateState(RecordState.PAUSE)
}
}

fun resumeRecording() {
if (isPaused()) {
audioEncoder?.resume()
updateState(RecordState.RECORD)
}
}
Expand Down Expand Up @@ -114,7 +115,6 @@ class RecordThread(
updateState(RecordState.RECORD)

completion.await()
} catch (ignored: InterruptedException) {
} catch (ex: Exception) {
recorderListener.onFailure(ex)
onEncoderStop()
Expand Down
Loading

0 comments on commit a754890

Please sign in to comment.