diff --git a/local_plugins/location_service/pubspec.yaml b/local_plugins/location_service/pubspec.yaml
index a7809ba9f..04f1a58f4 100644
--- a/local_plugins/location_service/pubspec.yaml
+++ b/local_plugins/location_service/pubspec.yaml
@@ -4,7 +4,7 @@ version: 0.0.1
homepage:
environment:
- sdk: ^3.8.1
+ sdk: '>=3.8.0'
flutter: '>=3.3.0'
dependencies:
diff --git a/local_plugins/realtime/.gitignore b/local_plugins/realtime/.gitignore
deleted file mode 100644
index f5af98475..000000000
--- a/local_plugins/realtime/.gitignore
+++ /dev/null
@@ -1,51 +0,0 @@
-# Miscellaneous
-*.class
-*.log
-*.pyc
-*.swp
-.DS_Store
-.atom/
-.buildlog/
-.history
-.svn/
-
-# IntelliJ related
-*.iml
-*.ipr
-*.iws
-.idea/
-
-# Visual Studio Code related
-.vscode/
-
-# Flutter/Dart/Pub related
-**/doc/api/
-.dart_tool/
-.flutter-plugins
-.flutter-plugins-dependencies
-.packages
-.pub-cache/
-.pub/
-build/
-
-# iOS related
-**/ios/.generated/
-**/ios/Flutter/Flutter.framework
-**/ios/Flutter/Flutter.podspec
-**/ios/Flutter/Generated.xcconfig
-**/ios/Flutter/app.flx
-**/ios/Flutter/app.zip
-**/ios/Flutter/flutter_assets/
-**/ios/Flutter/flutter_export_environment.sh
-**/ios/ServiceDefinitions.json
-**/ios/Runner/GeneratedPluginRegistrant.*
-.build/
-
-# Coverage
-coverage/
-
-# Exceptions to above rules.
-!**/ios/**/default.mode1v3
-!**/ios/**/default.mode2v3
-!**/ios/**/default.pbxuser
-!**/ios/**/default.perspectivev3
\ No newline at end of file
diff --git a/local_plugins/realtime/IMPLEMENTATION.md b/local_plugins/realtime/IMPLEMENTATION.md
deleted file mode 100644
index a5d49a976..000000000
--- a/local_plugins/realtime/IMPLEMENTATION.md
+++ /dev/null
@@ -1,190 +0,0 @@
-# Realtime Plugin 实现总结
-
-## 项目概述
-
-基于您提供的Android原生示例,我为DeepVoice项目创建了一个realtime插件,用于通过WebSocket接入Vocode服务器实现实时语音聊天功能。该插件已完成Android和iOS双平台实现。
-
-## 架构设计
-
-### 插件结构
-```
-local_plugins/realtime/
-├── android/ # Android实现
-│ ├── build.gradle # Android构建配置
-│ ├── src/main/
-│ │ ├── AndroidManifest.xml # Android权限配置
-│ │ └── kotlin/com/yunqiinnovation/realtime/
-│ │ ├── RealtimePlugin.kt # 主插件入口
-│ │ ├── RealtimeAudioManager.kt # 音频管理器(AudioRecord/AudioTrack)
-│ │ └── RealtimeWebSocketManager.kt # WebSocket管理器(OkHttp)
-├── ios/ # iOS实现
-│ ├── Classes/ # Objective-C桥接文件
-│ │ ├── RealtimePlugin.h
-│ │ └── RealtimePlugin.m
-│ ├── realtime/ # Swift Package
-│ │ ├── Package.swift
-│ │ └── Sources/realtime/
-│ │ ├── RealtimePlugin.swift # 主插件入口
-│ │ ├── RealtimeAudioManager.swift # 音频管理器(AVAudioEngine)
-│ │ └── RealtimeWebSocketManager.swift # WebSocket管理器(URLSession)
-│ └── realtime.podspec
-├── lib/
-│ └── realtime.dart # Flutter接口
-├── pubspec.yaml
-└── README.md
-```
-
-### 核心组件
-
-1. **RealtimePlugin (SwiftRealtimePlugin)**
- - 主插件入口,处理Flutter方法调用
- - 管理各组件间的协调
- - 处理事件分发
-
-2. **RealtimeAudioManager**
- - 使用AVAudioEngine进行音频录制
- - 使用AVAudioPlayerNode进行音频播放
- - 支持16kHz/16-bit/单声道格式
- - 实现20ms帧长的音频处理
-
-3. **RealtimeWebSocketManager**
- - 使用URLSessionWebSocketTask实现WebSocket通信
- - 支持音频数据和文本消息的收发
- - 自动连接状态管理
-
-## 技术实现对照
-
-| 功能 | Android示例 | iOS实现 |
-|------|-------------|---------|
-| 音频录制 | AudioRecord | AVAudioEngine + AVAudioInputNode |
-| 音频播放 | AudioTrack | AVAudioPlayerNode |
-| WebSocket | OkHttp WebSocket | URLSessionWebSocketTask |
-| 线程管理 | Kotlin协程 | DispatchQueue |
-| 音频格式 | 16kHz/16-bit/单声道 | 16kHz/16-bit/单声道 |
-| 帧长 | 20ms (320字节) | 20ms (320字节) |
-
-## Flutter接口
-
-### 主要类
-- `RealtimeService`: 主服务类
-- `RealtimeEvent`: 事件类
-- `ConnectionStatus`: 连接状态枚举
-- `VoiceStatus`: 语音状态枚举
-- `RealtimeException`: 异常类
-
-### 主要方法
-- `initialize()`: 初始化服务
-- `connect()`: 连接服务器
-- `disconnect()`: 断开连接
-- `startRecording()`: 开始录音
-- `stopRecording()`: 停止录音
-- `eventStream`: 事件流
-
-## 使用方式
-
-### 1. 在RealtimeController中集成
-已更新`lib/modules/realtime/controllers/realtime_controller.dart`使用真实的realtime插件,取代了原来的模拟实现。
-
-### 2. 事件监听
-```dart
-_realtimeService.eventStream.listen((event) {
- switch (event.type) {
- case 'connectionStatusChanged':
- // 处理连接状态变化
- case 'voiceStatusChanged':
- // 处理语音状态变化
- case 'textReceived':
- // 处理文本消息
- case 'error':
- // 处理错误
- }
-});
-```
-
-## 配置要求
-
-### 依赖配置
-已添加到主项目的`pubspec.yaml`:
-```yaml
-realtime:
- path: local_plugins/realtime
-```
-
-### 权限配置
-iOS的`Info.plist`已包含必要的麦克风权限:
-```xml
-NSMicrophoneUsageDescription
-需要麦克风权限用于语音识别和录音功能
-```
-
-## 与Android示例的对应关系
-
-1. **初始化对应**
- - Android: 创建AudioRecord, AudioTrack, OkHttpClient
- - iOS: 创建AVAudioEngine, AVAudioPlayerNode, URLSession
-
-2. **录音线程对应**
- - Android: `loopRecordSend()` 协程
- - iOS: AVAudioInputNode的installTap回调
-
-3. **播放线程对应**
- - Android: `loopPlayback()` 协程 + LinkedBlockingQueue
- - iOS: DispatchQueue + AVAudioPCMBuffer队列
-
-4. **WebSocket对应**
- - Android: OkHttp WebSocketListener
- - iOS: URLSessionWebSocketDelegate
-
-## 特性支持
-
-✅ **已实现**
-- Android和iOS双平台支持
-- 实时音频录制和播放(16kHz/16-bit/单声道)
-- WebSocket双向通信
-- 状态管理和事件通知
-- 错误处理
-- 资源管理和清理
-- 20ms帧长处理
-- Kotlin协程和DispatchQueue线程管理
-
-❌ **未实现(可扩展)**
-- 音频编码(Opus等)
-- 噪声消除/回声抑制
-- 自动重连机制
-- 音频质量自适应
-
-## 使用注意事项
-
-1. **服务器地址配置**
- ```dart
- // 需要替换为实际的Vocode服务器地址
- serverUrl: 'wss://your-vocode-server/ws'
- ```
-
-2. **音频格式一致性**
- - 确保服务器支持16kHz/16-bit/单声道格式
- - 帧长固定为20ms (320字节)
-
-3. **错误处理**
- - 监听事件流中的错误
- - 处理网络断线和重连
-
-4. **资源管理**
- - 及时调用dispose()释放资源
- - 避免内存泄漏
-
-## 测试建议
-
-1. **本地测试**
- - 先用echo服务器测试WebSocket连接
- - 验证音频录制和播放功能
-
-2. **集成测试**
- - 与真实Vocode服务器集成
- - 测试端到端语音交互
-
-3. **性能测试**
- - 测试长时间使用的稳定性
- - 监控内存和CPU使用
-
-这个实现为DeepVoice项目提供了完整的实时语音交互能力,可以直接与Vocode服务器进行通信,实现类似Android示例的功能。
\ No newline at end of file
diff --git a/local_plugins/realtime/README.md b/local_plugins/realtime/README.md
deleted file mode 100644
index a5bc746f0..000000000
--- a/local_plugins/realtime/README.md
+++ /dev/null
@@ -1,125 +0,0 @@
-# Realtime Plugin
-
-实时语音聊天插件,通过WebSocket连接Vocode服务器实现实时语音交互。
-
-## 功能特性
-
-- **实时音频录制**: 16kHz/16-bit/单声道格式录音
-- **WebSocket通信**: 与Vocode服务器进行实时数据传输
-- **音频播放**: 播放服务器返回的语音数据
-- **状态管理**: 连接状态和语音状态监控
-- **事件回调**: 支持各种事件的监听和处理
-
-## 技术实现
-
-### Android端实现
-- **音频录制**: 使用AudioRecord进行16kHz/16-bit/单声道录音
-- **音频播放**: 使用AudioTrack进行音频播放
-- **WebSocket**: 使用OkHttp WebSocket客户端
-- **线程管理**: 使用Kotlin协程处理录音和播放
-
-### iOS端实现
-- **音频录制**: 使用AVAudioEngine和AVAudioInputNode
-- **音频播放**: 使用AVAudioPlayerNode进行播放
-- **WebSocket**: 使用URLSessionWebSocketTask
-- **线程管理**: 使用DispatchQueue管理录音和播放线程
-
-### 核心组件
-- `RealtimePlugin`: 主插件入口,处理Flutter方法调用
-- `RealtimeAudioManager`: 音频管理器,负责录音和播放
-- `RealtimeWebSocketManager`: WebSocket管理器,负责网络通信
-
-**Android端组件**:
-- AudioRecord + AudioTrack + OkHttp + Kotlin协程
-- 对应您提供的Android原生示例功能
-
-**iOS端组件**:
-- AVAudioEngine + AVAudioPlayerNode + URLSessionWebSocketTask + DispatchQueue
-
-## 使用方法
-
-```dart
-import 'package:realtime/realtime.dart';
-
-final realtimeService = RealtimeService();
-
-// 初始化
-await realtimeService.initialize(
- serverUrl: 'wss://your-server/ws',
- sampleRate: 16000,
- channels: 1,
- bitsPerSample: 16,
-);
-
-// 监听事件
-realtimeService.eventStream.listen((event) {
- switch (event.type) {
- case 'connectionStatusChanged':
- // 处理连接状态变化
- break;
- case 'voiceStatusChanged':
- // 处理语音状态变化
- break;
- case 'textReceived':
- // 处理收到的文本消息
- break;
- case 'error':
- // 处理错误
- break;
- }
-});
-
-// 连接服务器
-await realtimeService.connect();
-
-// 开始录音
-await realtimeService.startRecording();
-
-// 停止录音
-await realtimeService.stopRecording();
-
-// 断开连接
-await realtimeService.disconnect();
-
-// 释放资源
-await realtimeService.dispose();
-```
-
-## 音频格式
-
-- **采样率**: 16000 Hz
-- **声道数**: 1(单声道)
-- **位深**: 16-bit
-- **帧长**: 20ms (320字节)
-
-## 权限要求
-
-### Android
-权限已自动包含在插件中:
-- `RECORD_AUDIO`: 音频录制权限
-- `INTERNET`: 网络访问权限
-- `ACCESS_NETWORK_STATE`: 网络状态权限
-
-### iOS
-在Info.plist中添加麦克风权限:
-```xml
-NSMicrophoneUsageDescription
-应用需要麦克风权限进行语音录制
-```
-
-## 注意事项
-
-1. 确保服务器地址正确且可访问
-2. 网络环境良好,避免频繁断线
-3. 音频格式与服务器保持一致
-4. 及时释放资源,避免内存泄漏
-
-## 错误处理
-
-插件会自动处理常见错误:
-- 网络连接失败
-- 音频设备不可用
-- 权限被拒绝
-- 服务器断开连接
-
-通过事件流可以监听这些错误并进行相应处理。
\ No newline at end of file
diff --git a/local_plugins/realtime/android/build.gradle.kts b/local_plugins/realtime/android/build.gradle.kts
deleted file mode 100644
index b787e4124..000000000
--- a/local_plugins/realtime/android/build.gradle.kts
+++ /dev/null
@@ -1,47 +0,0 @@
-plugins {
- id("com.android.library")
- id("org.jetbrains.kotlin.android")
- kotlin("plugin.serialization") version "1.9.24"
-}
-
-android {
- namespace = "com.yunqiinnovation.realtime"
- compileSdk = 35
-
- defaultConfig {
- minSdk = 21
- targetSdk = 33
- }
-
- compileOptions {
- sourceCompatibility = JavaVersion.VERSION_11
- targetCompatibility = JavaVersion.VERSION_11
- }
-
- kotlinOptions {
- jvmTarget = "11"
- }
-
- packaging {
- resources {
- excludes.add("META-INF/DEPENDENCIES")
- excludes.add("META-INF/LICENSE")
- excludes.add("META-INF/LICENSE.txt")
- excludes.add("META-INF/license.txt")
- excludes.add("META-INF/NOTICE")
- excludes.add("META-INF/NOTICE.txt")
- excludes.add("META-INF/notice.txt")
- excludes.add("META-INF/*.kotlin_module")
- }
- }
-}
-
-dependencies {
- implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.9.10")
- implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3")
- implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3")
-
- // OkHttp for WebSocket
- implementation(platform("com.squareup.okhttp3:okhttp-bom:4.12.0"))
- implementation("com.squareup.okhttp3:okhttp")
-}
\ No newline at end of file
diff --git a/local_plugins/realtime/android/settings.gradle.kts b/local_plugins/realtime/android/settings.gradle.kts
deleted file mode 100644
index dd4a80ad2..000000000
--- a/local_plugins/realtime/android/settings.gradle.kts
+++ /dev/null
@@ -1 +0,0 @@
-rootProject.name = "realtime"
\ No newline at end of file
diff --git a/local_plugins/realtime/android/src/main/AndroidManifest.xml b/local_plugins/realtime/android/src/main/AndroidManifest.xml
deleted file mode 100644
index 64df6a0ae..000000000
--- a/local_plugins/realtime/android/src/main/AndroidManifest.xml
+++ /dev/null
@@ -1,11 +0,0 @@
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/local_plugins/realtime/android/src/main/kotlin/com/yunqiinnovation/realtime/RealtimeAudioManager.kt b/local_plugins/realtime/android/src/main/kotlin/com/yunqiinnovation/realtime/RealtimeAudioManager.kt
deleted file mode 100644
index b954a2d6d..000000000
--- a/local_plugins/realtime/android/src/main/kotlin/com/yunqiinnovation/realtime/RealtimeAudioManager.kt
+++ /dev/null
@@ -1,355 +0,0 @@
-package com.yunqiinnovation.realtime
-
-import android.content.Context
-import android.media.*
-import android.util.Log
-import kotlinx.coroutines.*
-import java.util.concurrent.LinkedBlockingQueue
-
-/**
- * 实时音频管理器
- *
- * 负责音频的录制和播放,对应Android示例中的AudioRecord和AudioTrack功能
- */
-class RealtimeAudioManager(private val context: Context) {
- companion object {
- private const val TAG = "RealtimeAudioManager"
- }
-
- // 音频配置
- private var audioConfig: AudioConfig? = null
-
- // 录音相关
- private var audioRecord: AudioRecord? = null
- private var isRecording = false
- private var recordJob: Job? = null
-
- // 播放相关
- private var audioTrack: AudioTrack? = null
- private var isPlaying = false
- private var playJob: Job? = null
- private val playbackQueue = LinkedBlockingQueue()
-
- // 回调
- var onAudioData: ((ByteArray) -> Unit)? = null
- var onStatusChanged: ((VoiceStatus) -> Unit)? = null
-
- // 协程作用域
- private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
-
- /// 初始化音频管理器
- fun initialize(config: AudioConfig): Boolean {
- this.audioConfig = config
-
- return setupAudioRecord(config) && setupAudioTrack(config)
- }
-
- /// 设置AudioRecord(录音)
- private fun setupAudioRecord(config: AudioConfig): Boolean {
- try {
- val channelConfig = if (config.channels == 1) {
- AudioFormat.CHANNEL_IN_MONO
- } else {
- AudioFormat.CHANNEL_IN_STEREO
- }
-
- val audioFormat = when (config.bitsPerSample) {
- 16 -> AudioFormat.ENCODING_PCM_16BIT
- 8 -> AudioFormat.ENCODING_PCM_8BIT
- else -> AudioFormat.ENCODING_PCM_16BIT
- }
-
- val bufferSize = AudioRecord.getMinBufferSize(
- config.sampleRate,
- channelConfig,
- audioFormat
- )
-
- if (bufferSize == AudioRecord.ERROR || bufferSize == AudioRecord.ERROR_BAD_VALUE) {
- Log.e(TAG, "无法获取音频录制缓冲区大小")
- return false
- }
-
- audioRecord = AudioRecord(
- MediaRecorder.AudioSource.MIC,
- config.sampleRate,
- channelConfig,
- audioFormat,
- bufferSize
- )
-
- if (audioRecord?.state != AudioRecord.STATE_INITIALIZED) {
- Log.e(TAG, "AudioRecord初始化失败")
- return false
- }
-
- Log.i(TAG, "AudioRecord初始化成功,采样率: ${config.sampleRate}, 声道: ${config.channels}, 位深: ${config.bitsPerSample}")
- return true
- } catch (e: Exception) {
- Log.e(TAG, "设置AudioRecord失败: ${e.message}", e)
- return false
- }
- }
-
- /// 设置AudioTrack(播放)
- private fun setupAudioTrack(config: AudioConfig): Boolean {
- try {
- val channelConfig = if (config.channels == 1) {
- AudioFormat.CHANNEL_OUT_MONO
- } else {
- AudioFormat.CHANNEL_OUT_STEREO
- }
-
- val audioFormat = when (config.bitsPerSample) {
- 16 -> AudioFormat.ENCODING_PCM_16BIT
- 8 -> AudioFormat.ENCODING_PCM_8BIT
- else -> AudioFormat.ENCODING_PCM_16BIT
- }
-
- val bufferSize = AudioTrack.getMinBufferSize(
- config.sampleRate,
- channelConfig,
- audioFormat
- )
-
- if (bufferSize == AudioTrack.ERROR || bufferSize == AudioTrack.ERROR_BAD_VALUE) {
- Log.e(TAG, "无法获取音频播放缓冲区大小")
- return false
- }
-
- audioTrack = AudioTrack.Builder()
- .setAudioAttributes(
- AudioAttributes.Builder()
- .setUsage(AudioAttributes.USAGE_MEDIA)
- .setContentType(AudioAttributes.CONTENT_TYPE_SPEECH)
- .build()
- )
- .setAudioFormat(
- AudioFormat.Builder()
- .setSampleRate(config.sampleRate)
- .setChannelMask(channelConfig)
- .setEncoding(audioFormat)
- .build()
- )
- .setBufferSizeInBytes(bufferSize)
- .setTransferMode(AudioTrack.MODE_STREAM)
- .build()
-
- if (audioTrack?.state != AudioTrack.STATE_INITIALIZED) {
- Log.e(TAG, "AudioTrack初始化失败")
- return false
- }
-
- Log.i(TAG, "AudioTrack初始化成功")
- return true
- } catch (e: Exception) {
- Log.e(TAG, "设置AudioTrack失败: ${e.message}", e)
- return false
- }
- }
-
- /// 开始录音
- fun startRecording(): Boolean {
- if (isRecording || audioRecord == null) {
- return false
- }
-
- try {
- audioRecord?.startRecording()
- isRecording = true
- onStatusChanged?.invoke(VoiceStatus.RECORDING)
-
- // 启动录音协程
- recordJob = scope.launch {
- loopRecordSend()
- }
-
- Log.i(TAG, "开始录音")
- return true
- } catch (e: Exception) {
- Log.e(TAG, "开始录音失败: ${e.message}", e)
- return false
- }
- }
-
- /// 录音循环(对应Android示例中的loopRecordSend)
- private suspend fun loopRecordSend() {
- val config = audioConfig ?: return
- val frameSize = config.frameSize
- val buffer = ByteArray(frameSize)
-
- while (isRecording && audioRecord != null) {
- try {
- val bytesRead = audioRecord!!.read(buffer, 0, frameSize)
-
- if (bytesRead > 0) {
- // 确保读取的数据长度正确
- val audioData = if (bytesRead == frameSize) {
- buffer.copyOf()
- } else {
- buffer.copyOf(bytesRead)
- }
-
- // 回调音频数据
- onAudioData?.invoke(audioData)
- } else {
- Log.w(TAG, "录音读取数据失败: $bytesRead")
- }
- } catch (e: Exception) {
- Log.e(TAG, "录音循环异常: ${e.message}", e)
- break
- }
- }
- }
-
- /// 停止录音
- fun stopRecording(): Boolean {
- if (!isRecording) {
- return false
- }
-
- try {
- isRecording = false
- recordJob?.cancel()
- audioRecord?.stop()
- onStatusChanged?.invoke(VoiceStatus.IDLE)
-
- Log.i(TAG, "停止录音")
- return true
- } catch (e: Exception) {
- Log.e(TAG, "停止录音失败: ${e.message}", e)
- return false
- }
- }
-
- /// 播放音频数据
- fun playAudioData(data: ByteArray) {
- if (data.isNotEmpty()) {
- playbackQueue.offer(data)
-
- // 如果没在播放,开始播放
- if (!isPlaying) {
- startPlayback()
- }
- }
- }
-
- /// 开始播放
- private fun startPlayback() {
- if (isPlaying || audioTrack == null) {
- return
- }
-
- try {
- audioTrack?.play()
- isPlaying = true
- onStatusChanged?.invoke(VoiceStatus.PLAYING)
-
- // 启动播放协程
- playJob = scope.launch {
- loopPlayback()
- }
-
- Log.i(TAG, "开始播放")
- } catch (e: Exception) {
- Log.e(TAG, "开始播放失败: ${e.message}", e)
- }
- }
-
- /// 播放循环(对应Android示例中的loopPlayback)
- private suspend fun loopPlayback() {
- while (isPlaying && audioTrack != null) {
- try {
- val audioData = playbackQueue.poll()
-
- if (audioData != null) {
- val bytesWritten = audioTrack!!.write(audioData, 0, audioData.size)
-
- if (bytesWritten < 0) {
- Log.w(TAG, "音频播放写入失败: $bytesWritten")
- }
- } else {
- // 队列为空,检查是否应该停止播放
- delay(10) // 短暂等待
-
- if (playbackQueue.isEmpty()) {
- // 队列持续为空,停止播放
- break
- }
- }
- } catch (e: Exception) {
- Log.e(TAG, "播放循环异常: ${e.message}", e)
- break
- }
- }
-
- // 播放结束
- stopPlayback()
- }
-
- /// 停止播放
- fun stopPlaying(): Boolean {
- return stopPlayback()
- }
-
- private fun stopPlayback(): Boolean {
- if (!isPlaying) {
- return false
- }
-
- try {
- isPlaying = false
- playJob?.cancel()
- audioTrack?.stop()
- playbackQueue.clear()
- onStatusChanged?.invoke(VoiceStatus.IDLE)
-
- Log.i(TAG, "停止播放")
- return true
- } catch (e: Exception) {
- Log.e(TAG, "停止播放失败: ${e.message}", e)
- return false
- }
- }
-
- /// 更新配置
- fun updateConfig(config: AudioConfig): Boolean {
- // 如果正在录音或播放,先停止
- if (isRecording) {
- stopRecording()
- }
- if (isPlaying) {
- stopPlayback()
- }
-
- // 释放旧的音频对象
- releaseAudioObjects()
-
- return initialize(config)
- }
-
- /// 释放音频对象
- private fun releaseAudioObjects() {
- try {
- audioRecord?.release()
- audioRecord = null
-
- audioTrack?.release()
- audioTrack = null
- } catch (e: Exception) {
- Log.e(TAG, "释放音频对象失败: ${e.message}", e)
- }
- }
-
- /// 释放资源
- fun dispose() {
- stopRecording()
- stopPlayback()
-
- scope.cancel()
- releaseAudioObjects()
- playbackQueue.clear()
-
- Log.i(TAG, "音频管理器已释放")
- }
-}
\ No newline at end of file
diff --git a/local_plugins/realtime/android/src/main/kotlin/com/yunqiinnovation/realtime/RealtimePlugin.kt b/local_plugins/realtime/android/src/main/kotlin/com/yunqiinnovation/realtime/RealtimePlugin.kt
deleted file mode 100644
index a8e787656..000000000
--- a/local_plugins/realtime/android/src/main/kotlin/com/yunqiinnovation/realtime/RealtimePlugin.kt
+++ /dev/null
@@ -1,309 +0,0 @@
-package com.yunqiinnovation.realtime
-
-import android.content.Context
-import android.util.Log
-import io.flutter.embedding.engine.plugins.FlutterPlugin
-import io.flutter.plugin.common.EventChannel
-import io.flutter.plugin.common.MethodCall
-import io.flutter.plugin.common.MethodChannel
-import io.flutter.plugin.common.MethodChannel.MethodCallHandler
-import io.flutter.plugin.common.MethodChannel.Result
-import kotlinx.coroutines.*
-
-/**
- * 实时语音聊天插件
- *
- * 通过WebSocket连接Vocode服务器实现实时语音交互
- * 包含录音、播放、WebSocket通信等功能
- */
-class RealtimePlugin: FlutterPlugin, MethodCallHandler, EventChannel.StreamHandler {
- companion object {
- private const val TAG = "RealtimePlugin"
- private const val METHOD_CHANNEL = "realtime/methods"
- private const val EVENT_CHANNEL = "realtime/events"
- }
-
- private lateinit var context: Context
- private lateinit var methodChannel: MethodChannel
- private lateinit var eventChannel: EventChannel
- private var eventSink: EventChannel.EventSink? = null
-
- // 核心组件
- private lateinit var audioManager: RealtimeAudioManager
- private lateinit var webSocketManager: RealtimeWebSocketManager
-
- // 协程作用域
- private val scope = CoroutineScope(Dispatchers.Main + SupervisorJob())
-
- // 配置参数
- private var serverUrl: String = ""
- private var sampleRate: Int = 16000
- private var channels: Int = 1
- private var bitsPerSample: Int = 16
-
- // 状态
- @Volatile
- private var connectionStatus: ConnectionStatus = ConnectionStatus.DISCONNECTED
- @Volatile
- private var voiceStatus: VoiceStatus = VoiceStatus.IDLE
-
- override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
- context = flutterPluginBinding.applicationContext
-
- methodChannel = MethodChannel(flutterPluginBinding.binaryMessenger, METHOD_CHANNEL)
- methodChannel.setMethodCallHandler(this)
-
- eventChannel = EventChannel(flutterPluginBinding.binaryMessenger, EVENT_CHANNEL)
- eventChannel.setStreamHandler(this)
-
- // 初始化核心组件
- audioManager = RealtimeAudioManager(context)
- webSocketManager = RealtimeWebSocketManager()
-
- setupCallbacks()
-
- Log.i(TAG, "Realtime插件已附加到引擎")
- }
-
- override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
- methodChannel.setMethodCallHandler(null)
- eventChannel.setStreamHandler(null)
-
- // 清理资源
- scope.cancel()
- audioManager.dispose()
- webSocketManager.dispose()
-
- Log.i(TAG, "Realtime插件已从引擎分离")
- }
-
- /// 设置各组件的回调
- private fun setupCallbacks() {
- // 音频管理器回调
- audioManager.onAudioData = { data ->
- webSocketManager.sendAudioData(data)
- }
-
- audioManager.onStatusChanged = { status ->
- voiceStatus = status
- sendEvent("voiceStatusChanged", status.value)
- }
-
- // WebSocket管理器回调
- webSocketManager.onConnectionStatusChanged = { status ->
- connectionStatus = status
- sendEvent("connectionStatusChanged", status.value)
- }
-
- webSocketManager.onAudioReceived = { data ->
- audioManager.playAudioData(data)
- }
-
- webSocketManager.onTextReceived = { text ->
- sendEvent("textReceived", text)
- }
-
- webSocketManager.onError = { error ->
- sendEvent("error", error)
- }
- }
-
- /// 发送事件到Flutter端
- private fun sendEvent(type: String, data: Any?) {
- val event = mapOf(
- "type" to type,
- "data" to data
- )
-
- scope.launch {
- eventSink?.success(event)
- }
- }
-
- override fun onMethodCall(call: MethodCall, result: Result) {
- when (call.method) {
- "initialize" -> handleInitialize(call, result)
- "connect" -> handleConnect(result)
- "disconnect" -> handleDisconnect(result)
- "startRecording" -> handleStartRecording(result)
- "stopRecording" -> handleStopRecording(result)
- "stopPlaying" -> handleStopPlaying(result)
- "getConnectionStatus" -> result.success(connectionStatus.value)
- "getVoiceStatus" -> result.success(voiceStatus.value)
- "sendTextMessage" -> handleSendTextMessage(call, result)
- "setAudioConfig" -> handleSetAudioConfig(call, result)
- "dispose" -> handleDispose(result)
- else -> result.notImplemented()
- }
- }
-
- private fun handleInitialize(call: MethodCall, result: Result) {
- try {
- val args = call.arguments as? Map
- val serverUrl = args?.get("serverUrl") as? String
- ?: return result.error("INVALID_ARGUMENTS", "服务器地址不能为空", null)
-
- this.serverUrl = serverUrl
- this.sampleRate = args["sampleRate"] as? Int ?: 16000
- this.channels = args["channels"] as? Int ?: 1
- this.bitsPerSample = args["bitsPerSample"] as? Int ?: 16
-
- // 初始化音频管理器
- val audioConfig = AudioConfig(sampleRate, channels, bitsPerSample)
- val success = audioManager.initialize(audioConfig)
-
- if (success) {
- webSocketManager.initialize(serverUrl)
- Log.i(TAG, "实时语音插件初始化成功")
- } else {
- Log.e(TAG, "实时语音插件初始化失败")
- }
-
- result.success(success)
- } catch (e: Exception) {
- Log.e(TAG, "初始化失败: ${e.message}", e)
- result.error("INITIALIZATION_ERROR", "初始化失败: ${e.message}", null)
- }
- }
-
- private fun handleConnect(result: Result) {
- scope.launch {
- try {
- val success = webSocketManager.connect()
- result.success(success)
- } catch (e: Exception) {
- Log.e(TAG, "连接失败: ${e.message}", e)
- result.error("CONNECTION_ERROR", "连接失败: ${e.message}", null)
- }
- }
- }
-
- private fun handleDisconnect(result: Result) {
- scope.launch {
- try {
- webSocketManager.disconnect()
- result.success(true)
- } catch (e: Exception) {
- Log.e(TAG, "断开连接失败: ${e.message}", e)
- result.error("DISCONNECTION_ERROR", "断开连接失败: ${e.message}", null)
- }
- }
- }
-
- private fun handleStartRecording(result: Result) {
- scope.launch {
- try {
- val success = audioManager.startRecording()
- result.success(success)
- } catch (e: Exception) {
- Log.e(TAG, "开始录音失败: ${e.message}", e)
- result.error("RECORDING_ERROR", "开始录音失败: ${e.message}", null)
- }
- }
- }
-
- private fun handleStopRecording(result: Result) {
- scope.launch {
- try {
- val success = audioManager.stopRecording()
- result.success(success)
- } catch (e: Exception) {
- Log.e(TAG, "停止录音失败: ${e.message}", e)
- result.error("RECORDING_ERROR", "停止录音失败: ${e.message}", null)
- }
- }
- }
-
- private fun handleStopPlaying(result: Result) {
- scope.launch {
- try {
- val success = audioManager.stopPlaying()
- result.success(success)
- } catch (e: Exception) {
- Log.e(TAG, "停止播放失败: ${e.message}", e)
- result.error("PLAYBACK_ERROR", "停止播放失败: ${e.message}", null)
- }
- }
- }
-
- private fun handleSendTextMessage(call: MethodCall, result: Result) {
- try {
- val args = call.arguments as? Map
- val message = args?.get("message") as? String
- ?: return result.error("INVALID_ARGUMENTS", "消息内容不能为空", null)
-
- val success = webSocketManager.sendTextMessage(message)
- result.success(success)
- } catch (e: Exception) {
- Log.e(TAG, "发送文本消息失败: ${e.message}", e)
- result.error("MESSAGE_ERROR", "发送文本消息失败: ${e.message}", null)
- }
- }
-
- private fun handleSetAudioConfig(call: MethodCall, result: Result) {
- try {
- val args = call.arguments as? Map
- ?: return result.error("INVALID_ARGUMENTS", "参数无效", null)
-
- args["sampleRate"]?.let { this.sampleRate = it as Int }
- args["channels"]?.let { this.channels = it as Int }
- args["bitsPerSample"]?.let { this.bitsPerSample = it as Int }
-
- val audioConfig = AudioConfig(sampleRate, channels, bitsPerSample)
- val success = audioManager.updateConfig(audioConfig)
- result.success(success)
- } catch (e: Exception) {
- Log.e(TAG, "设置音频配置失败: ${e.message}", e)
- result.error("CONFIG_ERROR", "设置音频配置失败: ${e.message}", null)
- }
- }
-
- private fun handleDispose(result: Result) {
- scope.launch {
- try {
- audioManager.dispose()
- webSocketManager.dispose()
- result.success(null)
- } catch (e: Exception) {
- Log.e(TAG, "释放资源失败: ${e.message}", e)
- result.error("DISPOSE_ERROR", "释放资源失败: ${e.message}", null)
- }
- }
- }
-
- // EventChannel.StreamHandler 实现
- override fun onListen(arguments: Any?, events: EventChannel.EventSink?) {
- eventSink = events
- Log.i(TAG, "事件流监听已开始")
- }
-
- override fun onCancel(arguments: Any?) {
- eventSink = null
- Log.i(TAG, "事件流监听已取消")
- }
-}
-
-// 枚举定义
-enum class ConnectionStatus(val value: String) {
- DISCONNECTED("disconnected"),
- CONNECTING("connecting"),
- CONNECTED("connected"),
- ERROR("error")
-}
-
-enum class VoiceStatus(val value: String) {
- IDLE("idle"),
- RECORDING("recording"),
- PROCESSING("processing"),
- PLAYING("playing")
-}
-
-// 音频配置
-data class AudioConfig(
- val sampleRate: Int,
- val channels: Int,
- val bitsPerSample: Int
-) {
- val frameSize: Int
- get() = (sampleRate * channels * bitsPerSample / 8 * 20) / 1000 // 20ms帧
-}
\ No newline at end of file
diff --git a/local_plugins/realtime/android/src/main/kotlin/com/yunqiinnovation/realtime/RealtimeWebSocketManager.kt b/local_plugins/realtime/android/src/main/kotlin/com/yunqiinnovation/realtime/RealtimeWebSocketManager.kt
deleted file mode 100644
index 77a6ebb49..000000000
--- a/local_plugins/realtime/android/src/main/kotlin/com/yunqiinnovation/realtime/RealtimeWebSocketManager.kt
+++ /dev/null
@@ -1,218 +0,0 @@
-package com.yunqiinnovation.realtime
-
-import android.util.Log
-import kotlinx.coroutines.*
-import okhttp3.*
-import okio.ByteString
-import java.util.concurrent.TimeUnit
-
-/**
- * 实时WebSocket管理器
- *
- * 负责与Vocode服务器的WebSocket通信,对应Android示例中的OkHttp WebSocket功能
- */
-class RealtimeWebSocketManager : WebSocketListener() {
- companion object {
- private const val TAG = "RealtimeWebSocketManager"
- private const val CONNECT_TIMEOUT = 30L
- private const val READ_TIMEOUT = 60L
- private const val WRITE_TIMEOUT = 30L
- }
-
- // OkHttp客户端和WebSocket
- private var okHttpClient: OkHttpClient? = null
- private var webSocket: WebSocket? = null
-
- // 服务器URL
- private var serverUrl: String = ""
-
- // 连接状态
- @Volatile
- private var isConnected = false
- @Volatile
- private var isConnecting = false
-
- // 回调
- var onConnectionStatusChanged: ((ConnectionStatus) -> Unit)? = null
- var onAudioReceived: ((ByteArray) -> Unit)? = null
- var onTextReceived: ((String) -> Unit)? = null
- var onError: ((String) -> Unit)? = null
-
- // 协程作用域
- private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
-
- /// 初始化WebSocket管理器
- fun initialize(serverUrl: String) {
- this.serverUrl = serverUrl
-
- // 创建OkHttp客户端
- okHttpClient = OkHttpClient.Builder()
- .connectTimeout(CONNECT_TIMEOUT, TimeUnit.SECONDS)
- .readTimeout(READ_TIMEOUT, TimeUnit.SECONDS)
- .writeTimeout(WRITE_TIMEOUT, TimeUnit.SECONDS)
- .retryOnConnectionFailure(true)
- .build()
-
- Log.i(TAG, "WebSocket管理器初始化,服务器地址: $serverUrl")
- }
-
- /// 连接到服务器
- suspend fun connect(): Boolean = withContext(Dispatchers.IO) {
- if (isConnected || isConnecting) {
- return@withContext false
- }
-
- val client = okHttpClient ?: run {
- Log.e(TAG, "OkHttp客户端未初始化")
- onError?.invoke("客户端未初始化")
- return@withContext false
- }
-
- try {
- isConnecting = true
- onConnectionStatusChanged?.invoke(ConnectionStatus.CONNECTING)
-
- val request = Request.Builder()
- .url(serverUrl)
- .build()
-
- webSocket = client.newWebSocket(request, this@RealtimeWebSocketManager)
-
- // 等待连接建立
- var waitTime = 0
- while (isConnecting && waitTime < CONNECT_TIMEOUT * 1000) {
- delay(100)
- waitTime += 100
- }
-
- if (isConnected) {
- Log.i(TAG, "WebSocket连接成功")
- return@withContext true
- } else {
- Log.e(TAG, "WebSocket连接超时")
- isConnecting = false
- onConnectionStatusChanged?.invoke(ConnectionStatus.ERROR)
- return@withContext false
- }
- } catch (e: Exception) {
- Log.e(TAG, "WebSocket连接失败: ${e.message}", e)
- isConnecting = false
- onConnectionStatusChanged?.invoke(ConnectionStatus.ERROR)
- onError?.invoke("连接失败: ${e.message}")
- return@withContext false
- }
- }
-
- /// 断开连接
- fun disconnect() {
- try {
- isConnected = false
- isConnecting = false
-
- webSocket?.close(1000, "正常关闭")
- webSocket = null
-
- onConnectionStatusChanged?.invoke(ConnectionStatus.DISCONNECTED)
- Log.i(TAG, "WebSocket已断开连接")
- } catch (e: Exception) {
- Log.e(TAG, "断开WebSocket连接失败: ${e.message}", e)
- }
- }
-
- /// 发送音频数据
- fun sendAudioData(data: ByteArray) {
- if (!isConnected) {
- return
- }
-
- try {
- val byteString = ByteString.of(*data)
- val success = webSocket?.send(byteString) ?: false
-
- if (!success) {
- Log.w(TAG, "发送音频数据失败")
- }
- } catch (e: Exception) {
- Log.e(TAG, "发送音频数据异常: ${e.message}", e)
- }
- }
-
- /// 发送文本消息
- fun sendTextMessage(message: String): Boolean {
- if (!isConnected) {
- Log.w(TAG, "WebSocket未连接,无法发送文本消息")
- return false
- }
-
- return try {
- val success = webSocket?.send(message) ?: false
- if (!success) {
- Log.w(TAG, "发送文本消息失败")
- }
- success
- } catch (e: Exception) {
- Log.e(TAG, "发送文本消息异常: ${e.message}", e)
- false
- }
- }
-
- /// 释放资源
- fun dispose() {
- disconnect()
-
- scope.cancel()
-
- okHttpClient?.dispatcher?.executorService?.shutdown()
- okHttpClient = null
-
- Log.i(TAG, "WebSocket管理器已释放")
- }
-
- // WebSocketListener 回调实现
- override fun onOpen(webSocket: WebSocket, response: Response) {
- Log.i(TAG, "WebSocket连接已建立")
- isConnected = true
- isConnecting = false
- onConnectionStatusChanged?.invoke(ConnectionStatus.CONNECTED)
- }
-
- override fun onMessage(webSocket: WebSocket, text: String) {
- Log.d(TAG, "收到文本消息: $text")
- onTextReceived?.invoke(text)
- }
-
- override fun onMessage(webSocket: WebSocket, bytes: ByteString) {
- Log.d(TAG, "收到音频数据: ${bytes.size} 字节")
- onAudioReceived?.invoke(bytes.toByteArray())
- }
-
- override fun onClosing(webSocket: WebSocket, code: Int, reason: String) {
- Log.i(TAG, "WebSocket正在关闭,代码: $code, 原因: $reason")
- webSocket.close(1000, null)
- }
-
- override fun onClosed(webSocket: WebSocket, code: Int, reason: String) {
- Log.i(TAG, "WebSocket已关闭,代码: $code, 原因: $reason")
- isConnected = false
- isConnecting = false
- onConnectionStatusChanged?.invoke(ConnectionStatus.DISCONNECTED)
- }
-
- override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) {
- val errorMessage = when {
- t.message?.contains("Unable to parse TLS packet header") == true ->
- "SSL错误:请检查服务器是否支持wss://,本地开发请使用ws://"
- t.message?.contains("Connection refused") == true ->
- "连接被拒绝:请检查服务器是否运行在指定端口"
- t.message?.contains("timeout") == true ->
- "连接超时:请检查网络连接和服务器地址"
- else -> "连接失败: ${t.message}"
- }
-
- Log.e(TAG, "WebSocket连接失败: ${t.message}", t)
- isConnected = false
- isConnecting = false
- onConnectionStatusChanged?.invoke(ConnectionStatus.ERROR)
- onError?.invoke(errorMessage)
- }
-}
\ No newline at end of file
diff --git a/local_plugins/realtime/example.md b/local_plugins/realtime/example.md
deleted file mode 100644
index 74b8ebd44..000000000
--- a/local_plugins/realtime/example.md
+++ /dev/null
@@ -1,274 +0,0 @@
-# Realtime Plugin 使用示例
-
-## 基本用法
-
-```dart
-import 'package:realtime/realtime.dart';
-
-class VoiceCallPage extends StatefulWidget {
- @override
- _VoiceCallPageState createState() => _VoiceCallPageState();
-}
-
-class _VoiceCallPageState extends State {
- final RealtimeService _realtimeService = RealtimeService();
- StreamSubscription? _eventSubscription;
-
- ConnectionStatus _connectionStatus = ConnectionStatus.disconnected;
- VoiceStatus _voiceStatus = VoiceStatus.idle;
-
- @override
- void initState() {
- super.initState();
- _setupEventListener();
- _initializeService();
- }
-
- void _setupEventListener() {
- _eventSubscription = _realtimeService.eventStream.listen((event) {
- switch (event.type) {
- case 'connectionStatusChanged':
- setState(() {
- _connectionStatus = _parseConnectionStatus(event.data);
- });
- break;
- case 'voiceStatusChanged':
- setState(() {
- _voiceStatus = _parseVoiceStatus(event.data);
- });
- break;
- case 'textReceived':
- print('收到文本: ${event.data}');
- break;
- case 'error':
- print('错误: ${event.data}');
- break;
- }
- });
- }
-
- Future _initializeService() async {
- try {
- final success = await _realtimeService.initialize(
- serverUrl: 'wss://your-vocode-server.com/ws',
- sampleRate: 16000,
- channels: 1,
- bitsPerSample: 16,
- );
-
- if (success) {
- print('Realtime服务初始化成功');
- } else {
- print('Realtime服务初始化失败');
- }
- } catch (e) {
- print('初始化错误: $e');
- }
- }
-
- Future _connect() async {
- try {
- final success = await _realtimeService.connect();
- if (!success) {
- print('连接失败');
- }
- } catch (e) {
- print('连接错误: $e');
- }
- }
-
- Future _disconnect() async {
- try {
- await _realtimeService.disconnect();
- } catch (e) {
- print('断开连接错误: $e');
- }
- }
-
- Future _startRecording() async {
- try {
- final success = await _realtimeService.startRecording();
- if (!success) {
- print('开始录音失败');
- }
- } catch (e) {
- print('录音错误: $e');
- }
- }
-
- Future _stopRecording() async {
- try {
- final success = await _realtimeService.stopRecording();
- if (!success) {
- print('停止录音失败');
- }
- } catch (e) {
- print('停止录音错误: $e');
- }
- }
-
- ConnectionStatus _parseConnectionStatus(String status) {
- switch (status) {
- case 'connected': return ConnectionStatus.connected;
- case 'connecting': return ConnectionStatus.connecting;
- case 'disconnected': return ConnectionStatus.disconnected;
- case 'error': return ConnectionStatus.error;
- default: return ConnectionStatus.disconnected;
- }
- }
-
- VoiceStatus _parseVoiceStatus(String status) {
- switch (status) {
- case 'recording': return VoiceStatus.recording;
- case 'processing': return VoiceStatus.processing;
- case 'playing': return VoiceStatus.playing;
- case 'idle': return VoiceStatus.idle;
- default: return VoiceStatus.idle;
- }
- }
-
- @override
- Widget build(BuildContext context) {
- return Scaffold(
- appBar: AppBar(
- title: Text('实时语音聊天'),
- ),
- body: Center(
- child: Column(
- mainAxisAlignment: MainAxisAlignment.center,
- children: [
- // 连接状态显示
- Text('连接状态: ${_connectionStatus.name}'),
- SizedBox(height: 20),
-
- // 语音状态显示
- Text('语音状态: ${_voiceStatus.name}'),
- SizedBox(height: 40),
-
- // 连接按钮
- ElevatedButton(
- onPressed: _connectionStatus == ConnectionStatus.disconnected
- ? _connect
- : _disconnect,
- child: Text(_connectionStatus == ConnectionStatus.disconnected
- ? '连接'
- : '断开'),
- ),
- SizedBox(height: 20),
-
- // 录音按钮
- ElevatedButton(
- onPressed: _connectionStatus == ConnectionStatus.connected
- ? (_voiceStatus == VoiceStatus.recording
- ? _stopRecording
- : _startRecording)
- : null,
- child: Text(_voiceStatus == VoiceStatus.recording
- ? '停止录音'
- : '开始录音'),
- ),
- ],
- ),
- ),
- );
- }
-
- @override
- void dispose() {
- _eventSubscription?.cancel();
- _realtimeService.dispose();
- super.dispose();
- }
-}
-```
-
-## GetX控制器中的使用
-
-```dart
-import 'package:get/get.dart';
-import 'package:realtime/realtime.dart';
-
-class VoiceController extends GetxController {
- final RealtimeService _realtimeService = RealtimeService();
-
- final RxBool isConnected = false.obs;
- final RxBool isRecording = false.obs;
-
- StreamSubscription? _eventSubscription;
-
- @override
- void onInit() {
- super.onInit();
- _setupEventListener();
- _initializeService();
- }
-
- void _setupEventListener() {
- _eventSubscription = _realtimeService.eventStream.listen((event) {
- switch (event.type) {
- case 'connectionStatusChanged':
- isConnected.value = event.data == 'connected';
- break;
- case 'voiceStatusChanged':
- isRecording.value = event.data == 'recording';
- break;
- }
- });
- }
-
- Future _initializeService() async {
- await _realtimeService.initialize(
- serverUrl: 'wss://your-server/ws',
- );
- }
-
- Future toggleConnection() async {
- if (isConnected.value) {
- await _realtimeService.disconnect();
- } else {
- await _realtimeService.connect();
- }
- }
-
- Future toggleRecording() async {
- if (isRecording.value) {
- await _realtimeService.stopRecording();
- } else {
- await _realtimeService.startRecording();
- }
- }
-
- @override
- void onClose() {
- _eventSubscription?.cancel();
- _realtimeService.dispose();
- super.onClose();
- }
-}
-```
-
-## 权限配置
-
-### iOS
-在 `ios/Runner/Info.plist` 中添加:
-
-```xml
-NSMicrophoneUsageDescription
-应用需要麦克风权限进行语音录制
-```
-
-## 服务器配置示例
-
-需要一个支持WebSocket的Vocode服务器,具体实现可参考Vocode官方文档。
-
-服务器需要:
-1. 接收16kHz/16-bit/单声道的PCM音频数据
-2. 返回相同格式的音频数据
-3. 支持文本消息交换
-
-## 注意事项
-
-1. 确保网络连接稳定
-2. 音频格式必须与服务器保持一致
-3. 及时处理事件流中的错误
-4. 在适当时机释放资源
\ No newline at end of file
diff --git a/local_plugins/realtime/ios/realtime/Package.swift b/local_plugins/realtime/ios/realtime/Package.swift
deleted file mode 100644
index 514fbc1e1..000000000
--- a/local_plugins/realtime/ios/realtime/Package.swift
+++ /dev/null
@@ -1,18 +0,0 @@
-// swift-tools-version: 5.9
-import PackageDescription
-
-let package = Package(
- name: "realtime",
- platforms: [.iOS("18.0")],
- products: [
- .library(name: "realtime", targets: ["realtime"])
- ],
- dependencies: [],
- targets: [
- .target(
- name: "realtime",
- dependencies: [],
- path: "Sources/realtime"
- )
- ]
-)
\ No newline at end of file
diff --git a/local_plugins/realtime/ios/realtime/Sources/realtime/RealtimeAudioManager.swift b/local_plugins/realtime/ios/realtime/Sources/realtime/RealtimeAudioManager.swift
deleted file mode 100644
index 281907ce5..000000000
--- a/local_plugins/realtime/ios/realtime/Sources/realtime/RealtimeAudioManager.swift
+++ /dev/null
@@ -1,314 +0,0 @@
-import AVFoundation
-import os.log
-import Accelerate
-
-/**
- * 实时音频管理器
- *
- * 负责音频的录制和播放,对应Android示例中的AudioRecord和AudioTrack功能
- */
-class RealtimeAudioManager {
-
- private let log = OSLog(subsystem: "com.yunqiinnovation.realtime", category: "RealtimeAudioManager")
-
- // 音频配置
- private var audioConfig: AudioConfig?
-
- // 录音相关
- private var audioEngine: AVAudioEngine?
- private var inputNode: AVAudioInputNode?
- private var recordingFormat: AVAudioFormat?
- private var isRecording = false
-
- // 播放相关
- private var audioPlayer: AVAudioPlayerNode?
- private var playbackFormat: AVAudioFormat?
- private var playbackQueue: DispatchQueue
- private var audioBufferQueue: [AVAudioPCMBuffer] = []
- private var isPlaying = false
-
- // 回调
- var onAudioData: ((Data) -> Void)?
- var onStatusChanged: ((VoiceStatus) -> Void)?
-
- // 录音缓冲区
- private var recordingBuffer: AVAudioPCMBuffer?
- private var frameSize: Int = 320 // 20ms @ 16kHz
-
- init() {
- playbackQueue = DispatchQueue(label: "com.realtime.playback", qos: .userInitiated)
- setupAudioSession()
- }
-
- /// 设置音频会话
- private func setupAudioSession() {
- do {
- let session = AVAudioSession.sharedInstance()
- try session.setCategory(.playAndRecord,
- mode: .default,
- options: [.defaultToSpeaker, .allowBluetooth])
- try session.setActive(true)
- os_log("音频会话设置成功", log: log, type: .info)
- } catch {
- os_log("音频会话设置失败: %@", log: log, type: .error, error.localizedDescription)
- }
- }
-
- /// 初始化音频管理器
- func initialize(config: AudioConfig) -> Bool {
- self.audioConfig = config
- self.frameSize = config.frameSize
-
- guard setupAudioEngine(config: config) else {
- os_log("音频引擎初始化失败", log: log, type: .error)
- return false
- }
-
- guard setupAudioPlayer(config: config) else {
- os_log("音频播放器初始化失败", log: log, type: .error)
- return false
- }
-
- os_log("音频管理器初始化成功", log: log, type: .info)
- return true
- }
-
- /// 设置音频引擎(录音)
- private func setupAudioEngine(config: AudioConfig) -> Bool {
- audioEngine = AVAudioEngine()
-
- guard let audioEngine = audioEngine else { return false }
-
- inputNode = audioEngine.inputNode
-
- // 创建录音格式:16kHz, 16-bit, 单声道
- recordingFormat = AVAudioFormat(commonFormat: .pcmFormatInt16,
- sampleRate: Double(config.sampleRate),
- channels: AVAudioChannelCount(config.channels),
- interleaved: true)
-
- guard let recordingFormat = recordingFormat else {
- os_log("无法创建录音格式", log: log, type: .error)
- return false
- }
-
- // 创建录音缓冲区
- let frameCount = AVAudioFrameCount(config.frameSize / (config.bitsPerSample / 8))
- recordingBuffer = AVAudioPCMBuffer(pcmFormat: recordingFormat, frameCapacity: frameCount)
-
- return true
- }
-
- /// 设置音频播放器
- private func setupAudioPlayer(config: AudioConfig) -> Bool {
- guard let audioEngine = audioEngine else { return false }
-
- audioPlayer = AVAudioPlayerNode()
-
- // 创建播放格式:16kHz, 16-bit, 单声道
- playbackFormat = AVAudioFormat(commonFormat: .pcmFormatInt16,
- sampleRate: Double(config.sampleRate),
- channels: AVAudioChannelCount(config.channels),
- interleaved: true)
-
- guard let audioPlayer = audioPlayer,
- let playbackFormat = playbackFormat else {
- os_log("无法创建播放格式", log: log, type: .error)
- return false
- }
-
- // 连接播放器到音频引擎
- audioEngine.attach(audioPlayer)
- audioEngine.connect(audioPlayer, to: audioEngine.outputNode, format: playbackFormat)
-
- return true
- }
-
- /// 开始录音
- func startRecording() -> Bool {
- guard let audioEngine = audioEngine,
- let inputNode = inputNode,
- let recordingFormat = recordingFormat,
- !isRecording else {
- return false
- }
-
- // 安装录音tap
- inputNode.installTap(onBus: 0, bufferSize: AVAudioFrameCount(frameSize / 2), format: recordingFormat) { [weak self] buffer, time in
- self?.processRecordedAudio(buffer: buffer)
- }
-
- do {
- try audioEngine.start()
- isRecording = true
- onStatusChanged?(.recording)
- os_log("开始录音", log: log, type: .info)
- return true
- } catch {
- os_log("开始录音失败: %@", log: log, type: .error, error.localizedDescription)
- return false
- }
- }
-
- /// 处理录制的音频数据
- private func processRecordedAudio(buffer: AVAudioPCMBuffer) {
- guard let channelData = buffer.int16ChannelData?[0],
- buffer.frameLength > 0 else {
- return
- }
-
- // 确保数据长度匹配frameSize
- let dataLength = min(Int(buffer.frameLength) * 2, frameSize) // 2 bytes per sample
-
- // 转换为Data
- let data = Data(bytes: channelData, count: dataLength)
-
- // 回调音频数据
- onAudioData?(data)
- }
-
- /// 停止录音
- func stopRecording() -> Bool {
- guard let audioEngine = audioEngine,
- let inputNode = inputNode,
- isRecording else {
- return false
- }
-
- inputNode.removeTap(onBus: 0)
- audioEngine.stop()
- isRecording = false
- onStatusChanged?(.idle)
- os_log("停止录音", log: log, type: .info)
- return true
- }
-
- /// 播放音频数据
- func playAudioData(_ data: Data) {
- guard let playbackFormat = playbackFormat,
- let audioPlayer = audioPlayer else {
- return
- }
-
- playbackQueue.async { [weak self] in
- self?.enqueueAudioData(data, format: playbackFormat, player: audioPlayer)
- }
- }
-
- /// 将音频数据加入播放队列
- private func enqueueAudioData(_ data: Data, format: AVAudioFormat, player: AVAudioPlayerNode) {
- // 创建PCM缓冲区
- let frameCount = AVAudioFrameCount(data.count / 2) // 16-bit = 2 bytes per sample
-
- guard let buffer = AVAudioPCMBuffer(pcmFormat: format, frameCapacity: frameCount) else {
- os_log("无法创建播放缓冲区", log: log, type: .error)
- return
- }
-
- buffer.frameLength = frameCount
-
- // 复制数据到缓冲区
- guard let channelData = buffer.int16ChannelData?[0] else {
- return
- }
-
- data.withUnsafeBytes { bytes in
- let int16Pointer = bytes.bindMemory(to: Int16.self)
- channelData.update(from: int16Pointer.baseAddress!, count: Int(frameCount))
- }
-
- // 加入播放队列
- audioBufferQueue.append(buffer)
-
- // 如果没在播放,开始播放
- if !isPlaying {
- startPlayback()
- }
- }
-
- /// 开始播放
- private func startPlayback() {
- guard let audioEngine = audioEngine,
- let audioPlayer = audioPlayer,
- !audioBufferQueue.isEmpty else {
- return
- }
-
- if !audioEngine.isRunning {
- do {
- try audioEngine.start()
- } catch {
- os_log("启动音频引擎失败: %@", log: log, type: .error, error.localizedDescription)
- return
- }
- }
-
- if !audioPlayer.isPlaying {
- audioPlayer.play()
- }
-
- isPlaying = true
- onStatusChanged?(.playing)
-
- // 播放队列中的音频
- playNextBuffer()
- }
-
- /// 播放下一个缓冲区
- private func playNextBuffer() {
- guard let audioPlayer = audioPlayer,
- !audioBufferQueue.isEmpty else {
- isPlaying = false
- onStatusChanged?(.idle)
- return
- }
-
- let buffer = audioBufferQueue.removeFirst()
-
- audioPlayer.scheduleBuffer(buffer) { [weak self] in
- DispatchQueue.main.async {
- self?.playNextBuffer()
- }
- }
- }
-
- /// 停止播放
- func stopPlaying() -> Bool {
- guard let audioPlayer = audioPlayer else {
- return false
- }
-
- audioPlayer.stop()
- audioBufferQueue.removeAll()
- isPlaying = false
- onStatusChanged?(.idle)
- os_log("停止播放", log: log, type: .info)
- return true
- }
-
- /// 更新配置
- func updateConfig(config: AudioConfig) -> Bool {
- // 如果正在录音或播放,先停止
- if isRecording {
- _ = stopRecording()
- }
- if isPlaying {
- _ = stopPlaying()
- }
-
- return initialize(config: config)
- }
-
- /// 释放资源
- func dispose() {
- _ = stopRecording()
- _ = stopPlaying()
-
- audioEngine?.stop()
- audioEngine = nil
- audioPlayer = nil
- audioBufferQueue.removeAll()
-
- os_log("音频管理器已释放", log: log, type: .info)
- }
-}
\ No newline at end of file
diff --git a/local_plugins/realtime/ios/realtime/Sources/realtime/RealtimePlugin.swift b/local_plugins/realtime/ios/realtime/Sources/realtime/RealtimePlugin.swift
deleted file mode 100644
index 857e22d43..000000000
--- a/local_plugins/realtime/ios/realtime/Sources/realtime/RealtimePlugin.swift
+++ /dev/null
@@ -1,274 +0,0 @@
-import Flutter
-import UIKit
-import AVFoundation
-import os.log
-
-/**
- * 实时语音聊天插件
- *
- * 通过WebSocket连接Vocode服务器实现实时语音交互
- * 包含录音、播放、WebSocket通信等功能
- */
-@objc public class RealtimePlugin: NSObject, FlutterPlugin {
-
- private let log = OSLog(subsystem: "com.yunqiinnovation.realtime", category: "RealtimePlugin")
-
- // Flutter通道
- private var methodChannel: FlutterMethodChannel?
- private var eventChannel: FlutterEventChannel?
- private var eventSink: FlutterEventSink?
-
- // 核心组件
- private let audioManager = RealtimeAudioManager()
- private let websocketManager = RealtimeWebSocketManager()
-
- // 配置参数
- private var serverUrl: String = ""
- private var sampleRate: Int = 16000
- private var channels: Int = 1
- private var bitsPerSample: Int = 16
-
- // 状态
- private var connectionStatus: ConnectionStatus = .disconnected
- private var voiceStatus: VoiceStatus = .idle
-
- /// 插件注册
- public static func register(with registrar: FlutterPluginRegistrar) {
- let instance = RealtimePlugin()
-
- // 方法通道
- let methodChannel = FlutterMethodChannel(name: "realtime/methods", binaryMessenger: registrar.messenger())
- registrar.addMethodCallDelegate(instance, channel: methodChannel)
- instance.methodChannel = methodChannel
-
- // 事件通道
- let eventChannel = FlutterEventChannel(name: "realtime/events", binaryMessenger: registrar.messenger())
- eventChannel.setStreamHandler(instance)
- instance.eventChannel = eventChannel
-
- // 设置组件回调
- instance.setupCallbacks()
- }
-
- /// 设置各组件的回调
- private func setupCallbacks() {
- // 音频管理器回调
- audioManager.onAudioData = { [weak self] data in
- self?.websocketManager.sendAudioData(data)
- }
-
- audioManager.onStatusChanged = { [weak self] status in
- self?.voiceStatus = status
- self?.sendEvent(type: "voiceStatusChanged", data: status.rawValue)
- }
-
- // WebSocket管理器回调
- websocketManager.onConnectionStatusChanged = { [weak self] status in
- self?.connectionStatus = status
- self?.sendEvent(type: "connectionStatusChanged", data: status.rawValue)
- }
-
- websocketManager.onAudioReceived = { [weak self] data in
- self?.audioManager.playAudioData(data)
- }
-
- websocketManager.onTextReceived = { [weak self] text in
- self?.sendEvent(type: "textReceived", data: text)
- }
-
- websocketManager.onError = { [weak self] error in
- self?.sendEvent(type: "error", data: error)
- }
- }
-
- /// 发送事件到Flutter端
- private func sendEvent(type: String, data: Any?) {
- guard let eventSink = eventSink else { return }
-
- let event: [String: Any] = [
- "type": type,
- "data": data ?? NSNull()
- ]
-
- DispatchQueue.main.async {
- eventSink(event)
- }
- }
-}
-
-// MARK: - FlutterPlugin
-extension RealtimePlugin {
-
- public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
- switch call.method {
- case "initialize":
- handleInitialize(call, result)
- case "connect":
- handleConnect(result)
- case "disconnect":
- handleDisconnect(result)
- case "startRecording":
- handleStartRecording(result)
- case "stopRecording":
- handleStopRecording(result)
- case "stopPlaying":
- handleStopPlaying(result)
- case "getConnectionStatus":
- result(connectionStatus.rawValue)
- case "getVoiceStatus":
- result(voiceStatus.rawValue)
- case "sendTextMessage":
- handleSendTextMessage(call, result)
- case "setAudioConfig":
- handleSetAudioConfig(call, result)
- case "dispose":
- handleDispose(result)
- default:
- result(FlutterMethodNotImplemented)
- }
- }
-
- private func handleInitialize(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) {
- guard let args = call.arguments as? [String: Any],
- let serverUrl = args["serverUrl"] as? String else {
- result(FlutterError(code: "INVALID_ARGUMENTS", message: "服务器地址不能为空", details: nil))
- return
- }
-
- self.serverUrl = serverUrl
- self.sampleRate = args["sampleRate"] as? Int ?? 16000
- self.channels = args["channels"] as? Int ?? 1
- self.bitsPerSample = args["bitsPerSample"] as? Int ?? 16
-
- // 初始化音频管理器
- let audioConfig = AudioConfig(
- sampleRate: sampleRate,
- channels: channels,
- bitsPerSample: bitsPerSample
- )
-
- let success = audioManager.initialize(config: audioConfig)
- if success {
- websocketManager.initialize(serverUrl: serverUrl)
- os_log("实时语音插件初始化成功", log: log, type: .info)
- } else {
- os_log("实时语音插件初始化失败", log: log, type: .error)
- }
-
- result(success)
- }
-
- private func handleConnect(_ result: @escaping FlutterResult) {
- websocketManager.connect { [weak self] success in
- DispatchQueue.main.async {
- result(success)
- }
- }
- }
-
- private func handleDisconnect(_ result: @escaping FlutterResult) {
- websocketManager.disconnect()
- result(true)
- }
-
- private func handleStartRecording(_ result: @escaping FlutterResult) {
- let success = audioManager.startRecording()
- result(success)
- }
-
- private func handleStopRecording(_ result: @escaping FlutterResult) {
- let success = audioManager.stopRecording()
- result(success)
- }
-
- private func handleStopPlaying(_ result: @escaping FlutterResult) {
- let success = audioManager.stopPlaying()
- result(success)
- }
-
- private func handleSendTextMessage(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) {
- guard let args = call.arguments as? [String: Any],
- let message = args["message"] as? String else {
- result(FlutterError(code: "INVALID_ARGUMENTS", message: "消息内容不能为空", details: nil))
- return
- }
-
- let success = websocketManager.sendTextMessage(message)
- result(success)
- }
-
- private func handleSetAudioConfig(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) {
- guard let args = call.arguments as? [String: Any] else {
- result(FlutterError(code: "INVALID_ARGUMENTS", message: "参数无效", details: nil))
- return
- }
-
- if let sampleRate = args["sampleRate"] as? Int {
- self.sampleRate = sampleRate
- }
- if let channels = args["channels"] as? Int {
- self.channels = channels
- }
- if let bitsPerSample = args["bitsPerSample"] as? Int {
- self.bitsPerSample = bitsPerSample
- }
-
- let audioConfig = AudioConfig(
- sampleRate: sampleRate,
- channels: channels,
- bitsPerSample: bitsPerSample
- )
-
- let success = audioManager.updateConfig(config: audioConfig)
- result(success)
- }
-
- private func handleDispose(_ result: @escaping FlutterResult) {
- audioManager.dispose()
- websocketManager.dispose()
- result(nil)
- }
-}
-
-// MARK: - FlutterStreamHandler
-extension RealtimePlugin: FlutterStreamHandler {
-
- public func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? {
- self.eventSink = events
- return nil
- }
-
- public func onCancel(withArguments arguments: Any?) -> FlutterError? {
- self.eventSink = nil
- return nil
- }
-}
-
-// MARK: - 枚举定义
-
-enum ConnectionStatus: String {
- case disconnected = "disconnected"
- case connecting = "connecting"
- case connected = "connected"
- case error = "error"
-}
-
-enum VoiceStatus: String {
- case idle = "idle"
- case recording = "recording"
- case processing = "processing"
- case playing = "playing"
-}
-
-// MARK: - 音频配置
-
-struct AudioConfig {
- let sampleRate: Int
- let channels: Int
- let bitsPerSample: Int
-
- var frameSize: Int {
- // 20ms的帧大小
- return (sampleRate * channels * bitsPerSample / 8 * 20) / 1000
- }
-}
\ No newline at end of file
diff --git a/local_plugins/realtime/ios/realtime/Sources/realtime/RealtimeWebSocketManager.swift b/local_plugins/realtime/ios/realtime/Sources/realtime/RealtimeWebSocketManager.swift
deleted file mode 100644
index ed4428f8f..000000000
--- a/local_plugins/realtime/ios/realtime/Sources/realtime/RealtimeWebSocketManager.swift
+++ /dev/null
@@ -1,264 +0,0 @@
-import Foundation
-import Network
-import os.log
-
-/**
- * 实时WebSocket管理器
- *
- * 负责与Vocode服务器的WebSocket通信,对应Android示例中的OkHttp WebSocket功能
- */
-class RealtimeWebSocketManager: NSObject {
-
- private let log = OSLog(subsystem: "com.yunqiinnovation.realtime", category: "RealtimeWebSocketManager")
-
- // WebSocket连接
- private var webSocketTask: URLSessionWebSocketTask?
- private var urlSession: URLSession?
-
- // 服务器URL
- private var serverUrl: String = ""
-
- // 连接状态
- private var isConnected = false
- private var isConnecting = false
-
- // 回调
- var onConnectionStatusChanged: ((ConnectionStatus) -> Void)?
- var onAudioReceived: ((Data) -> Void)?
- var onTextReceived: ((String) -> Void)?
- var onError: ((String) -> Void)?
-
- // 发送队列
- private let sendQueue = DispatchQueue(label: "com.realtime.websocket.send", qos: .userInitiated)
- private let receiveQueue = DispatchQueue(label: "com.realtime.websocket.receive", qos: .userInitiated)
-
- override init() {
- super.init()
- setupURLSession()
- }
-
- /// 设置URL会话
- private func setupURLSession() {
- let config = URLSessionConfiguration.default
- config.timeoutIntervalForRequest = 30
- config.timeoutIntervalForResource = 60
- urlSession = URLSession(configuration: config, delegate: self, delegateQueue: nil)
- }
-
- /// 初始化WebSocket管理器
- func initialize(serverUrl: String) {
- self.serverUrl = serverUrl
- os_log("WebSocket管理器初始化,服务器地址: %@", log: log, type: .info, serverUrl)
- }
-
- /// 连接到服务器
- func connect(completion: @escaping (Bool) -> Void) {
- guard !isConnected && !isConnecting else {
- completion(false)
- return
- }
-
- guard let url = URL(string: serverUrl) else {
- os_log("无效的服务器地址: %@", log: log, type: .error, serverUrl)
- onError?("无效的服务器地址")
- completion(false)
- return
- }
-
- isConnecting = true
- onConnectionStatusChanged?(.connecting)
-
- // 创建WebSocket任务
- var request = URLRequest(url: url)
- request.timeoutInterval = 30
-
- webSocketTask = urlSession?.webSocketTask(with: request)
-
- // 开始监听消息
- startListening()
-
- // 开始连接
- webSocketTask?.resume()
-
- // 等待连接建立
- DispatchQueue.global().asyncAfter(deadline: .now() + 1.0) { [weak self] in
- self?.checkConnection(completion: completion)
- }
- }
-
- /// 检查连接状态
- private func checkConnection(completion: @escaping (Bool) -> Void) {
- guard let webSocketTask = webSocketTask else {
- isConnecting = false
- onConnectionStatusChanged?(.error)
- completion(false)
- return
- }
-
- switch webSocketTask.state {
- case .running:
- isConnected = true
- isConnecting = false
- onConnectionStatusChanged?(.connected)
- os_log("WebSocket连接成功", log: log, type: .info)
- completion(true)
- case .suspended:
- // 等待连接完成
- DispatchQueue.global().asyncAfter(deadline: .now() + 0.5) { [weak self] in
- self?.checkConnection(completion: completion)
- }
- default:
- isConnecting = false
- onConnectionStatusChanged?(.error)
- os_log("WebSocket连接失败", log: log, type: .error)
- completion(false)
- }
- }
-
- /// 开始监听消息
- private func startListening() {
- receiveMessage()
- }
-
- /// 接收消息
- private func receiveMessage() {
- webSocketTask?.receive { [weak self] result in
- switch result {
- case .success(let message):
- self?.handleReceivedMessage(message)
- // 继续监听下一条消息
- self?.receiveMessage()
- case .failure(let error):
- self?.handleReceiveError(error)
- }
- }
- }
-
- /// 处理接收到的消息
- private func handleReceivedMessage(_ message: URLSessionWebSocketTask.Message) {
- switch message {
- case .data(let data):
- // 音频数据
- onAudioReceived?(data)
- case .string(let text):
- // 文本数据
- onTextReceived?(text)
- @unknown default:
- os_log("收到未知类型消息", log: log, type: .error)
- }
- }
-
- /// 处理接收错误
- private func handleReceiveError(_ error: Error) {
- os_log("WebSocket接收错误: %@", log: log, type: .error, error.localizedDescription)
-
- // 如果连接断开,尝试重连
- if isConnected {
- isConnected = false
- onConnectionStatusChanged?(.error)
- onError?("连接断开: \(error.localizedDescription)")
- }
- }
-
- /// 发送音频数据
- func sendAudioData(_ data: Data) {
- guard isConnected else { return }
-
- sendQueue.async { [weak self] in
- self?.webSocketTask?.send(.data(data)) { error in
- if let error = error {
- self?.os_log("发送音频数据失败: %@", log: self?.log ?? OSLog.default, type: .error, error.localizedDescription)
- }
- }
- }
- }
-
- /// 发送文本消息
- func sendTextMessage(_ message: String) -> Bool {
- guard isConnected else {
- os_log("WebSocket未连接,无法发送文本消息", log: log, type: .error)
- return false
- }
-
- sendQueue.async { [weak self] in
- self?.webSocketTask?.send(.string(message)) { error in
- if let error = error {
- self?.os_log("发送文本消息失败: %@", log: self?.log ?? OSLog.default, type: .error, error.localizedDescription)
- }
- }
- }
-
- return true
- }
-
- /// 断开连接
- func disconnect() {
- guard isConnected || isConnecting else { return }
-
- webSocketTask?.cancel(with: .goingAway, reason: nil)
- webSocketTask = nil
-
- isConnected = false
- isConnecting = false
- onConnectionStatusChanged?(.disconnected)
- os_log("WebSocket已断开连接", log: log, type: .info)
- }
-
- /// 释放资源
- func dispose() {
- disconnect()
- urlSession?.invalidateAndCancel()
- urlSession = nil
- os_log("WebSocket管理器已释放", log: log, type: .info)
- }
-
- // 修复方法访问性问题
- private func os_log(_ message: String, log: OSLog, type: OSLogType, _ args: CVarArg...) {
- if args.isEmpty {
- os_log("%@", log: log, type: type, message)
- } else {
- os_log(message, log: log, type: type, args)
- }
- }
-}
-
-// MARK: - URLSessionWebSocketDelegate
-extension RealtimeWebSocketManager: URLSessionWebSocketDelegate {
-
- func urlSession(_ session: URLSession, webSocketTask: URLSessionWebSocketTask, didOpenWithProtocol protocol: String?) {
- os_log("WebSocket连接已建立", log: log, type: .info)
- isConnected = true
- isConnecting = false
- onConnectionStatusChanged?(.connected)
- }
-
- func urlSession(_ session: URLSession, webSocketTask: URLSessionWebSocketTask, didCloseWith closeCode: URLSessionWebSocketTask.CloseCode, reason: Data?) {
- os_log("WebSocket连接已关闭,关闭码: %d", log: log, type: .info, closeCode.rawValue)
- isConnected = false
- isConnecting = false
- onConnectionStatusChanged?(.disconnected)
- }
-}
-
-// MARK: - URLSessionDelegate
-extension RealtimeWebSocketManager: URLSessionDelegate {
-
- func urlSession(_ session: URLSession, didBecomeInvalidWithError error: Error?) {
- if let error = error {
- os_log("URLSession失效: %@", log: log, type: .error, error.localizedDescription)
- onError?("会话失效: \(error.localizedDescription)")
- }
- }
-
- func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
- if let error = error {
- os_log("URLSessionTask完成时出错: %@", log: log, type: .error, error.localizedDescription)
- if isConnected || isConnecting {
- isConnected = false
- isConnecting = false
- onConnectionStatusChanged?(.error)
- onError?("连接错误: \(error.localizedDescription)")
- }
- }
- }
-}
\ No newline at end of file
diff --git a/local_plugins/realtime/lib/realtime.dart b/local_plugins/realtime/lib/realtime.dart
deleted file mode 100644
index 237e9e3e8..000000000
--- a/local_plugins/realtime/lib/realtime.dart
+++ /dev/null
@@ -1,243 +0,0 @@
-import 'dart:async';
-import 'dart:typed_data';
-
-import 'package:flutter/services.dart';
-
-/// 实时语音服务异常
-class RealtimeException implements Exception {
- final String message;
-
- RealtimeException(this.message);
-
- @override
- String toString() => 'RealtimeException: $message';
-}
-
-/// 连接状态
-enum ConnectionStatus {
- disconnected,
- connecting,
- connected,
- error,
-}
-
-/// 语音状态
-enum VoiceStatus {
- idle,
- recording,
- processing,
- playing,
-}
-
-/// 实时语音事件
-class RealtimeEvent {
- final String type;
- final dynamic data;
-
- RealtimeEvent({required this.type, this.data});
-
- factory RealtimeEvent.fromMap(Map map) {
- return RealtimeEvent(
- type: map['type'] as String,
- data: map['data'],
- );
- }
-}
-
-/// 实时语音聊天服务
-class RealtimeService {
- static const MethodChannel _channel = MethodChannel('realtime/methods');
- static const EventChannel _eventChannel = EventChannel('realtime/events');
-
- StreamController? _eventStreamController;
- Stream? _eventStream;
-
- /// 获取事件流
- Stream get eventStream {
- if (_eventStream == null) {
- _eventStreamController = StreamController.broadcast();
- _eventStream = _eventStreamController!.stream;
-
- // 监听原生事件
- _eventChannel.receiveBroadcastStream().listen(
- (dynamic event) {
- if (event is Map) {
- final eventMap = Map.from(event);
- final realtimeEvent = RealtimeEvent.fromMap(eventMap);
- _eventStreamController!.add(realtimeEvent);
- }
- },
- onError: (error) {
- _eventStreamController!.addError(RealtimeException('事件流错误: $error'));
- },
- );
- }
-
- return _eventStream!;
- }
-
- /// 初始化实时语音服务
- ///
- /// [serverUrl] WebSocket服务器地址
- /// [sampleRate] 采样率,默认16000
- /// [channels] 声道数,默认1(单声道)
- /// [bitsPerSample] 位深,默认16
- Future initialize({
- required String serverUrl,
- int sampleRate = 16000,
- int channels = 1,
- int bitsPerSample = 16,
- }) async {
- try {
- final result = await _channel.invokeMethod(
- 'initialize',
- {
- 'serverUrl': serverUrl,
- 'sampleRate': sampleRate,
- 'channels': channels,
- 'bitsPerSample': bitsPerSample,
- },
- );
-
- return result ?? false;
- } catch (e) {
- throw RealtimeException('初始化失败: $e');
- }
- }
-
- /// 连接到服务器
- Future connect() async {
- try {
- final result = await _channel.invokeMethod('connect');
- return result ?? false;
- } catch (e) {
- throw RealtimeException('连接失败: $e');
- }
- }
-
- /// 断开连接
- Future disconnect() async {
- try {
- final result = await _channel.invokeMethod('disconnect');
- return result ?? false;
- } catch (e) {
- throw RealtimeException('断开连接失败: $e');
- }
- }
-
- /// 开始录音
- Future startRecording() async {
- try {
- final result = await _channel.invokeMethod('startRecording');
- return result ?? false;
- } catch (e) {
- throw RealtimeException('开始录音失败: $e');
- }
- }
-
- /// 停止录音
- Future stopRecording() async {
- try {
- final result = await _channel.invokeMethod('stopRecording');
- return result ?? false;
- } catch (e) {
- throw RealtimeException('停止录音失败: $e');
- }
- }
-
- /// 停止播放
- Future stopPlaying() async {
- try {
- final result = await _channel.invokeMethod('stopPlaying');
- return result ?? false;
- } catch (e) {
- throw RealtimeException('停止播放失败: $e');
- }
- }
-
- /// 获取连接状态
- Future getConnectionStatus() async {
- try {
- final result = await _channel.invokeMethod('getConnectionStatus');
- switch (result) {
- case 'disconnected':
- return ConnectionStatus.disconnected;
- case 'connecting':
- return ConnectionStatus.connecting;
- case 'connected':
- return ConnectionStatus.connected;
- case 'error':
- return ConnectionStatus.error;
- default:
- return ConnectionStatus.disconnected;
- }
- } catch (e) {
- throw RealtimeException('获取连接状态失败: $e');
- }
- }
-
- /// 获取语音状态
- Future getVoiceStatus() async {
- try {
- final result = await _channel.invokeMethod('getVoiceStatus');
- switch (result) {
- case 'idle':
- return VoiceStatus.idle;
- case 'recording':
- return VoiceStatus.recording;
- case 'processing':
- return VoiceStatus.processing;
- case 'playing':
- return VoiceStatus.playing;
- default:
- return VoiceStatus.idle;
- }
- } catch (e) {
- throw RealtimeException('获取语音状态失败: $e');
- }
- }
-
- /// 发送文本消息
- Future sendTextMessage(String message) async {
- try {
- final result = await _channel.invokeMethod(
- 'sendTextMessage',
- {'message': message},
- );
- return result ?? false;
- } catch (e) {
- throw RealtimeException('发送文本消息失败: $e');
- }
- }
-
- /// 设置音频参数
- Future setAudioConfig({
- int? sampleRate,
- int? channels,
- int? bitsPerSample,
- }) async {
- try {
- final params = {};
- if (sampleRate != null) params['sampleRate'] = sampleRate;
- if (channels != null) params['channels'] = channels;
- if (bitsPerSample != null) params['bitsPerSample'] = bitsPerSample;
-
- final result = await _channel.invokeMethod('setAudioConfig', params);
- return result ?? false;
- } catch (e) {
- throw RealtimeException('设置音频参数失败: $e');
- }
- }
-
- /// 释放资源
- Future dispose() async {
- try {
- await _channel.invokeMethod('dispose');
- _eventStreamController?.close();
- _eventStreamController = null;
- _eventStream = null;
- } catch (e) {
- throw RealtimeException('释放资源失败: $e');
- }
- }
-}
\ No newline at end of file
diff --git a/local_plugins/realtime/pubspec.yaml b/local_plugins/realtime/pubspec.yaml
deleted file mode 100644
index 0eed88a7d..000000000
--- a/local_plugins/realtime/pubspec.yaml
+++ /dev/null
@@ -1,29 +0,0 @@
-name: realtime
-description: 实时语音聊天插件,通过WebSocket连接Vocode服务器实现语音交互
-version: 0.0.1
-homepage:
-
-environment:
- sdk: ">=2.17.0 <3.0.0"
- flutter: ">=2.5.0"
-
-dependencies:
- flutter:
- sdk: flutter
-
-dev_dependencies:
- flutter_test:
- sdk: flutter
- flutter_lints: ^2.0.0
-
-# Flutter插件配置
-flutter:
- plugin:
- platforms:
- android:
- package: com.yunqiinnovation.realtime
- pluginClass: RealtimePlugin
- ios:
- pluginClass: RealtimePlugin
- swiftPackage:
- path: ios/realtime
\ No newline at end of file