Исключение MissingPluginException при использовании flutter_sound в flutter_foreground_task

Вопросы и ответы

Мне нужно реализовать функциональность, при которой микрофон пользователя записывается с помощью flutter_sound пакета и немедленно воспроизводится через динамик с помощью FlutterSoundPlayer для Android и iOS. Я уже выполнил эту часть задачи.

Запись и немедленное воспроизведение должны происходить в отдельном изоляте и продолжаться даже когда приложение свёрнуто/ неактивно, а также когда смартфон переходит в режим сна.

Пакет 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 и замените содержимое main.dart следующим кодом, основанным на официальном примере flutter_foreground_task (по запросу я могу сократить). Соответствующий 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: 'Привет от MyTaskHandler :)',
      notificationText: 'счет: $_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("https://stackoverflow.com/");
    print('onNotificationPressed');
  }

  @override
  void onNotificationDismissed() {
    print('onNotificationDismissed');
  }
}

class ExampleApp extends StatelessWidget {
  const ExampleApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      routes: {
        "https://stackoverflow.com/": (context) => const ExamplePage(),
      },
      initialRoute: "https://stackoverflow.com/",
    );
  }
}

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();
    }
    //разрешение на использование микрофона
    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: 'Уведомление фоновой службы',
        channelDescription:
            'Это уведомление появляется, когда фоновая служба работает.',
        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: 'Фоновая служба работает',
        notificationText: 'Нажмите, чтобы вернуться в приложение',
        notificationIcon: null,
        notificationButtons: [
          const NotificationButton(id: 'btn_hello', text: 'привет'),
        ],
        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'),
          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('Вы получили данные от 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('запустить службу', onPressed: _startService),
          buttonBuilder('остановить службу', onPressed: _stopService),
          buttonBuilder('увеличить счет', 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" />
    <!-- обязательно -->
    <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">
        <!-- Внимание: Не изменяйте имя службы. -->
        <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">
            <!-- Указывает тему Android, которую следует применять к этому классу Activity, как только
                 процесс Android запускается. Эта тема видима пользователю
                 во время инициализации Flutter UI. После этого эта тема продолжает
                 определять фоновое окно за 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>
        <!-- Не удаляйте метаданные ниже.
             Это используется инструментом Flutter для генерации GeneratedPluginRegistrant.java -->
        <meta-data
            android:name="flutterEmbedding"
            android:value="2" />
    </application>
    <!-- Необходимо для запроса активностей, которые могут обрабатывать текст, см.:
         https://developer.android.com/training/package-visibility и
         https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT.

         В частности, это используется движком Flutter в io.flutter.plugin.text.ProcessTextPlugin. -->
    <queries>
        <intent>
            <action android:name="android.intent.action.PROCESS_TEXT"/>
            <data android:mimeType="text/plain"/>
        </intent>
    </queries>
</manifest>

Ответ

Для решения проблемы с MissingPluginException при использовании flutter_sound в flutter_foreground_task, важно учитывать, что вызов плагинов Flutter из другого изолята может привести к подобным проблемам, так как плагины должны быть инициализированы в главном потоке. Вот как можно решить вашу проблему.

Шаг 1: Инициализация плагинов в основном изоляте

Перед тем, как перемещать код в onStart метод класса MyTaskHandler, убедитесь, что все плагины инициированы в основном изоляте. Для этого добавьте вызов инициализации в функцию main перед выполнением других операций.

void main() async {
  WidgetsFlutterBinding.ensureInitialized(); // Позволяет использовать плагины.
  await flutterSound.init(); // Инициализируйте плагин flutter_sound
  FlutterForegroundTask.initCommunicationPort();
  runApp(const ExampleApp());
}

Шаг 2: Перенос логики в другой изолят

После того как плажины инициализированы на главном потоке, вам необходимо будет создать отдельный метод для инициализации FlutterSoundPlayer и FlutterSoundRecorder уже в onStart методе. Измените ваш класс MyTaskHandler, добавив функцию для открытия плеера и рекордера.

class MyTaskHandler extends TaskHandler {
  // другие части класса

  @override
  void onStart(DateTime timestamp) async {
    print('onStart');

    // Инициализация плеера и рекордера
    await _initializeSoundDevices();

    _incrementCount();

    // Остальная часть логики
  }

  Future<void> _initializeSoundDevices() async {
    await _recorder.openRecorder();
    await _player.openPlayer();
    await _player.setVolume(1.0);
  }
}

Шаг 3: Обработчик ошибок

Для отладки вы также можете добавлять обработку ошибок, чтобы видеть, где может возникать проблема:

try {
  await _recorder.openRecorder();
  await _player.openPlayer();
} catch (e) {
  print('Ошибка инициализации звука: $e');
}

Шаг 4: Протестируйте приложение

После внесения этих изменений протестируйте приложение снова. Если всё сделано правильно, ошибка MissingPluginException должна исчезнуть.

Важно учесть

Убедитесь, что все необходимые разрешения были запрошены перед началом работы с микрофоном и воспроизведением звука. Вы уже включаете их в методе _requestPermissions(), что является правильным шагом.

Заключение

Теперь ваше приложение должно корректно работать с плагинами flutter_sound в фоновом режиме, записывая и воспроизводя звук. Если возникнут другие проблемы, вы можете также проверить совместимость версий пакетов или просмотреть документацию на наличие изменений в API.

Оцените материал
Добавить комментарий

Капча загружается...