Browse Source

[New]新增后台录音相关实现

zhipeng 1 year ago
parent
commit
d59044fa82

+ 12 - 0
android/app/src/main/AndroidManifest.xml

@@ -6,9 +6,16 @@
     <uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
     <!--    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />-->
 
+    <!-- required -->
+    <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
+    <uses-permission android:name="android.permission.WAKE_LOCK" />
+    <uses-permission android:name="android.permission.FOREGROUND_SERVICE_MICROPHONE" />
+    <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
+
     <application
         android:name=".MyApplication"
         android:allowBackup="false"
+        android:enableOnBackInvokedCallback="true"
         android:icon="@mipmap/ic_launcher"
         android:label="@string/app_name"
         android:theme="@style/Theme.ElecAsst"
@@ -46,6 +53,11 @@
         <meta-data
             android:name="flutterEmbedding"
             android:value="2" />
+
+        <service
+            android:name="com.pravera.flutter_foreground_task.service.ForegroundService"
+            android:exported="false"
+            android:foregroundServiceType="microphone" />
     </application>
     <!-- Required to query activities that can process text, see:
          https://developer.android.com/training/package-visibility and

+ 10 - 0
ios/Runner/AppDelegate.swift

@@ -8,6 +8,16 @@ import UIKit
     didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
   ) -> Bool {
     GeneratedPluginRegistrant.register(with: self)
+
+    SwiftFlutterForegroundTaskPlugin.setPluginRegistrantCallback(registerPlugins)
+    if #available(iOS 10.0, *) {
+      UNUserNotificationCenter.current().delegate = self as? UNUserNotificationCenterDelegate
+    }
+
     return super.application(application, didFinishLaunchingWithOptions: launchOptions)
   }
 }
+
+func registerPlugins(registry: FlutterPluginRegistry) {
+  GeneratedPluginRegistrant.register(with: registry)
+}

+ 1 - 0
ios/Runner/Runner-Bridging-Header.h

@@ -1 +1,2 @@
 #import "GeneratedPluginRegistrant.h"
+#import <flutter_foreground_task/FlutterForegroundTaskPlugin.h>

+ 3 - 0
lib/main.dart

@@ -7,6 +7,7 @@ import 'package:electronic_assistant/router/app_pages.dart';
 import 'package:electronic_assistant/utils/app_info_util.dart';
 import 'package:electronic_assistant/utils/mmkv_util.dart';
 import 'package:flutter/material.dart';
+import 'package:flutter_foreground_task/flutter_foreground_task.dart';
 import 'package:flutter_screenutil/flutter_screenutil.dart';
 import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
 import 'package:get/get.dart';
