audio_video_player

A comprehensive Flutter plugin for audio and video playback.

Replaces media_kit with a tightly integrated stack built on first-class packages:

Concern Package
Audio playback just_audio
Background audio / lock-screen controls just_audio_background + audio_service
Audio session management audio_session
Video playback video_player
Video UI & controls chewie
Offline downloads background_downloader
PiP (Picture-in-Picture) Native (Android / iOS)
AirPlay / Chromecast Native stub + Dart bridge

Features

  • Single-file and playlist/album audio with background playback and lock-screen / notification controls
  • Reactive streams for state, position, duration, queue, and current item
  • Shuffle, repeat (off / one / all), playback speed, and volume
  • Sleep timer with per-second countdown stream
  • Equalizer (Android) — per-band gain control with preset reset
  • Video player with full Chewie UI (full-screen, speed, subtitles)
  • Video playlist with auto-advance and repeat modes
  • Picture-in-Picture on Android and iOS
  • Offline downloads via background_downloader — progress streams, resume, delete
  • AirPlay (iOS) and Chromecast stub (Android) via CastController

Table of Contents

  1. Installation
  2. Platform setup
  3. Initialisation
  4. Models
  5. Audio playback
  6. Sleep Timer
  7. Equalizer (Android)
  8. Video playback
  9. Picture-in-Picture
  10. Downloads
  11. Cast / AirPlay
  12. Running the example app

Installation

Add the plugin to your project's pubspec.yaml:

From pub.dev (recommended):

dependencies:
  audio_video_player: ^0.1.0-beta.5

From a local path (during development):

dependencies:
  audio_video_player:
    path: ../audio_video_player

From a git repository:

dependencies:
  audio_video_player:
    git:
      url: https://github.com/Rameshwar-Amancha/audio_video_player
      ref: main

Then run:

flutter pub get

Platform setup

Android

Your app's android/app/src/main/AndroidManifest.xml must include the following permissions and activity attributes:

<manifest xmlns:android="http://schemas.android.com/apk/res/android">

    <!-- Required -->
    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.WAKE_LOCK" />
    <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
    <uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />

    <application ...>
        <activity
            android:name=".MainActivity"
            android:exported="true"
            android:launchMode="singleTop"
            android:supportsPictureInPicture="true"           <!-- Required for PiP -->
            android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
            ...>
            <!-- existing content -->
        </activity>
    </application>
</manifest>

Minimum SDK: The plugin requires minSdk 21 (Android 5.0). Ensure your android/app/build.gradle has minSdk = 21 or higher.

iOS

In your app's ios/Runner/Info.plist add the background audio mode and allow HTTP streams during development:

<key>UIBackgroundModes</key>
<array>
    <string>audio</string>
</array>

<!-- Allow cleartext HTTP for non-HTTPS streams (development only) -->
<key>NSAppTransportSecurity</key>
<dict>
    <key>NSAllowsArbitraryLoads</key>
    <true/>
</dict>

For production, remove NSAllowsArbitraryLoads and use HTTPS-only sources or add specific domain exceptions.

iOS deployment target: 13.0 or higher (set in Xcode under Runner → General → Minimum Deployments).


Initialisation

Call AudioVideoPlayerPlugin.init() once, before runApp():

import 'package:audio_video_player/audio_video_player.dart';
import 'package:flutter/material.dart';

void main() async {
  WidgetsFlutterBinding.ensureInitialized();

  await AudioVideoPlayerPlugin.init(
    // Required – identifies the Android foreground-service notification channel.
    androidNotificationChannelId: 'com.yourapp.audio',
    androidNotificationChannelName: 'Music',

    // Optional
    androidNotificationIcon: 'mipmap/ic_launcher', // drawable resource
    notificationColor: Colors.deepPurple,          // Android notification colour
    pauseWhenDucked: false,                        // true = pause on nav-app focus loss
  );

  runApp(const MyApp());
}

AudioServiceWidget is not requiredjust_audio_background handles foreground-service lifecycle internally.


Models

AVMediaItem

The unified media descriptor used by both stacks:

const item = AVMediaItem(
  id: 'track_001',                          // unique ID (UUID recommended)
  uri: 'https://example.com/track.mp3',     // https://, file://, or asset://
  type: AVMediaType.audio,                  // or AVMediaType.video
  title: 'My Song',
  artist: 'Artist Name',
  album: 'Album Title',
  albumArtUri: 'https://example.com/art.jpg',
  duration: Duration(minutes: 3, seconds: 45),
);

URI schemes:

