You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

在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()

复现步骤

  1. 创建空Flutter项目
  2. 替换main.dart为以下代码
  3. 配置对应的AndroidManifest.xml
  4. 在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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.18 08:36:04