Мне нужно реализовать функциональность, при которой микрофон пользователя записывается с помощью 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.