record_handler.dart 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329
  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) async {
  61. if (currentStatus.value == RecordStatus.pending ||
  62. !await FlutterForegroundTask.isRunningService) {
  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) async {
  145. currentStatus.value = status;
  146. if (status == RecordStatus.pending ||
  147. !await FlutterForegroundTask.isRunningService) {
  148. return;
  149. }
  150. NotificationUtil.showRecordNotification(
  151. _serviceId, status == RecordStatus.recording, currentDuration.value,
  152. channelId: _channelId, channelName: _channelName);
  153. }
  154. void _setWakeLock() {
  155. WakelockPlus.enable();
  156. }
  157. void _releaseWakeLock() {
  158. WakelockPlus.disable();
  159. }
  160. _initForegroundService() {
  161. WidgetsBinding.instance
  162. .addPostFrameCallback((_) => FlutterForegroundTask.init(
  163. androidNotificationOptions: AndroidNotificationOptions(
  164. channelId: _channelId,
  165. channelName: _channelName,
  166. channelDescription:
  167. StringName.recordNotificationChannelDescription.tr,
  168. channelImportance: NotificationChannelImportance.LOW,
  169. priority: NotificationPriority.LOW,
  170. ),
  171. iosNotificationOptions: const IOSNotificationOptions(
  172. showNotification: false,
  173. playSound: false,
  174. ),
  175. foregroundTaskOptions: ForegroundTaskOptions(
  176. eventAction: ForegroundTaskEventAction.once(),
  177. autoRunOnBoot: false,
  178. autoRunOnMyPackageReplaced: true,
  179. allowWakeLock: true,
  180. allowWifiLock: false,
  181. ),
  182. ));
  183. }
  184. _onRecordPermissionDenied() {
  185. ToastUtil.showToast("需要授予录音权限才能使用录音功能");
  186. }
  187. Future<void> startOrContinueRecord() async {
  188. bool hasPermission = await _record.hasPermission();
  189. if (!hasPermission) {
  190. _onRecordPermissionDenied();
  191. return;
  192. }
  193. await _requestForegroundTaskPermission().catchError((error) {
  194. debugPrint("requestForegroundTaskPermission error: $error");
  195. });
  196. File targetFile = await _getCurrentRecordFile();
  197. Stream<Uint8List> recordStream = await _record.startStream(_recordConfig);
  198. _setWakeLock();
  199. _startForegroundService();
  200. if (currentStatus.value != RecordStatus.recording) {
  201. _changeRecordStatus(RecordStatus.recording);
  202. }
  203. recordStream.listen((data) async {
  204. if (data.isEmpty) {
  205. return;
  206. }
  207. targetFile.writeAsBytesSync(data, mode: FileMode.append);
  208. currentDuration.value = currentDuration.value +
  209. _getPcmDuration(data.length, _recordConfig.sampleRate, 16,
  210. _recordConfig.numChannels);
  211. }, onDone: () {
  212. _changeRecordStatus(RecordStatus.paused);
  213. }, onError: (error) {
  214. _changeRecordStatus(RecordStatus.paused);
  215. });
  216. }
  217. Future<void> _requestForegroundTaskPermission() async {
  218. final NotificationPermission notificationPermission =
  219. await FlutterForegroundTask.checkNotificationPermission();
  220. if (notificationPermission != NotificationPermission.granted) {
  221. await FlutterForegroundTask.requestNotificationPermission();
  222. }
  223. if (Platform.isAndroid) {
  224. if (!await FlutterForegroundTask.isIgnoringBatteryOptimizations) {
  225. // This function requires `android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS` permission.
  226. await FlutterForegroundTask.requestIgnoreBatteryOptimization();
  227. }
  228. }
  229. }
  230. Future<void> deleteCurrentRecord() async {
  231. await stopRecord(isStopService: true);
  232. File file = await _getCurrentRecordFile();
  233. if (file.existsSync()) {
  234. file.deleteSync();
  235. }
  236. KVUtil.putString(keyLastRecordId, "");
  237. _changeRecordStatus(RecordStatus.pending);
  238. currentDuration.value = 0;
  239. }
  240. Future<void> _startForegroundService() async {
  241. final isRunningService = await FlutterForegroundTask.isRunningService;
  242. if (isRunningService) {
  243. return;
  244. }
  245. await FlutterForegroundTask.startService(
  246. serviceId: _serviceId,
  247. notificationTitle: StringName.appName.tr,
  248. notificationText: StringName.recordStatusRecording.tr,
  249. notificationIcon: null,
  250. notificationButtons: [],
  251. callback: setRecordCallback,
  252. );
  253. }
  254. /// 判断是否有未上传的录音
  255. static Future<bool> hasUnUploadRecord() async {
  256. String? lastRecordId = KVUtil.getString(keyLastRecordId, null);
  257. if (lastRecordId == null || lastRecordId.isEmpty) {
  258. return false;
  259. }
  260. Directory documentDir = await getApplicationDocumentsDirectory();
  261. File file = File("${documentDir.path}/.atmob/record/$lastRecordId");
  262. return await file.exists() && await file.length() > 0;
  263. }
  264. void onClose() async {}
  265. Future<void> getConvertWavFile(String talkId) async {
  266. File pcmFile = await _getCurrentRecordFile();
  267. if (pcmFile.existsSync()) {
  268. File wavFile = await getRecordFile(talkId);
  269. PcmWavConverter.convert(pcmFile, wavFile, _recordConfig.sampleRate,
  270. _recordConfig.numChannels, 16);
  271. pcmFile.delete();
  272. KVUtil.putString(keyLastRecordId, "");
  273. } else {
  274. throw Exception("pcm file not found");
  275. }
  276. }
  277. /// 获取录音文件地址
  278. static Future<File> getRecordFile(String talkId) async {
  279. Directory documentDir = await getApplicationDocumentsDirectory();
  280. return File("${documentDir.path}/.atmob/record/$talkId.wav");
  281. }
  282. Future<TalkBean> saveCurrentRecord() async {
  283. final currentDurationValue = currentDuration.value;
  284. if (currentDurationValue < minRecordDuration) {
  285. throw ServerErrorException(-1, "录音时长不足$minRecordDuration秒");
  286. }
  287. await recordHandler.stopRecord(isStopService: true);
  288. return talkRepository
  289. .talkCreate(recordHandler.lastRecordId, currentDuration.value.toInt())
  290. .then((talkInfo) async {
  291. await recordHandler.getConvertWavFile(talkInfo.id);
  292. return talkInfo;
  293. });
  294. }
  295. }
  296. final recordHandler = RecordHandler._();