import 'dart:io'; import 'dart:typed_data'; import 'package:electronic_assistant/module/record/record_task.dart'; import 'package:flutter/cupertino.dart'; import 'package:get/get.dart'; import 'package:path_provider/path_provider.dart'; import 'package:flutter_foreground_task/flutter_foreground_task.dart'; import 'package:get/get_rx/src/rx_types/rx_types.dart'; import 'package:record/record.dart'; import 'package:uuid/uuid.dart'; import 'package:wakelock_plus/wakelock_plus.dart'; import '../../resource/string.gen.dart'; import '../../utils/mmkv_util.dart'; import '../../utils/pcm_wav_converter.dart'; import '../../utils/toast_util.dart'; import 'constants.dart'; class RecordHandler { RecordHandler._(); static const String keyLastRecordId = "last_record_id"; final Rx currentStatus = RecordStatus.pending.obs; final RxDouble currentDuration = 0.0.obs; AudioRecorder? _record; final RecordConfig _recordConfig = RecordConfig( encoder: AudioEncoder.pcm16bits, bitRate: 16000, sampleRate: SampleRate.rate32k.value, numChannels: Channel.mono.value, ); String? _lastRecordId; String get lastRecordId => _lastRecordId ?? ''; void init() { if (currentStatus.value != RecordStatus.recording) { _record = AudioRecorder(); _initLastRecordId(); _initLastRecordStatus(); _initForegroundService(); } } void _initLastRecordId() { String? lastRecordId = KVUtil.getString(keyLastRecordId, null); if (lastRecordId == null || lastRecordId.isEmpty) { _lastRecordId = const Uuid().v4(); KVUtil.putString(keyLastRecordId, _lastRecordId); } else { _lastRecordId = lastRecordId; } } Future _initLastRecordStatus() async { var currentRecordFile = await _getCurrentRecordFile(); var fileLength = currentRecordFile.lengthSync(); if (currentRecordFile.existsSync() && fileLength > 0) { _changeRecordStatus(RecordStatus.paused); currentDuration.value = _getPcmDuration( fileLength, _recordConfig.sampleRate, 16, _recordConfig.numChannels); } else { currentDuration.value = 0; } } double _getPcmDuration( int fileSize, int sampleRate, int bitDepth, int channels) { final bytesPerSecond = sampleRate * (bitDepth / 8) * channels; final durationInSeconds = fileSize / bytesPerSecond; return durationInSeconds; } Future _getCurrentRecordFile() async { Directory documentDir = await getApplicationDocumentsDirectory(); File file = File("${documentDir.path}/.atmob/record/$_lastRecordId"); if (!file.existsSync()) { file.createSync(recursive: true); } return file; } Future stopRecord() async { _releaseWakeLock(); _record ?.stop() .then((_) => _changeRecordStatus(RecordStatus.paused)) .then((_) => FlutterForegroundTask.stopService()); ; } void _changeRecordStatus(RecordStatus status) { currentStatus.value = status; } void _setWakeLock() { WakelockPlus.enable(); } void _releaseWakeLock() { WakelockPlus.disable(); } _initForegroundService() { WidgetsBinding.instance .addPostFrameCallback((_) => FlutterForegroundTask.init( androidNotificationOptions: AndroidNotificationOptions( channelId: StringName.recordNotificationChannelId.tr, channelName: StringName.recordNotificationChannelName.tr, channelDescription: StringName.recordNotificationChannelDescription.tr, channelImportance: NotificationChannelImportance.LOW, priority: NotificationPriority.LOW, ), iosNotificationOptions: const IOSNotificationOptions( showNotification: false, playSound: false, ), foregroundTaskOptions: ForegroundTaskOptions( eventAction: ForegroundTaskEventAction.once(), autoRunOnBoot: false, autoRunOnMyPackageReplaced: true, allowWakeLock: true, allowWifiLock: false, ), )); } _onRecordPermissionDenied() { ToastUtil.showToast("需要授予录音权限才能使用录音功能"); } Future startOrContinueRecord() async { if (_record == null) { return; } bool hasPermission = await _record!.hasPermission(); if (!hasPermission) { _onRecordPermissionDenied(); return; } await _requestForegroundTaskPermission().catchError((error) { debugPrint("requestForegroundTaskPermission error: $error"); }); File targetFile = await _getCurrentRecordFile(); Stream recordStream = await _record!.startStream(_recordConfig); _setWakeLock(); _startForegroundService(); if (currentStatus.value != RecordStatus.recording) { _changeRecordStatus(RecordStatus.recording); } recordStream.listen((data) async { if (data.isEmpty) { return; } targetFile.writeAsBytesSync(data, mode: FileMode.append); currentDuration.value = currentDuration.value + _getPcmDuration(data.length, _recordConfig.sampleRate, 16, _recordConfig.numChannels); }, onDone: () { _changeRecordStatus(RecordStatus.paused); }, onError: (error) { _changeRecordStatus(RecordStatus.paused); }); } Future _requestForegroundTaskPermission() async { final NotificationPermission notificationPermission = await FlutterForegroundTask.checkNotificationPermission(); if (notificationPermission != NotificationPermission.granted) { await FlutterForegroundTask.requestNotificationPermission(); } if (Platform.isAndroid) { if (!await FlutterForegroundTask.isIgnoringBatteryOptimizations) { // This function requires `android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS` permission. await FlutterForegroundTask.requestIgnoreBatteryOptimization(); } } } Future deleteCurrentRecord() async { await stopRecord(); File file = await _getCurrentRecordFile(); if (file.existsSync()) { file.deleteSync(); } KVUtil.putString(keyLastRecordId, ""); currentDuration.value = 0; _changeRecordStatus(RecordStatus.pending); } Future _startForegroundService() async { if (await FlutterForegroundTask.isRunningService) { return FlutterForegroundTask.restartService(); } else { return FlutterForegroundTask.startService( serviceId: 256, notificationTitle: StringName.appName.tr, notificationText: StringName.recordStatusRecording.tr, notificationIcon: null, notificationButtons: [], callback: setRecordCallback, ); } } /// 判断是否有未上传的录音 static Future hasUnUploadRecord() async { String? lastRecordId = KVUtil.getString(keyLastRecordId, null); if (lastRecordId == null || lastRecordId.isEmpty) { return false; } Directory documentDir = await getApplicationDocumentsDirectory(); File file = File("${documentDir.path}/.atmob/record/$lastRecordId"); return await file.exists() && await file.length() > 0; } void onClose() async { if (currentStatus.value != RecordStatus.recording) { _record?.dispose(); } } Future getConvertWavFile(String talkId) async { File pcmFile = await _getCurrentRecordFile(); if (pcmFile.existsSync()) { File wavFile = await getRecordFile(talkId); PcmWavConverter.convert(pcmFile, wavFile, _recordConfig.sampleRate, _recordConfig.numChannels, 16); pcmFile.delete(); KVUtil.putString(keyLastRecordId, ""); } else { throw Exception("pcm file not found"); } } /// 获取录音文件地址 static Future getRecordFile(String talkId) async { Directory documentDir = await getApplicationDocumentsDirectory(); return File("${documentDir.path}/.atmob/record/$talkId.wav"); } } final recordHandler = RecordHandler._();