|
|
|
@ -31,6 +31,8 @@ enum TimerStatus { |
|
|
|
class MeetingRecordController extends GetxController |
|
|
|
with GetTickerProviderStateMixin, WidgetsBindingObserver { |
|
|
|
static const int _backgroundRecordingNotificationId = 91001; |
|
|
|
static const Duration _stopRecordTimeout = Duration(seconds: 12); |
|
|
|
static const Duration _fileOpTimeout = Duration(seconds: 8); |
|
|
|
|
|
|
|
static FlutterLocalNotificationsPlugin? _localNotificationsPlugin; |
|
|
|
final NativePlugin _nativePlugin = NativePlugin(); |
|
|
|
@ -160,11 +162,18 @@ class MeetingRecordController extends GetxController |
|
|
|
final typeText = _getRecordingTypeText(); |
|
|
|
final durationText = _formatTimeForFloatingBar(milliseconds.value); |
|
|
|
|
|
|
|
await _nativePlugin.showRecordingFloatingBar( |
|
|
|
duration: durationText, |
|
|
|
recordingType: typeText, |
|
|
|
isPaused: timerStatus.value == TimerStatus.paused, |
|
|
|
); |
|
|
|
try { |
|
|
|
await _nativePlugin |
|
|
|
.showRecordingFloatingBar( |
|
|
|
duration: durationText, |
|
|
|
recordingType: typeText, |
|
|
|
isPaused: timerStatus.value == TimerStatus.paused, |
|
|
|
) |
|
|
|
.timeout(const Duration(seconds: 3)); |
|
|
|
} catch (e) { |
|
|
|
Logger.error('显示录音悬浮条失败: ${e.toString()}'); |
|
|
|
return; |
|
|
|
} |
|
|
|
|
|
|
|
_recordingFloatingBarTimer?.cancel(); |
|
|
|
_recordingFloatingBarTimer = |
|
|
|
@ -185,7 +194,13 @@ class MeetingRecordController extends GetxController |
|
|
|
if (!Platform.isAndroid) return; |
|
|
|
_recordingFloatingBarTimer?.cancel(); |
|
|
|
_recordingFloatingBarTimer = null; |
|
|
|
await _nativePlugin.hideRecordingFloatingBar(); |
|
|
|
try { |
|
|
|
await _nativePlugin |
|
|
|
.hideRecordingFloatingBar() |
|
|
|
.timeout(const Duration(seconds: 3)); |
|
|
|
} catch (e) { |
|
|
|
Logger.error('隐藏录音悬浮条失败: ${e.toString()}'); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
Future<void> togglePauseResumeFromFloatingBar() async { |
|
|
|
@ -246,6 +261,7 @@ class MeetingRecordController extends GetxController |
|
|
|
final RxBool isMarked = false.obs; |
|
|
|
final RxBool showTextContent = false.obs; |
|
|
|
final RxBool isRecording = false.obs; |
|
|
|
final RxBool isSaving = false.obs; |
|
|
|
//final RxBool isVoiceRecognitionActive = false.obs; |
|
|
|
final RxDouble rms = 0.0.obs; |
|
|
|
|
|
|
|
@ -607,62 +623,152 @@ class MeetingRecordController extends GetxController |
|
|
|
} |
|
|
|
|
|
|
|
/// 保存录音并清理相关资源 |
|
|
|
Future<void> saveRecording() async { |
|
|
|
resetTimer(); |
|
|
|
Future<bool> saveRecording() async { |
|
|
|
if (isSaving.value) return false; |
|
|
|
if (!isRecording.value) return true; |
|
|
|
isSaving.value = true; |
|
|
|
|
|
|
|
final prevStatus = timerStatus.value; |
|
|
|
pauseTimer(); |
|
|
|
final prevElapsedMs = _elapsedMilliseconds; |
|
|
|
_stopWaveformTimer(); |
|
|
|
if (audioType.value != 0) { |
|
|
|
_bleManager.closeCodec(); |
|
|
|
} |
|
|
|
if (!isRecording.value) return; |
|
|
|
await _asrService.stopRecord(true); |
|
|
|
await clearBackgroundRecordingIndicator(); |
|
|
|
|
|
|
|
// 如果标题被修改则重命名文件 |
|
|
|
if (fileName.value != newName.value) { |
|
|
|
final appDir = await appDocDir; |
|
|
|
final dir = Directory("${appDir.path}/MeetingAudio/"); |
|
|
|
if (!await dir.exists()) await dir.create(recursive: true); |
|
|
|
if (isMarked.value) { |
|
|
|
final newDir = Directory("${appDir.path}/DestAudio/"); |
|
|
|
if (!await newDir.exists()) await newDir.create(recursive: true); |
|
|
|
await _asrService.moveFile( |
|
|
|
"${dir.path}${fileName.value}.wav", |
|
|
|
"${newDir.path}${fileName.value}.wav", |
|
|
|
); |
|
|
|
} else { |
|
|
|
await _asrService.renameFile( |
|
|
|
"${dir.path}${fileName.value}.wav", |
|
|
|
newName.value, |
|
|
|
); |
|
|
|
|
|
|
|
try { |
|
|
|
final bool stopped = await _asrService |
|
|
|
.stopRecord(true) |
|
|
|
.timeout(_stopRecordTimeout, onTimeout: () => false); |
|
|
|
if (!stopped) { |
|
|
|
_restoreRecordingUiAfterFailedStop(prevStatus, prevElapsedMs); |
|
|
|
Get.snackbar('error'.tr, 'meetingRecordStopTimeout'.tr); |
|
|
|
return false; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
_resetToDefaultState(); |
|
|
|
isRecording.value = false; |
|
|
|
rms.value = 0.0; |
|
|
|
_smoothedRms = 0.0; |
|
|
|
_latestRms = 0.0; |
|
|
|
_resetWaveform(); |
|
|
|
await _refreshRecordingIndicator(); |
|
|
|
if (audioType.value != 0) { |
|
|
|
_bleManager.closeCodec(); |
|
|
|
} |
|
|
|
|
|
|
|
try { |
|
|
|
await clearBackgroundRecordingIndicator() |
|
|
|
.timeout(const Duration(seconds: 3)); |
|
|
|
} catch (e) { |
|
|
|
Logger.error('清理录音指示器失败: ${e.toString()}'); |
|
|
|
} |
|
|
|
|
|
|
|
try { |
|
|
|
await _applyTitleChangeAfterSave(); |
|
|
|
} catch (e) { |
|
|
|
Logger.error('保存录音后的文件处理失败: ${e.toString()}'); |
|
|
|
} |
|
|
|
|
|
|
|
resetTimer(); |
|
|
|
_resetToDefaultState(); |
|
|
|
isRecording.value = false; |
|
|
|
rms.value = 0.0; |
|
|
|
_smoothedRms = 0.0; |
|
|
|
_latestRms = 0.0; |
|
|
|
_resetWaveform(); |
|
|
|
await _refreshRecordingIndicator(); |
|
|
|
return true; |
|
|
|
} catch (e) { |
|
|
|
Logger.error('保存录音失败: ${e.toString()}'); |
|
|
|
_restoreRecordingUiAfterFailedStop(prevStatus, prevElapsedMs); |
|
|
|
Get.snackbar('error'.tr, 'meetingRecordSaveFailed'.tr); |
|
|
|
return false; |
|
|
|
} finally { |
|
|
|
isSaving.value = false; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
Future<void> discardRecording() async { |
|
|
|
resetTimer(); |
|
|
|
Future<bool> discardRecording() async { |
|
|
|
if (!isRecording.value) return true; |
|
|
|
if (isSaving.value) return false; |
|
|
|
isSaving.value = true; |
|
|
|
|
|
|
|
final prevStatus = timerStatus.value; |
|
|
|
pauseTimer(); |
|
|
|
final prevElapsedMs = _elapsedMilliseconds; |
|
|
|
_stopWaveformTimer(); |
|
|
|
if (audioType.value != 0) { |
|
|
|
_bleManager.closeCodec(); |
|
|
|
|
|
|
|
try { |
|
|
|
final bool stopped = await _asrService |
|
|
|
.stopRecord(false) |
|
|
|
.timeout(_stopRecordTimeout, onTimeout: () => false); |
|
|
|
if (!stopped) { |
|
|
|
_restoreRecordingUiAfterFailedStop(prevStatus, prevElapsedMs); |
|
|
|
Get.snackbar('error'.tr, 'meetingRecordStopTimeout'.tr); |
|
|
|
return false; |
|
|
|
} |
|
|
|
|
|
|
|
if (audioType.value != 0) { |
|
|
|
_bleManager.closeCodec(); |
|
|
|
} |
|
|
|
|
|
|
|
try { |
|
|
|
await clearBackgroundRecordingIndicator() |
|
|
|
.timeout(const Duration(seconds: 3)); |
|
|
|
} catch (e) { |
|
|
|
Logger.error('清理录音指示器失败: ${e.toString()}'); |
|
|
|
} |
|
|
|
|
|
|
|
resetTimer(); |
|
|
|
_resetToDefaultState(); |
|
|
|
isRecording.value = false; |
|
|
|
rms.value = 0.0; |
|
|
|
_smoothedRms = 0.0; |
|
|
|
_latestRms = 0.0; |
|
|
|
_resetWaveform(); |
|
|
|
await _refreshRecordingIndicator(); |
|
|
|
return true; |
|
|
|
} catch (e) { |
|
|
|
Logger.error('结束录音不保存失败: ${e.toString()}'); |
|
|
|
_restoreRecordingUiAfterFailedStop(prevStatus, prevElapsedMs); |
|
|
|
Get.snackbar('error'.tr, 'meetingRecordStopFailed'.tr); |
|
|
|
return false; |
|
|
|
} finally { |
|
|
|
isSaving.value = false; |
|
|
|
} |
|
|
|
if (isRecording.value) { |
|
|
|
await _asrService.stopRecord(false); |
|
|
|
} |
|
|
|
|
|
|
|
void _restoreRecordingUiAfterFailedStop( |
|
|
|
TimerStatus prevStatus, |
|
|
|
int prevElapsedMs, |
|
|
|
) { |
|
|
|
_elapsedMilliseconds = prevElapsedMs; |
|
|
|
seconds.value = (prevElapsedMs / 1000).round(); |
|
|
|
milliseconds.value = prevElapsedMs; |
|
|
|
if (prevStatus == TimerStatus.running) { |
|
|
|
timerStatus.value = TimerStatus.stopped; |
|
|
|
startTimer(); |
|
|
|
} else { |
|
|
|
timerStatus.value = prevStatus; |
|
|
|
} |
|
|
|
await clearBackgroundRecordingIndicator(); |
|
|
|
_resetToDefaultState(); |
|
|
|
isRecording.value = false; |
|
|
|
rms.value = 0.0; |
|
|
|
_smoothedRms = 0.0; |
|
|
|
_latestRms = 0.0; |
|
|
|
_resetWaveform(); |
|
|
|
await _refreshRecordingIndicator(); |
|
|
|
_startWaveformTimer(); |
|
|
|
} |
|
|
|
|
|
|
|
Future<void> _applyTitleChangeAfterSave() async { |
|
|
|
if (fileName.value == newName.value) return; |
|
|
|
final appDir = await appDocDir; |
|
|
|
final dir = Directory("${appDir.path}/MeetingAudio/"); |
|
|
|
if (!await dir.exists()) await dir.create(recursive: true); |
|
|
|
|
|
|
|
if (isMarked.value) { |
|
|
|
final newDir = Directory("${appDir.path}/DestAudio/"); |
|
|
|
if (!await newDir.exists()) await newDir.create(recursive: true); |
|
|
|
await _asrService |
|
|
|
.moveFile( |
|
|
|
"${dir.path}${fileName.value}.wav", |
|
|
|
"${newDir.path}${fileName.value}.wav", |
|
|
|
) |
|
|
|
.timeout(_fileOpTimeout); |
|
|
|
return; |
|
|
|
} |
|
|
|
|
|
|
|
await _asrService |
|
|
|
.renameFile( |
|
|
|
"${dir.path}${fileName.value}.wav", |
|
|
|
newName.value, |
|
|
|
) |
|
|
|
.timeout(_fileOpTimeout); |
|
|
|
} |
|
|
|
|
|
|
|
/// 将控制器重置到初始状态 |
|
|
|
|