13 changed files with 61 additions and 867 deletions
@ -1,40 +0,0 @@ |
|||
import 'package:get/get.dart'; |
|||
import 'package:flutter/material.dart'; |
|||
import 'package:get_storage/get_storage.dart'; |
|||
|
|||
class AppController extends GetxController { |
|||
static AppController get to => Get.find(); |
|||
|
|||
final _isDarkMode = false.obs; |
|||
final _currentIndex = 0.obs; |
|||
final _storage = GetStorage(); |
|||
|
|||
bool get isDarkMode => _isDarkMode.value; |
|||
int get currentIndex => _currentIndex.value; |
|||
|
|||
@override |
|||
void onInit() { |
|||
super.onInit(); |
|||
_loadThemeMode(); |
|||
} |
|||
|
|||
void _loadThemeMode() { |
|||
_isDarkMode.value = _storage.read('isDarkMode') ?? false; |
|||
} |
|||
|
|||
Future<void> toggleThemeMode() async { |
|||
_isDarkMode.value = !_isDarkMode.value; |
|||
Get.changeThemeMode(_isDarkMode.value ? ThemeMode.dark : ThemeMode.light); |
|||
await _storage.write('isDarkMode', _isDarkMode.value); |
|||
} |
|||
|
|||
void changeIndex(int index) { |
|||
_currentIndex.value = index; |
|||
} |
|||
|
|||
@override |
|||
void onClose() { |
|||
// 在这里清理全局资源 |
|||
super.onClose(); |
|||
} |
|||
} |
|||
@ -1,46 +0,0 @@ |
|||
import 'package:flutter/material.dart'; |
|||
import 'package:get/get.dart'; |
|||
import '../controllers/app_controller.dart'; |
|||
import '../../modules/home/views/home_view.dart'; |
|||
import '../../modules/explore/views/explore_view.dart'; |
|||
import '../../modules/profile/views/profile_view.dart'; |
|||
|
|||
class MainView extends GetView<AppController> { |
|||
const MainView({Key? key}) : super(key: key); |
|||
|
|||
@override |
|||
Widget build(BuildContext context) { |
|||
return Scaffold( |
|||
body: Obx(() { |
|||
switch (controller.currentIndex) { |
|||
case 0: |
|||
return const HomeView(); |
|||
case 1: |
|||
return const ExploreView(); |
|||
case 2: |
|||
return const ProfileView(); |
|||
default: |
|||
return const HomeView(); |
|||
} |
|||
}), |
|||
bottomNavigationBar: Obx(() => BottomNavigationBar( |
|||
currentIndex: controller.currentIndex, |
|||
onTap: controller.changeIndex, |
|||
items: [ |
|||
BottomNavigationBarItem( |
|||
icon: const Icon(Icons.home), |
|||
label: 'home'.tr, |
|||
), |
|||
BottomNavigationBarItem( |
|||
icon: const Icon(Icons.explore), |
|||
label: 'explore'.tr, |
|||
), |
|||
BottomNavigationBarItem( |
|||
icon: const Icon(Icons.person), |
|||
label: 'profile'.tr, |
|||
), |
|||
], |
|||
)), |
|||
); |
|||
} |
|||
} |
|||
@ -0,0 +1,47 @@ |
|||
import 'package:flutter/material.dart'; |
|||
import 'package:get/get.dart'; |
|||
import '../routes/app_routes.dart'; |
|||
|
|||
class CommonBottomNav extends StatelessWidget { |
|||
final int currentIndex; |
|||
|
|||
const CommonBottomNav({ |
|||
Key? key, |
|||
required this.currentIndex, |
|||
}) : super(key: key); |
|||
|
|||
@override |
|||
Widget build(BuildContext context) { |
|||
return BottomNavigationBar( |
|||
currentIndex: currentIndex, |
|||
onTap: (index) { |
|||
// Navigate to the appropriate page based on index |
|||
switch (index) { |
|||
case 0: |
|||
Get.offAllNamed(Routes.home); |
|||
break; |
|||
case 1: |
|||
Get.offAllNamed(Routes.explore); |
|||
break; |
|||
case 2: |
|||
Get.offAllNamed(Routes.profile); |
|||
break; |
|||
} |
|||
}, |
|||
items: [ |
|||
BottomNavigationBarItem( |
|||
icon: const Icon(Icons.home), |
|||
label: 'home'.tr, |
|||
), |
|||
BottomNavigationBarItem( |
|||
icon: const Icon(Icons.explore), |
|||
label: 'explore'.tr, |
|||
), |
|||
BottomNavigationBarItem( |
|||
icon: const Icon(Icons.person), |
|||
label: 'profile'.tr, |
|||
), |
|||
], |
|||
); |
|||
} |
|||
} |
|||
@ -1,167 +0,0 @@ |
|||
// voice_recognition_service.dart |
|||
|
|||
import 'package:flutter_dotenv/flutter_dotenv.dart'; |
|||
import 'package:flutter/foundation.dart'; |
|||
import 'volcano_asr_service.dart'; |
|||
import 'package:get/get.dart'; |
|||
|
|||
class VoiceRecognitionService extends GetxService { |
|||
VolcanoAsrService? _asrService; |
|||
bool _isInitialized = false; |
|||
bool _isStoppingRecognition = false; |
|||
|
|||
final isRecognizing = false.obs; |
|||
|
|||
Function(String)? onRecognitionResult; |
|||
Function(String)? onError; |
|||
Function()? onRecognitionComplete; |
|||
Function(String)? onSentenceComplete; |
|||
Function(String)? onConnectionStatusChanged; |
|||
|
|||
VoiceRecognitionService(); |
|||
|
|||
void _logError(String message, [dynamic error, StackTrace? stackTrace]) { |
|||
debugPrint('🔴 VoiceRecognitionService Error: $message'); |
|||
if (error != null) debugPrint('Error details: $error'); |
|||
if (stackTrace != null) debugPrint('Stack trace: $stackTrace'); |
|||
} |
|||
|
|||
@override |
|||
Future<void> onInit() async { |
|||
super.onInit(); |
|||
if (!_isInitialized) { |
|||
try { |
|||
await _initializeServices(); |
|||
_isInitialized = true; |
|||
debugPrint('✅ VoiceRecognitionService initialized successfully'); |
|||
} catch (e, stackTrace) { |
|||
_logError('Failed to initialize services', e, stackTrace); |
|||
rethrow; |
|||
} |
|||
} |
|||
} |
|||
|
|||
Future<void> _initializeServices() async { |
|||
try { |
|||
final appKey = dotenv.env['VOLCANO_APP_KEY']; |
|||
final accessKey = dotenv.env['VOLCANO_ACCESS_KEY']; |
|||
if (appKey == null || accessKey == null) { |
|||
throw Exception('Volcano ASR service configuration is missing. Please check your .env file.'); |
|||
} |
|||
_asrService = VolcanoAsrService( |
|||
appKey: appKey, |
|||
accessKey: accessKey, |
|||
onResult: _handleRecognitionResult, |
|||
onError: _handleError, |
|||
onConnectionStatusChanged: _handleConnectionStatusChanged, |
|||
); |
|||
await _setupServices(); |
|||
} catch (e, stackTrace) { |
|||
_logError('Error during service initialization', e, stackTrace); |
|||
rethrow; |
|||
} |
|||
} |
|||
|
|||
Future<void> _setupServices() async { |
|||
try { |
|||
_asrService?.onResult = (result) { |
|||
debugPrint('ASR Result: $result'); |
|||
onRecognitionResult?.call(result); |
|||
onSentenceComplete?.call(result); |
|||
}; |
|||
_asrService?.onError = (error) { |
|||
_logError('ASR service error', error); |
|||
onError?.call('ASR error: $error'); |
|||
}; |
|||
_asrService?.onConnectionStatusChanged = (status) { |
|||
final isConnected = status == 'connected'; |
|||
debugPrint('ASR Connection status: $status'); |
|||
if (!isConnected && isRecognizing.value && !_isStoppingRecognition) { |
|||
stopRecognition(); |
|||
onRecognitionComplete?.call(); |
|||
} |
|||
onConnectionStatusChanged?.call(status); |
|||
}; |
|||
} catch (e, stackTrace) { |
|||
_logError('Error during services setup', e, stackTrace); |
|||
rethrow; |
|||
} |
|||
} |
|||
|
|||
Future<void> startRecognition() async { |
|||
if (isRecognizing.value || _asrService == null) { |
|||
debugPrint('⚠️ Cannot start recognition: ${isRecognizing.value ? 'Already recognizing' : 'ASR service not initialized'}'); |
|||
return; |
|||
} |
|||
try { |
|||
_isStoppingRecognition = false; |
|||
debugPrint('Starting voice recognition...'); |
|||
await _asrService?.startRecognition(); |
|||
isRecognizing.value = true; |
|||
} catch (e, stackTrace) { |
|||
_logError('Failed to start recognition', e, stackTrace); |
|||
onError?.call('Failed to start recognition: $e'); |
|||
await stopRecognition(); |
|||
} |
|||
} |
|||
|
|||
Future<void> stopRecognition() async { |
|||
if (!isRecognizing.value || _asrService == null || _isStoppingRecognition) return; |
|||
try { |
|||
_isStoppingRecognition = true; |
|||
debugPrint('Stopping voice recognition...'); |
|||
isRecognizing.value = false; |
|||
await _asrService?.stopRecognition().timeout( |
|||
const Duration(seconds: 3), |
|||
onTimeout: () { |
|||
debugPrint('ASR service stop timeout, forcing cleanup'); |
|||
_cleanupResources(); |
|||
}, |
|||
); |
|||
final complete = onRecognitionComplete; |
|||
if (complete != null) { |
|||
Future.microtask(complete); |
|||
} |
|||
} catch (e, stackTrace) { |
|||
_logError('Failed to stop recognition', e, stackTrace); |
|||
onError?.call('Failed to stop recognition: $e'); |
|||
_cleanupResources(); |
|||
} finally { |
|||
_isStoppingRecognition = false; |
|||
} |
|||
} |
|||
|
|||
void _cleanupResources() { |
|||
if (_asrService != null) { |
|||
_asrService!.onResult = null; |
|||
_asrService!.onError = null; |
|||
_asrService!.onConnectionStatusChanged = null; |
|||
} |
|||
isRecognizing.value = false; |
|||
_isStoppingRecognition = false; |
|||
} |
|||
|
|||
void _handleRecognitionResult(String result) { |
|||
onRecognitionResult?.call(result); |
|||
} |
|||
|
|||
void _handleError(String error) { |
|||
onError?.call(error); |
|||
} |
|||
|
|||
void _handleConnectionStatusChanged(String status) { |
|||
onConnectionStatusChanged?.call(status); |
|||
} |
|||
|
|||
@override |
|||
void onClose() { |
|||
debugPrint('Cleaning up VoiceRecognitionService...'); |
|||
stopRecognition().then((_) { |
|||
debugPrint('VoiceRecognitionService cleanup completed'); |
|||
super.onClose(); |
|||
}).catchError((e) { |
|||
debugPrint('Error during VoiceRecognitionService cleanup: $e'); |
|||
super.onClose(); |
|||
}); |
|||
} |
|||
} |
|||
@ -1,384 +0,0 @@ |
|||
import 'dart:async'; |
|||
import 'dart:convert'; |
|||
import 'dart:io'; |
|||
import 'package:uuid/uuid.dart'; |
|||
import 'package:get/get.dart'; |
|||
import 'package:flutter/foundation.dart'; |
|||
import 'package:record/record.dart'; |
|||
|
|||
/// 自定义异常类 |
|||
class AsrServiceException implements Exception { |
|||
final String message; |
|||
AsrServiceException(this.message); |
|||
|
|||
@override |
|||
String toString() => message; |
|||
} |
|||
|
|||
/// Volcano ASR 服务,负责音频采集、数据打包、WebSocket 连接和数据传输 |
|||
class VolcanoAsrService extends GetxService { |
|||
// 服务地址 |
|||
static const String _baseUrl = 'wss://openspeech.bytedance.com/api/v3/sauc/bigmodel'; |
|||
|
|||
// 配置参数 |
|||
final String appKey; |
|||
final String accessKey; |
|||
|
|||
// WebSocket 与录音相关 |
|||
WebSocket? _webSocket; |
|||
AudioRecorder? _audioRecorder; |
|||
StreamSubscription<List<int>>? _audioStreamSubscription; |
|||
|
|||
// 状态管理 |
|||
bool _isConnected = false; |
|||
bool _isRecording = false; |
|||
int _currentSequence = 1; |
|||
int _serverSequence = 0; // 添加服务器序列号跟踪 |
|||
bool _isInitialized = false; |
|||
bool _waitingForInitResponse = false; |
|||
Completer<void>? _initCompleter; |
|||
|
|||
// 回调 |
|||
Function(String)? onResult; |
|||
Function(String)? onError; |
|||
Function(String)? onConnectionStatusChanged; |
|||
|
|||
VolcanoAsrService({ |
|||
required this.appKey, |
|||
required this.accessKey, |
|||
this.onResult, |
|||
this.onError, |
|||
this.onConnectionStatusChanged, |
|||
}); |
|||
|
|||
Future<void> _connectWebSocket() async { |
|||
if (_webSocket != null) { |
|||
debugPrint('⚠️ ASR: WebSocket already exists'); |
|||
return; |
|||
} |
|||
|
|||
final connectId = const Uuid().v4(); |
|||
final headers = { |
|||
'X-Api-App-Key': appKey, |
|||
'X-Api-Access-Key': accessKey, |
|||
'X-Api-Resource-Id': 'volc.bigasr.sauc.duration', |
|||
'X-Api-Connect-Id': connectId, |
|||
'Connection': 'Upgrade', // 添加 WebSocket 升级头 |
|||
'Upgrade': 'websocket', |
|||
}; |
|||
|
|||
try { |
|||
_webSocket = await WebSocket.connect( |
|||
_baseUrl, |
|||
headers: headers, |
|||
protocols: ['binary'], // 指定二进制协议 |
|||
); |
|||
debugPrint('ℹ️ ASR: WebSocket connected'); |
|||
|
|||
_webSocket!.listen( |
|||
_handleWebSocketMessage, |
|||
onError: (error) { |
|||
debugPrint('🔴 ASR: WebSocket error: $error'); |
|||
onError?.call('WebSocket error: $error'); |
|||
_handleDisconnect(); |
|||
}, |
|||
onDone: () { |
|||
debugPrint('ℹ️ ASR: WebSocket closed'); |
|||
if (_waitingForInitResponse) { |
|||
_initCompleter?.completeError('WebSocket closed during initialization'); |
|||
_initCompleter = null; |
|||
} |
|||
_handleDisconnect(); |
|||
}, |
|||
cancelOnError: false, // 不要在错误时自动取消 |
|||
); |
|||
|
|||
_isConnected = true; |
|||
_currentSequence = 1; // 重置序列号 |
|||
_isInitialized = false; // 重置初始化状态 |
|||
onConnectionStatusChanged?.call('connected'); |
|||
|
|||
// 发送初始化请求前等待一下 |
|||
await Future.delayed(const Duration(milliseconds: 100)); |
|||
await _sendInitRequest(connectId); |
|||
|
|||
} catch (e) { |
|||
debugPrint('🔴 ASR: Failed to connect: $e'); |
|||
rethrow; |
|||
} |
|||
} |
|||
|
|||
Future<void> _sendInitRequest(String requestId) async { |
|||
_waitingForInitResponse = true; |
|||
_initCompleter = Completer<void>(); |
|||
|
|||
final initRequest = { |
|||
'audio': { |
|||
'format': 'raw', |
|||
'sample_rate': 16000, |
|||
'channels': 1, |
|||
'encoding': 'pcm', |
|||
'bit_depth': 16, |
|||
}, |
|||
'request': { |
|||
'request_id': requestId, |
|||
'language': 'zh', |
|||
'model_name': 'bigmodel', |
|||
'enable_punc': true, |
|||
'enable_itn': true, |
|||
'enable_voice_detection': true, |
|||
} |
|||
}; |
|||
|
|||
final jsonBytes = utf8.encode(json.encode(initRequest)); |
|||
debugPrint('ℹ️ ASR: Sending init request: ${json.encode(initRequest)}'); |
|||
|
|||
final header = Uint8List(4) |
|||
..[0] = 0x11 // version=1, header_size=1 |
|||
..[1] = 0x11 // message_type=1(FULL_CLIENT_REQUEST), flags=1(POS_SEQUENCE) |
|||
..[2] = 0x10 // serialization=1(JSON), compression=0 |
|||
..[3] = 0x00; // reserved |
|||
|
|||
final seqBytes = ByteData(4)..setInt32(0, _currentSequence, Endian.big); |
|||
final sizeBytes = ByteData(4)..setInt32(0, jsonBytes.length, Endian.big); |
|||
|
|||
final fullRequest = Uint8List(header.length + 8 + jsonBytes.length) |
|||
..setAll(0, header) |
|||
..setAll(4, seqBytes.buffer.asUint8List()) |
|||
..setAll(8, sizeBytes.buffer.asUint8List()) |
|||
..setAll(12, jsonBytes); |
|||
|
|||
_webSocket?.add(fullRequest); |
|||
debugPrint('ℹ️ ASR: Init request sent with sequence: $_currentSequence'); |
|||
|
|||
// 设置超时 |
|||
Future.delayed(const Duration(seconds: 10), () { |
|||
if (_waitingForInitResponse) { |
|||
debugPrint('⚠️ ASR: Init request timeout after 10 seconds'); |
|||
_waitingForInitResponse = false; |
|||
_initCompleter?.completeError('Init request timeout'); |
|||
_initCompleter = null; |
|||
} |
|||
}); |
|||
|
|||
try { |
|||
await _initCompleter?.future; |
|||
} catch (e) { |
|||
debugPrint('🔴 ASR: Init request failed: $e'); |
|||
rethrow; |
|||
} |
|||
} |
|||
|
|||
void _handleWebSocketMessage(dynamic message) { |
|||
debugPrint('ℹ️ ASR: Received message type: ${message.runtimeType}'); |
|||
|
|||
if (message is String) { |
|||
debugPrint('ℹ️ ASR: Received string message: $message'); |
|||
try { |
|||
final data = json.decode(message); |
|||
if (data is Map) { |
|||
if (data['error'] != null) { |
|||
debugPrint('🔴 ASR: Server error: ${data['error']}'); |
|||
if (_waitingForInitResponse) { |
|||
_waitingForInitResponse = false; |
|||
_initCompleter?.completeError(data['error']); |
|||
_initCompleter = null; |
|||
} |
|||
onError?.call(data['error']); |
|||
return; |
|||
} |
|||
} |
|||
} catch (e) { |
|||
debugPrint('⚠️ ASR: Failed to parse string message: $e'); |
|||
} |
|||
} |
|||
|
|||
if (message is! Uint8List || message.length < 12) { |
|||
debugPrint('⚠️ ASR: Invalid binary message format'); |
|||
return; |
|||
} |
|||
|
|||
final version = message[0] >> 4; |
|||
final headerSize = message[0] & 0x0F; |
|||
final messageType = message[1] >> 4; |
|||
final flags = message[1] & 0x0F; |
|||
final serialization = message[2] >> 4; |
|||
final compression = message[2] & 0x0F; |
|||
|
|||
debugPrint('ℹ️ ASR: Binary message: version=$version, headerSize=$headerSize, type=$messageType, flags=$flags, serialization=$serialization, compression=$compression'); |
|||
|
|||
final sequence = ByteData.view(message.buffer, 4, 4).getInt32(0, Endian.big); |
|||
debugPrint('ℹ️ ASR: Message sequence: $sequence'); |
|||
_serverSequence = sequence; |
|||
|
|||
final payloadSize = ByteData.view(message.buffer, 8, 4).getInt32(0, Endian.big); |
|||
if (message.length < 12 + payloadSize) { |
|||
debugPrint('⚠️ ASR: Incomplete message received'); |
|||
return; |
|||
} |
|||
|
|||
if (messageType == 0x0F) { // INIT_RESPONSE |
|||
if (_waitingForInitResponse) { |
|||
debugPrint('ℹ️ ASR: Received init response (type 15)'); |
|||
if (payloadSize > 0) { |
|||
final payload = message.sublist(12, 12 + payloadSize); |
|||
final jsonStr = utf8.decode(payload); |
|||
debugPrint('ℹ️ ASR: Init response payload: $jsonStr'); |
|||
try { |
|||
final response = json.decode(jsonStr); |
|||
if (response is Map) { |
|||
if (response['error'] != null) { |
|||
final error = response['error']; |
|||
debugPrint('🔴 ASR: Init response error: $error'); |
|||
_waitingForInitResponse = false; |
|||
_initCompleter?.completeError(error); |
|||
_initCompleter = null; |
|||
return; |
|||
} |
|||
debugPrint('ℹ️ ASR: Init response success'); |
|||
_waitingForInitResponse = false; |
|||
_currentSequence = (_serverSequence + 1); |
|||
_isInitialized = true; |
|||
_initCompleter?.complete(); |
|||
_initCompleter = null; |
|||
return; |
|||
} |
|||
} catch (e) { |
|||
debugPrint('⚠️ ASR: Failed to parse init response: $e'); |
|||
} |
|||
} else { |
|||
debugPrint('ℹ️ ASR: Empty init response success'); |
|||
_waitingForInitResponse = false; |
|||
_currentSequence = (_serverSequence + 1); |
|||
_isInitialized = true; |
|||
_initCompleter?.complete(); |
|||
_initCompleter = null; |
|||
return; |
|||
} |
|||
} |
|||
} else if (messageType == 0x09) { // FULL_SERVER_RESPONSE |
|||
final payload = message.sublist(12, 12 + payloadSize); |
|||
final jsonStr = utf8.decode(payload); |
|||
debugPrint('ℹ️ ASR: Received binary payload: $jsonStr'); |
|||
|
|||
try { |
|||
final response = json.decode(jsonStr); |
|||
if (response is Map && response['result'] != null) { |
|||
final result = response['result']; |
|||
if (result['text'] != null && result['text'].isNotEmpty) { |
|||
debugPrint('ℹ️ ASR: Result: ${result['text']}'); |
|||
onResult?.call(result['text']); |
|||
} |
|||
} |
|||
} catch (e) { |
|||
debugPrint('⚠️ ASR: Failed to parse binary payload: $e'); |
|||
} |
|||
} |
|||
} |
|||
|
|||
Future<void> _startRecording() async { |
|||
_audioRecorder ??= AudioRecorder(); |
|||
|
|||
try { |
|||
if (!await _audioRecorder!.hasPermission()) { |
|||
throw Exception('Microphone permission denied'); |
|||
} |
|||
|
|||
const config = RecordConfig( |
|||
encoder: AudioEncoder.pcm16bits, |
|||
bitRate: 256000, |
|||
sampleRate: 16000, |
|||
numChannels: 1, |
|||
); |
|||
|
|||
// 启动录音流 |
|||
final stream = await _audioRecorder!.startStream(config); |
|||
_isRecording = true; |
|||
|
|||
// 监听音频流 |
|||
_audioStreamSubscription = stream.listen( |
|||
(data) { |
|||
if (_isRecording && _isConnected) { |
|||
_sendAudioData(data, false); |
|||
} |
|||
}, |
|||
onError: (error) { |
|||
debugPrint('🔴 ASR: Stream error: $error'); |
|||
_stopRecording(); |
|||
}, |
|||
); |
|||
|
|||
} catch (e) { |
|||
debugPrint('🔴 ASR: Recording error: $e'); |
|||
await _stopRecording(); |
|||
rethrow; |
|||
} |
|||
} |
|||
|
|||
void _sendAudioData(List<int> audioData, bool isLast) { |
|||
if (!_isConnected || _webSocket == null || !_isInitialized) { |
|||
debugPrint('⚠️ ASR: Cannot send audio data: not ready'); |
|||
return; |
|||
} |
|||
|
|||
final header = Uint8List(4) |
|||
..[0] = 0x11 // version=1, header_size=1 |
|||
..[1] = isLast ? 0x23 : 0x21 // message_type=2(AUDIO_ONLY), flags=3/1 |
|||
..[2] = 0x00 // no serialization, no compression |
|||
..[3] = 0x00; // reserved |
|||
|
|||
final seqBytes = ByteData(4)..setInt32(0, _currentSequence, Endian.big); |
|||
final sizeBytes = ByteData(4)..setInt32(0, audioData.length, Endian.big); |
|||
|
|||
final packet = Uint8List(12 + audioData.length) |
|||
..setAll(0, header) |
|||
..setAll(4, seqBytes.buffer.asUint8List()) |
|||
..setAll(8, sizeBytes.buffer.asUint8List()) |
|||
..setAll(12, audioData); |
|||
|
|||
_webSocket?.add(packet); |
|||
debugPrint('🔍 ASR: Sent audio data with sequence: $_currentSequence'); |
|||
_currentSequence++; // 递增序列号 |
|||
} |
|||
|
|||
void _handleDisconnect() { |
|||
_webSocket = null; |
|||
_isConnected = false; |
|||
_isInitialized = false; // 重置初始化状态 |
|||
_currentSequence = 1; // 重置序列号 |
|||
onConnectionStatusChanged?.call('disconnected'); |
|||
_stopRecording(); |
|||
} |
|||
|
|||
Future<void> _stopRecording() async { |
|||
_isRecording = false; |
|||
|
|||
try { |
|||
await _audioStreamSubscription?.cancel(); |
|||
_audioStreamSubscription = null; |
|||
|
|||
await _audioRecorder?.stop(); |
|||
_audioRecorder = null; |
|||
} catch (e) { |
|||
debugPrint('⚠️ ASR: Error stopping recording: $e'); |
|||
} |
|||
} |
|||
|
|||
Future<void> startRecognition() async { |
|||
await _connectWebSocket(); |
|||
await _startRecording(); |
|||
} |
|||
|
|||
Future<void> stopRecognition() async { |
|||
await _stopRecording(); |
|||
_webSocket?.close(); |
|||
_handleDisconnect(); |
|||
} |
|||
|
|||
@override |
|||
void onClose() { |
|||
_audioStreamSubscription?.cancel(); |
|||
stopRecognition(); |
|||
super.onClose(); |
|||
} |
|||
} |
|||
Loading…
Reference in new issue