Bläddra i källkod

[new]优化录音文件保存流程

zk 11 månader sedan
förälder
incheckning
400b9375f4

+ 1 - 1
lib/data/api/network_module.dart

@@ -125,7 +125,7 @@ class _NetworkModule {
 
   static Dio _createFileDio() {
     Dio dio = Dio(BaseOptions(
-      sendTimeout: const Duration(seconds: 60 * 30), //半个小时
+      sendTimeout: const Duration(seconds: 60 * 60),
     ));
     dio.interceptors.add(PrettyDioLogger(
       requestHeader: true,

+ 6 - 1
lib/data/bean/talks.dart

@@ -23,6 +23,9 @@ class TalkBean {
   @JsonKey(name: 'characters')
   int? characters;
 
+  @JsonKey(name: 'requestId')
+  String? requestId;
+
   @JsonKey(name: 'localAudioUrl')
   String? localAudioUrl;
 
@@ -71,6 +74,7 @@ class TalkBean {
     this.createTime,
     this.isExample,
     this.oversizeFile,
+    this.requestId,
     this.uploadType,
     this.localAudioUrl,
     required this.progress,
@@ -95,6 +99,7 @@ class TalkBean {
     summary.value = talkBean.summary.value;
     templateId = talkBean.templateId;
     createTime = talkBean.createTime;
+    requestId = talkBean.requestId;
     isExample = talkBean.isExample;
     oversizeFile = talkBean.oversizeFile;
     uploadType = talkBean.uploadType;
@@ -105,7 +110,7 @@ class TalkBean {
 
   @override
   String toString() {
-    return 'TalkBean{id: $id, taskId: $taskId, ssid: $ssid, audioUrl: $audioUrl, duration: $duration, characters: $characters, localAudioUrl: $localAudioUrl, status: $status, title: $title, summary: $summary, createTime: $createTime, templateId: $templateId, isExample: $isExample, oversizeFile: $oversizeFile, uploadType: $uploadType, progress: $progress, progressContent: $progressContent}';
+    return 'TalkBean{id: $id, taskId: $taskId, requestId:$requestId , ssid: $ssid, audioUrl: $audioUrl, duration: $duration, characters: $characters, localAudioUrl: $localAudioUrl, status: $status, title: $title, summary: $summary, createTime: $createTime, templateId: $templateId, isExample: $isExample, oversizeFile: $oversizeFile, uploadType: $uploadType, progress: $progress, progressContent: $progressContent}';
   }
 }
 

+ 6 - 0
lib/data/repositories/talk_repository.dart

@@ -107,6 +107,12 @@ class TalkRepository {
   }
 
   void addNewTalkData(TalkBean talkInfo) {
+    for (int i = 0; i < _talkList.length; i++) {
+      if (_talkList[i].id == talkInfo.id) {
+        _talkList[i].updateBean(talkInfo);
+        return;
+      }
+    }
     _talkList.insert(0, talkInfo);
   }
 

+ 76 - 34
lib/module/record/record_handler.dart

@@ -2,6 +2,7 @@ import 'dart:async';
 import 'dart:io';
 import 'dart:typed_data';
 import 'package:custom_notification/custom_notification.dart';
+import 'package:electronic_assistant/dialog/loading_dialog.dart';
 import 'package:electronic_assistant/module/record/record_task.dart';
 import 'package:electronic_assistant/resource/colors.gen.dart';
 import 'package:electronic_assistant/router/app_pages.dart';
@@ -46,7 +47,7 @@ class RecordHandler {
   final FlutterSoundRecorder _soundPlayer = FlutterSoundRecorder();
   StreamController<Uint8List>? recordingDataController;
 
-  final RecordConfig _recordConfig = RecordConfig(
+  static final RecordConfig recordConfig = RecordConfig(
     codec: Codec.pcm16,
     sampleRate: SampleRate.rate44_1k.value,
     numChannels: Channel.mono.value,
@@ -131,7 +132,7 @@ class RecordHandler {
     if (currentRecordFile.existsSync() && fileLength > 0) {
       _changeRecordStatus(RecordStatus.paused);
       double time = _getPcmDuration(
-          fileLength, _recordConfig.sampleRate, 16, _recordConfig.numChannels);
+          fileLength, recordConfig.sampleRate, 16, recordConfig.numChannels);
       currentDuration.value = time;
     } else {
       currentDuration.value = 0;
@@ -269,8 +270,8 @@ class RecordHandler {
       }
       targetFile.writeAsBytesSync(data, mode: FileMode.append);
       currentDuration.value = currentDuration.value +
-          _getPcmDuration(data.length, _recordConfig.sampleRate, 16,
-              _recordConfig.numChannels);
+          _getPcmDuration(data.length, recordConfig.sampleRate, 16,
+              recordConfig.numChannels);
     }, onDone: () {
       _changeRecordStatus(RecordStatus.paused);
     }, onError: (error) {
@@ -280,9 +281,9 @@ class RecordHandler {
     _isSoundInited = true;
     await _soundPlayer.startRecorder(
         toStream: recordingDataController!.sink,
-        codec: _recordConfig.codec,
-        numChannels: _recordConfig.numChannels,
-        sampleRate: _recordConfig.sampleRate);
+        codec: recordConfig.codec,
+        numChannels: recordConfig.numChannels,
+        sampleRate: recordConfig.sampleRate);
     _setWakeLock();
     _startForegroundService();
     if (currentStatus.value != RecordStatus.recording) {
@@ -331,24 +332,6 @@ class RecordHandler {
     );
   }
 
-  /// 判断是否有未上传的录音
-  static Future<bool> 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) {
-      releaseSoundRecorder();
-      _cancelRecorderSubscriptions();
-    }
-  }
-
   void releaseSoundRecorder() {
     if (_isSoundInited == true) {
       _soundPlayer.closeRecorder();
@@ -365,19 +348,36 @@ class RecordHandler {
     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, "");
+      try {
+        LoadingDialog.show('正在保存录音,请稍等...');
+        await PcmWavConverter.convert(pcmFile, wavFile, recordConfig.sampleRate,
+            recordConfig.numChannels, 16);
+      } catch (e) {
+        LoadingDialog.hide();
+        throw ServerErrorException(-1, '录音文件保存失败');
+      } finally {
+        LoadingDialog.hide();
+      }
+      //检查pcm文件是否可删除
+      await checkRecordSourceCanDelete(pcmFile, wavFile);
     } else {
-      throw Exception("pcm file not found");
+      throw ServerErrorException(-1, "录音文件不存在");
     }
   }
 
-  /// 获取录音文件地址
-  static Future<File> getRecordFile(String talkId) async {
-    Directory documentDir = await getApplicationDocumentsDirectory();
-    return File("${documentDir.path}/.atmob/record/$talkId.wav");
+  Future<void> checkRecordSourceCanDelete(File pcmFile, File wavFile) async {
+    if (!pcmFile.existsSync() || !wavFile.existsSync()) {
+      throw ServerErrorException(-1, "录音文件不存在");
+    }
+    int wavFileLength = wavFile.lengthSync();
+    if (wavFileLength <= 44) {
+      throw ServerErrorException(-1, "录音文件异常");
+    }
+    if (wavFileLength < pcmFile.lengthSync()) {
+      throw ServerErrorException(-1, "录音文件未转换成功");
+    }
+    pcmFile.delete();
+    KVUtil.putString(keyLastRecordId, "");
   }
 
   Future<TalkBean> saveCurrentRecord() async {
@@ -386,6 +386,10 @@ class RecordHandler {
       throw ServerErrorException(-1, "录音时长不足$minRecordDuration秒");
     }
     await recordHandler.stopRecord(isStopService: true);
+    //检查录音文件
+    if (!await checkRecordFile()) {
+      throw ServerErrorException(-1, "录音文件不存在");
+    }
     return talkRepository
         .talkCreate(recordHandler.lastRecordId, currentDuration.value.toInt())
         .then((talkInfo) async {
@@ -393,6 +397,44 @@ class RecordHandler {
       return talkInfo;
     });
   }
+
+  Future<bool> checkRecordFile() async {
+    File pcmFile = await _getCurrentRecordFile();
+    if (pcmFile.existsSync()) {
+      return true;
+    }
+    return false;
+  }
+
+  void onClose() async {
+    if (currentStatus.value != RecordStatus.recording) {
+      releaseSoundRecorder();
+      _cancelRecorderSubscriptions();
+    }
+  }
+
+  //********************静态方法***************************/
+  /// 判断是否有未上传的录音
+  static Future<bool> 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;
+  }
+
+  /// 获取录音文件地址
+  static Future<File> getRecordFile(String talkId) async {
+    Directory documentDir = await getApplicationDocumentsDirectory();
+    return File("${documentDir.path}/.atmob/record/$talkId.wav");
+  }
+
+  static Future<File> getPcmRecordFile(String requestId) async {
+    Directory documentDir = await getApplicationDocumentsDirectory();
+    return File("${documentDir.path}/.atmob/record/$requestId");
+  }
 }
 
 class RecordConfig {

+ 49 - 1
lib/module/talk/controller.dart

@@ -51,6 +51,7 @@ import '../../dialog/rename_dialog.dart';
 import '../../dialog/talk_share_dialog.dart';
 import '../../utils/common_utils.dart';
 import '../../utils/event_bus.dart';
+import '../../utils/pcm_wav_converter.dart';
 import '../../utils/system_share_util.dart';
 import '../../utils/toast_util.dart';
 import 'package:webview_flutter/webview_flutter.dart';
@@ -226,6 +227,54 @@ class TalkController extends BaseController {
     super.onReady();
     _initAudioPlayer();
     eventReport(EventId.event_101001, params: {EventId.id: eventTag});
+    _checkTalkFileIntegrityTask();
+  }
+
+  _checkTalkFileIntegrityTask() async {
+    String? talkId = talkBean.value?.id;
+    String? requestId = talkBean.value?.requestId;
+    if (talkId == null || requestId == null) {
+      return;
+    }
+    File pcmFile = await RecordHandler.getPcmRecordFile(requestId);
+    File recordFile = await RecordHandler.getRecordFile(talkId);
+    //pcm源文件还存在,检查录音文件是否完成
+    if (pcmFile.existsSync()) {
+      if (recordFile.existsSync() &&
+          recordFile.lengthSync() > pcmFile.lengthSync()) {
+        pcmFile.delete();
+      } else {
+        //转换后的文件不完整,重新转换
+        reConvertFile(pcmFile, recordFile);
+      }
+    }
+  }
+
+  void reConvertFile(File pcmFile, File recordFile) async {
+    LoadingDialog.show('检测到文件未成功转换,请稍等..');
+    try {
+      await PcmWavConverter.convert(
+          pcmFile,
+          recordFile,
+          RecordHandler.recordConfig.sampleRate,
+          RecordHandler.recordConfig.numChannels,
+          16);
+    } catch (e) {
+      LoadingDialog.hide();
+      ToastUtil.showToast('转换异常:$e');
+      return;
+    } finally {
+      LoadingDialog.hide();
+    }
+    int wavFileLength = recordFile.lengthSync();
+    //转换成功
+    if (wavFileLength <= 44 || wavFileLength < pcmFile.lengthSync()) {
+      ToastUtil.showToast('转换异常');
+      return;
+    }
+    pcmFile.delete();
+    ToastUtil.showToast('转换成功');
+    _loadAudioFile(talkBean.value);
   }
 
   void _fitTabsBean(bool isIncludeMind, String? talkId) {
@@ -605,7 +654,6 @@ class TalkController extends BaseController {
     }
   }
 
-
   void onEditModelClick() async {
     if (!await checkLogin()) {
       return;

+ 14 - 6
lib/utils/pcm_wav_converter.dart

@@ -1,16 +1,24 @@
 import 'dart:io';
 
 class PcmWavConverter {
-  static convert(File pcmFile, File wavFile, int sampleRate, int channels,
-      int bitDepth) {
+  static Future<void> convert(File pcmFile, File wavFile, int sampleRate,
+      int channels, int bitDepth) async {
     int dataLength = pcmFile.lengthSync();
-    List<int> wavHeader = _createWavHeader(dataLength, sampleRate, channels, bitDepth);
+    List<int> wavHeader =
+        _createWavHeader(dataLength, sampleRate, channels, bitDepth);
     wavFile.writeAsBytesSync(wavHeader, mode: FileMode.write);
-    wavFile.writeAsBytesSync(pcmFile.readAsBytesSync(), mode: FileMode.append);
+    await _streamCopy(pcmFile, wavFile);
   }
 
-  static List<int> _createWavHeader(int dataLength, int sampleRate,
-      int channels, int bitDepth) {
+  static Future<void> _streamCopy(File source, File destination) async {
+    final sourceStream = source.openRead();
+    final destinationSink = destination.openWrite(mode: FileMode.append);
+
+    await sourceStream.pipe(destinationSink);
+  }
+
+  static List<int> _createWavHeader(
+      int dataLength, int sampleRate, int channels, int bitDepth) {
     int byteRate = sampleRate * channels * bitDepth ~/ 8;
     int wavSize = dataLength + 36;
     List<int> header = List<int>.filled(44, 0);