record_handler.dart 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331
  1. import 'dart:async';
  2. import 'dart:io';
  3. import 'dart:typed_data';
  4. import 'package:custom_notification/custom_notification.dart';
  5. import 'package:electronic_assistant/module/record/record_task.dart';
  6. import 'package:electronic_assistant/router/app_pages.dart';
  7. import 'package:flutter/cupertino.dart';
  8. import 'package:get/get.dart';
  9. import 'package:path_provider/path_provider.dart';
  10. import 'package:flutter_foreground_task/flutter_foreground_task.dart';
  11. import 'package:get/get_rx/src/rx_types/rx_types.dart';
  12. import 'package:record/record.dart';
  13. import 'package:uuid/uuid.dart';
  14. import 'package:wakelock_plus/wakelock_plus.dart';
  15. import '../../data/bean/talks.dart';
  16. import '../../data/consts/error_code.dart';
  17. import '../../data/consts/event_report_id.dart';
  18. import '../../data/repositories/talk_repository.dart';
  19. import '../../resource/string.gen.dart';
  20. import '../../utils/http_handler.dart';
  21. import '../../utils/mmkv_util.dart';
  22. import '../../utils/notification_util.dart';
  23. import '../../utils/pcm_wav_converter.dart';
  24. import '../../utils/toast_util.dart';
  25. import '../talk/view.dart';
  26. import 'constants.dart';
  27. class RecordHandler {
  28. RecordHandler._();
  29. static const int minRecordDuration = 3;
  30. static const String recordDone = 'done';
  31. static const String recordPause = 'pause';
  32. static const String recordRecording = 'recording';
  33. static const String keyLastRecordId = "last_record_id";
  34. final Rx<RecordStatus> currentStatus = RecordStatus.pending.obs;
  35. final RxDouble currentDuration = 0.0.obs;
  36. final AudioRecorder _record = AudioRecorder();
  37. final RecordConfig _recordConfig = RecordConfig(
  38. encoder: AudioEncoder.pcm16bits,
  39. bitRate: 16000,
  40. sampleRate: SampleRate.rate32k.value,
  41. numChannels: Channel.mono.value,
  42. );
  43. String? _lastRecordId;
  44. final int _serviceId = 256;
  45. final String _channelId = StringName.recordNotificationChannelId.tr;
  46. final String _channelName = StringName.recordNotificationChannelName.tr;
  47. String get lastRecordId => _lastRecordId ?? '';
  48. StreamSubscription? _currentDurationListener;
  49. StreamSubscription? _recordActionListener;
  50. void init() {
  51. if (currentStatus.value != RecordStatus.recording) {
  52. _initLastRecordId();
  53. _initLastRecordStatus();
  54. _initForegroundService();
  55. _initRecordDurationStream();
  56. }
  57. }
  58. void _initRecordDurationStream() {
  59. _currentDurationListener?.cancel();
  60. _currentDurationListener = currentDuration.listen((event) {
  61. debugPrint('currentDuration: $event');
  62. if (currentStatus.value == RecordStatus.pending) {
  63. return;
  64. }
  65. NotificationUtil.showRecordNotification(
  66. _serviceId, currentStatus.value == RecordStatus.recording, event,
  67. channelId: _channelId, channelName: _channelName);
  68. });
  69. _recordActionListener?.cancel();
  70. _recordActionListener =
  71. CustomNotification.recordActionStream().listen((action) {
  72. if (action == recordDone) {
  73. _recordNotificationDone();
  74. } else if (action == recordPause) {
  75. stopRecord();
  76. } else if (action == recordRecording) {
  77. startOrContinueRecord();
  78. }
  79. });
  80. }
  81. void _recordNotificationDone() {
  82. saveCurrentRecord().then((talkInfo) {
  83. if (Get.currentRoute == RoutePath.record) {
  84. Get.back();
  85. }
  86. TalkPage.start(talkInfo, eventTag: EventId.id_001);
  87. }).catchError((error) {
  88. if (error is ServerErrorException) {
  89. if (error.code == ErrorCode.errorCodeNoLogin) {
  90. ToastUtil.showToast("录音已保存,请登录");
  91. } else {
  92. ToastUtil.showToast("${error.message}");
  93. }
  94. } else {
  95. ToastUtil.showToast("录音已保存,请检查网络并重试");
  96. }
  97. });
  98. }
  99. void _initLastRecordId() {
  100. String? lastRecordId = KVUtil.getString(keyLastRecordId, null);
  101. if (lastRecordId == null || lastRecordId.isEmpty) {
  102. _lastRecordId = const Uuid().v4();
  103. KVUtil.putString(keyLastRecordId, _lastRecordId);
  104. } else {
  105. _lastRecordId = lastRecordId;
  106. }
  107. }
  108. Future<void> _initLastRecordStatus() async {
  109. var currentRecordFile = await _getCurrentRecordFile();
  110. var fileLength = currentRecordFile.lengthSync();
  111. if (currentRecordFile.existsSync() && fileLength > 0) {
  112. _changeRecordStatus(RecordStatus.paused);
  113. double time = _getPcmDuration(
  114. fileLength, _recordConfig.sampleRate, 16, _recordConfig.numChannels);
  115. currentDuration.value = time;
  116. } else {
  117. currentDuration.value = 0;
  118. }
  119. }
  120. double _getPcmDuration(
  121. int fileSize, int sampleRate, int bitDepth, int channels) {
  122. final bytesPerSecond = sampleRate * (bitDepth / 8) * channels;
  123. final durationInSeconds = fileSize / bytesPerSecond;
  124. return durationInSeconds;
  125. }
  126. Future<File> _getCurrentRecordFile() async {
  127. Directory documentDir = await getApplicationDocumentsDirectory();
  128. File file = File("${documentDir.path}/.atmob/record/$_lastRecordId");
  129. if (!file.existsSync()) {
  130. file.createSync(recursive: true);
  131. }
  132. return file;
  133. }
  134. Future<void> stopRecord({bool? isStopService}) async {
  135. _releaseWakeLock();
  136. if (await _record.isRecording()) {
  137. await _record.stop();
  138. }
  139. _changeRecordStatus(RecordStatus.paused);
  140. if (isStopService == true) {
  141. await FlutterForegroundTask.stopService();
  142. }
  143. }
  144. void _changeRecordStatus(RecordStatus status) {
  145. currentStatus.value = status;
  146. if (status == RecordStatus.pending) {
  147. return;
  148. }
  149. NotificationUtil.showRecordNotification(
  150. _serviceId, status == RecordStatus.recording, currentDuration.value,
  151. channelId: _channelId, channelName: _channelName);
  152. }
  153. void _setWakeLock() {
  154. WakelockPlus.enable();
  155. }
  156. void _releaseWakeLock() {
  157. WakelockPlus.disable();
  158. }
  159. _initForegroundService() {
  160. WidgetsBinding.instance
  161. .addPostFrameCallback((_) => FlutterForegroundTask.init(
  162. androidNotificationOptions: AndroidNotificationOptions(
  163. channelId: _channelId,
  164. channelName: _channelName,
  165. channelDescription:
  166. StringName.recordNotificationChannelDescription.tr,
  167. channelImportance: NotificationChannelImportance.LOW,
  168. priority: NotificationPriority.LOW,
  169. ),
  170. iosNotificationOptions: const IOSNotificationOptions(
  171. showNotification: false,
  172. playSound: false,
  173. ),
  174. foregroundTaskOptions: ForegroundTaskOptions(
  175. eventAction: ForegroundTaskEventAction.once(),
  176. autoRunOnBoot: false,
  177. autoRunOnMyPackageReplaced: true,
  178. allowWakeLock: true,
  179. allowWifiLock: false,
  180. ),
  181. ));
  182. }
  183. _onRecordPermissionDenied() {
  184. ToastUtil.showToast("需要授予录音权限才能使用录音功能");
  185. }
  186. Future<void> startOrContinueRecord() async {
  187. bool hasPermission = await _record.hasPermission();
  188. if (!hasPermission) {
  189. _onRecordPermissionDenied();
  190. return;
  191. }
  192. await _requestForegroundTaskPermission().catchError((error) {
  193. debugPrint("requestForegroundTaskPermission error: $error");
  194. });
  195. File targetFile = await _getCurrentRecordFile();
  196. Stream<Uint8List> recordStream = await _record.startStream(_recordConfig);
  197. _setWakeLock();
  198. _startForegroundService();
  199. if (currentStatus.value != RecordStatus.recording) {
  200. _changeRecordStatus(RecordStatus.recording);
  201. }
  202. recordStream.listen((data) async {
  203. if (data.isEmpty) {
  204. return;
  205. }
  206. targetFile.writeAsBytesSync(data, mode: FileMode.append);
  207. currentDuration.value = currentDuration.value +
  208. _getPcmDuration(data.length, _recordConfig.sampleRate, 16,
  209. _recordConfig.numChannels);
  210. }, onDone: () {
  211. _changeRecordStatus(RecordStatus.paused);
  212. }, onError: (error) {
  213. _changeRecordStatus(RecordStatus.paused);
  214. });
  215. }
  216. Future<void> _requestForegroundTaskPermission() async {
  217. final NotificationPermission notificationPermission =
  218. await FlutterForegroundTask.checkNotificationPermission();
  219. if (notificationPermission != NotificationPermission.granted) {
  220. await FlutterForegroundTask.requestNotificationPermission();
  221. }
  222. if (Platform.isAndroid) {
  223. if (!await FlutterForegroundTask.isIgnoringBatteryOptimizations) {
  224. // This function requires `android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS` permission.
  225. await FlutterForegroundTask.requestIgnoreBatteryOptimization();
  226. }
  227. }
  228. }
  229. Future<void> deleteCurrentRecord() async {
  230. await stopRecord(isStopService: true);
  231. File file = await _getCurrentRecordFile();
  232. if (file.existsSync()) {
  233. file.deleteSync();
  234. }
  235. KVUtil.putString(keyLastRecordId, "");
  236. _changeRecordStatus(RecordStatus.pending);
  237. currentDuration.value = 0;
  238. }
  239. Future<void> _startForegroundService() async {
  240. final isRunningService = await FlutterForegroundTask.isRunningService;
  241. if (isRunningService) {
  242. return;
  243. }
  244. await FlutterForegroundTask.startService(
  245. serviceId: _serviceId,
  246. notificationTitle: StringName.appName.tr,
  247. notificationText: StringName.recordStatusRecording.tr,
  248. notificationIcon: null,
  249. notificationButtons: [],
  250. callback: setRecordCallback,
  251. );
  252. // NotificationUtil.showRecordNotification(
  253. // _serviceId, true, currentDuration.value,
  254. // channelId: _channelId, channelName: _channelName);
  255. }
  256. /// 判断是否有未上传的录音
  257. static Future<bool> hasUnUploadRecord() async {
  258. String? lastRecordId = KVUtil.getString(keyLastRecordId, null);
  259. if (lastRecordId == null || lastRecordId.isEmpty) {
  260. return false;
  261. }
  262. Directory documentDir = await getApplicationDocumentsDirectory();
  263. File file = File("${documentDir.path}/.atmob/record/$lastRecordId");
  264. return await file.exists() && await file.length() > 0;
  265. }
  266. void onClose() async {}
  267. Future<void> getConvertWavFile(String talkId) async {
  268. File pcmFile = await _getCurrentRecordFile();
  269. if (pcmFile.existsSync()) {
  270. File wavFile = await getRecordFile(talkId);
  271. PcmWavConverter.convert(pcmFile, wavFile, _recordConfig.sampleRate,
  272. _recordConfig.numChannels, 16);
  273. pcmFile.delete();
  274. KVUtil.putString(keyLastRecordId, "");
  275. } else {
  276. throw Exception("pcm file not found");
  277. }
  278. }
  279. /// 获取录音文件地址
  280. static Future<File> getRecordFile(String talkId) async {
  281. Directory documentDir = await getApplicationDocumentsDirectory();
  282. return File("${documentDir.path}/.atmob/record/$talkId.wav");
  283. }
  284. Future<TalkBean> saveCurrentRecord() async {
  285. final currentDurationValue = currentDuration.value;
  286. if (currentDurationValue < minRecordDuration) {
  287. throw ServerErrorException(-1, "录音时长不足$minRecordDuration秒");
  288. }
  289. await recordHandler.stopRecord(isStopService: true);
  290. return talkRepository
  291. .talkCreate(recordHandler.lastRecordId, currentDuration.value.toInt())
  292. .then((talkInfo) async {
  293. await recordHandler.getConvertWavFile(talkInfo.id);
  294. return talkInfo;
  295. });
  296. }
  297. }
  298. final recordHandler = RecordHandler._();