Scheme Example Use case
https:// https://cdn.example.com/track.mp3 Remote stream
file:// file:///data/user/0/.../track.mp3 Local file
asset:// asset://assets/audio/demo.mp3 Flutter asset

Playlist

final playlist = Playlist(
  id: 'pl_001',
  name: 'My Playlist',
  description: 'Optional description',
  artUri: 'https://example.com/art.jpg',
  items: [item1, item2, item3],
);

// Mutations return a new Playlist (immutable pattern):
final updated = playlist.add(item4);
final reordered = playlist.reorder(0, 2); // move item at index 0 to index 2
final removed = playlist.remove('track_001');

Album

final album = Album(
  id: 'alb_001',
  name: 'Greatest Hits',
  artist: 'Artist Name',
  year: 2026,
  artUri: 'https://example.com/cover.jpg',
  items: tracks,
);

Audio playback

Single track

final audio = AudioController();

// Remote URL
await audio.open(
  URISource(AVMediaItem(id: '1', uri: 'https://example.com/track.mp3', type: AVMediaType.audio, title: 'Track')),
  autoPlay: true,
);

// Flutter asset
await audio.open(
  AssetSource(
    AVMediaItem(id: '2', uri: 'asset://assets/audio/demo.mp3', type: AVMediaType.audio, title: 'Demo'),
    'assets/audio/demo.mp3',
  ),
  autoPlay: true,
);

// Local file
await audio.open(
  FileSource(
    AVMediaItem(id: '3', uri: 'file:///path/to/file.mp3', type: AVMediaType.audio, title: 'Local'),
    File('/path/to/file.mp3'),
  ),
  autoPlay: true,
);

Playlist & Album

final audio = AudioController();

// Playlist (starts from item at index 2)
await audio.open(PlaylistSource(myPlaylist, initialIndex: 2), autoPlay: true);

// Album
await audio.open(AlbumSource(myAlbum), autoPlay: true);

// Queue management
await audio.skipToNext();
await audio.skipToPrevious();
await audio.skipToIndex(3);
await audio.add(newItem);          // append one item
await audio.addAll([item4, item5]); // append multiple items
await audio.removeAt(1);           // remove by index
await audio.move(0, 4);            // reorder: move index 0 to index 4
await audio.clear();               // empty the queue

Shuffle & Repeat

await audio.setShuffleMode(true);
await audio.setRepeatMode(RepeatMode.all);   // off | one | all

Audio streams reference

final audio = AudioController();

// Subscribe in initState / inside a StreamBuilder

audio.playerStateStream       // Stream<AVPlayerState>
audio.positionStream          // Stream<Duration>
audio.durationStream          // Stream<Duration?>
audio.bufferedPositionStream  // Stream<Duration>
audio.volumeStream            // Stream<double>        (0.0–1.0)
audio.speedStream             // Stream<double>
audio.repeatModeStream        // Stream<RepeatMode>
audio.shuffleEnabledStream    // Stream<bool>
audio.queueStream             // Stream<List<AVMediaItem>>
audio.currentItemStream       // Stream<AVMediaItem?>
audio.currentIndexStream      // Stream<int?>

AVPlayerState values: idleloadingreadyplayingpausedcompletederror

Audio controls reference

await audio.play();
await audio.pause();
await audio.stop();
await audio.seek(Duration(seconds: 90));  // AudioController uses seek()
await audio.setVolume(0.8);               // 0.0–1.0
await audio.setSpeed(1.5);               // e.g. 0.5, 1.0, 1.5, 2.0
await audio.skipToNext();
await audio.skipToPrevious();
await audio.skipToIndex(2);              // 0-based queue index
await audio.add(item);                   // append to queue
await audio.addAll([item2, item3]);      // append multiple
await audio.removeAt(1);                 // remove by index
await audio.move(0, 3);                  // reorder
await audio.clear();                     // empty the queue
await audio.setShuffleMode(true);
await audio.setRepeatMode(RepeatMode.one);

// Sync getters
audio.playerState    // AVPlayerState
audio.position       // Duration
audio.duration       // Duration?
audio.volume         // double
audio.speed          // double
audio.repeatMode     // RepeatMode
audio.shuffleEnabled // bool
audio.queue          // List<AVMediaItem>
audio.currentItem    // AVMediaItem?
audio.currentIndex   // int?
audio.isPlaying      // bool

// Always dispose when done
await audio.dispose();

Example widget

class AudioPlayerWidget extends StatefulWidget { ... }

