|
|
@ -161,6 +161,9 @@ class TranslationController extends GetxController with WidgetsBindingObserver { |
|
|
StreamSubscription? _jieliCallInUplinkSub; |
|
|
StreamSubscription? _jieliCallInUplinkSub; |
|
|
StreamSubscription? _jieliCallInDownlinkSub; |
|
|
StreamSubscription? _jieliCallInDownlinkSub; |
|
|
StreamSubscription? _jieliCallAstTtsSub; |
|
|
StreamSubscription? _jieliCallAstTtsSub; |
|
|
|
|
|
// Android 悬浮窗"被用户叉掉"的事件订阅 — 用于在 audioVideo 翻译中 |
|
|
|
|
|
// 把"关掉浮窗"语义升级为"结束翻译",避免后台还在跑识别 + 录屏 |
|
|
|
|
|
StreamSubscription<void>? _floatingWindowClosedSubscription; |
|
|
Timer? _translationDebounceTimer; |
|
|
Timer? _translationDebounceTimer; |
|
|
|
|
|
|
|
|
// ==================== 重新初始化守卫 ==================== |
|
|
// ==================== 重新初始化守卫 ==================== |
|
|
@ -187,6 +190,21 @@ class TranslationController extends GetxController with WidgetsBindingObserver { |
|
|
Get.put(FloatingWindowManager(), permanent: true); |
|
|
Get.put(FloatingWindowManager(), permanent: true); |
|
|
} |
|
|
} |
|
|
_floatingWindowManager = Get.find<FloatingWindowManager>(); |
|
|
_floatingWindowManager = Get.find<FloatingWindowManager>(); |
|
|
|
|
|
|
|
|
|
|
|
// 监听用户主动关闭浮窗:在 Android 音视频翻译里浮窗就是用户唯一能看 |
|
|
|
|
|
// 翻译的入口,叉掉它意味着"结束翻译",顺势停掉识别 + 录屏前台服务, |
|
|
|
|
|
// 避免后台空跑。其它模式(call 等)保持原行为,仅同步本地标志位。 |
|
|
|
|
|
_floatingWindowClosedSubscription?.cancel(); |
|
|
|
|
|
_floatingWindowClosedSubscription = |
|
|
|
|
|
_floatingWindowManager.onClosedByUser.listen((_) { |
|
|
|
|
|
Logger.info('悬浮窗被用户关闭,同步翻译会话状态'); |
|
|
|
|
|
isFloatingWindowEnabled.value = false; |
|
|
|
|
|
if (currentMode.value == 'audioVideo' && |
|
|
|
|
|
(isRecognizing.value || isPreparing.value)) { |
|
|
|
|
|
stopAll(); |
|
|
|
|
|
} |
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
Logger.info('悬浮窗管理器初始化完成'); |
|
|
Logger.info('悬浮窗管理器初始化完成'); |
|
|
} catch (e) { |
|
|
} catch (e) { |
|
|
Logger.error('悬浮窗管理器初始化失败: $e'); |
|
|
Logger.error('悬浮窗管理器初始化失败: $e'); |
|
|
@ -969,6 +987,8 @@ class TranslationController extends GetxController with WidgetsBindingObserver { |
|
|
_voiceInteractionSubscription?.cancel(); |
|
|
_voiceInteractionSubscription?.cancel(); |
|
|
_deviceCallTranslationSubscription?.cancel(); |
|
|
_deviceCallTranslationSubscription?.cancel(); |
|
|
_deviceCallTranslationSubscription = null; |
|
|
_deviceCallTranslationSubscription = null; |
|
|
|
|
|
_floatingWindowClosedSubscription?.cancel(); |
|
|
|
|
|
_floatingWindowClosedSubscription = null; |
|
|
_detachJieliCallTranslationBridge(); |
|
|
_detachJieliCallTranslationBridge(); |
|
|
_translationDebounceTimer?.cancel(); |
|
|
_translationDebounceTimer?.cancel(); |
|
|
restoreOtherServices(); |
|
|
restoreOtherServices(); |
|
|
@ -1101,10 +1121,26 @@ class TranslationController extends GetxController with WidgetsBindingObserver { |
|
|
|
|
|
|
|
|
try { |
|
|
try { |
|
|
await _initializeSession(); |
|
|
await _initializeSession(); |
|
|
|
|
|
|
|
|
|
|
|
// Android 音视频翻译:先把悬浮窗起来,让用户切到其他 App 后还能看到翻译。 |
|
|
|
|
|
// 浮窗权限被拒绝则直接放弃启动 — 不开浮窗就没意义(用户切走就看不见结果)。 |
|
|
|
|
|
final isAndroidAudioVideo = |
|
|
|
|
|
Platform.isAndroid && currentMode.value == 'audioVideo'; |
|
|
|
|
|
if (isAndroidAudioVideo && !isFloatingWindowEnabled.value) { |
|
|
|
|
|
await _enableFloatingWindow(); |
|
|
|
|
|
if (!isFloatingWindowEnabled.value) { |
|
|
|
|
|
return; |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
await _configureAudioMode(); |
|
|
await _configureAudioMode(); |
|
|
if (currentMode.value != 'faceToFace') { |
|
|
if (currentMode.value != 'faceToFace') { |
|
|
|
|
|
// Android 上音视频翻译改走 MediaProjection,PCM 由系统播放流提供, |
|
|
|
|
|
// 不再需要 BLE codec 处于激活状态。其它平台/模式仍按旧逻辑校验: |
|
|
|
|
|
// 拿不到 BLE 编解码就放弃启动识别。 |
|
|
if ((currentMode.value == 'audioVideo' || |
|
|
if ((currentMode.value == 'audioVideo' || |
|
|
currentMode.value == 'call') && |
|
|
currentMode.value == 'call') && |
|
|
|
|
|
!isAndroidAudioVideo && |
|
|
!bleManager.isCodecActive) { |
|
|
!bleManager.isCodecActive) { |
|
|
return; |
|
|
return; |
|
|
} |
|
|
} |
|
|
@ -1123,6 +1159,24 @@ class TranslationController extends GetxController with WidgetsBindingObserver { |
|
|
await _startAsrService('normal'); |
|
|
await _startAsrService('normal'); |
|
|
} |
|
|
} |
|
|
await _finalizeRecognitionStart(); |
|
|
await _finalizeRecognitionStart(); |
|
|
|
|
|
|
|
|
|
|
|
// Android 音视频翻译:识别就绪后才弹"录屏"授权 — 用户同意的瞬间 |
|
|
|
|
|
// 系统音频开始流入已经准备好的 ASR 通路,可以立即切到目标 App。 |
|
|
|
|
|
if (isAndroidAudioVideo) { |
|
|
|
|
|
final captured = await _asrService.requestScreenCapture(); |
|
|
|
|
|
if (!captured) { |
|
|
|
|
|
// 拒绝则回滚已经启动的识别 + 关掉浮窗,避免空浮窗悬空在桌面 |
|
|
|
|
|
await stopRecognition(); |
|
|
|
|
|
if (isFloatingWindowEnabled.value) { |
|
|
|
|
|
await _disableFloatingWindow(); |
|
|
|
|
|
} |
|
|
|
|
|
Get.snackbar( |
|
|
|
|
|
'tip'.tr, |
|
|
|
|
|
'meetingRecordScreenCapturePermissionDenied'.tr, |
|
|
|
|
|
); |
|
|
|
|
|
return; |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
} |
|
|
} |
|
|
} catch (e) { |
|
|
} catch (e) { |
|
|
isPreparing.value = false; |
|
|
isPreparing.value = false; |
|
|
@ -1172,6 +1226,12 @@ class TranslationController extends GetxController with WidgetsBindingObserver { |
|
|
/// 配置音视频模式 |
|
|
/// 配置音视频模式 |
|
|
Future<void> _configureAudioVideoMode() async { |
|
|
Future<void> _configureAudioVideoMode() async { |
|
|
isPreparing.value = true; |
|
|
isPreparing.value = true; |
|
|
|
|
|
if (Platform.isAndroid) { |
|
|
|
|
|
// Android 改走 MediaProjection。这里只设置音频源标志,真正的录屏授权 |
|
|
|
|
|
// 推迟到 startRecognition 末尾申请:先把浮窗、识别都准备好,最后弹授权 |
|
|
|
|
|
// 让用户立刻就能切到 YouTube 等 App 看翻译结果。 |
|
|
|
|
|
isPreparing.value = false; |
|
|
|
|
|
} else { |
|
|
await bleManager.openDecoder(); |
|
|
await bleManager.openDecoder(); |
|
|
int attempts = 0; |
|
|
int attempts = 0; |
|
|
while (!bleManager.isCodecActive && attempts < 10) { |
|
|
while (!bleManager.isCodecActive && attempts < 10) { |
|
|
@ -1183,6 +1243,7 @@ class TranslationController extends GetxController with WidgetsBindingObserver { |
|
|
Get.snackbar('tip'.tr, 'operationFailedPleaseRetry'.tr); |
|
|
Get.snackbar('tip'.tr, 'operationFailedPleaseRetry'.tr); |
|
|
throw Exception('lxm--BLE编解码打开失败'); |
|
|
throw Exception('lxm--BLE编解码打开失败'); |
|
|
} |
|
|
} |
|
|
|
|
|
} |
|
|
_audioSourceType = true; |
|
|
_audioSourceType = true; |
|
|
isTtsEnabled.value = false; |
|
|
isTtsEnabled.value = false; |
|
|
Logger.info('发送ble音乐或者通话远端声音'); |
|
|
Logger.info('发送ble音乐或者通话远端声音'); |
|
|
@ -2002,7 +2063,12 @@ class TranslationController extends GetxController with WidgetsBindingObserver { |
|
|
isPreparing.value = false; |
|
|
isPreparing.value = false; |
|
|
if (currentMode.value == "audioVideo" || currentMode.value == "call") { |
|
|
if (currentMode.value == "audioVideo" || currentMode.value == "call") { |
|
|
_detachJieliCallTranslationBridge(); |
|
|
_detachJieliCallTranslationBridge(); |
|
|
|
|
|
if (currentMode.value == "audioVideo" && Platform.isAndroid) { |
|
|
|
|
|
// Android 音视频翻译走 MediaProjection,关掉对应前台服务即可。 |
|
|
|
|
|
await _asrService.stopScreenCapture(); |
|
|
|
|
|
} else { |
|
|
bleManager.closeCodec(); |
|
|
bleManager.closeCodec(); |
|
|
|
|
|
} |
|
|
Logger.info('关闭ble编解码器'); |
|
|
Logger.info('关闭ble编解码器'); |
|
|
} |
|
|
} |
|
|
if (isRecognizing.value) { |
|
|
if (isRecognizing.value) { |
|
|
@ -2032,8 +2098,12 @@ class TranslationController extends GetxController with WidgetsBindingObserver { |
|
|
_translationDebounceTimer?.cancel(); |
|
|
_translationDebounceTimer?.cancel(); |
|
|
await _ttsService.stop(); |
|
|
await _ttsService.stop(); |
|
|
if (currentMode.value == "call" || currentMode.value == "audioVideo") { |
|
|
if (currentMode.value == "call" || currentMode.value == "audioVideo") { |
|
|
|
|
|
if (currentMode.value == "audioVideo" && Platform.isAndroid) { |
|
|
|
|
|
await _asrService.stopScreenCapture(); |
|
|
|
|
|
} else { |
|
|
bleManager.closeCodec(); |
|
|
bleManager.closeCodec(); |
|
|
} |
|
|
} |
|
|
|
|
|
} |
|
|
} catch (e) { |
|
|
} catch (e) { |
|
|
Logger.error('[STS] 终止通话服务失败: $e'); |
|
|
Logger.error('[STS] 终止通话服务失败: $e'); |
|
|
} |
|
|
} |
|
|
|