Implementing Background Audio Playback in Flutter

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
  1. Add the following permissions to your AndroidManifest.xml file:

    
    <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 -->
    
  2. Add a service definition inside the <application> tag in AndroidManifest.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>
    
  3. **Optional: Customize notification appearance**. Copy and modify the drawables located inside `/res/drawable` according to your own taste (see “Android resources”)
iOS Configuration
  1. Add the following keys to your Info.plist file:

    
    <key>UIBackgroundModes</key>
    <array>
        <string>audio</string>
        <string>fetch</string>
        <string>processing</string>
    </array>
    
  2. 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?&gt();

  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?&gt();

  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.xml and Info.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.