class _AudioPlayerWidgetState extends State<AudioPlayerWidget> {
  final _audio = AudioController();

  @override
  void initState() {
    super.initState();
    _audio.open(URISource(myItem), autoPlay: true);
  }

  @override
  void dispose() {
    _audio.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return StreamBuilder<AVPlayerState>(
      stream: _audio.playerStateStream,
      builder: (context, snapshot) {
        final state = snapshot.data ?? AVPlayerState.idle;
        return IconButton(
          icon: Icon(state == AVPlayerState.playing ? Icons.pause : Icons.play_arrow),
          onPressed: state == AVPlayerState.playing ? _audio.pause : _audio.play,
        );
      },
    );
  }
}

Sleep Timer

The SleepTimer is already wired to the AudioController — access it via audio.sleepTimer:

final audio = AudioController();

// Start a 30-minute sleep timer
audio.sleepTimer.start(const Duration(minutes: 30));

// Cancel early
audio.sleepTimer.cancel();

// Show countdown in the UI
StreamBuilder<Duration?>(
  stream: audio.sleepTimer.remainingStream,
  builder: (context, snap) {
    final remaining = snap.data;
    if (remaining == null) return const Text('No sleep timer');
    final m = remaining.inMinutes.remainder(60).toString().padLeft(2, '0');
    final s = remaining.inSeconds.remainder(60).toString().padLeft(2, '0');
    return Text('Sleep in $m:$s');
  },
);

Equalizer (Android)

Available on Android only. Access via audio.equalizer:

final audio = AudioController();
final eq = audio.equalizer;

if (eq.isAvailable) {
  // Enable
  await eq.setEnabled(true);

  // Get bands and adjust gain
  final bands = await eq.getBands();
  for (final band in bands) {
    print('${band.centerFrequency} Hz: ${band.gain} dB');
  }

  // Set 60 Hz band to +3 dB
  await eq.setBandGain(0, 3.0);

  // Reset all bands to 0 dB
  await eq.reset();

  // Disable
  await eq.setEnabled(false);
}

Video playback

Single video

final vc = VideoController(
  autoPlay: true,
  looping: false,
  allowFullScreen: true,
  allowPlaybackSpeedChanging: true,
);

await vc.open(AVMediaItem(
  id: 'v1',
  uri: 'https://example.com/video.mp4',
  type: AVMediaType.video,
  title: 'My Video',
));

In your widget, pass vc.chewieController to a Chewie widget:

import 'package:chewie/chewie.dart';

@override
Widget build(BuildContext context) {
  final chewieController = vc.chewieController;
  if (chewieController == null) return const CircularProgressIndicator();
  return AspectRatio(
    aspectRatio: vc.videoPlayerController!.value.aspectRatio,
    child: Chewie(controller: chewieController),
  );
}

Video controls:

await vc.play();
await vc.pause();
await vc.seekTo(Duration(seconds: 30));
await vc.setVolume(0.5);
await vc.setSpeed(1.25);
await vc.dispose();

Video playlist

final vpc = VideoPlaylistController(
  items: myAlbum.items,   // List<AVMediaItem>
  initialIndex: 0,
  autoPlay: true,
  repeatMode: RepeatMode.all,
);

await vpc.start();

// Navigation
await vpc.skipToNext();
await vpc.skipToPrevious();
await vpc.skipToIndex(2);

// Queue management
vpc.add(newItem);                   // append item
await vpc.remove('track_id_here'); // remove by AVMediaItem.id

// Expose in widget
StreamBuilder<int>(
  stream: vpc.currentIndexStream,
  builder: (context, snap) {
    final ctrl = vpc.currentController;
    if (ctrl?.chewieController == null) return const SizedBox();
    return Chewie(controller: ctrl!.chewieController!);
  },
);

await vpc.dispose();

Picture-in-Picture

PipController is accessible via VideoController.pip:

final vc = VideoController();
await vc.open(myVideoItem);

// Check availability
final supported = await vc.pip.isPipAvailable();

// Enter PiP (Android – typically called from onPause or a UI button)
if (supported) {
  await vc.pip.enterPip();
}

// Exit PiP (iOS only; Android handles this via the system back gesture)
await vc.pip.exitPip();

Android: PiP triggers when the user presses the Home button while the app activity has android:supportsPictureInPicture="true". Call enterPip() from your FlutterActivity's onUserLeaveHint() override, or from a UI button.

iOS: PiP is managed automatically by AVPictureInPictureController. Call enterPip() to activate it programmatically.


Downloads

final dl = DownloadManager.instance;

