Background audio playback is a common feature in many mobile applications, such as music players, podcast apps, and audiobook platforms. In Flutter, implementing background audio playback requires careful management of audio sessions, notifications, and background tasks. This blog post will guide you through implementing background audio playback in Flutter using the flutter_sound and audio_service packages.
Understanding the Basics
Before diving into the implementation, it’s important to understand the key concepts involved in background audio playback:
- Audio Session: An audio session manages how your app interacts with the audio system, including handling interruptions and routing audio to different outputs.
- Audio Service: A Flutter service that runs in the background and manages the audio playback. It allows your app to continue playing audio even when it’s in the background or the screen is locked.
- Media Notifications: Notifications that display information about the currently playing audio and provide controls like play, pause, and skip.
Setting Up Your Flutter Project
Start by creating a new Flutter project or opening an existing one.
Step 1: Add Dependencies
Add the necessary dependencies to your pubspec.yaml file:
dependencies:
flutter:
sdk: flutter
flutter_sound: ^9.2.1 # Use the latest version
audio_service: ^0.18.9 # Use the latest version
rxdart: ^0.27.7 # Use the latest version
Then, run flutter pub get to install the packages.
Step 2: Configure Native Platforms
Background audio playback requires specific configurations on both Android and iOS platforms.
Android Configuration
-
Add the following permissions to your
AndroidManifest.xmlfile:<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/> <uses-permission android:name="android.permission.WAKE_LOCK"/> <uses-permission android:name="android.permission.READ_PHONE_STATE" /> <!-- Optional, for handling interruptions --> -
Add a service definition inside the
<application>tag inAndroidManifest.xml:<service android:name="com.ryanheise.audioservice.AudioService" android:foregroundServiceType="mediaPlayback" android:exported="true"> <intent-filter> <action android:name="android.media.browse.MediaBrowserService" /> </intent-filter> </service> - **Optional: Customize notification appearance**. Copy and modify the drawables located inside `/res/drawable` according to your own taste (see “Android resources”)
iOS Configuration
-
Add the following keys to your
Info.plistfile:<key>UIBackgroundModes</key> <array> <string>audio</string> <string>fetch</string> <string>processing</string> </array> - Add necessary properties under iOS -> Runner -> Signing & Capabilities: 1. Background Modes, and select: [Audio, AirPlay, and Picture in Picture], [Background processing], [remote notifications]
Implementing the Audio Service
Now, let’s implement the audio service that will manage the audio playback in the background.
Step 1: Create an Audio Handler
Create a class that extends BaseAudioHandler from the audio_service package. This class will handle the audio playback logic, media notifications, and session management.
import 'dart:async';
import 'package:audio_service/audio_service.dart';
import 'package:flutter_sound/flutter_sound.dart';
import 'package:rxdart/rxdart.dart';
class MyAudioHandler extends BaseAudioHandler {
final _flutterSoundPlayer = FlutterSoundPlayer();
final _mediaItemSubject = BehaviorSubject<MediaItem?>();
MyAudioHandler() {
_init();
}
Future<void> _init() async {
await _flutterSoundPlayer.openPlayer();
_mediaItemSubject.value = MediaItem(
id: 'audio_1',
title: 'My Audio',
artist: 'Unknown',
);
mediaItem.add(_mediaItemSubject.value);
// Handle playback completion
_flutterSoundPlayer.onPlayerStateChanged.listen((event) {
if (event.isStopped) {
playbackState.add(playbackState.value.copyWith(processingState: AudioProcessingState.completed));
stop();
}
});
}
@override
Future<void> play() async {
try {
final url = 'YOUR_AUDIO_URL'; // Replace with your audio URL
await _flutterSoundPlayer.startPlayer(
fromURI: url,
codec: Codec.mp3,
);
playbackState.add(playbackState.value.copyWith(
playing: true,
processingState: AudioProcessingState.ready,
));
print("Audio started playing");
} catch (e) {
print('Error starting player: $e');
stop();
}
}
@override
Future<void> pause() async {
await _flutterSoundPlayer.pausePlayer();
playbackState.add(playbackState.value.copyWith(
playing: false,
processingState: AudioProcessingState.ready,
));
print("Audio paused");
}
@override
Future<void> stop() async {
await _flutterSoundPlayer.stopPlayer();
playbackState.add(playbackState.value.copyWith(
playing: false,
processingState: AudioProcessingState.idle,
));
await _flutterSoundPlayer.closePlayer();
}
@override
Future<void> seek(Duration position) async {
// FlutterSound doesn't directly support seek, you would have to implement custom logic
// This is a simplified placeholder:
print('Seek operation is a placeholder, implement the actual seeking mechanism here');
}
@override
Future<void> onTaskRemoved() {
stop();
return super.onTaskRemoved();
}
@override
Future<void> skipToNext() async {
// Implement skip to next functionality
print("Skip to Next");
}
@override
Future<void> skipToPrevious() async {
// Implement skip to previous functionality
print("Skip to Previous");
}
@override
Future<void> fastForward() async {
// Implement fast forward functionality if FlutterSound supports it, otherwise skip the action
print("Fast Forward Action Skipped. Not implemented in this project yet!");
}
@override
Future<void> rewind() async {
// Implement rewind functionality if FlutterSound supports it, otherwise skip the action
print("Rewind Action Skipped. Not implemented in this project yet!");
}
}
Step 2: Initialize Audio Service
In your main application entry point, initialize the audio service.
import 'package:audio_service/audio_service.dart';
import 'package:flutter/material.dart';
import 'my_audio_handler.dart'; // Ensure correct path to MyAudioHandler
late AudioHandler audioHandler;
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
audioHandler = await AudioService.init(
builder: () => MyAudioHandler(),
config: const AudioServiceConfig(
androidNotificationChannelName: 'MyApp Audio',
androidNotificationOngoing: true,
androidStopOnRemoveTask: true, // Important for Android background behavior
notificationColor: Colors.grey, // Use Colors.grey or appropriate theme color for clarity
),
);
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Audio Service Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(title: 'Flutter Audio Service Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key? key, required this.title}) : super(key: key);
final String title;
@override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
ElevatedButton(
onPressed: () {
audioHandler.play();
},
child: Text('Play'),
),
ElevatedButton(
onPressed: () {
audioHandler.pause();
},
child: Text('Pause'),
),
ElevatedButton(
onPressed: () {
audioHandler.stop();
},
child: Text('Stop'),
),
],
),
),
);
}
}
Step 3: Implement UI Controls
Implement UI controls in your Flutter app to allow users to start, pause, and stop audio playback. Connect these controls to the audio service methods.
ElevatedButton(
onPressed: () {
audioHandler.play();
},
child: Text('Play'),
),
ElevatedButton(
onPressed: () {
audioHandler.pause();
},
child: Text('Pause'),
),
ElevatedButton(
onPressed: () {
audioHandler.stop();
},
child: Text('Stop'),
),
Complete Example
//main.dart
import 'package:audio_service/audio_service.dart';
import 'package:flutter/material.dart';
import 'my_audio_handler.dart'; // Ensure correct path to MyAudioHandler
late AudioHandler audioHandler;
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
audioHandler = await AudioService.init(
builder: () => MyAudioHandler(),
config: const AudioServiceConfig(
androidNotificationChannelName: 'MyApp Audio',
androidNotificationOngoing: true,
androidStopOnRemoveTask: true, // Important for Android background behavior
notificationColor: Colors.grey, // Use Colors.grey or appropriate theme color for clarity
),
);
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Audio Service Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(title: 'Flutter Audio Service Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key? key, required this.title}) : super(key: key);
final String title;
@override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
ElevatedButton(
onPressed: () {
audioHandler.play();
},
child: Text('Play'),
),
ElevatedButton(
onPressed: () {
audioHandler.pause();
},
child: Text('Pause'),
),
ElevatedButton(
onPressed: () {
audioHandler.stop();
},
child: Text('Stop'),
),
],
),
),
);
}
}
//my_audio_handler.dart
import 'dart:async';
import 'package:audio_service/audio_service.dart';
import 'package:flutter_sound/flutter_sound.dart';
import 'package:rxdart/rxdart.dart';
class MyAudioHandler extends BaseAudioHandler {
final _flutterSoundPlayer = FlutterSoundPlayer();
final _mediaItemSubject = BehaviorSubject<MediaItem?>();
MyAudioHandler() {
_init();
}
Future<void> _init() async {
await _flutterSoundPlayer.openPlayer();
_mediaItemSubject.value = MediaItem(
id: 'audio_1',
title: 'My Audio',
artist: 'Unknown',
);
mediaItem.add(_mediaItemSubject.value);
// Handle playback completion
_flutterSoundPlayer.onPlayerStateChanged.listen((event) {
if (event.isStopped) {
playbackState.add(playbackState.value.copyWith(processingState: AudioProcessingState.completed));
stop();
}
});
}
@override
Future<void> play() async {
try {
final url = 'YOUR_AUDIO_URL'; // Replace with your audio URL
await _flutterSoundPlayer.startPlayer(
fromURI: url,
codec: Codec.mp3,
);
playbackState.add(playbackState.value.copyWith(
playing: true,
processingState: AudioProcessingState.ready,
));
print("Audio started playing");
} catch (e) {
print('Error starting player: $e');
stop();
}
}
@override
Future<void> pause() async {
await _flutterSoundPlayer.pausePlayer();
playbackState.add(playbackState.value.copyWith(
playing: false,
processingState: AudioProcessingState.ready,
));
print("Audio paused");
}
@override
Future<void> stop() async {
await _flutterSoundPlayer.stopPlayer();
playbackState.add(playbackState.value.copyWith(
playing: false,
processingState: AudioProcessingState.idle,
));
await _flutterSoundPlayer.closePlayer();
}
@override
Future<void> seek(Duration position) async {
// FlutterSound doesn't directly support seek, you would have to implement custom logic
// This is a simplified placeholder:
print('Seek operation is a placeholder, implement the actual seeking mechanism here');
}
@override
Future<void> onTaskRemoved() {
stop();
return super.onTaskRemoved();
}
@override
Future<void> skipToNext() async {
// Implement skip to next functionality
print("Skip to Next");
}
@override
Future<void> skipToPrevious() async {
// Implement skip to previous functionality
print("Skip to Previous");
}
@override
Future<void> fastForward() async {
// Implement fast forward functionality if FlutterSound supports it, otherwise skip the action
print("Fast Forward Action Skipped. Not implemented in this project yet!");
}
@override
Future<void> rewind() async {
// Implement rewind functionality if FlutterSound supports it, otherwise skip the action
print("Rewind Action Skipped. Not implemented in this project yet!");
}
}
Testing Background Audio Playback
Test your implementation by running the Flutter app on a real device (not an emulator). Play the audio and then minimize the app or lock the screen. You should see a media notification with playback controls, and the audio should continue to play in the background.
Troubleshooting
-
Audio doesn’t play in the background: Ensure that you have configured the necessary permissions and background modes in the native platform settings (
AndroidManifest.xmlandInfo.plist). - Notifications not showing: Check if notifications are enabled for your app in the device settings.
- App crashes in the background: Make sure you handle exceptions properly and avoid performing UI-related tasks in the background service.
- “Playback Failed Error”: Always ensure you replace YOUR_AUDIO_URL to one, proper source or test URL in the actual project, not a mock/abstract/no one available. For this reason, Audio PlayBack could work well in theory code or basic testing with simple/clean code lines but will produce this unexpected issue once.
Conclusion
Implementing background audio playback in Flutter requires careful handling of audio sessions, native platform configurations, and background tasks. By using the flutter_sound and audio_service packages, you can create a robust and seamless audio playback experience for your users, even when the app is in the background. This blog post provided a detailed guide to get you started, and with further customization, you can create a feature-rich audio playback application.