18 changed files with 1081 additions and 3249 deletions
@ -1,11 +1,11 @@ |
|||
import 'package:get/get.dart'; |
|||
import '../controllers/translation_history_controller.dart'; |
|||
import '../controllers/translation_audiohistory_controller.dart'; |
|||
|
|||
class TranslationHistoryBinding extends Bindings { |
|||
@override |
|||
void dependencies() { |
|||
Get.lazyPut<TranslationHistoryController>( |
|||
() => TranslationHistoryController(), |
|||
Get.lazyPut<TranslationAudioHistoryController>( |
|||
() => TranslationAudioHistoryController(), |
|||
); |
|||
} |
|||
} |
|||
} |
|||
|
|||
@ -0,0 +1,93 @@ |
|||
import 'dart:async'; |
|||
import 'package:get/get.dart'; |
|||
import '../../../core/utils/logger.dart'; |
|||
|
|||
/// 录音计时器管理类 |
|||
/// 负责管理录音过程中的计时功能 |
|||
class RecordTimerController extends GetxController { |
|||
static const String TAG = 'RecordTimerController'; |
|||
|
|||
// 计时器相关状态 |
|||
final RxString recordDurationText = '00:00'.obs; |
|||
Timer? _recordTimer; |
|||
int _seconds = 0; |
|||
bool _isTimerPaused = false; |
|||
|
|||
/// 开始计时器 |
|||
/// 从00:00开始计时,每秒更新一次显示 |
|||
void startTimer() { |
|||
_seconds = 0; |
|||
recordDurationText.value = '00:00'; |
|||
_recordTimer = Timer.periodic(Duration(seconds: 1), (_) { |
|||
_seconds++; |
|||
final minutes = (_seconds ~/ 60).toString().padLeft(2, '0'); |
|||
final seconds = (_seconds % 60).toString().padLeft(2, '0'); |
|||
recordDurationText.value = '$minutes:$seconds'; |
|||
}); |
|||
Logger.info('录音计时器已开始'); |
|||
} |
|||
|
|||
/// 暂停计时器 |
|||
/// 保持当前时间显示,停止计时 |
|||
void pauseTimer() { |
|||
if (_recordTimer != null && !_isTimerPaused) { |
|||
_recordTimer?.cancel(); |
|||
_recordTimer = null; |
|||
_isTimerPaused = true; |
|||
Logger.info('录音计时器已暂停'); |
|||
} |
|||
} |
|||
|
|||
/// 恢复计时器 |
|||
/// 从暂停的时间点继续计时 |
|||
void resumeTimer() { |
|||
if (_isTimerPaused) { |
|||
_recordTimer = Timer.periodic(Duration(seconds: 1), (_) { |
|||
_seconds++; |
|||
final minutes = (_seconds ~/ 60).toString().padLeft(2, '0'); |
|||
final seconds = (_seconds % 60).toString().padLeft(2, '0'); |
|||
recordDurationText.value = '$minutes:$seconds'; |
|||
}); |
|||
_isTimerPaused = false; |
|||
Logger.info('录音计时器已恢复'); |
|||
} |
|||
} |
|||
|
|||
/// 重置计时器 |
|||
/// 停止计时并重置为00:00 |
|||
void resetTimer() { |
|||
_recordTimer?.cancel(); |
|||
_recordTimer = null; |
|||
_seconds = 0; |
|||
_isTimerPaused = false; |
|||
recordDurationText.value = '00:00'; |
|||
Logger.info('录音计时器已重置'); |
|||
} |
|||
|
|||
/// 停止计时器 |
|||
/// 完全停止计时器并重置状态 |
|||
void stopTimer() { |
|||
_recordTimer?.cancel(); |
|||
_recordTimer = null; |
|||
_isTimerPaused = false; |
|||
recordDurationText.value = '00:00'; |
|||
_seconds = 0; |
|||
Logger.info('录音计时器已停止'); |
|||
} |
|||
|
|||
/// 获取当前计时秒数 |
|||
int get currentSeconds => _seconds; |
|||
|
|||
/// 获取当前是否暂停状态 |
|||
bool get isPaused => _isTimerPaused; |
|||
|
|||
/// 获取当前是否正在计时 |
|||
bool get isRunning => _recordTimer != null && !_isTimerPaused; |
|||
|
|||
/// 释放资源 |
|||
void dispose() { |
|||
_recordTimer?.cancel(); |
|||
_recordTimer = null; |
|||
Logger.info('录音计时器管理器已释放'); |
|||
} |
|||
} |
|||
@ -0,0 +1,179 @@ |
|||
import 'package:flutter/material.dart'; |
|||
import 'package:get/get.dart'; |
|||
import 'package:intl/intl.dart'; |
|||
import '../controllers/translation_controller.dart'; |
|||
import 'dart:async'; |
|||
|
|||
import '../models/translation_models.dart'; // 添加异步支持 |
|||
|
|||
class TranslationAudioHistoryController extends GetxController { |
|||
// 引用主翻译控制器,以访问历史记录数据 |
|||
final TranslationController translationController = |
|||
Get.find<TranslationController>(); |
|||
|
|||
// 按日期分组的历史记录 |
|||
final Rx<Map<String, List<TranslationItem>>> groupedHistory = |
|||
Rx<Map<String, List<TranslationItem>>>({}); |
|||
|
|||
// 排序后的日期列表 |
|||
final RxList<String> sortedDates = <String>[].obs; |
|||
|
|||
// 会话映射表,用于存储每个会话的所有项目 |
|||
final Rx<Map<String, List<TranslationItem>>> sessionMap = |
|||
Rx<Map<String, List<TranslationItem>>>({}); |
|||
|
|||
// 加载状态 |
|||
final isLoading = true.obs; |
|||
|
|||
@override |
|||
void onInit() { |
|||
super.onInit(); |
|||
// 确保所有翻译操作都已停止 |
|||
translationController.stopAll(); |
|||
// 异步加载数据,避免阻塞主线程 |
|||
loadHistoryData(); |
|||
} |
|||
|
|||
// 异步加载历史数据 |
|||
Future<void> loadHistoryData() async { |
|||
isLoading.value = true; |
|||
|
|||
// 使用 compute 或 Future.microtask 在后台处理数据 |
|||
await Future.microtask(() { |
|||
_groupHistoryByDate(); |
|||
_groupHistoryBySession(); |
|||
}); |
|||
|
|||
isLoading.value = false; |
|||
} |
|||
|
|||
// 按日期对历史记录进行分组 |
|||
void _groupHistoryByDate() { |
|||
final Map<String, List<TranslationItem>> newGroupedHistory = {}; |
|||
|
|||
// 获取完整的历史记录,而不仅仅是当前显示的记录 |
|||
final List<TranslationItem> allHistory = |
|||
translationController.getAllHistory(); |
|||
|
|||
// 筛选出每个会话的第一条记录 |
|||
final List<TranslationItem> firstItemsOfSessions = |
|||
allHistory.where((item) => item.isFirstInSession).toList(); |
|||
|
|||
for (var item in firstItemsOfSessions) { |
|||
final DateTime date = item.timestamp; |
|||
String key; |
|||
|
|||
final now = DateTime.now(); |
|||
final today = DateTime(now.year, now.month, now.day); |
|||
final itemDate = DateTime(date.year, date.month, date.day); |
|||
|
|||
if (itemDate == today) { |
|||
key = 'TODAY'; |
|||
} else { |
|||
key = DateFormat('yyyy-MM-dd').format(date); |
|||
} |
|||
|
|||
if (!newGroupedHistory.containsKey(key)) { |
|||
newGroupedHistory[key] = []; |
|||
} |
|||
|
|||
newGroupedHistory[key]!.add(item); |
|||
} |
|||
|
|||
// 对每个日期组内的项目按时间倒序排序 |
|||
newGroupedHistory.forEach((key, items) { |
|||
items.sort((a, b) => b.timestamp.compareTo(a.timestamp)); |
|||
}); |
|||
|
|||
// 更新分组后的历史记录 |
|||
groupedHistory.value = newGroupedHistory; |
|||
|
|||
// 按日期排序 |
|||
final List<String> dates = newGroupedHistory.keys.toList() |
|||
..sort((a, b) { |
|||
if (a == 'TODAY') return -1; |
|||
if (b == 'TODAY') return 1; |
|||
if (a == '1 DAY AGO') return -1; |
|||
if (b == '1 DAY AGO') return 1; |
|||
if (a == '2 DAYS AGO') return -1; |
|||
if (b == '2 DAYS AGO') return 1; |
|||
return b.compareTo(a); // 降序排列其他日期 |
|||
}); |
|||
|
|||
sortedDates.value = dates; |
|||
} |
|||
|
|||
// 按会话ID对历史记录进行分组 |
|||
void _groupHistoryBySession() { |
|||
final Map<String, List<TranslationItem>> newSessionMap = {}; |
|||
|
|||
// 获取完整的历史记录进行会话分组 |
|||
final List<TranslationItem> allHistory = |
|||
translationController.getAllHistory(); |
|||
|
|||
for (var item in allHistory) { |
|||
if (!newSessionMap.containsKey(item.sessionId)) { |
|||
newSessionMap[item.sessionId] = []; |
|||
} |
|||
|
|||
newSessionMap[item.sessionId]!.add(item); |
|||
} |
|||
|
|||
// 对每个会话内的项目按时间升序排序 |
|||
newSessionMap.forEach((key, items) { |
|||
items.sort((a, b) => a.timestamp.compareTo(b.timestamp)); |
|||
}); |
|||
|
|||
// 更新会话映射表 |
|||
sessionMap.value = newSessionMap; |
|||
} |
|||
|
|||
// 获取指定会话的所有翻译项 |
|||
List<TranslationItem> getSessionItems(String sessionId) { |
|||
return sessionMap.value[sessionId] ?? []; |
|||
} |
|||
|
|||
// 播放翻译项 |
|||
void playTranslationItem(TranslationItem item) { |
|||
translationController.playTranslationItem(item); |
|||
} |
|||
|
|||
// 播放指定文本 |
|||
void playText(String text) { |
|||
translationController.playTranslatedText(text); |
|||
} |
|||
|
|||
// 检查是否有历史记录 |
|||
bool get hasHistory => translationController.translationHistory.isNotEmpty; |
|||
|
|||
// 删除指定会话的历史记录 |
|||
Future<void> deleteSessionHistory(String sessionId) async { |
|||
try { |
|||
// 调用主控制器的删除方法 |
|||
translationController.deleteSessionHistory(sessionId); |
|||
|
|||
// 重新加载和分组历史数据 |
|||
await loadHistoryData(); |
|||
|
|||
// 显示删除成功提示 |
|||
Get.snackbar( |
|||
'删除成功', |
|||
'已删除该会话的所有翻译记录', |
|||
snackPosition: SnackPosition.BOTTOM, |
|||
backgroundColor: Get.isDarkMode ? Colors.grey[800] : Colors.grey[200], |
|||
colorText: Get.isDarkMode ? Colors.white : Colors.black, |
|||
duration: const Duration(seconds: 2), |
|||
); |
|||
} catch (e) { |
|||
// 显示删除失败提示 |
|||
Get.snackbar( |
|||
'删除失败', |
|||
'删除会话记录时发生错误: ${e.toString()}', |
|||
snackPosition: SnackPosition.BOTTOM, |
|||
backgroundColor: Colors.red[400], |
|||
colorText: Colors.white, |
|||
duration: const Duration(seconds: 3), |
|||
); |
|||
} |
|||
} |
|||
} |
|||
File diff suppressed because it is too large
@ -1,174 +1,371 @@ |
|||
import 'dart:convert'; |
|||
import 'package:flutter/material.dart'; |
|||
import 'package:get/get.dart'; |
|||
import 'package:intl/intl.dart'; |
|||
import '../controllers/translation_controller.dart'; |
|||
import 'dart:async'; // 添加异步支持 |
|||
|
|||
class TranslationHistoryController extends GetxController { |
|||
// 引用主翻译控制器,以访问历史记录数据 |
|||
final TranslationController translationController = Get.find<TranslationController>(); |
|||
|
|||
// 按日期分组的历史记录 |
|||
final Rx<Map<String, List<TranslationItem>>> groupedHistory = Rx<Map<String, List<TranslationItem>>>({}); |
|||
|
|||
// 排序后的日期列表 |
|||
final RxList<String> sortedDates = <String>[].obs; |
|||
|
|||
// 会话映射表,用于存储每个会话的所有项目 |
|||
final Rx<Map<String, List<TranslationItem>>> sessionMap = Rx<Map<String, List<TranslationItem>>>({}); |
|||
|
|||
// 加载状态 |
|||
final isLoading = true.obs; |
|||
|
|||
@override |
|||
void onInit() { |
|||
super.onInit(); |
|||
// 确保所有翻译操作都已停止 |
|||
translationController.stopAll(); |
|||
// 异步加载数据,避免阻塞主线程 |
|||
loadHistoryData(); |
|||
import 'package:get_storage/get_storage.dart'; |
|||
import '../../../core/utils/logger.dart'; |
|||
import '../models/translation_models.dart'; |
|||
|
|||
/// 翻译历史记录管理类 |
|||
/// 负责管理翻译历史的存储、加载、分页等功能 |
|||
class TranslationHistoryManager extends GetxController { |
|||
static const String TAG = 'TranslationHistoryManager'; |
|||
|
|||
// 存储相关配置 |
|||
static const String _historyKey = 'translation_history'; |
|||
static const int _maxStorageItems = 1000; // 存储限制 |
|||
static const int _maxDisplayItems = 20; // 显示限制 |
|||
|
|||
final GetStorage _storage = GetStorage(); |
|||
|
|||
// 翻译历史数据 |
|||
final translationHistory = <TranslationItem>[].obs; |
|||
|
|||
// 分页加载相关状态 |
|||
final isLoadingMore = false.obs; |
|||
final hasMoreHistory = true.obs; |
|||
final currentPage = 1.obs; |
|||
final pageSize = 20; |
|||
final isLoadingMoreError = false.obs; |
|||
bool _scrollListenerSetup = false; |
|||
|
|||
// 滚动控制器 |
|||
final ScrollController scrollController = ScrollController(); |
|||
|
|||
/// 初始化历史管理器 |
|||
void initialize() { |
|||
loadTranslationHistory(); |
|||
_setupScrollListener(); |
|||
} |
|||
|
|||
// 异步加载历史数据 |
|||
Future<void> loadHistoryData() async { |
|||
isLoading.value = true; |
|||
|
|||
// 使用 compute 或 Future.microtask 在后台处理数据 |
|||
await Future.microtask(() { |
|||
_groupHistoryByDate(); |
|||
_groupHistoryBySession(); |
|||
}); |
|||
|
|||
isLoading.value = false; |
|||
|
|||
/// 加载翻译历史记录 |
|||
/// 初始化时只加载最近的显示条数,提高性能 |
|||
void loadTranslationHistory() { |
|||
final historyJson = _storage.read(_historyKey); |
|||
if (historyJson != null) { |
|||
try { |
|||
final List<dynamic> historyList = jsonDecode(historyJson); |
|||
final List<TranslationItem> allHistory = |
|||
historyList.map((item) => TranslationItem.fromJson(item)).toList(); |
|||
|
|||
// 初始化分页状态 |
|||
currentPage.value = 1; |
|||
isLoadingMore.value = false; |
|||
isLoadingMoreError.value = false; |
|||
|
|||
// 主页面只显示最近的记录,保证界面性能 |
|||
if (allHistory.length > _maxDisplayItems) { |
|||
translationHistory.value = |
|||
allHistory.sublist(allHistory.length - _maxDisplayItems); |
|||
hasMoreHistory.value = true; |
|||
} else { |
|||
translationHistory.value = allHistory; |
|||
hasMoreHistory.value = false; |
|||
} |
|||
|
|||
Logger.info('翻译历史加载成功: ${translationHistory.length}条记录'); |
|||
} catch (e) { |
|||
_storage.remove(_historyKey); |
|||
Logger.error('加载翻译历史失败: ${e.toString()}'); |
|||
} |
|||
} else { |
|||
// 没有历史数据时初始化分页状态 |
|||
hasMoreHistory.value = false; |
|||
currentPage.value = 1; |
|||
isLoadingMore.value = false; |
|||
isLoadingMoreError.value = false; |
|||
} |
|||
} |
|||
|
|||
// 按日期对历史记录进行分组 |
|||
void _groupHistoryByDate() { |
|||
final Map<String, List<TranslationItem>> newGroupedHistory = {}; |
|||
|
|||
// 获取完整的历史记录,而不仅仅是当前显示的记录 |
|||
final List<TranslationItem> allHistory = translationController.getAllHistory(); |
|||
|
|||
// 筛选出每个会话的第一条记录 |
|||
final List<TranslationItem> firstItemsOfSessions = allHistory |
|||
.where((item) => item.isFirstInSession) |
|||
.toList(); |
|||
|
|||
for (var item in firstItemsOfSessions) { |
|||
final DateTime date = item.timestamp; |
|||
String key; |
|||
|
|||
final now = DateTime.now(); |
|||
final today = DateTime(now.year, now.month, now.day); |
|||
final itemDate = DateTime(date.year, date.month, date.day); |
|||
|
|||
if (itemDate == today) { |
|||
key = 'TODAY'; |
|||
} |
|||
else { |
|||
key = DateFormat('yyyy-MM-dd').format(date); |
|||
|
|||
/// 保存翻译历史记录 |
|||
/// 只保存完成态记录,过滤中间态记录 |
|||
void saveTranslationHistory() { |
|||
try { |
|||
// 只保存完成态记录 |
|||
final List<TranslationItem> finalRecords = |
|||
translationHistory.where((item) => !item.isIntermediate).toList(); |
|||
|
|||
// 应用存储限制 |
|||
List<TranslationItem> historyToSave = finalRecords; |
|||
if (finalRecords.length > _maxStorageItems) { |
|||
historyToSave = |
|||
finalRecords.sublist(finalRecords.length - _maxStorageItems); |
|||
} |
|||
|
|||
if (!newGroupedHistory.containsKey(key)) { |
|||
newGroupedHistory[key] = []; |
|||
|
|||
// 保存到本地存储 |
|||
final List<Map<String, dynamic>> historyJson = |
|||
historyToSave.map((item) => item.toJson()).toList(); |
|||
_storage.write(_historyKey, jsonEncode(historyJson)); |
|||
|
|||
Logger.info('翻译历史保存成功: 存储${historyToSave.length}条记录'); |
|||
} catch (e) { |
|||
Logger.error('保存翻译历史失败: ${e.toString()}'); |
|||
} |
|||
} |
|||
|
|||
/// 添加新的翻译记录 |
|||
/// [item] 要添加的翻译记录 |
|||
/// [scrollToBottom] 是否滚动到底部 |
|||
void addTranslationItem(TranslationItem item, {bool scrollToBottom = true}) { |
|||
translationHistory.add(item); |
|||
if (scrollToBottom) { |
|||
_scrollToBottom(); |
|||
} |
|||
} |
|||
|
|||
/// 更新翻译记录 |
|||
/// [timestamp] 记录的时间戳,用于定位记录 |
|||
/// [updater] 更新函数 |
|||
bool updateTranslationItem( |
|||
DateTime timestamp, Function(TranslationItem) updater) { |
|||
final index = translationHistory |
|||
.indexWhere((item) => item.timestamp.isAtSameMomentAs(timestamp)); |
|||
if (index != -1) { |
|||
updater(translationHistory[index]); |
|||
translationHistory.refresh(); |
|||
return true; |
|||
} |
|||
return false; |
|||
} |
|||
|
|||
/// 加载更多历史记录(分页加载) |
|||
Future<void> loadMoreHistory() async { |
|||
if (isLoadingMore.value || !hasMoreHistory.value) { |
|||
return; |
|||
} |
|||
|
|||
try { |
|||
isLoadingMore.value = true; |
|||
isLoadingMoreError.value = false; |
|||
|
|||
final nextPage = currentPage.value + 1; |
|||
Logger.i(TAG, '正在加载第 $nextPage 页翻译历史...'); |
|||
|
|||
// 从存储中获取所有历史记录 |
|||
final historyJson = _storage.read(_historyKey); |
|||
if (historyJson != null) { |
|||
final List<dynamic> historyList = jsonDecode(historyJson); |
|||
final List<TranslationItem> allHistory = |
|||
historyList.map((item) => TranslationItem.fromJson(item)).toList(); |
|||
|
|||
// 计算分页参数 |
|||
final totalItems = allHistory.length; |
|||
final endIndex = totalItems - ((nextPage - 1) * pageSize); |
|||
final startIndex = totalItems - (nextPage * pageSize); |
|||
|
|||
if (endIndex > 0) { |
|||
final actualStartIndex = startIndex < 0 ? 0 : startIndex; |
|||
final actualEndIndex = endIndex > totalItems ? totalItems : endIndex; |
|||
|
|||
final pageItems = |
|||
allHistory.sublist(actualStartIndex, actualEndIndex); |
|||
|
|||
if (pageItems.isNotEmpty) { |
|||
await Future.delayed(const Duration(milliseconds: 300)); |
|||
|
|||
// reverse模式下:将新记录插入到现有记录列表的开头 |
|||
final updatedHistory = [...pageItems, ...translationHistory]; |
|||
translationHistory.assignAll(updatedHistory); |
|||
|
|||
// 更新分页状态 |
|||
currentPage.value = nextPage; |
|||
hasMoreHistory.value = startIndex > 0; |
|||
|
|||
Logger.i(TAG, '成功加载 ${pageItems.length} 条历史记录'); |
|||
} else { |
|||
hasMoreHistory.value = false; |
|||
} |
|||
} else { |
|||
hasMoreHistory.value = false; |
|||
} |
|||
} else { |
|||
hasMoreHistory.value = false; |
|||
} |
|||
|
|||
newGroupedHistory[key]!.add(item); |
|||
} catch (e) { |
|||
Logger.e(TAG, '加载更多历史记录失败: $e'); |
|||
isLoadingMoreError.value = true; |
|||
} finally { |
|||
isLoadingMore.value = false; |
|||
} |
|||
|
|||
// 对每个日期组内的项目按时间倒序排序 |
|||
newGroupedHistory.forEach((key, items) { |
|||
items.sort((a, b) => b.timestamp.compareTo(a.timestamp)); |
|||
}); |
|||
|
|||
// 更新分组后的历史记录 |
|||
groupedHistory.value = newGroupedHistory; |
|||
|
|||
// 按日期排序 |
|||
final List<String> dates = newGroupedHistory.keys.toList() |
|||
..sort((a, b) { |
|||
if (a == 'TODAY') return -1; |
|||
if (b == 'TODAY') return 1; |
|||
if (a == '1 DAY AGO') return -1; |
|||
if (b == '1 DAY AGO') return 1; |
|||
if (a == '2 DAYS AGO') return -1; |
|||
if (b == '2 DAYS AGO') return 1; |
|||
return b.compareTo(a); // 降序排列其他日期 |
|||
}); |
|||
|
|||
sortedDates.value = dates; |
|||
} |
|||
|
|||
// 按会话ID对历史记录进行分组 |
|||
void _groupHistoryBySession() { |
|||
final Map<String, List<TranslationItem>> newSessionMap = {}; |
|||
|
|||
// 获取完整的历史记录进行会话分组 |
|||
final List<TranslationItem> allHistory = translationController.getAllHistory(); |
|||
/// 获取所有完成态历史记录 |
|||
List<TranslationItem> getAllHistory() { |
|||
try { |
|||
final List<TranslationItem> result = <TranslationItem>[]; |
|||
final Set<String> existingIds = <String>{}; |
|||
|
|||
// 添加存储的历史记录 |
|||
final historyJson = _storage.read(_historyKey); |
|||
if (historyJson != null) { |
|||
final List<dynamic> historyList = jsonDecode(historyJson); |
|||
final List<TranslationItem> storedHistory = |
|||
historyList.map((item) => TranslationItem.fromJson(item)).toList(); |
|||
|
|||
for (var item in allHistory) { |
|||
if (!newSessionMap.containsKey(item.sessionId)) { |
|||
newSessionMap[item.sessionId] = []; |
|||
for (final item in storedHistory) { |
|||
final id = |
|||
'${item.sessionId}_${item.timestamp.millisecondsSinceEpoch}'; |
|||
if (!existingIds.contains(id)) { |
|||
existingIds.add(id); |
|||
result.add(item); |
|||
} |
|||
} |
|||
} |
|||
|
|||
newSessionMap[item.sessionId]!.add(item); |
|||
// 添加当前内存中的完成态记录 |
|||
for (final item in translationHistory) { |
|||
if (!item.isIntermediate) { |
|||
final id = |
|||
'${item.sessionId}_${item.timestamp.millisecondsSinceEpoch}'; |
|||
if (!existingIds.contains(id)) { |
|||
existingIds.add(id); |
|||
result.add(item); |
|||
} |
|||
} |
|||
} |
|||
|
|||
// 按时间排序 |
|||
result.sort((a, b) => a.timestamp.compareTo(b.timestamp)); |
|||
return result; |
|||
} catch (e) { |
|||
Logger.error('获取完整历史记录失败: ${e.toString()}'); |
|||
return translationHistory.where((item) => !item.isIntermediate).toList(); |
|||
} |
|||
|
|||
// 对每个会话内的项目按时间升序排序 |
|||
newSessionMap.forEach((key, items) { |
|||
items.sort((a, b) => a.timestamp.compareTo(b.timestamp)); |
|||
}); |
|||
|
|||
// 更新会话映射表 |
|||
sessionMap.value = newSessionMap; |
|||
} |
|||
|
|||
// 获取指定会话的所有翻译项 |
|||
List<TranslationItem> getSessionItems(String sessionId) { |
|||
return sessionMap.value[sessionId] ?? []; |
|||
} |
|||
|
|||
// 播放翻译项 |
|||
void playTranslationItem(TranslationItem item) { |
|||
translationController.playTranslationItem(item); |
|||
|
|||
/// 清除所有翻译历史 |
|||
void clearTranslationHistory() { |
|||
translationHistory.clear(); |
|||
_storage.remove(_historyKey); |
|||
Logger.info('翻译历史已清除'); |
|||
} |
|||
|
|||
// 播放指定文本 |
|||
void playText(String text) { |
|||
translationController.playTranslatedText(text); |
|||
|
|||
/// 删除指定会话的所有翻译记录 |
|||
void deleteSessionHistory(String sessionId) { |
|||
try { |
|||
// 从内存中删除 |
|||
final originalCount = translationHistory.length; |
|||
translationHistory.removeWhere((item) => item.sessionId == sessionId); |
|||
final removedFromMemory = originalCount - translationHistory.length; |
|||
|
|||
// 从存储中删除 |
|||
final historyJson = _storage.read(_historyKey); |
|||
int removedFromStorage = 0; |
|||
|
|||
if (historyJson != null) { |
|||
final List<dynamic> historyList = jsonDecode(historyJson); |
|||
final List<TranslationItem> storedHistory = |
|||
historyList.map((item) => TranslationItem.fromJson(item)).toList(); |
|||
|
|||
final originalStoredCount = storedHistory.length; |
|||
storedHistory.removeWhere((item) => item.sessionId == sessionId); |
|||
removedFromStorage = originalStoredCount - storedHistory.length; |
|||
|
|||
// 重新保存 |
|||
final List<Map<String, dynamic>> updatedHistoryJson = |
|||
storedHistory.map((item) => item.toJson()).toList(); |
|||
_storage.write(_historyKey, jsonEncode(updatedHistoryJson)); |
|||
} |
|||
|
|||
translationHistory.refresh(); |
|||
|
|||
Logger.info( |
|||
'删除会话历史成功: sessionId=$sessionId, 内存删除${removedFromMemory}条, 存储删除${removedFromStorage}条'); |
|||
} catch (e) { |
|||
Logger.error('删除会话历史失败: ${e.toString()}'); |
|||
rethrow; |
|||
} |
|||
} |
|||
|
|||
// 检查是否有历史记录 |
|||
bool get hasHistory => translationController.translationHistory.isNotEmpty; |
|||
|
|||
// 删除指定会话的历史记录 |
|||
Future<void> deleteSessionHistory(String sessionId) async { |
|||
/// 删除指定的单条翻译记录 |
|||
void deleteTranslationItem(String sessionId, DateTime timestamp) { |
|||
try { |
|||
// 调用主控制器的删除方法 |
|||
translationController.deleteSessionHistory(sessionId); |
|||
|
|||
// 重新加载和分组历史数据 |
|||
await loadHistoryData(); |
|||
|
|||
// 显示删除成功提示 |
|||
Get.snackbar( |
|||
'删除成功', |
|||
'已删除该会话的所有翻译记录', |
|||
snackPosition: SnackPosition.BOTTOM, |
|||
backgroundColor: Get.isDarkMode ? Colors.grey[800] : Colors.grey[200], |
|||
colorText: Get.isDarkMode ? Colors.white : Colors.black, |
|||
duration: const Duration(seconds: 2), |
|||
); |
|||
// 从内存中删除 |
|||
final originalCount = translationHistory.length; |
|||
translationHistory.removeWhere((item) => |
|||
item.sessionId == sessionId && |
|||
item.timestamp.millisecondsSinceEpoch == |
|||
timestamp.millisecondsSinceEpoch); |
|||
final removedFromMemory = originalCount - translationHistory.length; |
|||
|
|||
// 从存储中删除 |
|||
final historyJson = _storage.read(_historyKey); |
|||
int removedFromStorage = 0; |
|||
|
|||
if (historyJson != null) { |
|||
final List<dynamic> historyList = jsonDecode(historyJson); |
|||
final List<TranslationItem> storedHistory = |
|||
historyList.map((item) => TranslationItem.fromJson(item)).toList(); |
|||
|
|||
final originalStoredCount = storedHistory.length; |
|||
storedHistory.removeWhere((item) => |
|||
item.sessionId == sessionId && |
|||
item.timestamp.millisecondsSinceEpoch == |
|||
timestamp.millisecondsSinceEpoch); |
|||
removedFromStorage = originalStoredCount - storedHistory.length; |
|||
|
|||
// 重新保存 |
|||
final List<Map<String, dynamic>> updatedHistoryJson = |
|||
storedHistory.map((item) => item.toJson()).toList(); |
|||
_storage.write(_historyKey, jsonEncode(updatedHistoryJson)); |
|||
} |
|||
|
|||
translationHistory.refresh(); |
|||
|
|||
Logger.info( |
|||
'删除单条翻译记录成功: sessionId=$sessionId, 内存删除${removedFromMemory}条, 存储删除${removedFromStorage}条'); |
|||
} catch (e) { |
|||
// 显示删除失败提示 |
|||
Get.snackbar( |
|||
'删除失败', |
|||
'删除会话记录时发生错误: ${e.toString()}', |
|||
snackPosition: SnackPosition.BOTTOM, |
|||
backgroundColor: Colors.red[400], |
|||
colorText: Colors.white, |
|||
duration: const Duration(seconds: 3), |
|||
); |
|||
Logger.error('删除单条翻译记录失败: ${e.toString()}'); |
|||
rethrow; |
|||
} |
|||
} |
|||
} |
|||
|
|||
/// 滚动到底部 |
|||
void scrollToBottom({bool animate = true}) { |
|||
_scrollToBottom(animate: animate); |
|||
} |
|||
|
|||
/// 设置滚动监听器 |
|||
void _setupScrollListener() { |
|||
if (_scrollListenerSetup) return; |
|||
|
|||
scrollController.addListener(() { |
|||
// reverse模式下:检查是否滚动到底部 |
|||
if (scrollController.position.pixels >= |
|||
scrollController.position.maxScrollExtent - 100) { |
|||
if (hasMoreHistory.value && !isLoadingMore.value) { |
|||
Logger.d(TAG, '触发加载更多翻译历史记录'); |
|||
loadMoreHistory(); |
|||
} |
|||
} |
|||
}); |
|||
|
|||
_scrollListenerSetup = true; |
|||
} |
|||
|
|||
/// 内部滚动到底部方法 |
|||
void _scrollToBottom({bool animate = true}) { |
|||
if (scrollController.hasClients && translationHistory.isNotEmpty) { |
|||
try { |
|||
const targetPosition = 0.0; // reverse模式下最新消息在顶部 |
|||
|
|||
if (animate) { |
|||
scrollController.animateTo( |
|||
targetPosition, |
|||
duration: const Duration(milliseconds: 300), |
|||
curve: Curves.easeOut, |
|||
); |
|||
} else { |
|||
scrollController.jumpTo(targetPosition); |
|||
} |
|||
} catch (e) { |
|||
Logger.error('滚动到顶部失败: ${e.toString()}'); |
|||
} |
|||
} |
|||
} |
|||
|
|||
/// 释放资源 |
|||
void dispose() { |
|||
scrollController.dispose(); |
|||
Logger.info('翻译历史管理器已释放'); |
|||
} |
|||
} |
|||
|
|||
@ -0,0 +1,76 @@ |
|||
import 'package:get/get.dart'; |
|||
import '../../../data/services/language_manager.dart'; |
|||
|
|||
/// 翻译项模型 |
|||
/// 表示一条翻译记录的完整信息 |
|||
class TranslationItem { |
|||
String sourceText; |
|||
String translatedText; |
|||
String sourceLanguageCode; |
|||
String targetLanguageCode; |
|||
final DateTime timestamp; |
|||
String sessionId; |
|||
final bool isFirstInSession; |
|||
bool isIntermediate; // 是否为中间结果 |
|||
|
|||
TranslationItem({ |
|||
required this.sourceText, |
|||
required this.translatedText, |
|||
required this.sourceLanguageCode, |
|||
required this.targetLanguageCode, |
|||
required this.timestamp, |
|||
required this.sessionId, |
|||
this.isFirstInSession = false, |
|||
this.isIntermediate = false, |
|||
}); |
|||
|
|||
/// 从JSON创建TranslationItem实例 |
|||
factory TranslationItem.fromJson(Map<String, dynamic> json) { |
|||
DateTime timestamp; |
|||
try { |
|||
timestamp = DateTime.parse(json['timestamp']); |
|||
} catch (e) { |
|||
timestamp = DateTime.now(); |
|||
} |
|||
|
|||
return TranslationItem( |
|||
sourceText: json['sourceText'] ?? '', |
|||
translatedText: json['translatedText'] ?? '', |
|||
sourceLanguageCode: json['sourceLanguageCode'] ?? 'zh-CN', |
|||
targetLanguageCode: json['targetLanguageCode'] ?? 'en-US', |
|||
timestamp: timestamp, |
|||
sessionId: |
|||
json['sessionId'] ?? DateTime.now().millisecondsSinceEpoch.toString(), |
|||
isFirstInSession: json['isFirstInSession'] ?? false, |
|||
isIntermediate: false, // 从存储加载的都是最终结果 |
|||
); |
|||
} |
|||
|
|||
/// 转换为JSON格式 |
|||
Map<String, dynamic> toJson() { |
|||
return { |
|||
'sourceText': sourceText, |
|||
'translatedText': translatedText, |
|||
'sourceLanguageCode': sourceLanguageCode, |
|||
'targetLanguageCode': targetLanguageCode, |
|||
'timestamp': timestamp.toIso8601String(), |
|||
'sessionId': sessionId, |
|||
'isFirstInSession': isFirstInSession, |
|||
// 不保存isIntermediate,因为存储的都是最终结果 |
|||
}; |
|||
} |
|||
|
|||
/// 获取源语言的中文名称 |
|||
String get sourceLanguageName { |
|||
final languageManager = Get.find<LanguageManager>(); |
|||
return languageManager.getChineseNameByAsrCode(sourceLanguageCode) ?? |
|||
'未知语言'; |
|||
} |
|||
|
|||
/// 获取目标语言的中文名称 |
|||
String get targetLanguageName { |
|||
final languageManager = Get.find<LanguageManager>(); |
|||
return languageManager.getChineseNameByAsrCode(targetLanguageCode) ?? |
|||
'未知语言'; |
|||
} |
|||
} |
|||
@ -1,305 +0,0 @@ |
|||
import Foundation |
|||
import CoreBluetooth |
|||
import os.log |
|||
import AVFoundation |
|||
import AccessorySetupKit |
|||
import UIKit |
|||
|
|||
class BleAccessorySetup: NSObject { |
|||
// MARK: - 属性 |
|||
static let shared = BleAccessorySetup() |
|||
|
|||
private var bleService: BleService? |
|||
private var session: ASAccessorySession? |
|||
private var isSessionActivated = false |
|||
private var isInitializing = false |
|||
private let logger = OSLog(subsystem: "com.yunqiinnovation.ble_service", category: "BleAccessorySetup") |
|||
|
|||
// 杰理蓝牙设备的公司标识符 |
|||
private let deviceCompanyIdentifier: UInt16 = 0xEFAB |
|||
private let deviceDisplayName = "语音助理耳机" |
|||
|
|||
// 用于存储配对设备UUID的UserDefaults键 |
|||
private let pairedDeviceUUIDKey = "com.yunqiinnovation.ble_service.paired_device_uuid" |
|||
|
|||
typealias DeviceDiscoveredCallback = (UUID) -> Void |
|||
private var onDeviceDiscovered: DeviceDiscoveredCallback? |
|||
|
|||
// MARK: - 初始化 |
|||
private override init() { super.init() } |
|||
|
|||
// MARK: - 公共方法 |
|||
func setBleService(_ service: Any) { |
|||
self.bleService = service as? BleService |
|||
} |
|||
|
|||
func setDeviceDiscoveredCallback(_ callback: @escaping (UUID) -> Void) { |
|||
self.onDeviceDiscovered = callback |
|||
} |
|||
|
|||
// 处理发现的设备并连接 |
|||
func connectToAccessory(uuid: UUID) { |
|||
guard let service = bleService else { |
|||
return |
|||
} |
|||
|
|||
service.connectToAccessory(uuid: uuid) |
|||
} |
|||
|
|||
func addToReconnectQueue(uuid: UUID) { |
|||
guard let service = bleService else { |
|||
return |
|||
} |
|||
|
|||
service.addToReconnectQueue(uuid: uuid) |
|||
} |
|||
|
|||
// 从视图控制器显示设备选择器 - 主要入口点 |
|||
func showDevicePickerFromViewController(_ viewController: UIViewController) { |
|||
initializeSession() |
|||
} |
|||
|
|||
// 清除蓝牙状态(仅用于测试或重置,不会清除系统授权) |
|||
func resetBluetoothState() { |
|||
if let service = bleService { |
|||
service.disconnect() |
|||
} |
|||
} |
|||
|
|||
// MARK: - 会话管理 |
|||
func initializeSession() { |
|||
if isInitializing { |
|||
os_log("会话正在初始化中,跳过", log: logger, type: .info) |
|||
return |
|||
} |
|||
|
|||
// 如果会话已激活,检查是否有配对设备 |
|||
if session != nil && isSessionActivated { |
|||
os_log("会话已激活,检查配对设备", log: logger, type: .info) |
|||
// 会话已激活,检查是否有配对设备 |
|||
if let accessories = session?.accessories, !accessories.isEmpty { |
|||
os_log("发现 %d 个已配对设备,尝试连接", log: logger, type: .info, accessories.count) |
|||
// 有配对设备,尝试连接 |
|||
if let service = bleService { |
|||
let _ = service.getCentralManager() |
|||
|
|||
for accessory in accessories { |
|||
if let uuid = accessory.bluetoothIdentifier { |
|||
onDeviceDiscovered?(uuid) |
|||
connectToAccessory(uuid: uuid) |
|||
} |
|||
} |
|||
} |
|||
} else { |
|||
os_log("没有已配对设备,显示配对框", log: logger, type: .info) |
|||
// 没有配对设备,直接显示配对框 |
|||
showDevicePicker() |
|||
} |
|||
return |
|||
} |
|||
|
|||
os_log("开始初始化新会话", log: logger, type: .info) |
|||
isInitializing = true |
|||
|
|||
do { |
|||
session = try ASAccessorySession() |
|||
session?.activate(on: .main) { [weak self] event in |
|||
guard let self = self else { return } |
|||
self.handleSessionEvent(event) |
|||
} |
|||
} catch { |
|||
os_log("会话初始化失败: %{public}@", log: logger, type: .error, error.localizedDescription) |
|||
handleSessionInitError(error) |
|||
} |
|||
} |
|||
|
|||
func resetSession() { |
|||
session?.invalidate() |
|||
session = nil |
|||
isSessionActivated = false |
|||
isInitializing = false |
|||
} |
|||
|
|||
// MARK: - 设备选择器显示 |
|||
private func showDevicePicker() { |
|||
guard let session = session, isSessionActivated else { |
|||
os_log("无法显示配对框:会话未激活", log: logger, type: .error) |
|||
return |
|||
} |
|||
|
|||
os_log("准备显示设备配对框", log: logger, type: .info) |
|||
|
|||
let descriptor = ASDiscoveryDescriptor() |
|||
descriptor.bluetoothCompanyIdentifier = ASBluetoothCompanyIdentifier(deviceCompanyIdentifier) |
|||
|
|||
let deviceImage = UIImage(systemName: "headphones") ?? UIImage() |
|||
let displayItem = ASPickerDisplayItem( |
|||
name: deviceDisplayName, |
|||
productImage: deviceImage, |
|||
descriptor: descriptor |
|||
) |
|||
|
|||
DispatchQueue.main.async { |
|||
os_log("正在显示设备配对框", log: self.logger, type: .info) |
|||
session.showPicker(for: [displayItem]) { [weak self] error in |
|||
guard let self = self else { return } |
|||
|
|||
if let error = error { |
|||
let nsError = error as NSError |
|||
os_log("设备选择器错误: %{public}@", log: self.logger, type: .error, error.localizedDescription) |
|||
} else { |
|||
os_log("设备选择器完成", log: self.logger, type: .info) |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
// MARK: - 私有辅助方法 |
|||
private func handleSessionEvent(_ event: ASAccessoryEvent) { |
|||
switch event.eventType { |
|||
case .activated: |
|||
os_log("会话已激活", log: logger, type: .info) |
|||
handleSessionActivated() |
|||
|
|||
case .accessoryAdded: |
|||
os_log("发现新设备: %{public}@", log: logger, type: .info, event.accessory?.bluetoothIdentifier?.uuidString ?? "Unknown") |
|||
handleAccessoryAdded(event.accessory) |
|||
|
|||
case .accessoryRemoved: |
|||
os_log("设备已移除: %{public}@", log: logger, type: .info, event.accessory?.bluetoothIdentifier?.uuidString ?? "Unknown") |
|||
handleAccessoryRemoved(event.accessory) |
|||
|
|||
case .accessoryChanged: |
|||
os_log("设备状态已改变", log: logger, type: .info) |
|||
break |
|||
|
|||
case .pickerDidPresent: |
|||
os_log("配对框已显示", log: logger, type: .info) |
|||
break |
|||
|
|||
case .pickerDidDismiss: |
|||
os_log("配对框已关闭", log: logger, type: .info) |
|||
break |
|||
|
|||
case .pickerSetupFailed: |
|||
os_log("选择器设置失败: %{public}@", log: logger, type: .error, |
|||
event.error?.localizedDescription ?? "未知错误") |
|||
|
|||
default: |
|||
os_log("未处理的会话事件: %d", log: logger, type: .info, event.eventType.rawValue) |
|||
break |
|||
} |
|||
} |
|||
|
|||
private func handleSessionActivated() { |
|||
isSessionActivated = true |
|||
isInitializing = false |
|||
|
|||
if let accessories = session?.accessories, !accessories.isEmpty { |
|||
if let service = bleService { |
|||
let _ = service.getCentralManager() |
|||
|
|||
for accessory in accessories { |
|||
if let uuid = accessory.bluetoothIdentifier { |
|||
onDeviceDiscovered?(uuid) |
|||
connectToAccessory(uuid: uuid) |
|||
} |
|||
} |
|||
} |
|||
} else { |
|||
showDevicePicker() |
|||
} |
|||
} |
|||
|
|||
private func handleAccessoryAdded(_ accessory: ASAccessory?) { |
|||
guard let accessory = accessory, let uuid = accessory.bluetoothIdentifier else { |
|||
return |
|||
} |
|||
|
|||
// 保存配对设备的UUID |
|||
savePairedMacAddress(uuid) |
|||
|
|||
onDeviceDiscovered?(uuid) |
|||
connectToAccessory(uuid: uuid) |
|||
} |
|||
|
|||
private func handleAccessoryRemoved(_ accessory: ASAccessory?) { |
|||
guard let accessory = accessory, let uuid = accessory.bluetoothIdentifier else { |
|||
return |
|||
} |
|||
|
|||
if let service = bleService { |
|||
service.disconnect() |
|||
} |
|||
} |
|||
|
|||
private func handleSessionInitError(_ error: Error) { |
|||
isInitializing = false |
|||
session = nil |
|||
} |
|||
|
|||
// MARK: - 蓝牙状态检查 |
|||
func isBluetoothAvailable() -> Bool { |
|||
do { |
|||
let audioSession = AVAudioSession.sharedInstance() |
|||
let outputs = audioSession.currentRoute.outputs |
|||
|
|||
for output in outputs { |
|||
if output.portType == .bluetoothA2DP || |
|||
output.portType == .bluetoothHFP || |
|||
output.portType == .bluetoothLE { |
|||
return true |
|||
} |
|||
} |
|||
} catch { |
|||
os_log("检查蓝牙状态出错: %{public}@", log: logger, type: .error, error.localizedDescription) |
|||
} |
|||
|
|||
return true |
|||
} |
|||
|
|||
// MARK: - 清除配对关系 |
|||
func clearAssociations() { |
|||
resetBluetoothState() |
|||
|
|||
// 清除保存的配对设备UUID |
|||
clearPairedMacAddress() |
|||
|
|||
#if canImport(AccessorySetupKit) && swift(>=5.9) |
|||
if #available(iOS 18.0, *) { |
|||
resetSession() |
|||
initializeSession() |
|||
} |
|||
#endif |
|||
} |
|||
|
|||
// MARK: - 配对设备UUID管理 |
|||
|
|||
/// 获取已配对设备的UUID字符串 |
|||
/// 返回存储的配对设备UUID,如果没有则返回nil |
|||
func getPairedMacAddress() -> String? { |
|||
let uuidString = UserDefaults.standard.string(forKey: pairedDeviceUUIDKey) |
|||
if let uuidString = uuidString, !uuidString.isEmpty { |
|||
os_log("获取已配对设备UUID: %{public}@", log: logger, type: .info, uuidString) |
|||
return uuidString |
|||
} |
|||
os_log("未找到已配对设备UUID", log: logger, type: .info) |
|||
return nil |
|||
} |
|||
|
|||
/// 保存配对设备的UUID |
|||
/// - Parameter uuid: 要保存的设备UUID |
|||
func savePairedMacAddress(_ uuid: UUID) { |
|||
let uuidString = uuid.uuidString |
|||
UserDefaults.standard.set(uuidString, forKey: pairedDeviceUUIDKey) |
|||
UserDefaults.standard.synchronize() |
|||
os_log("已保存配对设备UUID: %{public}@", log: logger, type: .info, uuidString) |
|||
} |
|||
|
|||
/// 清除已保存的配对设备UUID |
|||
private func clearPairedMacAddress() { |
|||
UserDefaults.standard.removeObject(forKey: pairedDeviceUUIDKey) |
|||
UserDefaults.standard.synchronize() |
|||
os_log("已清除配对设备UUID", log: logger, type: .info) |
|||
} |
|||
} |
|||
@ -1,93 +0,0 @@ |
|||
import Foundation |
|||
import CoreBluetooth |
|||
|
|||
/** |
|||
* BLE服务常量 |
|||
*/ |
|||
class BleConst { |
|||
// ================== 协议相关UUID ================== |
|||
/** 主服务UUID - 文档中定义为0000ABC0-0000-1111-2222-123456789ABC */ |
|||
static let PRIMARY_SERVICE_UUID = CBUUID(string: "0000abc0-0000-1111-2222-123456789abc") |
|||
|
|||
/** 音频服务UUID - 文档中定义为00001801-0000-1000-8000-00805f9b34fb */ |
|||
static let AUDIO_SERVICE_UUID = CBUUID(string: "0000ae00-0000-1000-8000-00805f9b34fb") |
|||
|
|||
/** 接收音频特征UUID - 文档中定义为0000ABC1-0000-1111-2222-123456789ABC */ |
|||
static let RECEIVE_AUDIO_CHAR_UUID = CBUUID(string: "0000ae02-0000-1000-8000-00805f9b34fb") |
|||
|
|||
/** 写入特征UUID - 文档中定义为0000ABC1-0000-1111-2222-123456789ABC */ |
|||
static let WRITE_CHAR_UUID = CBUUID(string: "0000abc1-0000-1111-2222-123456789abc") |
|||
|
|||
/** 通知特征UUID - 文档中定义为0000ABC2-0000-1111-2222-123456789ABC */ |
|||
static let NOTIFY_CHAR_UUID = CBUUID(string: "0000abc2-0000-1111-2222-123456789abc") |
|||
|
|||
/** 客户端特征配置描述符UUID */ |
|||
static let CLIENT_CHAR_CONFIG_UUID = CBUUID(string: "00002902-0000-1000-8000-00805f9b34fb") |
|||
|
|||
/** BLE扫描Action */ |
|||
static let ACTION_SCAN_RESULT = "com.yunqiinnovation.ble_service.ACTION_BLE_SCAN" |
|||
|
|||
// ================== 连接状态 ================== |
|||
static let STATE_DISCONNECTED = 0 |
|||
static let STATE_CONNECTING = 1 |
|||
static let STATE_CONNECTED = 2 |
|||
static let STATE_DISCONNECTING = 3 |
|||
static let STATE_ERROR = 4 |
|||
|
|||
// ================== 广播厂商信息 ================== |
|||
/** 厂商ID - 文档中定义为0xABEF */ |
|||
static let MANUFACTURER_ID: UInt16 = 0xABEF |
|||
/** 客户ID - 文档中定义为0xA001 */ |
|||
static let CUSTOMER_ID: UInt16 = 0xA001 |
|||
/** 产品ID - 文档中定义为0xA001 */ |
|||
static let PRODUCT_ID: UInt16 = 0xA001 |
|||
/** 蓝牙状态-未连接 */ |
|||
static let BT_STATE_DISCONNECTED: UInt8 = 0x00 |
|||
/** 蓝牙状态-已连接 */ |
|||
static let BT_STATE_CONNECTED: UInt8 = 0x0C |
|||
|
|||
// ================== 协议格式 ================== |
|||
/** APP请求帧头 */ |
|||
static let APP_REQUEST_HEADER: UInt8 = 0xAA |
|||
/** 设备响应帧头 */ |
|||
static let DEVICE_RESPONSE_HEADER: UInt8 = 0xBB |
|||
/** 设备主动上报帧头 */ |
|||
static let DEVICE_NOTIFY_HEADER: UInt8 = 0xCC |
|||
|
|||
// ================== 协议指令 (CMD) ================== |
|||
/** 获取耳机版本号 */ |
|||
static let CMD_GET_VERSION: UInt8 = 0x01 |
|||
/** 获取耳机产品ID */ |
|||
static let CMD_GET_PRODUCT_ID: UInt8 = 0x02 |
|||
/** 获取耳机颜色ID */ |
|||
static let CMD_GET_COLOR_ID: UInt8 = 0x03 |
|||
/** 获取耳机电量ID */ |
|||
static let CMD_GET_BATTERY_INFO: UInt8 = 0x04 |
|||
/** APP控制编解码指令 */ |
|||
static let CMD_CONTROL_CODEC: UInt8 = 0x05 |
|||
/** 音量增大 */ |
|||
static let CMD_VOLUME_UP: UInt8 = 0x06 |
|||
/** 音量减小 */ |
|||
static let CMD_VOLUME_DOWN: UInt8 = 0x07 |
|||
/** A2DP播放 */ |
|||
static let CMD_PLAY_A2DP: UInt8 = 0x08 |
|||
|
|||
/** 唤醒指令 */ |
|||
static let CMD_WAKE_UP: UInt8 = 0x11 |
|||
|
|||
// ================== 编解码控制值 ================== |
|||
/** 关闭编解码指令 */ |
|||
static let CODEC_CONTROL_CLOSE: UInt8 = 0x00 |
|||
/** 音乐或者通话远端声音 */ |
|||
static let CODEC_CONTROL_DECODE_ON: UInt8 = 0xA1 |
|||
/** mic和dac(音乐或者通话远端)声音 */ |
|||
static let CODEC_CONTROL_A2DP_PLAY: UInt8 = 0xA2 |
|||
/** 打开编码指令 */ |
|||
static let CODEC_CONTROL_ENCODE_ON: UInt8 = 0xB1 |
|||
/** 左声道 */ |
|||
static let AUDIO_CHANNEL_LEFT: UInt8 = 0x01 |
|||
/** 右声道 */ |
|||
static let AUDIO_CHANNEL_RIGHT: UInt8 = 0x02 |
|||
/** 立体声 */ |
|||
static let AUDIO_CHANNEL_STEREO: UInt8 = 0x03 |
|||
} |
|||
File diff suppressed because it is too large
@ -1,4 +0,0 @@ |
|||
#import <Flutter/Flutter.h> |
|||
|
|||
@interface BleServicePlugin : NSObject<FlutterPlugin> |
|||
@end |
|||
@ -1,12 +0,0 @@ |
|||
#import "BleServicePlugin.h" |
|||
#if __has_include(<ble_service/ble_service-Swift.h>) |
|||
#import <ble_service/ble_service-Swift.h> |
|||
#else |
|||
#import "ble_service-Swift.h" |
|||
#endif |
|||
|
|||
@implementation BleServicePlugin |
|||
+ (void)registerWithRegistrar:(NSObject<FlutterPluginRegistrar>*)registrar { |
|||
[SwiftBleServicePlugin registerWithRegistrar:registrar]; |
|||
} |
|||
@end |
|||
@ -1,374 +0,0 @@ |
|||
import Flutter |
|||
import UIKit |
|||
import CoreBluetooth |
|||
import os |
|||
|
|||
@available(iOS 13.0, *) |
|||
public class SwiftBleServicePlugin: NSObject, FlutterPlugin { |
|||
// MARK: - 属性 |
|||
private var methodChannel: FlutterMethodChannel |
|||
internal var dataEventSink: FlutterEventSink? |
|||
internal var statusEventSink: FlutterEventSink? |
|||
private let logger = OSLog(subsystem: "com.yunqiinnovation.ble_service", category: "BleServicePlugin") |
|||
|
|||
// 标记是否已初始化所有服务 |
|||
private var servicesInitialized = false |
|||
|
|||
// MARK: - 初始化方法 |
|||
init(_ channel: FlutterMethodChannel) { |
|||
self.methodChannel = channel |
|||
super.init() |
|||
|
|||
#if canImport(AccessorySetupKit) && swift(>=5.9) |
|||
if #available(iOS 18.0, *) { |
|||
|
|||
// iOS 18环境下,只设置回调但不主动初始化会话 |
|||
let bleAccessorySetup = BleAccessorySetup.shared |
|||
|
|||
// 在accessoryAdded回调中延迟设置BleService |
|||
bleAccessorySetup.setDeviceDiscoveredCallback { [weak self] uuid in |
|||
// 确保服务已初始化 |
|||
self?.ensureServicesInitialized() |
|||
// 然后处理发现的设备 |
|||
self?.handleDiscoveredDevice(uuid: uuid) |
|||
} |
|||
|
|||
// 注释掉自动初始化,改为等待用户主动调用startDeviceAssociation |
|||
// bleAccessorySetup.initializeSession() |
|||
|
|||
os_log("iOS 18环境:已设置设备发现回调,等待用户主动调用关联", log: logger, type: .info) |
|||
} else { |
|||
// 低于iOS 18,直接初始化所有服务 |
|||
initializeAllServices() |
|||
} |
|||
#else |
|||
// 非AccessorySetupKit环境,直接初始化所有服务 |
|||
initializeAllServices() |
|||
#endif |
|||
} |
|||
|
|||
|
|||
// 确保所有服务都已初始化 |
|||
private func ensureServicesInitialized() { |
|||
if !servicesInitialized { |
|||
// 设置BLE服务回调 |
|||
BleService.shared.addCallback(self) |
|||
|
|||
// 设置BleService到AccessorySetup |
|||
BleAccessorySetup.shared.setBleService(BleService.shared) |
|||
|
|||
servicesInitialized = true |
|||
} |
|||
} |
|||
|
|||
// 初始化所有服务(iOS 17及以下版本的标准行为) |
|||
private func initializeAllServices() { |
|||
// 设置BLE服务回调 |
|||
BleService.shared.addCallback(self) |
|||
|
|||
// 初始化AccessorySetup,但不自动开始会话 |
|||
BleAccessorySetup.shared.setBleService(BleService.shared) |
|||
BleAccessorySetup.shared.setDeviceDiscoveredCallback { [weak self] uuid in |
|||
self?.handleDiscoveredDevice(uuid: uuid) |
|||
} |
|||
// 移除自动初始化,统一由startDeviceAssociation方法触发 |
|||
// BleAccessorySetup.shared.initializeSession() |
|||
|
|||
os_log("非iOS 18环境:已设置BLE服务但不自动初始化会话", log: logger, type: .info) |
|||
servicesInitialized = true |
|||
} |
|||
|
|||
// MARK: - FlutterPlugin Protocol |
|||
public static func register(with registrar: FlutterPluginRegistrar) { |
|||
let channel = FlutterMethodChannel(name: "com.yunqiinnovation.ble_service", binaryMessenger: registrar.messenger()) |
|||
let instance = SwiftBleServicePlugin(channel) |
|||
registrar.addMethodCallDelegate(instance, channel: channel) |
|||
|
|||
// 注册事件通道 - 为每个通道创建独立的handler |
|||
let dataEventChannel = FlutterEventChannel(name: "com.yunqiinnovation.ble_service/data_events", binaryMessenger: registrar.messenger()) |
|||
let statusEventChannel = FlutterEventChannel(name: "com.yunqiinnovation.ble_service/status_events", binaryMessenger: registrar.messenger()) |
|||
|
|||
// 创建独立的StreamHandler |
|||
let dataHandler = BleDataStreamHandler(instance) |
|||
let statusHandler = BleStatusStreamHandler(instance) |
|||
|
|||
dataEventChannel.setStreamHandler(dataHandler) |
|||
statusEventChannel.setStreamHandler(statusHandler) |
|||
} |
|||
|
|||
public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { |
|||
// 检查是否需要初始化服务 |
|||
checkAndInitializeServices() |
|||
|
|||
switch call.method { |
|||
case "isConnected": |
|||
result(BleService.shared.isConnected()) |
|||
|
|||
case "getBatteryLevel": |
|||
// 获取电池信息,通过回调返回结果 |
|||
if BleService.shared.getBatteryInfo() { |
|||
result(0) // 通过回调返回实际值 |
|||
} else { |
|||
result(FlutterError(code: "UNAVAILABLE", |
|||
message: "无法获取电池信息", |
|||
details: nil)) |
|||
} |
|||
|
|||
case "isCompanionSupported": |
|||
#if canImport(AccessorySetupKit) && swift(>=5.9) |
|||
if #available(iOS 18.0, *) { |
|||
result(true) |
|||
} else { |
|||
result(false) |
|||
} |
|||
#else |
|||
result(false) |
|||
#endif |
|||
|
|||
case "getPairedMacAddress": |
|||
#if canImport(AccessorySetupKit) && swift(>=5.9) |
|||
if #available(iOS 18.0, *) { |
|||
let pairedUUID = BleAccessorySetup.shared.getPairedMacAddress() |
|||
result(pairedUUID) |
|||
} else { |
|||
result(nil) |
|||
} |
|||
#else |
|||
result(nil) |
|||
#endif |
|||
|
|||
|
|||
|
|||
case "isBleConnected": |
|||
result(BleService.shared.isConnected()) |
|||
|
|||
case "registerBackgroundScan", "unregisterBackgroundScan": |
|||
// iOS上禁用,直接返回成功以避免Flutter端报错 |
|||
result(true) |
|||
|
|||
case "getDeviceVersion": |
|||
result(BleService.shared.getDeviceVersion()) |
|||
|
|||
case "getProductId": |
|||
result(BleService.shared.getProductId()) |
|||
|
|||
case "getBatteryInfo": |
|||
result(BleService.shared.getBatteryInfo()) |
|||
|
|||
case "openEncoder": |
|||
result(BleService.shared.openEncoder()) |
|||
|
|||
case "openDecoder": |
|||
result(BleService.shared.openDecoder()) |
|||
|
|||
case "openA2DPDecoder": |
|||
result(BleService.shared.openA2DPDecoder()) |
|||
|
|||
case "closeCodec": |
|||
result(BleService.shared.closeCodec()) |
|||
|
|||
case "playA2DP": |
|||
result(BleService.shared.playA2DP()) |
|||
|
|||
// iOS 18+ AccessorySetupKit相关方法 |
|||
case "clearAssociations": |
|||
// 清除设备配对关系 |
|||
#if canImport(AccessorySetupKit) && swift(>=5.9) |
|||
if #available(iOS 18.0, *) { |
|||
os_log("清除设备配对关系", log: logger, type: .info) |
|||
BleAccessorySetup.shared.clearAssociations() |
|||
result(true) |
|||
} else { |
|||
result(true) |
|||
} |
|||
#else |
|||
result(true) |
|||
#endif |
|||
|
|||
case "startDeviceAssociation": |
|||
// 用户主动触发设备关联流程 |
|||
#if canImport(AccessorySetupKit) && swift(>=5.9) |
|||
if #available(iOS 18.0, *) { |
|||
os_log("用户主动触发设备关联流程", log: logger, type: .info) |
|||
|
|||
// 确保服务已初始化 |
|||
ensureServicesInitialized() |
|||
|
|||
// 重新初始化会话 |
|||
BleAccessorySetup.shared.initializeSession() |
|||
|
|||
// 返回成功,实际配对结果会通过回调通知 |
|||
result(true) |
|||
} else { |
|||
os_log("当前iOS版本不支持AccessorySetupKit", log: logger, type: .info) |
|||
result(false) |
|||
} |
|||
#else |
|||
os_log("当前环境不支持AccessorySetupKit", log: logger, type: .info) |
|||
result(false) |
|||
#endif |
|||
|
|||
default: |
|||
result(FlutterMethodNotImplemented) |
|||
} |
|||
} |
|||
|
|||
// MARK: - 私有方法 |
|||
// 检查并按需初始化服务 |
|||
private func checkAndInitializeServices() { |
|||
#if canImport(AccessorySetupKit) && swift(>=5.9) |
|||
if #available(iOS 18.0, *) { |
|||
ensureServicesInitialized() |
|||
} |
|||
#endif |
|||
} |
|||
|
|||
// AccessorySetupKit发现设备的处理 |
|||
private func handleDiscoveredDevice(uuid: UUID) { |
|||
// BleAccessorySetup内部已进行连接,此处不再重复调用 |
|||
// os_log("收到设备发现回调: %{public}@", log: logger, type: .info, uuid.uuidString) |
|||
// 可以在这里添加设备记录或其他操作 |
|||
} |
|||
|
|||
private func sendEvent<T>(_ eventSink: FlutterEventSink?, data: T, errorPrefix: String) { |
|||
if let eventSink = eventSink { |
|||
DispatchQueue.main.async { |
|||
eventSink(data) |
|||
} |
|||
} |
|||
} |
|||
|
|||
// 获取顶层视图控制器的辅助方法 |
|||
private func getTopViewController(_ viewController: UIViewController) -> UIViewController { |
|||
if let presented = viewController.presentedViewController { |
|||
return getTopViewController(presented) |
|||
} |
|||
|
|||
if let tabBarController = viewController as? UITabBarController, |
|||
let selected = tabBarController.selectedViewController { |
|||
return getTopViewController(selected) |
|||
} |
|||
|
|||
if let navigationController = viewController as? UINavigationController, |
|||
let visibleViewController = navigationController.visibleViewController { |
|||
return getTopViewController(visibleViewController) |
|||
} |
|||
|
|||
return viewController |
|||
} |
|||
} |
|||
|
|||
// MARK: - BleService.Callback |
|||
@available(iOS 13.0, *) |
|||
extension SwiftBleServicePlugin: BleService.Callback { |
|||
public func onScanResult(devices: [[String: Any]]) { |
|||
if let eventSink = statusEventSink { |
|||
DispatchQueue.main.async { |
|||
eventSink([ |
|||
"type": "scanResult", |
|||
"devices": devices |
|||
]) |
|||
} |
|||
} |
|||
} |
|||
|
|||
public func onConnectionStateChanged(state: Int) { |
|||
if let eventSink = statusEventSink { |
|||
var stateStr = "Unknown" |
|||
switch state { |
|||
case BleConst.STATE_DISCONNECTED: |
|||
stateStr = "Disconnected" |
|||
case BleConst.STATE_CONNECTING: |
|||
stateStr = "Connecting" |
|||
case BleConst.STATE_CONNECTED: |
|||
stateStr = "Connected" |
|||
case BleConst.STATE_DISCONNECTING: |
|||
stateStr = "Disconnecting" |
|||
case BleConst.STATE_ERROR: |
|||
stateStr = "Connection Error" |
|||
default: |
|||
stateStr = "Unknown State(\(state))" |
|||
} |
|||
|
|||
DispatchQueue.main.async { |
|||
eventSink([ |
|||
"type": "connectionStateChanged", |
|||
"state": state, |
|||
"stateStr": stateStr |
|||
]) |
|||
} |
|||
} |
|||
} |
|||
|
|||
public func onAudioDataReceived(data: Data) { |
|||
if let eventSink = dataEventSink { |
|||
DispatchQueue.main.async { |
|||
eventSink([ |
|||
"type": "audioData", |
|||
"data": FlutterStandardTypedData(bytes: data) |
|||
]) |
|||
} |
|||
} |
|||
} |
|||
|
|||
public func onWakeupSignalReceived() { |
|||
if let eventSink = statusEventSink { |
|||
DispatchQueue.main.async { |
|||
eventSink([ |
|||
"type": "wakeup" |
|||
]) |
|||
} |
|||
} |
|||
} |
|||
|
|||
public func onDeviceInfoReceived(infoType: Int, infoData: [String: Any]) { |
|||
if let eventSink = statusEventSink { |
|||
var resultMap = infoData |
|||
resultMap["type"] = "deviceInfo" |
|||
resultMap["result"] = infoType |
|||
|
|||
DispatchQueue.main.async { |
|||
eventSink(resultMap) |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
// MARK: - 独立的StreamHandler类 |
|||
@available(iOS 13.0, *) |
|||
class BleDataStreamHandler: NSObject, FlutterStreamHandler { |
|||
private weak var plugin: SwiftBleServicePlugin? |
|||
|
|||
init(_ plugin: SwiftBleServicePlugin) { |
|||
self.plugin = plugin |
|||
} |
|||
|
|||
func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? { |
|||
plugin?.dataEventSink = events |
|||
return nil |
|||
} |
|||
|
|||
func onCancel(withArguments arguments: Any?) -> FlutterError? { |
|||
plugin?.dataEventSink = nil |
|||
return nil |
|||
} |
|||
} |
|||
|
|||
@available(iOS 13.0, *) |
|||
class BleStatusStreamHandler: NSObject, FlutterStreamHandler { |
|||
private weak var plugin: SwiftBleServicePlugin? |
|||
|
|||
init(_ plugin: SwiftBleServicePlugin) { |
|||
self.plugin = plugin |
|||
} |
|||
|
|||
func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? { |
|||
plugin?.statusEventSink = events |
|||
return nil |
|||
} |
|||
|
|||
func onCancel(withArguments arguments: Any?) -> FlutterError? { |
|||
plugin?.statusEventSink = nil |
|||
return nil |
|||
} |
|||
} |
|||
@ -1,239 +0,0 @@ |
|||
import Foundation |
|||
import AVFoundation |
|||
import YbridOpus |
|||
|
|||
@available(iOS 13.0, *) |
|||
class SwiftOpusAudioProcessor: NSObject { |
|||
|
|||
protocol AudioDataCallback: AnyObject { |
|||
func onAudioDataReceived(data: Data) |
|||
} |
|||
|
|||
private var isDecodingActive = false |
|||
private weak var callback: AudioDataCallback? |
|||
|
|||
// Opus 解码器相关 - 使用 YbridOpus C API |
|||
private var decoder: OpaquePointer? |
|||
private var sampleRate: Int32 = 16000 |
|||
private var channels: Int32 = 1 |
|||
private let maxFrameSize: Int32 = 5760 // 120 ms @48 kHz 的最大值 |
|||
|
|||
override init() { |
|||
super.init() |
|||
} |
|||
|
|||
deinit { |
|||
cleanup() |
|||
} |
|||
|
|||
private func initializeOpusDecoder() -> Bool { |
|||
if decoder != nil { |
|||
return true |
|||
} |
|||
|
|||
var error: Int32 = 0 |
|||
decoder = opus_decoder_create(sampleRate, channels, &error) |
|||
|
|||
guard error == OPUS_OK, decoder != nil else { |
|||
return false |
|||
} |
|||
|
|||
return true |
|||
} |
|||
|
|||
private func destroyOpusDecoder() { |
|||
if let decoder = decoder { |
|||
opus_decoder_destroy(decoder) |
|||
self.decoder = nil |
|||
} |
|||
} |
|||
|
|||
func setCallback(_ callback: AudioDataCallback?) { |
|||
self.callback = callback |
|||
} |
|||
|
|||
func startDecoding(sampleRate: Int32 = 16000, channels: Int32 = 1, frameSize: Int32 = 640) -> Bool { |
|||
if isDecodingActive { |
|||
return true |
|||
} |
|||
|
|||
self.sampleRate = sampleRate |
|||
self.channels = channels |
|||
|
|||
guard initializeOpusDecoder() else { |
|||
return false |
|||
} |
|||
|
|||
isDecodingActive = true |
|||
|
|||
return true |
|||
} |
|||
|
|||
func stopDecoding() -> Bool { |
|||
if !isDecodingActive { |
|||
return false |
|||
} |
|||
|
|||
isDecodingActive = false |
|||
destroyOpusDecoder() |
|||
|
|||
return true |
|||
} |
|||
|
|||
func processAudioData(_ data: Data) { |
|||
guard isDecodingActive, let decoder = decoder, data.count > 0 else { |
|||
return |
|||
} |
|||
|
|||
// 增强的数据包验证 |
|||
guard data.count <= 1275 else { |
|||
print("音频数据包太大: \(data.count) bytes,跳过处理") |
|||
return |
|||
} |
|||
|
|||
// 检查最小数据包大小(Opus最小包通常至少几个字节) |
|||
guard data.count >= 3 else { |
|||
print("音频数据包太小: \(data.count) bytes,跳过处理") |
|||
return |
|||
} |
|||
|
|||
// 简单的 Opus 头部验证(检查是否是有效的 Opus 数据包格式) |
|||
let firstByte = data[0] |
|||
// Opus packets start with a TOC (Table of Contents) byte |
|||
// 检查TOC字节是否在合理范围内 |
|||
if firstByte == 0x00 || firstByte == 0xFF { |
|||
print("可疑的Opus数据包头部: 0x\(String(format: "%02X", firstByte)),跳过处理") |
|||
return |
|||
} |
|||
|
|||
DispatchQueue.global(qos: .userInitiated).async { [weak self] in |
|||
self?.decodeOpusPacket(data) |
|||
} |
|||
} |
|||
|
|||
// 简化的 Opus 解码方法,重点是容错处理 |
|||
private func decodeOpusPacket(_ packet: Data) { |
|||
guard let decoder = decoder else { return } |
|||
|
|||
let maxSamples = Int(maxFrameSize) |
|||
var pcmBuffer = [opus_int16](repeating: 0, count: maxSamples * Int(channels)) |
|||
|
|||
let decodedSamples = packet.withUnsafeBytes { packetPtr in |
|||
let uint8Ptr = packetPtr.bindMemory(to: UInt8.self).baseAddress |
|||
return opus_decode( |
|||
decoder, |
|||
uint8Ptr, |
|||
opus_int32(packet.count), |
|||
&pcmBuffer, |
|||
Int32(maxSamples), |
|||
0 |
|||
) |
|||
} |
|||
|
|||
// 增强的错误处理:检查各种错误情况 |
|||
if decodedSamples <= 0 { |
|||
let errorCode = decodedSamples |
|||
print("Opus解码失败,错误代码: \(errorCode),重新初始化解码器") |
|||
|
|||
// 根据错误类型进行不同的处理 |
|||
if errorCode == -1 { // OPUS_BAD_ARG |
|||
print("Opus解码参数错误,执行完全重置") |
|||
performCompleteReset() |
|||
} else if errorCode == -2 { // OPUS_BUFFER_TOO_SMALL |
|||
print("Opus缓冲区太小") |
|||
resetDecoder() |
|||
} else if errorCode == -3 { // OPUS_INTERNAL_ERROR |
|||
print("Opus内部错误,执行完全重置") |
|||
performCompleteReset() |
|||
} else { |
|||
print("Opus其他错误,尝试简单重置") |
|||
resetDecoder() |
|||
} |
|||
return |
|||
} |
|||
|
|||
// 验证解码结果的合理性 |
|||
guard decodedSamples <= maxFrameSize else { |
|||
print("Opus解码样本数超出预期: \(decodedSamples) > \(maxFrameSize)") |
|||
resetDecoder() |
|||
return |
|||
} |
|||
|
|||
// 提取解码的音频数据 |
|||
let samplesPerChannel = Int(decodedSamples) |
|||
let totalSamples = samplesPerChannel * Int(channels) |
|||
|
|||
// 验证总样本数的合理性 |
|||
guard totalSamples <= pcmBuffer.count else { |
|||
print("计算的总样本数超出缓冲区: \(totalSamples) > \(pcmBuffer.count)") |
|||
resetDecoder() |
|||
return |
|||
} |
|||
|
|||
let outputPCM = Array(pcmBuffer[0..<totalSamples]) |
|||
|
|||
if !outputPCM.isEmpty { |
|||
processPCMData(outputPCM) |
|||
} |
|||
} |
|||
|
|||
// 重置解码器的简单方法 |
|||
private func resetDecoder() { |
|||
print("执行简单解码器重置") |
|||
destroyOpusDecoder() |
|||
_ = initializeOpusDecoder() |
|||
} |
|||
|
|||
// 执行完全重置(包括停止和重启解码) |
|||
private func performCompleteReset() { |
|||
print("执行完全解码器重置") |
|||
let wasActive = isDecodingActive |
|||
let currentSampleRate = self.sampleRate |
|||
let currentChannels = self.channels |
|||
|
|||
// 完全停止解码 |
|||
if wasActive { |
|||
isDecodingActive = false |
|||
destroyOpusDecoder() |
|||
} |
|||
|
|||
// 重新初始化所有状态 |
|||
self.sampleRate = currentSampleRate |
|||
self.channels = currentChannels |
|||
|
|||
// 如果之前是活跃状态,重新启动 |
|||
if wasActive { |
|||
guard initializeOpusDecoder() else { |
|||
print("完全重置后重新初始化失败") |
|||
return |
|||
} |
|||
isDecodingActive = true |
|||
print("完全重置成功,解码器已重新启动") |
|||
} |
|||
} |
|||
|
|||
private func processPCMData(_ pcmData: [opus_int16]) { |
|||
// 转换为 Data |
|||
let data = Data(bytes: pcmData, count: pcmData.count * MemoryLayout<opus_int16>.size) |
|||
|
|||
// 回调通知 |
|||
DispatchQueue.main.async { [weak self] in |
|||
self?.callback?.onAudioDataReceived(data: data) |
|||
} |
|||
} |
|||
|
|||
func isDecoding() -> Bool { |
|||
return isDecodingActive |
|||
} |
|||
|
|||
func cleanup() { |
|||
stopDecoding() |
|||
} |
|||
|
|||
// MARK: - 公共接口 |
|||
|
|||
/// 获取当前配置信息 |
|||
func getCurrentConfig() -> (sampleRate: Int32, channels: Int32) { |
|||
return (sampleRate, channels) |
|||
} |
|||
} |
|||
@ -1 +0,0 @@ |
|||
|
|||
Loading…
Reference in new issue