// Download a track (returns a stream of progress events synchronously)
final stream = dl.download(myItem);
stream.listen((progress) {
  if (progress.isComplete) {
    print('Downloaded! local path in item.extras["localPath"]');
  } else {
    print('${(progress.progress * 100).toStringAsFixed(0)}%');
  }
});

// Check / use downloaded file
final isDownloaded = await dl.isDownloaded(myItem);
final downloaded = await dl.getDownloadedItems();  // List<AVMediaItem> with localPath set

// Play a downloaded item offline (localPath is injected into extras)
final localItem = downloaded.first;
final localUri = localItem.extras['localPath'] as String;
final offlineItem = localItem.copyWith(uri: 'file://$localUri', isLocal: true);
await audio.open(URISource(offlineItem), autoPlay: true);

// Cancel / delete
await dl.cancel(myItem.id);          // cancel an in-progress download
await dl.deleteDownload(myItem.id); // delete a completed local file

Downloaded files are stored under:
<appDocuments>/audio_video_player/downloads/<itemId>.<ext>


Cast / AirPlay

final cast = CastController();

// iOS – show the system AirPlay route picker sheet
await cast.showAirPlayRoutePicker();

// Android – Chromecast (requires Google Cast SDK integration in host app)
final available = await cast.isAvailable();
if (available) {
  final devices = await cast.discoverDevices();
  await cast.connect(devices.first);
  await cast.cast(myItem.uri);
  await cast.disconnect();
}

Chromecast on Android requires manually adding the Google Cast SDK to your host app. See android/src/main/kotlin/.../CastHandler.kt for setup instructions.


Running the example app

Prerequisites

Requirement Version
Flutter SDK ≥ 3.27.0
Dart SDK ≥ 3.9.0
Android device / emulator API 21+ (Android 5)
iOS device / simulator iOS 13+

1. Install dependencies

From the plugin root:

cd audio_video_player
flutter pub get

cd example
flutter pub get

2. Verify your device is connected

flutter devices

You should see your physical device or emulator listed, e.g.:

sdk gphone64 arm64 (mobile) • emulator-5554 • android-arm64  • Android 14 (API 34)
Pixel 7 (mobile)             • ZX12345678   • android-arm64  • Android 14 (API 34)

3. Run on your attached Android device

From the example/ directory:

cd example
flutter run

To target a specific device by ID:

flutter run -d ZX12345678

To run a release build (better performance, tests background audio properly):

flutter run --release

4. Run on iOS

flutter run -d iPhone   # or use the simulator device ID from flutter devices

5. What the example app demonstrates

Screen How to trigger
Single Audio Tap "Single Audio Track" — plays a remote MP3 with play/pause/seek controls
Audio Playlist Tap "Audio Playlist" — queue of 3 tracks with shuffle, repeat, sleep timer
Video Player Tap "Video Player" — Chewie UI with full-screen, speed selector, PiP button

Testing background audio (Android):

  1. Open the Audio Playlist screen and start playback
  2. Press the Home button — audio continues in the background
  3. Pull down the notification shade — media controls appear on the lock screen
  4. Swipe the notification to stop playback

Testing Picture-in-Picture (Android):

  1. Open the Video Player screen and start a video
  2. Tap the PiP button (bottom-right of the video) or press the Home button
  3. Video shrinks into the system PiP window

API quick reference

Classes

Class Purpose
AudioVideoPlayerPlugin One-time plugin initialiser — call .init() in main()
AVMediaItem Unified media descriptor (audio + video)
Playlist Ordered collection of AVMediaItems
Album Playlist subtype with artist and year
MediaSource Sealed: URISource, FileSource, AssetSource, PlaylistSource, AlbumSource
AudioController Full audio playback controller (streams, queue, equalizer, sleep timer)
SleepTimer Countdown timer that pauses AudioController
EqualizerController Android-only per-band EQ (access via AudioController.equalizer)
VideoController Single video + Chewie integration + PiP
VideoPlaylistController Video queue with auto-advance
PipController Enter/exit native Picture-in-Picture (access via VideoController.pip)
DownloadManager Offline download queue with progress streams
CastController Chromecast (Android stub) + AirPlay (iOS) bridge

Enums

Enum Values
AVMediaType audio, video
AVPlayerState idle, loading, ready, playing, paused, completed, error
RepeatMode off, one, all
CastDeviceType chromecast, airPlay, unknown

Libraries

audio_video_player
A comprehensive Flutter plugin for audio and video playback.
audio_video_player_method_channel
audio_video_player_platform_interface