Browse Source

feat(录音): 添加后台录音通知和前台服务

添加后台录音时的持续通知显示
实现Android前台服务保持录音进程
添加iOS本地通知提醒用户录音进行中
weicu
lxm 9 months ago
parent
commit
4812ee5588
  1. 3
      lib/core/translations/language/en_us.dart
  2. 2
      lib/core/translations/language/zh_cn.dart
  3. 65
      lib/modules/meeting/controllers/meeting_record_controller.dart
  4. 1
      lib/modules/meeting/views/meeting_record_view.dart
  5. 17
      local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureSpeechPlugin.kt

3
lib/core/translations/language/en_us.dart

@ -443,6 +443,9 @@ const Map<String, String> enUS = {
'meetingRecordEndWithoutSave': 'End recording without saving', // 结束录音不保存
'meetingRecordContinueInBackground':
'Continue recording in background', // 后台继续录音
'meetingRecordRecordingNotificationTitle': 'Recording in progress', // 录音进行中
'meetingRecordRecordingNotificationBody':
'Recording in background. Tap to return to the app.', // 正在后台录音,点击返回应用
'transcribing': 'Transcribing...',
'summarizing': 'Summarizing...',
'analyzing': 'Analyzing...',

2
lib/core/translations/language/zh_cn.dart

@ -419,6 +419,8 @@ const Map<String, String> zhCN = {
'meetingRecordEndAndSave': '结束录音并保存', // 结束录音并保存
'meetingRecordEndWithoutSave': '结束录音不保存', // 结束录音不保存
'meetingRecordContinueInBackground': '后台继续录音', // 后台继续录音
'meetingRecordRecordingNotificationTitle': '录音进行中', // 录音进行中
'meetingRecordRecordingNotificationBody': '正在后台录音,点击返回应用', // 正在后台录音,点击返回应用
'transcribing': '正在转写...',
'summarizing': '正在总结...',
'analyzing': '正在分析...',

65
lib/modules/meeting/controllers/meeting_record_controller.dart

@ -11,6 +11,7 @@ import 'package:get/get.dart';
import 'package:intl/intl.dart';
import 'package:path_provider/path_provider.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import '../../../data/services/asr_service.dart';
import '../../../data/services/ble_manager.dart';
@ -25,6 +26,66 @@ enum TimerStatus {
/// 负责会议记录的状态管理、音频处理、语音识别和UI交互
class MeetingRecordController extends GetxController
with GetTickerProviderStateMixin {
static const int _backgroundRecordingNotificationId = 91001;
static FlutterLocalNotificationsPlugin? _localNotificationsPlugin;
Future<FlutterLocalNotificationsPlugin>
_ensureLocalNotificationsPlugin() async {
final existing = _localNotificationsPlugin;
if (existing != null) return existing;
final plugin = FlutterLocalNotificationsPlugin();
const androidSettings =
AndroidInitializationSettings('@mipmap/ic_launcher');
const iosSettings = DarwinInitializationSettings();
const settings = InitializationSettings(
android: androidSettings,
iOS: iosSettings,
);
await plugin.initialize(settings);
_localNotificationsPlugin = plugin;
return plugin;
}
Future<void> _requestNotificationPermissionIfNeeded() async {
await PermissionUtil.instance.requestPermission(
permissionType: Permission.notification,
permissionName: 'notificationPermission'.tr,
explanationText: 'notificationPermissionExplanation'.tr,
permanentDenialText: 'notificationPermissionPermanentDenial'.tr,
settingsButtonText: 'goToSettings'.tr,
cancelButtonText: 'cancel'.tr,
);
}
Future<void> showBackgroundRecordingIndicator() async {
await _requestNotificationPermissionIfNeeded();
if (!Platform.isIOS) return;
final plugin = await _ensureLocalNotificationsPlugin();
final details = NotificationDetails(
iOS: DarwinNotificationDetails(
presentAlert: true,
presentBadge: false,
presentSound: false,
),
);
await plugin.show(
_backgroundRecordingNotificationId,
'meetingRecordRecordingNotificationTitle'.tr,
'meetingRecordRecordingNotificationBody'.tr,
details,
);
}
Future<void> clearBackgroundRecordingIndicator() async {
if (!Platform.isIOS) return;
final plugin = await _ensureLocalNotificationsPlugin();
await plugin.cancel(_backgroundRecordingNotificationId);
}
// 服务
final AsrService _asrService = Get.find<AsrService>();
final BleManager _bleManager = Get.find<BleManager>();
@ -225,6 +286,8 @@ class MeetingRecordController extends GetxController
return;
}
await _requestNotificationPermissionIfNeeded();
// 根据当前计时器状态执行相应操作
switch (timerStatus.value) {
case TimerStatus.stopped:
@ -389,6 +452,7 @@ class MeetingRecordController extends GetxController
}
if (!isRecording.value) return;
await _asrService.stopRecord(true);
await clearBackgroundRecordingIndicator();
// 如果标题被修改则重命名文件
if (fileName.value != newName.value) {
@ -427,6 +491,7 @@ class MeetingRecordController extends GetxController
if (isRecording.value) {
await _asrService.stopRecord(false);
}
await clearBackgroundRecordingIndicator();
_resetToDefaultState();
isRecording.value = false;
rms.value = 0.0;

1
lib/modules/meeting/views/meeting_record_view.dart

@ -226,6 +226,7 @@ class MeetingRecordView extends GetView<MeetingRecordController> {
return;
case _MeetingRecordExitAction.continueInBackground:
await controller.saveTitleChange();
await controller.showBackgroundRecordingIndicator();
Get.back(result: 'meeting_record_background');
return;
case _MeetingRecordExitAction.cancel:

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

@ -5,6 +5,7 @@ import android.os.Handler
import android.os.Looper
import androidx.annotation.NonNull
import com.yunqiinnovation.azure_speech.utils.FileLogger
import com.yunqiinnovation.azure_speech.tools.AudioRecordingForegroundService
import com.yunqiinnovation.azure_speech.tools.SimpleAudioReceiver
import com.yunqiinnovation.azure_speech.tools.SimpleAudioPlayer
import io.flutter.embedding.engine.plugins.FlutterPlugin
@ -582,6 +583,12 @@ class AzureSpeechPlugin : BleService.Callback, FlutterPlugin {
}
FileLogger.d(tag, "音频文件名称为: ${filePath}")
try {
AudioRecordingForegroundService.startService(context)
} catch (e: Exception) {
FileLogger.w(tag, "启动录音前台服务失败: ${e.message}")
}
azureAsrHelper.enableRecord(audioSourceType, filePath, if(acceptAudioData) {
// 创建音频数据回调,将音频数据发送到 Flutter 层
object : SimpleAudioReceiver.AudioDataCallback {
@ -631,12 +638,20 @@ class AzureSpeechPlugin : BleService.Callback, FlutterPlugin {
try {
if(azureAsrHelper.audioSourceType == AzureAsrHelper.AudioSourceType.EXTERNAL){
isRecord = false
recordfile?.closeFile(true)
recordfile?.closeFile(isSave)
}
else
{
azureAsrHelper.stopRecord(isSave)
}
if (!azureAsrHelper.isContinuousRecognitionActive()) {
try {
AudioRecordingForegroundService.stopService(context)
} catch (e: Exception) {
FileLogger.w(tag, "停止录音前台服务失败: ${e.message}")
}
}
result.success(true)
} catch (e: Exception) {
result.error("ENABLERECORD_ERROR", e.message, null)

Loading…
Cancel
Save