mpv_audio_kit
Audio engine for Flutter.
Made for your media library
The package is an audio player for Flutter apps that play a media library, from a server or from the device. It runs mpv and FFmpeg behind a typed Dart API, on Android, iOS, macOS, Windows, Linux and the web.
![]() |
Try MPV Studio in your browser The demo app runs the package with libmpv as WebAssembly. Its source is in the mpv_studio repository. |
| Jellyfin Stream your library directly or transcoded. Transcoded streams arrive as HLS, and you can still seek in them. |
|
| Plex Play tracks and playlists over HLS or DASH. Each entry asks the server for its stream only when it opens, so a long queue starts at once. |
|
| Samba Play straight from a NAS or any network share over SMB2 and SMB3. |
|
| Tuning and analysis Tune the cache and the network, shape the sound with a chain of FFmpeg effects, and watch it with a live spectrum, waveform and loudness meter. |
Getting started
dependencies:
mpv_audio_kit: ^0.7.0
The build downloads the libmpv binary for each native target and checks its SHA-256, so the first build needs network access. On the web the module ships with the package.
| Platform | Minimum | Architectures | Size |
|---|---|---|---|
| Android | 7.0 (SDK 24) | arm64-v8a, armeabi-v7a, x86_64 | 7.7 to 8.3 MB per ABI |
| iOS | 15.0 | arm64, simulator arm64 and x86_64 | 7.3 MB |
| macOS | 12.0 | arm64, x86_64 | 15.1 MB universal |
| tvOS | 15.0 | arm64, simulator arm64 and x86_64 | 7.3 MB |
| Tizen | 6.0 | armv7l, emulator x86_64 | 6.0 MB, 8.6 MB on the emulator |
| webOS | 26 | armv7l | 5.8 MB |
| Windows | 10 | arm64, x86_64 | 7.5 to 7.8 MB |
| Linux | glibc 2.31 | aarch64, x86_64 | 8.4 to 8.7 MB |
| Web | a cross-origin isolated page | wasm32 | 4.3 MB, 2.0 MB zipped |
The package needs Flutter 3.38.10 or later, and Flutter 3.47.4 to build for iOS. Linux needs ALSA, and uses PipeWire or PulseAudio when they are installed.
Play a file
void main() {
WidgetsFlutterBinding.ensureInitialized();
MpvAudioKit.ensureInitialized();
runApp(const MyApp());
}
final player = Player();
await player.open(Media('https://example.com/audio.flac'));
await player.play();
player.stream.position.listen((position) => print(position));
await player.dispose(); // when you are done
player.state holds the current values, and player.stream sends each change.
How it works
The player lives on your main isolate and calls libmpv without blocking it. A background isolate waits for libmpv's events and brings state back. On the web the module wakes the player on the main thread.
Working with the player
A queue
await player.openAll([track1, track2, track3]);
await player.insert(player.state.playback.playlist.index + 1, track4); // play next
await player.next();
await player.setLoop(Loop.playlist);
await player.setPrefetchPlaylist(true); // gapless over the network
See Playback for sources, headers, chapters, resume points and live PCM.
Effects
await player.setAudioEffects(const [
EqualizerEffect(frequency: 100, width: 50, widthType: EqualizerWidthType.h, gain: 3),
LoudnormEffect(i: -16),
AlimiterEffect(limit: 0.9),
]);
Each of the 86 effects is an FFmpeg filter with typed options. See Effects for the whole chain.
Errors
player.stream.error.listen((error) {
switch (error) {
case EndFileError():
print('The file stopped: ${error.message}');
case LogError():
print('mpv reported: ${error.message}');
}
});
See Errors and logs for every error type and the log stream.
A URL that expires
A source resolver gives each entry its stream URL when it opens, so the tokens in a queue never go stale.
await player.setSourceResolver((request) async {
final id = request.media.extras?['trackId'] as String?;
return id == null ? null : api.streamUrl(id); // null keeps the URL
});
await player.openAll([
Media('resolve://1', extras: {'trackId': '1'}),
Media('resolve://2', extras: {'trackId': '2'}),
]);
See Hooks and raw access for the resolver, mpv hooks and raw properties.
Lock screen and notification
await player.setMediaSession(const MediaSession());
The player then shows in the system media controls on every platform. On Android this also keeps it playing in the background. See Media session for the metadata, the buttons and each platform.
Visualizer
player.stream.fft.listen((frame) => paintSpectrum(frame));
The player gives a live spectrum, the raw PCM, a waveform of the whole file and a loudness scan. See Visualizer and analysis for each of them.
The browser
The page needs two headers and a user gesture before it plays. The players of a page share at most 512 MiB of memory, 256 MiB on an iPhone or an iPad. Test on a real iPhone: a desktop browser does not show the limits of iOS. See Web for the setup and the browser's limits.
Permissions
Android
Add this permission to your app's manifest for network sources:
<uses-permission android:name="android.permission.INTERNET" />
The package already declares FOREGROUND_SERVICE, FOREGROUND_SERVICE_MEDIA_PLAYBACK and POST_NOTIFICATIONS. You do not need to request POST_NOTIFICATIONS at runtime. Media notifications are exempt.
Background playback needs the media session. setMediaSession runs a foreground service of type mediaPlayback, which keeps the app alive after the user leaves it. Without a session, Android may freeze or stop the app in the background.
- After 10 minutes paused, the service leaves the foreground and Android may stop the app. A pause caused by a phone call under
pauseAndResumedoes not count. - If Android refuses the foreground service to a play started in the background, the player pauses.
- The player takes no wake lock. If a network stall pauses the output with the screen off, the device may sleep until something wakes it.
- Pressing Back on the root route, or swiping the app away from recent apps, can destroy the Flutter engine and stop its players. To keep playing after the activity closes, keep the engine alive, cached or run by a service.
iOS
For background playback, add the Background Modes capability in Signing & Capabilities and check Audio, AirPlay, and Picture in Picture. This writes to Info.plist:
<key>UIBackgroundModes</key>
<array>
<string>audio</string>
</array>
Android TV
Android TV, Fire TV and Google TV run the Android build. The app needs a leanback entry and a banner in its manifest, and the media session for the keys of the remote. See Android TV for the manifest and HDMI passthrough.
tvOS
Build with flutter-tvos. It installs no code asset, so on tvOS the plugin's tvos/Package.swift links libmpv instead of the build hook: Swift Package Manager downloads libmpv_tvos.xcframework.zip of the release and checks its checksum. A libmpv of your own goes to tvos/libmpv.xcframework in a checkout of the package: the hook's libmpv user define does not reach tvOS.
Tizen
Build with flutter-tizen. The build hook bundles the Tizen libmpv for the TVs, and for the TV emulator when the app sets the user define tizen: true. See Tizen.
webOS
Build with flutter-webos, for webOS 26 and later. It runs no build hook, so the package's webos/CMakeLists.txt bundles libmpv instead. See webOS.
macOS
For network sources, add to DebugProfile.entitlements and Release.entitlements:
<key>com.apple.security.network.client</key>
<true/>
Troubleshooting
Building and testing in containers
These steps apply to WSL, Docker and Distrobox. libmpv comes prebuilt, so building needs only the Flutter desktop tools. At runtime libmpv needs libasound2, and a PipeWire (0.3.57 or later) or PulseAudio client to reach the host's sound server. Desktop distributions ship them. A minimal container does not, so install them:
sudo apt update
# Flutter desktop build tools
sudo apt install clang cmake ninja-build pkg-config libgtk-3-dev liblzma-dev
# Runtime libraries to play sound through the host
sudo apt install libasound2 libpipewire-0.3-0 libpulse0
Inside a container the alsa driver cannot reach the sound card. Use pipewire or pulse. On WSL only pulse works.
Documentation
- API reference, with these guides:
- Playback: sources, the queue, controls and resume points
- Effects: the FFmpeg effects chain, ReplayGain and gapless
- Output: drivers, devices, formats and exclusive mode
- Network and cache: the cache, timeouts, TLS, proxies and reconnects
- Metadata: tags and cover art
- State and streams: reading and following the player
- Errors and logs: typed errors, end of file and logs
- Hooks and raw access: the source resolver, mpv hooks and raw properties
- Visualizer and analysis: spectrum, PCM, waveform and loudness
- Media session: lock screen, notification and system controls
- Web: serving the page, autoplay and the browser's limits
- Example app and mpv_studio, a full player
- CHANGELOG and MIGRATION
Licenses
The package is BSD-3-Clause. The libmpv it bundles is LGPL, version 3 or later, and your app keeps its own license. showLicensePage lists the libmpv texts with nothing to set up. Your users must be able to replace libmpv and get its source, which the release's licenses.zip names. LICENSES.md has the details and the GPL build with Rubber Band.
Project background
I wrote the native bindings, the isolate code and most of the architecture with Claude Code.
Jellyfin, Plex and Samba are trademarks of their respective owners. mpv_audio_kit is not affiliated with them.
Developed by Alessandro Di Ronza
Libraries
- mpv_audio_kit
- mpv_audio_kit: a Flutter audio player powered by libmpv.
