在flutter_foreground_task中使用flutter_sound时遭遇MissingPluginException
问题:Flutter前台服务中使用flutter_sound出现MissingPluginException
需求
通过flutter_sound包的FlutterSoundRecorder录制用户麦克风音频,再用FlutterSoundPlayer即时通过扬声器播放,功能需在独立隔离线程运行,应用后台、手机休眠时也能持续执行。
异常信息
将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()
复现步骤
- 创建空Flutter项目
- 替换
main.dart为以下代码 - 配置对应的
AndroidManifest.xml - 在
pubspec.yaml中添加flutter_foreground_task、flutter_sound、permission_handler依赖
main.dart代码
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), ], ), ); } }
AndroidManifest.xml代码
<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>
Android端解决方法
1. 修正前台服务类型
在AndroidManifest.xml中,将ForegroundService的foregroundServiceType改为与音频功能匹配的类型,替换原有的dataSync|remoteMessaging:
<service android:name="com.pravera.flutter_foreground_task.service.ForegroundService" android:foregroundServiceType="microphone|mediaPlayback" android:exported="false" />
同时确保已声明对应权限(已存在可跳过):
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MICROPHONE"/> <uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />
2. 在隔离线程中初始化插件注册
修改startCallback,在设置TaskHandler前确保插件注册完成:
@pragma('vm:entry-point') void startCallback() { if (Platform.isAndroid) { DartPluginRegistrant.ensureInitialized(); } FlutterForegroundTask.setTaskHandler(MyTaskHandler()); }
注意:需导入package:flutter/services.dart以使用DartPluginRegistrant。
3. 延迟初始化flutter_sound实例
将FlutterSoundRecorder和FlutterSoundPlayer的初始化移至onStart方法内,避免提前初始化导致插件未注册:
class MyTaskHandler extends TaskHandler { // ...其他成员 FlutterSoundRecorder? _recorder; FlutterSoundPlayer? _player; final StreamController<Uint8List> _streamController = StreamController<Uint8List>.broadcast(); @override void onStart(DateTime timestamp) async { print('onStart'); _incrementCount(); // 初始化实例 _player = FlutterSoundPlayer(); _recorder = FlutterSoundRecorder(); 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 onDestroy(DateTime timestamp) async { print('onDestroy'); await _player?.stopPlayer(); await _player?.closePlayer(); _player = null; await _recorder?.stopRecorder(); await _recorder?.closeRecorder(); _recorder = null; await _streamController.close(); } }
4. 确保麦克风权限主动请求
完善_requestPermissions方法,主动请求麦克风权限:
Future<void> _requestPermissions() async { final NotificationPermission notificationPermission = await FlutterForegroundTask.checkNotificationPermission(); if (notificationPermission != NotificationPermission.granted) { await FlutterForegroundTask.requestNotificationPermission(); } // 主动请求麦克风权限 final PermissionStatus microphoneStatus = await Permission.microphone.request(); if (!microphoneStatus.isGranted) { // 处理权限拒绝逻辑,如弹窗提示用户开启权限 return; } if (Platform.isAndroid) { if (!await FlutterForegroundTask.canDrawOverlays) { await FlutterForegroundTask.openSystemAlertWindowSettings(); } if (!await FlutterForegroundTask.isIgnoringBatteryOptimizations) { await FlutterForegroundTask.requestIgnoreBatteryOptimization(); } if (!await FlutterForegroundTask.canScheduleExactAlarms) { await FlutterForegroundTask.openAlarmsAndRemindersSettings(); } } }
5. 优化音频流处理逻辑
为避免播放流阻塞,可添加缓冲或错误处理:
_streamController.stream.listen((Uint8List food) { try { if (_player?.isPlaying == true) { _player?.foodSink?.add(FoodData(food)); } } catch (e) { print('Audio stream error: $e'); } });
内容的提问来源于stack exchange,提问作者L483
相关产品推荐
相关产品推荐

