Browse Source

修改ble通信

newdev_shunjiawei
fdp 1 year ago
parent
commit
a15cf42bbb
  1. 20
      local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt
  2. 2
      local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentServicePlugin.kt
  3. 8
      local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureAsrHelper.kt
  4. 21
      local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureSpeechPlugin.kt
  5. 2
      local_plugins/ble_service/android/build.gradle.kts
  6. 2
      local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleConst.kt
  7. 395
      local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleService.kt
  8. 49
      local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/ChatAgent.kt
  9. 18
      local_plugins/jieli/android/src/main/kotlin/com/yunqiinnovation/jieli/BleService.kt
  10. 695
      local_plugins/jl_opus/android/src/main/kotlin/com/yunqiinnovation/jl_opus/JlOpusPlugin.kt

20
local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt

@ -50,7 +50,7 @@ object AgentService : CoroutineScope {
private var openaiModel: String = "gpt-3.5-turbo"
// Azure服务
private var azureAsrHelper: AzureAsrHelper? = null
var azureAsrHelper: AzureAsrHelper? = null
private var azureTtsHelper: AzureTtsHelper? = null
// OpenAI服务
@ -112,7 +112,7 @@ object AgentService : CoroutineScope {
* @param config 配置参数,包含所需的所有API密钥和设置
* @return 是否初始化成功
*/
fun initialize(appContext: Context, config: Map<String, Any>): Boolean {
fun initialize(appContext: Context, config: Map<String, Any>,isExternalActive:Boolean): Boolean {
if (isInitialized) return true
try {
@ -149,7 +149,8 @@ object AgentService : CoroutineScope {
initialize(
subscriptionKey = azureSpeechKey,
region = azureSpeechRegion,
audioSourceType = AzureAsrHelper.AudioSourceType.MICROPHONE,
audioSourceType = if (isExternalActive) AzureAsrHelper.AudioSourceType.EXTERNAL
else AzureAsrHelper.AudioSourceType.MICROPHONE
)
}
@ -277,7 +278,7 @@ object AgentService : CoroutineScope {
/**
* 开始语音识别
*/
fun startRecognition(): Boolean {
fun startRecognition(isExternalActive:Boolean): Boolean {
if (!isInitialized) {
FileLogger.e(TAG, "服务未初始化")
return false
@ -295,7 +296,14 @@ object AgentService : CoroutineScope {
isRecognitionActive = true
hasSpeechDetected = false
try {
try {
FileLogger.d(TAG, "选择音频源类型: ${isExternalActive}") //
// 选择音频源类型
val audioSourceType = if (isExternalActive) {
AzureAsrHelper.AudioSourceType.EXTERNAL
} else {
AzureAsrHelper.AudioSourceType.MICROPHONE
}
azureAsrHelper?.startContinuousRecognition(object : AzureAsrHelper.ContinuousRecognizeCallback {
override fun onRecognizing(recognizing: String, detectedLanguage: String) {
if (recognizing.isNotEmpty()) {
@ -370,7 +378,7 @@ object AgentService : CoroutineScope {
"message" to error
))
}
})
},audioSourceType)
return true
} catch (e: Exception) {
isRecognitionActive = false

2
local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentServicePlugin.kt

@ -39,7 +39,7 @@ class AgentServicePlugin : FlutterPlugin, MethodCallHandler, EventChannel.Stream
"startConversation" -> {
try {
val isStarted = AgentService.startRecognition()
val isStarted = AgentService.startRecognition(false)
result.success(isStarted)
} catch (e: Exception) {
FileLogger.e(TAG, "启动对话失败", e)

8
local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureAsrHelper.kt

@ -254,12 +254,12 @@ class AzureAsrHelper(private val context: Context) {
*
* @param callback 识别结果回调
*/
fun recognizeOnce(callback: RecognizeCallback) {
fun recognizeOnce(callback: RecognizeCallback, audioSourceType: AudioSourceType = AudioSourceType.MICROPHONE) {
if (speechConfig == null) {
callback.onError("语音服务未初始化")
return
}
this.audioSourceType = audioSourceType
// 确保不在连续识别中
if (isContinuousRecognitionActive) {
stopContinuousRecognition()
@ -299,7 +299,7 @@ class AzureAsrHelper(private val context: Context) {
* @param callback 连续识别结果回调
* @return 是否成功开始识别
*/
fun startContinuousRecognition(callback: ContinuousRecognizeCallback): Boolean {
fun startContinuousRecognition(callback: ContinuousRecognizeCallback, audioSourceType: AudioSourceType = AudioSourceType.MICROPHONE): Boolean {
if (speechConfig == null) {
callback.onError("语音服务未初始化")
return false
@ -308,7 +308,7 @@ class AzureAsrHelper(private val context: Context) {
if (isContinuousRecognitionActive) {
return true
}
this.audioSourceType = audioSourceType
// 重置识别器
if (!setupRecognizer()) {
callback.onError("重置识别器失败")

21
local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureSpeechPlugin.kt

@ -123,6 +123,7 @@ class AzureSpeechPlugin: FlutterPlugin, CoroutineScope {
val useExternalAudio = call.argument<Boolean>("useExternalAudio") ?: false
try {
FileLogger.d(tag, "选择音频源类型: ${useExternalAudio}") //
// 选择音频源类型
val audioSourceType = if (useExternalAudio) {
AzureAsrHelper.AudioSourceType.EXTERNAL
@ -156,7 +157,15 @@ class AzureSpeechPlugin: FlutterPlugin, CoroutineScope {
}
}
"recognizeOnce" -> {
val useExternalAudio = call.argument<Boolean>("useExternalAudio") ?: false
try {
FileLogger.d(tag, "选择音频源类型: ${useExternalAudio}") //
// 选择音频源类型
val audioSourceType = if (useExternalAudio) {
AzureAsrHelper.AudioSourceType.EXTERNAL
} else {
AzureAsrHelper.AudioSourceType.MICROPHONE
}
// 直接使用同步方式进行一次性识别
azureAsrHelper.recognizeOnce(object : AzureAsrHelper.RecognizeCallback {
override fun onResult(text: String, detectedLanguage: String) {
@ -170,12 +179,13 @@ class AzureSpeechPlugin: FlutterPlugin, CoroutineScope {
override fun onError(error: String) {
result.error("RECOGNITION_ERROR", error, null)
}
})
},audioSourceType)
} catch (e: Exception) {
result.error("RECOGNITION_ERROR", "识别过程异常: ${e.message}", null)
}
}
"startContinuousRecognition" -> {
val useExternalAudio = call.argument<Boolean>("useExternalAudio") ?: false
// 确保事件通道已准备好
if (asrEventSink == null) {
result.error("EVENT_CHANNEL_NOT_READY", "事件通道未准备好,无法开始连续识别", null)
@ -183,6 +193,13 @@ class AzureSpeechPlugin: FlutterPlugin, CoroutineScope {
}
try {
FileLogger.d(tag, "选择音频源类型: ${useExternalAudio}") //
// 选择音频源类型
val audioSourceType = if (useExternalAudio) {
AzureAsrHelper.AudioSourceType.EXTERNAL
} else {
AzureAsrHelper.AudioSourceType.MICROPHONE
}
val success = azureAsrHelper.startContinuousRecognition(object : AzureAsrHelper.ContinuousRecognizeCallback {
override fun onResult(text: String, detectedLanguage: String) {
sendAsrEvent(mapOf(
@ -222,7 +239,7 @@ class AzureSpeechPlugin: FlutterPlugin, CoroutineScope {
"message" to error
))
}
})
},audioSourceType)
result.success(success)
} catch (e: Exception) {
result.error("START_RECOGNITION_ERROR", e.message, null)

2
local_plugins/ble_service/android/build.gradle.kts

@ -35,4 +35,6 @@ dependencies {
implementation("androidx.security:security-crypto:1.1.0-alpha03")
implementation(project(":deepvoice_utils"))
implementation(project(":agent_service"))
implementation(project(":azure_speech"))
implementation(project(":jl_opus"))
}

2
local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleConst.kt

@ -23,7 +23,7 @@ object BleConst {
val NOTIFY_CHAR_UUID: UUID = UUID.fromString("0000abc2-0000-1111-2222-123456789abc")
/** 客户端特征配置描述符UUID */
val CLIENT_CHAR_CONFIG_UUID: UUID = UUID.fromString("0000fd7b-0000-1000-8000-00805f9b34fb")
val CLIENT_CHAR_CONFIG_UUID: UUID = UUID.fromString("00002902-0000-1000-8000-00805f9b34fb")
/** BLE扫描Action */
const val ACTION_SCAN_RESULT = "com.yunqiinnovation.ble_service.ACTION_BLE_SCAN"

395
local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleService.kt

@ -23,6 +23,7 @@ import androidx.core.content.ContextCompat
import java.util.concurrent.ConcurrentHashMap
import android.content.pm.ServiceInfo
import java.util.concurrent.CopyOnWriteArrayList
import com.yunqiinnovation.jl_opus.JlOpusPlugin
/**
* BLE服务类:提供蓝牙低功耗设备的扫描、连接和通信功能
@ -44,14 +45,14 @@ object BleService {
interface Callback {
// 扫描相关回调
fun onScanResult(devices: List<Map<String, Any>>)
// 连接状态相关回调
fun onConnectionStateChanged(state: Int)
// 数据相关回调
fun onAudioDataReceived(data: ByteArray)
fun onWakeupSignalReceived()
// 设备信息相关回调 - 统一回调接口
fun onDeviceInfoReceived(infoType: Int, infoData: Map<String, Any>)
}
@ -59,7 +60,7 @@ object BleService {
// ======================================================================================================
// 成员变量
// ======================================================================================================
// 上下文
private lateinit var context: Context
@ -84,8 +85,10 @@ object BleService {
// 回调集合 - 使用线程安全的集合存储所有回调
private val callbacks = CopyOnWriteArrayList<Callback>()
// 获取插件实例
private val opusPlugin = JlOpusPlugin()
// 初始化状态
private var isInitialized = false
@ -96,18 +99,19 @@ object BleService {
*/
fun initialize(appContext: Context): Boolean {
if (isInitialized) return true
try {
this.context = appContext.applicationContext
// 初始化蓝牙管理器和适配器
bluetoothManager = context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
bluetoothManager =
context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
bluetoothAdapter = bluetoothManager.adapter
bluetoothLeScanner = bluetoothAdapter?.bluetoothLeScanner
// 初始化Handler
scanHandler = Handler(Looper.getMainLooper())
isInitialized = true
FileLogger.d(TAG, "BLE服务初始化成功")
return true
@ -116,7 +120,7 @@ object BleService {
return false
}
}
/**
* 添加回调监听器
* @param callback 要添加的回调监听器
@ -130,7 +134,7 @@ object BleService {
}
return false
}
/**
* 移除回调监听器
* @param callback 要移除的回调监听器
@ -143,7 +147,7 @@ object BleService {
}
return removed
}
/**
* 移除所有回调监听器
*/
@ -165,7 +169,7 @@ object BleService {
FileLogger.e(TAG, "BLE服务未初始化")
return false
}
// 获取一个局部引用,避免并发访问问题
val scanner = bluetoothLeScanner ?: return false
if (isScanning) return false
@ -211,17 +215,17 @@ object BleService {
// 获取一个局部引用,避免并发访问问题
val scanner = bluetoothLeScanner ?: return false
if (!isScanning) return false
FileLogger.i(TAG, "停止主动扫描")
try {
scanner.stopScan(scanCallback)
scanHandler.removeCallbacksAndMessages(null)
isScanning = false
// 重新注册后台扫描,保证唤醒能力
registerBackgroundScan()
return true
} catch (e: Exception) {
FileLogger.e(TAG, "停止扫描失败: ${e.message}", e)
@ -235,24 +239,25 @@ object BleService {
private val scanCallback = object : ScanCallback() {
override fun onScanResult(type: Int, result: ScanResult) {
val dev = result.device ?: return
// 打印设备信息和服务UUID
printDeviceInfo(result)
// 直接上报每个扫描结果
try {
// 将单个扫描结果转换为需要的格式
val uuids = result.scanRecord?.serviceUuids?.map { it.uuid.toString() } ?: emptyList()
val uuids =
result.scanRecord?.serviceUuids?.map { it.uuid.toString() } ?: emptyList()
val deviceMap = mapOf(
"id" to dev.address,
"name" to (dev.name ?: "未知"),
"rssi" to result.rssi,
"uuids" to uuids
)
// 直接回调单个设备结果
notifyScanResult(listOf(deviceMap))
// 更新缓存
val idx = scanResults.indexOfFirst { it.device.address == dev.address }
if (idx >= 0) scanResults[idx] = result else scanResults.add(result)
@ -273,17 +278,20 @@ object BleService {
private fun printDeviceInfo(result: ScanResult) {
val device = result.device
val scanRecord = result.scanRecord
val serviceUuids = scanRecord?.serviceUuids?.joinToString(", ") { it.uuid.toString() } ?: "无"
FileLogger.d(TAG, "扫描到设备: 名称=${device.name ?: "未知"}, 地址=${device.address}, " +
"RSSI=${result.rssi}, 服务UUID=[$serviceUuids]")
val serviceUuids =
scanRecord?.serviceUuids?.joinToString(", ") { it.uuid.toString() } ?: "无"
FileLogger.d(
TAG, "扫描到设备: 名称=${device.name ?: "未知"}, 地址=${device.address}, " +
"RSSI=${result.rssi}, 服务UUID=[$serviceUuids]"
)
// 打印广播数据的详细信息
val scanData = scanRecord?.bytes
if (scanData != null) {
val hexString = scanData.joinToString("") { String.format("%02X", it) }
FileLogger.d(TAG, "广播数据: $hexString")
// 解析服务数据
scanRecord.serviceData?.forEach { (uuid, data) ->
val dataHex = data.joinToString("") { String.format("%02X", it) }
@ -304,13 +312,13 @@ object BleService {
FileLogger.w(TAG, "已经连接中,忽略连接请求")
return
}
// 获取一个局部引用,避免并发访问问题
val adapter = bluetoothAdapter ?: run {
updateConnectionState(BleConst.STATE_ERROR)
return
}
// 关闭现有连接
disconnectGatt()
@ -372,10 +380,12 @@ object BleService {
updateConnectionState(BleConst.STATE_CONNECTED)
g.discoverServices()
}
newState == BluetoothProfile.STATE_DISCONNECTED -> {
updateConnectionState(BleConst.STATE_DISCONNECTED)
disconnectGatt()
}
else -> {
updateConnectionState(BleConst.STATE_ERROR)
disconnectGatt()
@ -391,13 +401,14 @@ object BleService {
// 获取主服务特征
val primarySvc = g.getService(BleConst.PRIMARY_SERVICE_UUID)
notifyChar = primarySvc?.getCharacteristic(BleConst.NOTIFY_CHAR_UUID)
writeChar = primarySvc?.getCharacteristic(BleConst.WRITE_CHAR_UUID)
// 获取音频服务特征
val audioSvc = g.getService(BleConst.AUDIO_SERVICE_UUID)
audioChar = audioSvc?.getCharacteristic(BleConst.RECEIVE_AUDIO_CHAR_UUID)
if (notifyChar == null || writeChar == null) {
FileLogger.e(TAG, "未找到主服务所需特征")
updateConnectionState(BleConst.STATE_ERROR)
@ -405,10 +416,7 @@ object BleService {
}
writeChar?.writeType = BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE
// 设置主服务的通知
setupNotifications(g, notifyChar)
//要先设置音频服务的通知,否则接收不到
// 设置音频服务的通知(如果存在)
if (audioChar != null) {
setupNotifications(g, audioChar)
@ -416,13 +424,22 @@ object BleService {
} else {
FileLogger.w(TAG, "音频服务特征未找到")
}
// 设置主服务的通知
setupNotifications(g, notifyChar)
//开启解码
opusPlugin?.startOpusDecodeStream(false, 1, 16000, 40, object :
JlOpusPlugin.CallbackOpus {
override fun onAudioDataReceivedOpus(pcmData: ByteArray) {
// 回调给上层
notifyAudioDataReceived(pcmData)
}
})
}
override fun onCharacteristicChanged(g: BluetoothGatt, c: BluetoothGattCharacteristic) {
FileLogger.d(TAG, "收到特征数据: ${c.uuid}, 数据=${c.value?.joinToString(", ") { "0x${(it.toInt() and 0xFF).toString(16)}" }}")
val data = c.value ?: return
// 根据特征UUID区分处理
when (c.uuid) {
// 音频特征数据
@ -443,13 +460,23 @@ object BleService {
}
// 其他未知数据
else -> {
FileLogger.d(TAG, "收到未知数据: ${data.joinToString(", ") { "0x%02X".format(it) }}")
FileLogger.d(
TAG,
"收到未知数据: ${data.joinToString(", ") { "0x%02X".format(it) }}"
)
}
}
}
// 其他未知特征
else -> {
FileLogger.d(TAG, "收到未知特征数据: ${c.uuid}, 数据=${data.joinToString(", ") { "0x%02X".format(it) }}")
FileLogger.d(
TAG,
"收到未知特征数据: ${c.uuid}, 数据=${
data.joinToString(", ") {
"0x%02X".format(it)
}
}"
)
}
}
}
@ -457,7 +484,10 @@ object BleService {
override fun onCharacteristicWrite(
g: BluetoothGatt, c: BluetoothGattCharacteristic, status: Int
) {
FileLogger.i(TAG, "写入 ${if (status == BluetoothGatt.GATT_SUCCESS) "成功" else "失败"}")
FileLogger.i(
TAG,
"写入 ${if (status == BluetoothGatt.GATT_SUCCESS) "成功" else "失败"}"
)
}
override fun onDescriptorWrite(
@ -476,7 +506,7 @@ object BleService {
FileLogger.w(TAG, "特征不支持通知功能: ${characteristic?.uuid}")
return
}
g.setCharacteristicNotification(characteristic, true)
val desc = characteristic.getDescriptor(BleConst.CLIENT_CHAR_CONFIG_UUID)
desc?.value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE
@ -489,11 +519,9 @@ object BleService {
*/
private fun processAudioData(data: ByteArray) {
try {
// 记录日志
FileLogger.d(TAG, "收到音频数据: ${data.size}字节")
// 回调给上层
notifyAudioDataReceived(data)
opusPlugin?.handleDecodeRequest(data)
} catch (e: Exception) {
FileLogger.e(TAG, "处理音频数据异常: ${e.message}", e)
}
@ -503,15 +531,18 @@ object BleService {
* 处理设备响应数据 - 帧头为0xBB
*/
private fun processDeviceResponse(data: ByteArray) {
FileLogger.d(TAG, "收到设备响应: ${data.joinToString(", ") { "0x${(it.toInt() and 0xFF).toString(16)}" }}")
FileLogger.d(
TAG,
"收到设备响应: ${data.joinToString(", ") { "0x${(it.toInt() and 0xFF).toString(16)}" }}"
)
if (data.size < 3) {
FileLogger.e(TAG, "设备响应数据格式错误:数据长度过短")
return
}
val header = data[0] // 0xBB
val cmdType = data[1].toInt() and 0xFF
// 判断命令失败响应(长度为0)
if (data.size == 4 && data[2].toInt() and 0xFF == 0) {
FileLogger.w(TAG, "命令 0x${cmdType.toString(16)} 执行失败")
@ -524,34 +555,46 @@ object BleService {
notifyDeviceInfoReceived(cmdType, resultMap)
return
}
// 正常响应处理
val length = data[2].toInt() and 0xFF
// 检查数据长度
if (data.size != length + 4) { // 帧头 + 命令 + 长度 + 数据 + CRC
FileLogger.e(TAG, "设备响应数据长度不匹配: 期望=${length+4}, 实际=${data.size}")
FileLogger.e(TAG, "设备响应数据长度不匹配: 期望=${length + 4}, 实际=${data.size}")
return
}
// 计算CRC校验 - 修正:计算除最后一个CRC字节外的所有字节
val payload = data.sliceArray(0 until data.size - 1)
val calculatedCrc = crc8Maxim(payload, payload.size)
val receivedCrc = data[data.size - 1].toInt() and 0xFF
if (calculatedCrc.toInt() and 0xFF != receivedCrc) {
FileLogger.e(TAG, "设备响应CRC校验失败: 计算=${calculatedCrc.toInt() and 0xFF}, 接收=${receivedCrc}")
FileLogger.e(
TAG,
"设备响应CRC校验失败: 计算=${calculatedCrc.toInt() and 0xFF}, 接收=${receivedCrc}"
)
return
}
// 提取响应数据
val responseData = if (length > 0) ByteArray(length) else ByteArray(0)
if (length > 0) {
System.arraycopy(data, 3, responseData, 0, length)
}
FileLogger.d(TAG, "收到设备响应: CMD=0x${cmdType.toString(16)}, 数据=${responseData.joinToString(", ") { "0x${(it.toInt() and 0xFF).toString(16)}" }}")
FileLogger.d(
TAG,
"收到设备响应: CMD=0x${cmdType.toString(16)}, 数据=${
responseData.joinToString(", ") {
"0x${
(it.toInt() and 0xFF).toString(16)
}"
}
}"
)
// 根据命令类型处理不同的响应
when (cmdType) {
BleConst.CMD_GET_VERSION -> {
@ -560,64 +603,74 @@ object BleService {
"command" to "versionInfo",
"rawData" to responseData
)
if (responseData.isNotEmpty()) {
// 转换为字符串前,确认是否是有效的ASCII字符
val version = responseData.joinToString(".") { (it.toInt() and 0xFF).toString() }
val version =
responseData.joinToString(".") { (it.toInt() and 0xFF).toString() }
resultMap["version"] = version
FileLogger.i(TAG, "接收到目标版本号: $version")
}
notifyDeviceInfoReceived(cmdType, resultMap)
}
BleConst.CMD_GET_PRODUCT_ID -> {
val resultMap = mutableMapOf<String, Any>(
"success" to true,
"command" to "productInfo",
"rawData" to responseData
)
if (responseData.isNotEmpty() && responseData.size >= 2) {
val productId = "0x${(responseData[0].toInt() and 0xFF).toString(16)}${(responseData[1].toInt() and 0xFF).toString(16)}"
val productId = "0x${(responseData[0].toInt() and 0xFF).toString(16)}${
(responseData[1].toInt() and 0xFF).toString(16)
}"
resultMap["productId"] = productId
FileLogger.i(TAG, "接收到产品ID: $productId")
}
notifyDeviceInfoReceived(cmdType, resultMap)
}
BleConst.CMD_GET_COLOR_ID -> {
val resultMap = mutableMapOf<String, Any>(
"success" to true,
"command" to "colorInfo",
"rawData" to responseData
)
if (responseData.isNotEmpty()) {
val colorId = (responseData[0].toInt() and 0xFF).toString()
resultMap["colorId"] = colorId
FileLogger.i(TAG, "接收到耳机颜色ID: $colorId")
}
notifyDeviceInfoReceived(cmdType, resultMap)
}
BleConst.CMD_GET_BATTERY_INFO -> {
val resultMap = mutableMapOf<String, Any>(
"success" to true,
"command" to "batteryInfo",
"rawData" to responseData
)
if (responseData.isNotEmpty() && responseData.size >= 3) {
// 处理电池信息 - 左耳、右耳、充电仓
val leftEarBattery = responseData[0].toInt() and 0x7F
val leftEarCharging = (responseData[0].toInt() and 0x80) != 0
val rightEarBattery = if (responseData.size > 1) responseData[1].toInt() and 0x7F else 0
val rightEarCharging = if (responseData.size > 1) (responseData[1].toInt() and 0x80) != 0 else false
val caseBattery = if (responseData.size > 2) responseData[2].toInt() and 0x7F else 0
val caseCharging = if (responseData.size > 2) (responseData[2].toInt() and 0x80) != 0 else false
val rightEarBattery =
if (responseData.size > 1) responseData[1].toInt() and 0x7F else 0
val rightEarCharging =
if (responseData.size > 1) (responseData[1].toInt() and 0x80) != 0 else false
val caseBattery =
if (responseData.size > 2) responseData[2].toInt() and 0x7F else 0
val caseCharging =
if (responseData.size > 2) (responseData[2].toInt() and 0x80) != 0 else false
// 添加到结果Map
resultMap["leftEarBattery"] = leftEarBattery
resultMap["leftEarCharging"] = leftEarCharging
@ -626,26 +679,29 @@ object BleService {
resultMap["caseBattery"] = caseBattery
resultMap["caseCharging"] = caseCharging
resultMap["batteryLevel"] = leftEarBattery // 整体电量用左耳表示
FileLogger.i(TAG, "接收到电量信息: 左耳=${leftEarBattery}% " +
"(${if(leftEarCharging) "充电中" else "未充电"}), 右耳=${rightEarBattery}% " +
"(${if(rightEarCharging) "充电中" else "未充电"}), 充电仓=${caseBattery}% " +
"(${if(caseCharging) "充电中" else "未充电"})")
FileLogger.i(
TAG, "接收到电量信息: 左耳=${leftEarBattery}% " +
"(${if (leftEarCharging) "充电中" else "未充电"}), 右耳=${rightEarBattery}% " +
"(${if (rightEarCharging) "充电中" else "未充电"}), 充电仓=${caseBattery}% " +
"(${if (caseCharging) "充电中" else "未充电"})"
)
}
notifyDeviceInfoReceived(cmdType, resultMap)
}
BleConst.CMD_CONTROL_CODEC -> {
val resultMap = mutableMapOf<String, Any>(
"success" to true,
"command" to cmdType,
"rawData" to responseData
)
if (responseData.isNotEmpty() && responseData.size >= 2) {
val codecStatus = responseData[0].toInt() and 0xFF
val channelMode = responseData[1].toInt() and 0xFF
// 解码状态描述
val statusDesc = when (codecStatus) {
BleConst.CODEC_CONTROL_CLOSE -> "已关闭编解码"
@ -654,7 +710,7 @@ object BleService {
BleConst.CODEC_CONTROL_ENCODE_ON -> "已打开编码"
else -> "未知状态($codecStatus)"
}
// 声道模式描述
val channelDesc = when (channelMode) {
BleConst.AUDIO_CHANNEL_LEFT -> "左声道"
@ -662,17 +718,18 @@ object BleService {
BleConst.AUDIO_CHANNEL_STEREO -> "立体声"
else -> "未知声道($channelMode)"
}
resultMap["codecStatus"] = codecStatus
resultMap["channelMode"] = channelMode
resultMap["statusDesc"] = statusDesc
resultMap["channelDesc"] = channelDesc
FileLogger.i(TAG, "编解码控制响应: $statusDesc, $channelDesc")
}
notifyDeviceInfoReceived(cmdType, resultMap)
}
else -> {
// 对于未知命令,返回原始数据
val resultMap = mapOf(
@ -686,45 +743,60 @@ object BleService {
}
}
}
/**
* 处理设备主动上报数据 - 帧头为0xCC
*/
private fun processDeviceNotification(data: ByteArray) {
FileLogger.d(TAG, "收到设备主动上报: ${data.joinToString(", ") { "0x${(it.toInt() and 0xFF).toString(16)}" }}")
FileLogger.d(
TAG,
"收到设备主动上报: ${data.joinToString(", ") { "0x${(it.toInt() and 0xFF).toString(16)}" }}"
)
if (data.size < 3) {
FileLogger.e(TAG, "设备主动上报数据格式错误:数据长度过短")
return
}
val header = data[0] // 0xCC
val infoType = data[1].toInt() and 0xFF
val length = data[2].toInt() and 0xFF
// 检查数据长度
if (data.size != length + 4) { // 帧头 + 类型 + 长度 + 数据 + CRC
FileLogger.e(TAG, "设备主动上报数据长度不匹配: 期望=${length+4}, 实际=${data.size}")
FileLogger.e(TAG, "设备主动上报数据长度不匹配: 期望=${length + 4}, 实际=${data.size}")
return
}
// 计算CRC校验 - 修正:计算除最后一个CRC字节外的所有字节
val payload = data.sliceArray(0 until data.size - 1)
val calculatedCrc = crc8Maxim(payload, payload.size)
val receivedCrc = data[data.size - 1].toInt() and 0xFF
if (calculatedCrc.toInt() and 0xFF != receivedCrc) {
FileLogger.e(TAG, "设备主动上报CRC校验失败: 计算=${calculatedCrc.toInt() and 0xFF}, 接收=${receivedCrc}")
FileLogger.e(
TAG,
"设备主动上报CRC校验失败: 计算=${calculatedCrc.toInt() and 0xFF}, 接收=${receivedCrc}"
)
return
}
// 提取上报数据
val notifyData = if (length > 0) ByteArray(length) else ByteArray(0)
if (length > 0) {
System.arraycopy(data, 3, notifyData, 0, length)
}
FileLogger.d(TAG, "收到设备主动上报: 类型=0x${infoType.toString(16)}, 数据=${notifyData.joinToString(", ") { "0x${(it.toInt() and 0xFF).toString(16)}" }}")
FileLogger.d(
TAG,
"收到设备主动上报: 类型=0x${infoType.toString(16)}, 数据=${
notifyData.joinToString(", ") {
"0x${
(it.toInt() and 0xFF).toString(16)
}"
}
}"
)
// 根据上报类型处理
when (infoType) {
BleConst.CMD_GET_BATTERY_INFO -> { // 电量信息
@ -734,19 +806,22 @@ object BleService {
"rawData" to notifyData,
"isNotification" to true // 标记为主动上报
)
if (notifyData.isNotEmpty() && notifyData.size >= 3) {
// 处理电池信息 - 左耳、右耳、充电仓
val leftEarBattery = notifyData[0].toInt() and 0x7F
val leftEarCharging = (notifyData[0].toInt() and 0x80) != 0
val rightEarBattery = if (notifyData.size > 1) notifyData[1].toInt() and 0x7F else 0
val rightEarCharging = if (notifyData.size > 1) (notifyData[1].toInt() and 0x80) != 0 else false
val rightEarBattery =
if (notifyData.size > 1) notifyData[1].toInt() and 0x7F else 0
val rightEarCharging =
if (notifyData.size > 1) (notifyData[1].toInt() and 0x80) != 0 else false
val caseBattery = if (notifyData.size > 2) notifyData[2].toInt() and 0x7F else 0
val caseCharging = if (notifyData.size > 2) (notifyData[2].toInt() and 0x80) != 0 else false
val caseCharging =
if (notifyData.size > 2) (notifyData[2].toInt() and 0x80) != 0 else false
// 添加到结果Map
resultMap["leftEarBattery"] = leftEarBattery
resultMap["leftEarCharging"] = leftEarCharging
@ -755,15 +830,18 @@ object BleService {
resultMap["caseBattery"] = caseBattery
resultMap["caseCharging"] = caseCharging
resultMap["batteryLevel"] = leftEarBattery // 整体电量用左耳表示
FileLogger.i(TAG, "收到主动上报电量信息: 左耳=${leftEarBattery}% " +
"(${if(leftEarCharging) "充电中" else "未充电"}), 右耳=${rightEarBattery}% " +
"(${if(rightEarCharging) "充电中" else "未充电"}), 充电仓=${caseBattery}% " +
"(${if(caseCharging) "充电中" else "未充电"})")
FileLogger.i(
TAG, "收到主动上报电量信息: 左耳=${leftEarBattery}% " +
"(${if (leftEarCharging) "充电中" else "未充电"}), 右耳=${rightEarBattery}% " +
"(${if (rightEarCharging) "充电中" else "未充电"}), 充电仓=${caseBattery}% " +
"(${if (caseCharging) "充电中" else "未充电"})"
)
}
notifyDeviceInfoReceived(infoType, resultMap)
}
BleConst.CMD_WAKE_UP -> {
FileLogger.i(TAG, "收到唤醒指令")
// 接收到唤醒信号,打开编码器
@ -797,7 +875,7 @@ object BleService {
FileLogger.i(TAG, "获取耳机版本号...")
return sendCommand(BleConst.CMD_GET_VERSION.toByte(), byteArrayOf(0xFF.toByte()))
}
/**
* 获取产品ID
*/
@ -805,7 +883,7 @@ object BleService {
FileLogger.i(TAG, "获取耳机产品ID...")
return sendCommand(BleConst.CMD_GET_PRODUCT_ID.toByte(), byteArrayOf(0xFF.toByte()))
}
/**
* 获取耳机颜色ID
*/
@ -813,7 +891,7 @@ object BleService {
FileLogger.i(TAG, "获取耳机颜色ID...")
return sendCommand(BleConst.CMD_GET_COLOR_ID.toByte(), byteArrayOf(0xFF.toByte()))
}
/**
* 获取耳机电量信息
*/
@ -821,23 +899,29 @@ object BleService {
FileLogger.i(TAG, "获取耳机电量信息...")
return sendCommand(BleConst.CMD_GET_BATTERY_INFO.toByte(), byteArrayOf(0xFF.toByte()))
}
/**
* 控制编解码 - 关闭编解码
*/
fun closeCodec(): Boolean {
FileLogger.i(TAG, "关闭编解码...")
return sendCommand(BleConst.CMD_CONTROL_CODEC.toByte(), byteArrayOf(BleConst.CODEC_CONTROL_CLOSE.toByte()))
return sendCommand(
BleConst.CMD_CONTROL_CODEC.toByte(),
byteArrayOf(BleConst.CODEC_CONTROL_CLOSE.toByte())
)
}
/**
* 控制编解码 - 打开解码
*/
fun openDecoder(): Boolean {
FileLogger.i(TAG, "打开解码...")
return sendCommand(BleConst.CMD_CONTROL_CODEC.toByte(), byteArrayOf(BleConst.CODEC_CONTROL_DECODE_ON.toByte()))
return sendCommand(
BleConst.CMD_CONTROL_CODEC.toByte(),
byteArrayOf(BleConst.CODEC_CONTROL_DECODE_ON.toByte())
)
}
/**
* 控制编解码 - A2DP播放
*/
@ -845,15 +929,21 @@ object BleService {
FileLogger.i(TAG, "切换至A2DP播放模式...")
return sendCommand(BleConst.CMD_PLAY_A2DP.toByte())
}
/**
* 控制编解码 - 打开编码
*/
fun openEncoder(): Boolean {
FileLogger.i(TAG, "打开编码...")
return sendCommand(BleConst.CMD_CONTROL_CODEC.toByte(), byteArrayOf(BleConst.CODEC_CONTROL_ENCODE_ON.toByte()))
return sendCommand(
BleConst.CMD_CONTROL_CODEC.toByte(),
byteArrayOf(
BleConst.CODEC_CONTROL_ENCODE_ON.toByte(),
BleConst.AUDIO_CHANNEL_RIGHT.toByte()
)
)
}
/**
* 设置声道模式
* @param channelMode 声道模式:AUDIO_CHANNEL_LEFT/AUDIO_CHANNEL_RIGHT/AUDIO_CHANNEL_STEREO
@ -868,14 +958,16 @@ object BleService {
return false
}
}
// 高级控制:设置命令类型、长度和两个参数
return sendCommand(BleConst.CMD_CONTROL_CODEC.toByte(), byteArrayOf(
0xA1.toByte(), // 状态码 - 使用解码状态
channelMode.toByte() // 声道模式
))
return sendCommand(
BleConst.CMD_CONTROL_CODEC.toByte(), byteArrayOf(
0xA1.toByte(), // 状态码 - 使用解码状态
channelMode.toByte() // 声道模式
)
)
}
/**
* 音量增大
*/
@ -883,7 +975,7 @@ object BleService {
FileLogger.i(TAG, "设备音量增加...")
return sendCommand(BleConst.CMD_VOLUME_UP.toByte())
}
/**
* 音量减小
*/
@ -901,21 +993,21 @@ object BleService {
fun isConnected(): Boolean {
return connectionState.value == BleConst.STATE_CONNECTED
}
/**
* 连接状态检查
*/
private fun checkConn(): Boolean =
bluetoothGatt != null && writeChar != null &&
connectionState.value == BleConst.STATE_CONNECTED
/**
* 获取已连接设备地址
*/
fun getConnectedDeviceAddress(): String? {
return bluetoothGatt?.device?.address
}
/**
* 获取蓝牙适配器
*/
@ -976,7 +1068,7 @@ object BleService {
val scanIntent = Intent(BleConst.ACTION_SCAN_RESULT)
.setClass(context, BleWakeReceiver::class.java)
.addFlags(Intent.FLAG_RECEIVER_FOREGROUND)
val pi = PendingIntent.getBroadcast(
context, 0, scanIntent,
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
@ -1022,34 +1114,37 @@ object BleService {
try {
// 计算命令长度
val cmdLength = cmdData.size
// 创建命令包
val cmdPacket = ByteArray(4 + cmdLength) // 帧头 + 命令 + 长度 + 数据 + CRC
// 帧头
cmdPacket[0] = BleConst.APP_REQUEST_HEADER
// 命令类型
cmdPacket[1] = cmdType
// 命令长度
cmdPacket[2] = cmdLength.toByte()
// 填充命令数据(如果有)
if (cmdLength > 0) {
System.arraycopy(cmdData, 0, cmdPacket, 3, cmdLength)
}
// 计算CRC - 对帧头到数据的所有字节进行校验 (不包括CRC自身)
val crcPayload = cmdPacket.sliceArray(0 until cmdPacket.size - 1)
val crc = crc8Maxim(crcPayload, crcPayload.size)
// 设置CRC
cmdPacket[3 + cmdLength] = crc
// 记录命令详情
FileLogger.d(TAG, "发送命令: CMD=0x${(cmdType.toInt() and 0xFF).toString(16)}, 长度=$cmdLength, " +
"数据=${cmdData.joinToString(", ") { "0x${(it.toInt() and 0xFF).toString(16)}" }}, " +
"CRC=0x${(crc.toInt() and 0xFF).toString(16)}")
FileLogger.d(
TAG,
"发送命令: CMD=0x${(cmdType.toInt() and 0xFF).toString(16)}, 长度=$cmdLength, " +
"数据=${cmdData.joinToString(", ") { "0x${(it.toInt() and 0xFF).toString(16)}" }}, " +
"CRC=0x${(crc.toInt() and 0xFF).toString(16)}"
)
// 发送数据
writeChar!!.value = cmdPacket
return bluetoothGatt?.writeCharacteristic(writeChar) ?: false
@ -1091,7 +1186,7 @@ object BleService {
}
}
}
/**
* 向所有回调监听器分发连接状态变化
*/
@ -1104,7 +1199,7 @@ object BleService {
}
}
}
/**
* 向所有回调监听器分发音频数据
*/
@ -1117,7 +1212,7 @@ object BleService {
}
}
}
/**
* 向所有回调监听器分发唤醒信号
*/
@ -1130,7 +1225,7 @@ object BleService {
}
}
}
/**
* 向所有回调监听器分发设备信息
*/

49
local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/ChatAgent.kt

@ -12,13 +12,13 @@ import com.yunqiinnovation.deepvoice_utils.FileLogger
*/
object ChatAgent : BleService.Callback, AgentServiceListener {
private const val TAG = "ChatAgent"
// 是否已初始化
private var isInitialized = false
// 是否已注册BLE回调
private var isRegisteredToBle = false
/**
* 初始化语音代理
* @param context 上下文
@ -26,16 +26,16 @@ object ChatAgent : BleService.Callback, AgentServiceListener {
*/
fun initialize(context: Context, config: Map<String, Any>): Boolean {
if (isInitialized) return true
try {
FileLogger.d(TAG, "初始化语音代理..., $config")
// 1. 初始化AgentService
if (!AgentService.initialize(context, config)) {
if (!AgentService.initialize(context, config, true)) {
FileLogger.e(TAG, "语音服务初始化失败")
return false
}
// 2. 初始化BleService并注册回调
if (BleService.initialize(context)) {
if (BleService.addCallback(this)) {
@ -48,7 +48,7 @@ object ChatAgent : BleService.Callback, AgentServiceListener {
FileLogger.e(TAG, "BLE服务初始化失败")
return false
}
isInitialized = true
FileLogger.d(TAG, "语音代理初始化成功")
return true
@ -57,7 +57,7 @@ object ChatAgent : BleService.Callback, AgentServiceListener {
return false
}
}
/**
* 手动启动语音识别
*/
@ -66,31 +66,31 @@ object ChatAgent : BleService.Callback, AgentServiceListener {
FileLogger.e(TAG, "语音代理未初始化")
return false
}
return AgentService.startRecognition()
return AgentService.startRecognition(true)
}
/**
* 停止语音识别
*/
fun stopListening() {
AgentService.stopRecognition()
}
/**
* 处理文本输入
*/
fun processTextInput(text: String, speakResponse: Boolean = false): Boolean {
return AgentService.processTextInput(text, speakResponse)
}
/**
* 停止TTS
*/
fun stopTts() {
AgentService.stopTts()
}
/**
* 清理资源
*/
@ -100,21 +100,21 @@ object ChatAgent : BleService.Callback, AgentServiceListener {
BleService.removeCallback(this)
isRegisteredToBle = false
}
// 释放AgentService资源
AgentService.dispose()
isInitialized = false
FileLogger.d(TAG, "语音代理资源已释放")
}
//============================================================================================
// BleService.Callback 接口实现
//============================================================================================
override fun onScanResult(devices: List<Map<String, Any>>) {
// 不处理扫描结果
}
override fun onConnectionStateChanged(state: Int) {
// 可选:处理连接状态变化
if (state == BleConst.STATE_CONNECTED) {
@ -125,18 +125,19 @@ object ChatAgent : BleService.Callback, AgentServiceListener {
// stopListening()
}
}
override fun onAudioDataReceived(data: ByteArray) {
AgentService.azureAsrHelper?.pushAudioData(data)
// 可选:处理音频数据
}
/**
* 处理唤醒信号
* 在收到唤醒信号时启动语音识别
*/
override fun onWakeupSignalReceived() {
FileLogger.d(TAG, "收到唤醒信号,启动语音识别")
// 停止当前TTS,避免冲突
AgentService.interruptCurrentResponse()
AgentService.speakText("我在!")
@ -148,11 +149,11 @@ object ChatAgent : BleService.Callback, AgentServiceListener {
startListening()
}
override fun onDeviceInfoReceived(infoType: Int, infoData: Map<String, Any>) {
// 不处理设备信息
}
//============================================================================================
// AgentServiceListener 接口实现
//============================================================================================

18
local_plugins/jieli/android/src/main/kotlin/com/yunqiinnovation/jieli/BleService.kt

@ -70,6 +70,7 @@ class BleService(private val context: Context) {
// 获取插件实例
private val opusPlugin = JlOpusPlugin()
// 蓝牙管理器和适配器
private val bluetoothManager =
context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
@ -325,13 +326,14 @@ class BleService(private val context: Context) {
// 注册特征通知
setupNotifications(gatt)
opusPlugin?.startOpusDecodeStream(false, 1, 16000, 40, object :
JlOpusPlugin.Callback1 {
override fun onAudioDataReceived1(pcmData: ByteArray) {
// Log.i(TAG, "收到解码控制通知: ${pcmData.contentToString()}")
opusPlugin?.startOpusDecodeStream(false, 1, 16000, 40, object :
JlOpusPlugin.CallbackOpus {
override fun onAudioDataReceivedOpus(pcmData: ByteArray) {
// Log.i(TAG, "收到解码控制通知: ${pcmData.contentToString()}")
callback?.onAudioDataReceived(pcmData)
// 处理解码后的 PCM 数据(如播放或写入文件)
}})
}
})
} else {
Log.e(TAG, "未找到所需特征")
updateConnectionState(STATE_ERROR)
@ -356,7 +358,7 @@ class BleService(private val context: Context) {
characteristic: BluetoothGattCharacteristic
) {
val characteristicUuid = characteristic.uuid.toString()
// Log.d(TAG, "收到特征变化通知: $characteristicUuid")
// Log.d(TAG, "收到特征变化通知: $characteristicUuid")
when (characteristicUuid) {
// NOTIFY_CHAR_UUID -> {
@ -369,7 +371,7 @@ class BleService(private val context: Context) {
NOTIFY_CHAR_UUID -> {
// 处理控制消息
val value = characteristic.value
// Log.i(TAG, "收到控制通知: ${value.size}")
// Log.i(TAG, "收到控制通知: ${value.size}")
// 判断是否收到音频数据
if (value.isNotEmpty() && (value[0].toInt() and 0xFF == 0x4b || value[0].toInt() and 0xFF == 0x48)) {
@ -378,7 +380,7 @@ class BleService(private val context: Context) {
opusPlugin?.handleDecodeRequest(value)
}
// 判断是否是协议格式响应
if (value.isNotEmpty() && value[0].toInt() and 0xFF == 0xA5) {

695
local_plugins/jl_opus/android/src/main/kotlin/com/yunqiinnovation/jl_opus/JlOpusPlugin.kt

@ -19,359 +19,408 @@ import io.flutter.plugin.common.MethodChannel.Result
import java.io.File
/** JlOpusPlugin */
class JlOpusPlugin: FlutterPlugin, MethodCallHandler {
private lateinit var channel: MethodChannel
private lateinit var eventChannel: EventChannel
private lateinit var context: Context
private var opusManager: OpusManager? = null
private var streamSink: EventChannel.EventSink? = null
// 修正1:正确的主构造函数写法
constructor() {
class JlOpusPlugin : FlutterPlugin, MethodCallHandler {
private lateinit var channel: MethodChannel
private lateinit var eventChannel: EventChannel
private lateinit var context: Context
private var opusManager: OpusManager? = null
private var streamSink: EventChannel.EventSink? = null
// 修正1:正确的主构造函数写法
constructor() {
opusManager = OpusManager()
Log.d(TAG, "OpusManager初始化成功")
}
// 添加主线程Handler用于确保在主线程回调
private val mainHandler = Handler(Looper.getMainLooper())
interface Callback1 {
fun onAudioDataReceived1(data: ByteArray)
}
// 添加主线程Handler用于确保在主线程回调
private val mainHandler = Handler(Looper.getMainLooper())
interface CallbackOpus {
fun onAudioDataReceivedOpus(data: ByteArray)
companion object {
private const val TAG = "JlOpusPlugin"
private const val CHANNEL_NAME = "com.yunqiinnovation.jl_opus"
private const val EVENT_CHANNEL_NAME = "com.yunqiinnovation.jl_opus/events"
}
override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
context = flutterPluginBinding.applicationContext
channel = MethodChannel(flutterPluginBinding.binaryMessenger, CHANNEL_NAME)
channel.setMethodCallHandler(this)
eventChannel = EventChannel(flutterPluginBinding.binaryMessenger, EVENT_CHANNEL_NAME)
eventChannel.setStreamHandler(object : EventChannel.StreamHandler {
override fun onListen(arguments: Any?, events: EventChannel.EventSink?) {
streamSink = events
}
override fun onCancel(arguments: Any?) {
streamSink = null
}
})
// 初始化OpusManager
try {
opusManager = OpusManager()
Log.d(TAG, "OpusManager初始化成功")
} catch (e: OpusException) {
Log.e(TAG, "OpusManager初始化失败: ${e.message}", e)
}
}
// 在主线程上安全发送事件
private fun sendEventOnMainThread(event: Map<String, Any?>) {
if (streamSink == null) return
if (Looper.myLooper() == Looper.getMainLooper()) {
// 如果已经在主线程,直接发送
streamSink?.success(event)
} else {
// 否则切换到主线程
mainHandler.post {
streamSink?.success(event)
}
}
}
// 在主线程上安全返回结果
private fun sendResultOnMainThread(result: Result, value: Any?) {
if (Looper.myLooper() == Looper.getMainLooper()) {
// 如果已经在主线程,直接返回
result.success(value)
} else {
// 否则切换到主线程
mainHandler.post {
result.success(value)
}
}
}
// 在主线程上安全返回错误
private fun sendErrorOnMainThread(result: Result, errorCode: String, errorMessage: String, errorDetails: Any?) {
if (Looper.myLooper() == Looper.getMainLooper()) {
// 如果已经在主线程,直接返回
result.error(errorCode, errorMessage, errorDetails)
} else {
// 否则切换到主线程
mainHandler.post {
result.error(errorCode, errorMessage, errorDetails)
}
}
}
override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) {
if (opusManager == null) {
result.error("OPUS_MANAGER_NULL", "OpusManager未初始化", null)
return
companion object {
private const val TAG = "JlOpusPlugin"
private const val CHANNEL_NAME = "com.yunqiinnovation.jl_opus"
private const val EVENT_CHANNEL_NAME = "com.yunqiinnovation.jl_opus/events"
}
when (call.method) {
"initOpusDecoder" -> {
result.success(opusManager != null)
}
"decodeOpusFile" -> {
val inPath = call.argument<String>("inPath")
val outPath = call.argument<String>("outPath")
val hasHeader = call.argument<Boolean>("hasHeader") ?: false
val channel = call.argument<Int>("channel") ?: 1
val sampleRate = call.argument<Int>("sampleRate") ?: 16000
val packetSize = call.argument<Int>("packetSize") ?: 40
if (inPath == null || outPath == null) {
result.error("INVALID_ARGS", "输入或输出路径不能为空", null)
return
}
// 确保输入文件存在
val inFile = File(inPath)
if (!inFile.exists()) {
result.error("FILE_NOT_FOUND", "输入文件不存在: $inPath", null)
return
}
// 创建解码参数
val option = OpusOption()
.setHasHead(hasHeader)
.setChannel(channel)
.setSampleRate(sampleRate)
.setPacketSize(packetSize)
Log.d(TAG, "开始解码文件, 参数: $option, 输入: $inPath, 输出: $outPath")
opusManager?.decodeFile(inPath, outPath, option, object : OnStateCallback {
override fun onStart() {
Log.d(TAG, "文件解码开始")
sendEventOnMainThread(mapOf(
"event" to "onStart",
"type" to "file"
))
}
override fun onComplete(outFilePath: String?) {
Log.d(TAG, "文件解码完成: $outFilePath")
sendEventOnMainThread(mapOf(
"event" to "onComplete",
"type" to "file",
"filePath" to outFilePath
))
sendResultOnMainThread(result, outFilePath)
}
override fun onError(code: Int, message: String?) {
Log.e(TAG, "文件解码错误: [$code] $message")
sendEventOnMainThread(mapOf(
"event" to "onError",
"type" to "file",
"code" to code,
"message" to message
))
sendErrorOnMainThread(result, "DECODE_ERROR", "解码出错: [$code] $message", null)
}
})
}
"startDecodeStream" -> {
val hasHeader = call.argument<Boolean>("hasHeader") ?: false
val channel = call.argument<Int>("channel") ?: 1
val sampleRate = call.argument<Int>("sampleRate") ?: 16000
val packetSize = call.argument<Int>("packetSize") ?: 40
// 如果已经在解码流,先停止
if (opusManager?.isDecodeStream == true) {
opusManager?.stopDecodeStream()
}
// 创建解码参数
val option = OpusOption()
.setHasHead(hasHeader)
.setChannel(channel)
.setSampleRate(sampleRate)
.setPacketSize(packetSize)
Log.d(TAG, "开始解码数据流, 参数: $option")
opusManager?.startDecodeStream(option, object : OnDecodeStreamCallback {
override fun onDecodeStream(data: ByteArray?) {
if (data != null) {
sendEventOnMainThread(mapOf(
"event" to "onDecodeStream",
"data" to data
))
override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
context = flutterPluginBinding.applicationContext
channel = MethodChannel(flutterPluginBinding.binaryMessenger, CHANNEL_NAME)
channel.setMethodCallHandler(this)
eventChannel = EventChannel(flutterPluginBinding.binaryMessenger, EVENT_CHANNEL_NAME)
eventChannel.setStreamHandler(object : EventChannel.StreamHandler {
override fun onListen(arguments: Any?, events: EventChannel.EventSink?) {
streamSink = events
}
override fun onCancel(arguments: Any?) {
streamSink = null
}
}
override fun onStart() {
Log.d(TAG, "数据流解码开始")
sendEventOnMainThread(mapOf(
"event" to "onStart",
"type" to "stream"
))
sendResultOnMainThread(result, true)
}
override fun onComplete(outPath: String?) {
Log.d(TAG, "数据流解码完成: $outPath")
sendEventOnMainThread(mapOf(
"event" to "onComplete",
"type" to "stream"
))
}
override fun onError(code: Int, message: String?) {
Log.e(TAG, "数据流解码错误: [$code] $message")
sendEventOnMainThread(mapOf(
"event" to "onError",
"type" to "stream",
"code" to code,
"message" to message
))
sendErrorOnMainThread(result, "DECODE_STREAM_ERROR", "数据流解码出错: [$code] $message", null)
}
})
}
"stopDecodeStream" -> {
if (opusManager?.isDecodeStream == true) {
opusManager?.stopDecodeStream()
result.success(true)
// 初始化OpusManager
try {
opusManager = OpusManager()
Log.d(TAG, "OpusManager初始化成功")
} catch (e: OpusException) {
Log.e(TAG, "OpusManager初始化失败: ${e.message}", e)
}
}
// 在主线程上安全发送事件
private fun sendEventOnMainThread(event: Map<String, Any?>) {
if (streamSink == null) return
if (Looper.myLooper() == Looper.getMainLooper()) {
// 如果已经在主线程,直接发送
streamSink?.success(event)
} else {
result.success(false)
// 否则切换到主线程
mainHandler.post {
streamSink?.success(event)
}
}
}
"writeAudioStream" -> {
val data = call.argument<ByteArray>("data")
if (data == null) {
result.error("INVALID_ARGS", "音频数据不能为空", null)
return
}
// 在主线程上安全返回结果
private fun sendResultOnMainThread(result: Result, value: Any?) {
if (Looper.myLooper() == Looper.getMainLooper()) {
// 如果已经在主线程,直接返回
result.success(value)
} else {
// 否则切换到主线程
mainHandler.post {
result.success(value)
}
}
if (opusManager?.isDecodeStream == true) {
opusManager?.writeAudioStream(data)
result.success(true)
}
// 在主线程上安全返回错误
private fun sendErrorOnMainThread(
result: Result,
errorCode: String,
errorMessage: String,
errorDetails: Any?
) {
if (Looper.myLooper() == Looper.getMainLooper()) {
// 如果已经在主线程,直接返回
result.error(errorCode, errorMessage, errorDetails)
} else {
result.error("NOT_DECODING", "当前没有处于解码状态", null)
// 否则切换到主线程
mainHandler.post {
result.error(errorCode, errorMessage, errorDetails)
}
}
}
"isDecoding" -> {
result.success(opusManager?.isDecodeStream == true)
}
"dispose" -> {
opusManager?.let {
if (it.isDecodeStream) {
it.stopDecodeStream()
}
it.release()
}
override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) {
if (opusManager == null) {
result.error("OPUS_MANAGER_NULL", "OpusManager未初始化", null)
return
}
when (call.method) {
"initOpusDecoder" -> {
result.success(opusManager != null)
}
"decodeOpusFile" -> {
val inPath = call.argument<String>("inPath")
val outPath = call.argument<String>("outPath")
val hasHeader = call.argument<Boolean>("hasHeader") ?: false
val channel = call.argument<Int>("channel") ?: 1
val sampleRate = call.argument<Int>("sampleRate") ?: 16000
val packetSize = call.argument<Int>("packetSize") ?: 40
if (inPath == null || outPath == null) {
result.error("INVALID_ARGS", "输入或输出路径不能为空", null)
return
}
// 确保输入文件存在
val inFile = File(inPath)
if (!inFile.exists()) {
result.error("FILE_NOT_FOUND", "输入文件不存在: $inPath", null)
return
}
// 创建解码参数
val option = OpusOption()
.setHasHead(hasHeader)
.setChannel(channel)
.setSampleRate(sampleRate)
.setPacketSize(packetSize)
Log.d(TAG, "开始解码文件, 参数: $option, 输入: $inPath, 输出: $outPath")
opusManager?.decodeFile(inPath, outPath, option, object : OnStateCallback {
override fun onStart() {
Log.d(TAG, "文件解码开始")
sendEventOnMainThread(
mapOf(
"event" to "onStart",
"type" to "file"
)
)
}
override fun onComplete(outFilePath: String?) {
Log.d(TAG, "文件解码完成: $outFilePath")
sendEventOnMainThread(
mapOf(
"event" to "onComplete",
"type" to "file",
"filePath" to outFilePath
)
)
sendResultOnMainThread(result, outFilePath)
}
override fun onError(code: Int, message: String?) {
Log.e(TAG, "文件解码错误: [$code] $message")
sendEventOnMainThread(
mapOf(
"event" to "onError",
"type" to "file",
"code" to code,
"message" to message
)
)
sendErrorOnMainThread(
result,
"DECODE_ERROR",
"解码出错: [$code] $message",
null
)
}
})
}
"startDecodeStream" -> {
val hasHeader = call.argument<Boolean>("hasHeader") ?: false
val channel = call.argument<Int>("channel") ?: 1
val sampleRate = call.argument<Int>("sampleRate") ?: 16000
val packetSize = call.argument<Int>("packetSize") ?: 40
// 如果已经在解码流,先停止
if (opusManager?.isDecodeStream == true) {
opusManager?.stopDecodeStream()
}
// 创建解码参数
val option = OpusOption()
.setHasHead(hasHeader)
.setChannel(channel)
.setSampleRate(sampleRate)
.setPacketSize(packetSize)
Log.d(TAG, "开始解码数据流, 参数: $option")
opusManager?.startDecodeStream(option, object : OnDecodeStreamCallback {
override fun onDecodeStream(data: ByteArray?) {
if (data != null) {
sendEventOnMainThread(
mapOf(
"event" to "onDecodeStream",
"data" to data
)
)
}
}
override fun onStart() {
Log.d(TAG, "数据流解码开始")
sendEventOnMainThread(
mapOf(
"event" to "onStart",
"type" to "stream"
)
)
sendResultOnMainThread(result, true)
}
override fun onComplete(outPath: String?) {
Log.d(TAG, "数据流解码完成: $outPath")
sendEventOnMainThread(
mapOf(
"event" to "onComplete",
"type" to "stream"
)
)
}
override fun onError(code: Int, message: String?) {
Log.e(TAG, "数据流解码错误: [$code] $message")
sendEventOnMainThread(
mapOf(
"event" to "onError",
"type" to "stream",
"code" to code,
"message" to message
)
)
sendErrorOnMainThread(
result,
"DECODE_STREAM_ERROR",
"数据流解码出错: [$code] $message",
null
)
}
})
}
"stopDecodeStream" -> {
if (opusManager?.isDecodeStream == true) {
opusManager?.stopDecodeStream()
result.success(true)
} else {
result.success(false)
}
}
"writeAudioStream" -> {
val data = call.argument<ByteArray>("data")
if (data == null) {
result.error("INVALID_ARGS", "音频数据不能为空", null)
return
}
if (opusManager?.isDecodeStream == true) {
opusManager?.writeAudioStream(data)
result.success(true)
} else {
result.error("NOT_DECODING", "当前没有处于解码状态", null)
}
}
"isDecoding" -> {
result.success(opusManager?.isDecodeStream == true)
}
"dispose" -> {
opusManager?.let {
if (it.isDecodeStream) {
it.stopDecodeStream()
}
it.release()
}
opusManager = null
result.success(true)
}
else -> {
result.notImplemented()
}
}
opusManager = null
result.success(true)
}
else -> {
result.notImplemented()
}
}
}
fun handleDecodeRequest(opusdata: ByteArray) {
fun handleDecodeRequest(opusdata: ByteArray) {
// Log.d(TAG, "handleDecodeRequest${opusdata}")
if (opusdata == null) {
return
return
}
if (opusManager?.isDecodeStream == true) {
opusManager?.writeAudioStream(opusdata)
if (opusManager?.isDecodeStream == true) {
opusManager?.writeAudioStream(opusdata)
} else {
}
}
fun startOpusDecodeStream(
hasHeader: Boolean,
channel: Int,
sampleRate: Int,
packetSize: Int,
callback: Callback1
) {
Log.d(TAG, "开始解码数据流,startOpusDecodeStream: $opusManager")
// 如果已经在解码流,先停止
}
fun startOpusDecodeStream(
hasHeader: Boolean,
channel: Int,
sampleRate: Int,
packetSize: Int,
callback: CallbackOpus
) {
Log.d(TAG, "开始解码数据流,startOpusDecodeStream: $opusManager")
// 如果已经在解码流,先停止
if (opusManager?.isDecodeStream == true) {
opusManager?.stopDecodeStream()
opusManager?.stopDecodeStream()
}
// 创建解码参数
val option = OpusOption()
.setHasHead(hasHeader)
.setChannel(channel)
.setSampleRate(sampleRate)
.setPacketSize(packetSize)
.setHasHead(hasHeader)
.setChannel(channel)
.setSampleRate(sampleRate)
.setPacketSize(packetSize)
Log.d(TAG, "开始解码数据流, 参数: $option")
opusManager?.startDecodeStream(option, object : OnDecodeStreamCallback {
override fun onDecodeStream(data: ByteArray?) {
if (data != null) {
//Log.d(TAG, "onDecodeStream${data}")
callback.onAudioDataReceived1(data)
sendEventOnMainThread(mapOf(
"event" to "onDecodeStream",
"data" to data
))
override fun onDecodeStream(data: ByteArray?) {
if (data != null) {
//Log.d(TAG, "onDecodeStream${data}")
callback.onAudioDataReceivedOpus(data)
sendEventOnMainThread(
mapOf(
"event" to "onDecodeStream",
"data" to data
)
)
}
}
override fun onStart() {
Log.d(TAG, "数据流解码开始")
sendEventOnMainThread(
mapOf(
"event" to "onStart",
"type" to "stream"
)
)
}
override fun onComplete(outPath: String?) {
Log.d(TAG, "数据流解码完成: $outPath")
sendEventOnMainThread(
mapOf(
"event" to "onComplete",
"type" to "stream"
)
)
}
override fun onError(code: Int, message: String?) {
Log.e(TAG, "数据流解码错误: [$code] $message")
sendEventOnMainThread(
mapOf(
"event" to "onError",
"type" to "stream",
"code" to code,
"message" to message
)
)
}
}
override fun onStart() {
Log.d(TAG, "数据流解码开始")
sendEventOnMainThread(mapOf(
"event" to "onStart",
"type" to "stream"
))
}
override fun onComplete(outPath: String?) {
Log.d(TAG, "数据流解码完成: $outPath")
sendEventOnMainThread(mapOf(
"event" to "onComplete",
"type" to "stream"
))
}
override fun onError(code: Int, message: String?) {
Log.e(TAG, "数据流解码错误: [$code] $message")
sendEventOnMainThread(mapOf(
"event" to "onError",
"type" to "stream",
"code" to code,
"message" to message
))
}
})
}
override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) {
channel.setMethodCallHandler(null)
eventChannel.setStreamHandler(null)
opusManager?.let {
if (it.isDecodeStream) {
it.stopDecodeStream()
}
it.release()
}
opusManager = null
}
}
override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) {
channel.setMethodCallHandler(null)
eventChannel.setStreamHandler(null)
opusManager?.let {
if (it.isDecodeStream) {
it.stopDecodeStream()
}
it.release()
}
opusManager = null
}
}
Loading…
Cancel
Save