@@ -16,6 +17,8 @@ import 'package:pull_to_refresh/pull_to_refresh.dart';
 void main() async {
   WidgetsFlutterBinding.ensureInitialized();
 
+  FlutterForegroundTask.initCommunicationPort();
+
   //全局配置smartDialog
   smartConfig();
   //获取包信息

+ 88 - 0
lib/module/record/constants.dart

@@ -10,6 +10,94 @@ enum RecordStatus {
   paused,
 }
 
+RecordStatus recordStatusFromName(String name) {
+  switch (name) {
+    case "pending":
+      return RecordStatus.pending;
+    case "recording":
+      return RecordStatus.recording;
+    case "paused":
+      return RecordStatus.paused;
+    default:
+      return RecordStatus.pending;
+  }
+}
+
+enum SampleRate {
+  rate8k,
+  rate12_8k,
+  rate16k,
+  rate22_05k,
+  rate24k,
+  rate32k,
+  rate44_1k,
+}
+
+enum Channel {
+  mono,
+  stereo,
+}
+
+extension SampleRateExtension on SampleRate {
+  int get value {
+    switch (this) {
+      case SampleRate.rate8k:
+        return 8000;
+      case SampleRate.rate12_8k:
+        return 12800;
+      case SampleRate.rate16k:
+        return 16000;
+      case SampleRate.rate22_05k:
+        return 22050;
+      case SampleRate.rate24k:
+        return 24000;
+      case SampleRate.rate32k:
+        return 32000;
+      case SampleRate.rate44_1k:
+        return 44100;
+    }
+  }
+
+  String get desc {
+    switch (this) {
+      case SampleRate.rate8k:
+        return "8k";
+      case SampleRate.rate12_8k:
+        return "12.8k";
+      case SampleRate.rate16k:
+        return "16k";
+      case SampleRate.rate22_05k:
+        return "22.05k";
+      case SampleRate.rate24k:
+        return "24k";
+      case SampleRate.rate32k:
+        return "32k";
+      case SampleRate.rate44_1k:
+        return "44.1k";
+    }
+  }
+}
+
+extension ChannelExtension on Channel {
+  int get value {
+    switch (this) {
+      case Channel.mono:
+        return 1;
+      case Channel.stereo:
+        return 2;
+    }
+  }
+
+  String get desc {
+    switch (this) {
+      case Channel.mono:
+        return "Mono";
+      case Channel.stereo:
+        return "Stereo";
+    }
+  }
+}
+
 extension RecordStatusExtension on RecordStatus {
   String get desc {
     switch (this) {

+ 81 - 10
lib/module/record/controller.dart

@@ -6,11 +6,15 @@ import 'package:electronic_assistant/data/consts/error_code.dart';
 import 'package:electronic_assistant/data/repositories/talk_repository.dart';
 import 'package:electronic_assistant/dialog/alert_dialog.dart';
 import 'package:electronic_assistant/module/record/constants.dart';
+import 'package:electronic_assistant/module/record/record_task.dart';
 import 'package:electronic_assistant/module/talk/view.dart';
+import 'package:electronic_assistant/resource/string.gen.dart';
 import 'package:electronic_assistant/router/app_pages.dart';
 import 'package:electronic_assistant/utils/http_handler.dart';
 import 'package:electronic_assistant/utils/mmkv_util.dart';
 import 'package:electronic_assistant/utils/toast_util.dart';
+import 'package:flutter/cupertino.dart';
+import 'package:flutter_foreground_task/flutter_foreground_task.dart';
 import 'package:get/get.dart';
 import 'package:path_provider/path_provider.dart';
 import 'package:record/record.dart';
@@ -28,11 +32,12 @@ class RecordController extends BaseController {
   final Rx<RecordStatus> currentStatus = RecordStatus.pending.obs;
   final RxDouble currentDuration = 0.0.obs;
   final AudioRecorder _record = AudioRecorder();
-  final RecordConfig _recordConfig = const RecordConfig(
-      encoder: AudioEncoder.pcm16bits,
-      bitRate: 128000,
-      sampleRate: 44100,
-      numChannels: 2);
+  final RecordConfig _recordConfig = RecordConfig(
+    encoder: AudioEncoder.pcm16bits,
+    bitRate: 16000,
+    sampleRate: SampleRate.rate32k.value,
+    numChannels: Channel.mono.value,
+  );
   late final String _lastRecordId;
 
   @override
@@ -40,6 +45,7 @@ class RecordController extends BaseController {
     super.onInit();
     _initLastRecordId();
     _initLastRecordStatus();
+    _initForegroundService();
   }
 
   @override
@@ -68,6 +74,31 @@ class RecordController extends BaseController {
     }
   }
 
+  _initForegroundService() {
+    WidgetsBinding.instance
+        .addPostFrameCallback((_) => FlutterForegroundTask.init(
+              androidNotificationOptions: AndroidNotificationOptions(
+                channelId: StringName.recordNotificationChannelId,
+                channelName: StringName.recordNotificationChannelName,
+                channelDescription:
+                    StringName.recordNotificationChannelDescription,
+                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,
+              ),
+            ));
+  }
+
   void addShortcut() {}
 
   void onBackClick() {
@@ -131,10 +162,21 @@ class RecordController extends BaseController {
       _onRecordPermissionDenied();
       return;
     }
+
+    await _requestForegroundTaskPermission().catchError((error) {
+      debugPrint("requestForegroundTaskPermission error: $error");
+    });
+
     File targetFile = await _getCurrentRecordFile();
     Stream<Uint8List> recordStream = await _record.startStream(_recordConfig);
-    _changeRecordStatus(RecordStatus.recording);
+    _startForegroundService();
     recordStream.listen((data) async {
+      if (data.isEmpty) {
+        return;
+      }
+      if (currentStatus.value != RecordStatus.recording) {
+        _changeRecordStatus(RecordStatus.recording);
+      }
       targetFile.writeAsBytesSync(data, mode: FileMode.append);
       currentDuration.value = currentDuration.value +
           _getPcmDuration(data.length, _recordConfig.sampleRate, 16,
@@ -151,7 +193,8 @@ class RecordController extends BaseController {
   Future<void> _stopRecord() {
     return _record
         .pause()
-        .then((_) => _changeRecordStatus(RecordStatus.paused));
+        .then((_) => _changeRecordStatus(RecordStatus.paused))
+        .then((_) => FlutterForegroundTask.stopService());
   }
 
   Future<File> _getCurrentRecordFile() async {
@@ -211,9 +254,7 @@ class RecordController extends BaseController {
         ToastUtil.showToast("${error.message}");
         if (error.code == ErrorCode.errorCodeNoLogin) {
           Get.toNamed(RoutePath.login)?.then((loginSuccess) {
-            loginSuccess != null && loginSuccess
-                ? _saveCurrentRecord()
-                : null;
+            loginSuccess != null && loginSuccess ? _saveCurrentRecord() : null;
           });
         }
       } else {
@@ -227,6 +268,36 @@ class RecordController extends BaseController {
     status == RecordStatus.recording ? frameAnimationController.play() : null;
   }
 
+  Future<void> _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<ServiceRequestResult> _startForegroundService() async {
+    if (await FlutterForegroundTask.isRunningService) {
+      return FlutterForegroundTask.restartService();
+    } else {
+      return FlutterForegroundTask.startService(
+        serviceId: 256,
+        notificationTitle: StringName.appName,
+        notificationText: StringName.recordStatusRecording,
+        notificationIcon: null,
+        notificationButtons: [],
+        callback: setRecordCallback,
+      );
+    }
+  }
+
   /// 获取录音文件地址
   static Future<File> getRecordFile(String talkId) async {
     Directory documentDir = await getApplicationDocumentsDirectory();

+ 24 - 0
lib/module/record/record_task.dart

@@ -0,0 +1,24 @@
+import 'package:flutter_foreground_task/flutter_foreground_task.dart';
+
+class RecordTaskHandler extends TaskHandler {
+  @override
+  void onDestroy(DateTime timestamp) {
+    // TODO: implement onDestroy
+  }
+
+  @override
+  void onRepeatEvent(DateTime timestamp) {
+    // TODO: implement onRepeatEvent
+  }
+
+  @override
+  void onStart(DateTime timestamp) {
+    // TODO: implement onStart
+  }
+}
+
+@pragma('vm:entry-point')
+void setRecordCallback() {
+  // The setTaskHandler function must be called to handle the task in the background.
+  FlutterForegroundTask.setTaskHandler(RecordTaskHandler());
+}

+ 64 - 0
pubspec.lock

@@ -342,6 +342,14 @@ packages:
       url: "https://pub.dev"
     source: hosted
     version: "3.4.1"
+  flutter_foreground_task:
+    dependency: "direct main"
+    description:
+      name: flutter_foreground_task
+      sha256: "6e0b5de3d1cceb3bd608793af0dde3346194465f9f664fdb8bd87638dbe847e9"
+      url: "https://pub.dev"
+    source: hosted
+    version: "8.8.1+1"
   flutter_gen_core:
     dependency: transitive
     description:
@@ -1008,6 +1016,62 @@ packages:
       url: "https://pub.dev"
     source: hosted
     version: "0.28.0"
+  shared_preferences:
+    dependency: transitive
+    description:
+      name: shared_preferences
+      sha256: "746e5369a43170c25816cc472ee016d3a66bc13fcf430c0bc41ad7b4b2922051"
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.3.2"
+  shared_preferences_android:
+    dependency: transitive
+    description:
+      name: shared_preferences_android
+      sha256: "480ba4345773f56acda9abf5f50bd966f581dac5d514e5fc4a18c62976bbba7e"
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.3.2"
+  shared_preferences_foundation:
+    dependency: transitive
+    description:
+      name: shared_preferences_foundation
+      sha256: c4b35f6cb8f63c147312c054ce7c2254c8066745125264f0c88739c417fc9d9f
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.5.2"
+  shared_preferences_linux:
+    dependency: transitive
+    description:
+      name: shared_preferences_linux
+      sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f"
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.4.1"
+  shared_preferences_platform_interface:
+    dependency: transitive
+    description:
+      name: shared_preferences_platform_interface
+      sha256: "57cbf196c486bc2cf1f02b85784932c6094376284b3ad5779d1b1c6c6a816b80"
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.4.1"
+  shared_preferences_web:
+    dependency: transitive
+    description:
+      name: shared_preferences_web
+      sha256: d2ca4132d3946fec2184261726b355836a82c33d7d5b67af32692aff18a4684e
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.4.2"
+  shared_preferences_windows:
+    dependency: transitive
+    description:
+      name: shared_preferences_windows
+      sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1"
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.4.1"
   shelf:
     dependency: transitive
     description:

+ 2 - 1
pubspec.yaml

@@ -80,7 +80,8 @@ dependencies:
   #html
   flutter_widget_from_html: ^0.15.2
 
-
+  #前台任务
+  flutter_foreground_task: ^8.8.1+1
 
 dev_dependencies:
   flutter_test: