我必须实现一项功能,即通过
flutter_sound
包的 FlutterSoundRecorder
录制用户的麦克风,并立即通过适用于 Android 和 iOS 的 FlutterSoundPlayer
的扬声器进行回放。我已经完成了这部分任务。
录制和立即播放必须在单独的隔离中进行,并且即使在应用程序最小化/未聚焦以及智能手机进入睡眠模式时也应继续进行。
这个套餐
flutter_foreground_task
看起来非常有前途。我尝试运行它的示例,效果完美。
当我尝试将
flutter_sound
代码移至由 flutter_foreground_task
包创建的隔离区并开始运行应用程序时,问题就开始了:
MissingPluginException (MissingPluginException(No implementation found for method resetPlugin on channel xyz.canardoux.flutter_sound_player))
我尝试了
flutter clean
以及 flutter pub cache clean
并在添加所有依赖项后重建项目。我尝试了WidgetsFlutterBinding.ensureInitialized()
和DartPluginRegistrant.ensureInitialized()
,但没有成功。
我知道在另一个隔离区中运行代码以及在构建过程中仅通过
@pragma('vm:entry-point')
不被忽略的代码可能会在执行其中的本机代码或插件时造成麻烦。
如何解决这个异常?目前我只针对Android进行了测试,iOS会是稍后的问题。
要重现它,请生成一个新的空 flutter 项目,并根据
官方 flutter_foreground_task 示例(我可以根据要求进一步缩短)将
main.dart
内容替换为以下代码。相应的 AndroidManifest.xml
位于代码下方。另外,将以下三个包添加到项目的 pubspec.yaml
中:flutter_foreground_task
、flutter_sound
和 permission_handler
。
import 'dart:async';
import 'dart:io';
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:flutter_foreground_task/flutter_foreground_task.dart';
import 'package:flutter_sound/flutter_sound.dart';
import 'package:permission_handler/permission_handler.dart';
void main() {
FlutterForegroundTask.initCommunicationPort();
runApp(const ExampleApp());
}
@pragma('vm:entry-point')
void startCallback() {
FlutterForegroundTask.setTaskHandler(MyTaskHandler());
}
class MyTaskHandler extends TaskHandler {
static const String incrementCountCommand = 'incrementCount';
int _count = 0;
FlutterSoundRecorder _recorder = FlutterSoundRecorder();
FlutterSoundPlayer _player = FlutterSoundPlayer();
final StreamController<Uint8List> _streamController =
StreamController<Uint8List>.broadcast();
void _incrementCount() {
_count++;
FlutterForegroundTask.updateService(
notificationTitle: 'Hello MyTaskHandler :)',
notificationText: 'count: $_count',
);
FlutterForegroundTask.sendDataToMain(_count);
}
@override
void onStart(DateTime timestamp) async {
print('onStart');
_incrementCount();
await _player.openPlayer();
await _player.setVolume(1.0);
await _recorder.openRecorder();
_streamController.stream.listen((Uint8List food) {
if (_player.isPlaying) {
_player.foodSink!.add(FoodData(food));
}
});
await _player.startPlayerFromStream(
sampleRate: 44100,
whenFinished: _streamController.close,
);
await _recorder.startRecorder(
toStream: _streamController.sink,
codec: Codec.pcm16,
sampleRate: 44100,
);
}
@override
void onRepeatEvent(DateTime timestamp) {
_incrementCount();
}
@override
void onDestroy(DateTime timestamp) async {
print('onDestroy');
await _player.stopPlayer();
await _player.closePlayer();
await _recorder.stopRecorder();
await _recorder.closeRecorder();
await _streamController.close();
}
@override
void onReceiveData(Object data) {
print('onReceiveData: $data');
if (data == incrementCountCommand) {
_incrementCount();
}
}
@override
void onNotificationButtonPressed(String id) {
print('onNotificationButtonPressed: $id');
}
@override
void onNotificationPressed() {
FlutterForegroundTask.launchApp('/');
print('onNotificationPressed');
}
@override
void onNotificationDismissed() {
print('onNotificationDismissed');
}
}
class ExampleApp extends StatelessWidget {
const ExampleApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
routes: {
'/': (context) => const ExamplePage(),
},
initialRoute: '/',
);
}
}
class ExamplePage extends StatefulWidget {
const ExamplePage({super.key});
@override
State<StatefulWidget> createState() => _ExamplePageState();
}
class _ExamplePageState extends State<ExamplePage> {
final ValueNotifier<Object?> _receivedTaskData = ValueNotifier(null);
Future<void> _requestPermissions() async {
final NotificationPermission notificationPermission =
await FlutterForegroundTask.checkNotificationPermission();
if (notificationPermission != NotificationPermission.granted) {
await FlutterForegroundTask.requestNotificationPermission();
}
//microphone permission
final PermissionStatus microphonePermission =
await Permission.microphone.status;
if (Platform.isAndroid) {
if (!await FlutterForegroundTask.canDrawOverlays) {
await FlutterForegroundTask.openSystemAlertWindowSettings();
}
if (!await FlutterForegroundTask.isIgnoringBatteryOptimizations) {
await FlutterForegroundTask.requestIgnoreBatteryOptimization();
}
if (!await FlutterForegroundTask.canScheduleExactAlarms) {
await FlutterForegroundTask.openAlarmsAndRemindersSettings();
}
}
}
void _initService() {
FlutterForegroundTask.init(
androidNotificationOptions: AndroidNotificationOptions(
channelId: 'foreground_service',
channelName: 'Foreground Service Notification',
channelDescription:
'This notification appears when the foreground service is running.',
channelImportance: NotificationChannelImportance.LOW,
priority: NotificationPriority.LOW,
),
iosNotificationOptions: const IOSNotificationOptions(
showNotification: false,
playSound: false,
),
foregroundTaskOptions: ForegroundTaskOptions(
eventAction: ForegroundTaskEventAction.repeat(5000),
autoRunOnBoot: true,
autoRunOnMyPackageReplaced: true,
allowWakeLock: true,
allowWifiLock: true,
),
);
}
Future<ServiceRequestResult> _startService() async {
if (await FlutterForegroundTask.isRunningService) {
return FlutterForegroundTask.restartService();
} else {
return FlutterForegroundTask.startService(
serviceId: 256,
notificationTitle: 'Foreground Service is running',
notificationText: 'Tap to return to the app',
notificationIcon: null,
notificationButtons: [
const NotificationButton(id: 'btn_hello', text: 'hello'),
],
callback: startCallback,
);
}
}
Future<ServiceRequestResult> _stopService() async {
return FlutterForegroundTask.stopService();
}
void _onReceiveTaskData(Object data) {
print('onReceiveTaskData: $data');
_receivedTaskData.value = data;
}
void _incrementCount() {
FlutterForegroundTask.sendDataToTask(MyTaskHandler.incrementCountCommand);
}
@override
void initState() {
super.initState();
FlutterForegroundTask.addTaskDataCallback(_onReceiveTaskData);
WidgetsBinding.instance.addPostFrameCallback((_) {
_requestPermissions();
_initService();
});
}
@override
void dispose() {
FlutterForegroundTask.removeTaskDataCallback(_onReceiveTaskData);
_receivedTaskData.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return WithForegroundTask(
child: Scaffold(
appBar: AppBar(
title: const Text('Flutter Foreground Task'),
centerTitle: true,
),
body: _buildContentView(),
),
);
}
Widget _buildContentView() {
return Column(
children: [
Expanded(child: _buildCommunicationText()),
_buildServiceControlButtons(),
],
);
}
Widget _buildCommunicationText() {
return ValueListenableBuilder(
valueListenable: _receivedTaskData,
builder: (context, data, _) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Text('You received data from TaskHandler:'),
Text('$data', style: Theme.of(context).textTheme.headlineMedium),
],
),
);
},
);
}
Widget _buildServiceControlButtons() {
buttonBuilder(String text, {VoidCallback? onPressed}) {
return ElevatedButton(
onPressed: onPressed,
child: Text(text),
);
}
return Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
buttonBuilder('start service', onPressed: _startService),
buttonBuilder('stop service', onPressed: _stopService),
buttonBuilder('increment count', onPressed: _incrementCount),
],
),
);
}
}
<manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MICROPHONE"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />
<!-- required -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<!-- foregroundServiceType: dataSync -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
<!-- foregroundServiceType: remoteMessaging -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_REMOTE_MESSAGING" />
<application
tools:replace="android:icon, android:label"
android:label="flutter_isolate_test"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">
<!-- Warning: Do not change service name. -->
<service
android:name="com.pravera.flutter_foreground_task.service.ForegroundService"
android:foregroundServiceType="dataSync|remoteMessaging"
android:exported="false" />
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTop"
android:taskAffinity=""
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
<!-- Specifies an Android theme to apply to this Activity as soon as
the Android process has started. This theme is visible to the user
while the Flutter UI initializes. After that, this theme continues
to determine the Window background behind the Flutter UI. -->
<meta-data
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme"
/>
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
<meta-data
android:name="flutterEmbedding"
android:value="2" />
</application>
<!-- Required to query activities that can process text, see:
https://developer.android.com/training/package-visibility and
https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT.
In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->
<queries>
<intent>
<action android:name="android.intent.action.PROCESS_TEXT"/>
<data android:mimeType="text/plain"/>
</intent>
</queries>
</manifest>
你找到解决办法了吗?