{"id":50684022,"url":"https://github.com/ales-drnz/mpv_audio_kit","last_synced_at":"2026-06-08T21:00:48.906Z","repository":{"id":344573943,"uuid":"1178039434","full_name":"ales-drnz/mpv_audio_kit","owner":"ales-drnz","description":"mpv_audio_kit is an audio library built on mpv v0.41.0. It provides a dedicated background event loop, a complete DSP pipeline, and direct access to every property, making it the most capable audio library available for Flutter.","archived":false,"fork":false,"pushed_at":"2026-06-07T11:21:17.000Z","size":189260,"stargazers_count":19,"open_issues_count":2,"forks_count":4,"subscribers_count":3,"default_branch":"main","last_synced_at":"2026-06-07T12:22:30.807Z","etag":null,"topics":["audio-player","cross-platform","dsp","ffmpeg","libmpv"],"latest_commit_sha":null,"homepage":"https://pub.dev/packages/mpv_audio_kit","language":"Dart","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"bsd-3-clause","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/ales-drnz.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-03-10T16:16:26.000Z","updated_at":"2026-06-05T19:19:10.000Z","dependencies_parsed_at":null,"dependency_job_id":"1da74a5d-7a42-414d-bc94-fcfbdb01a466","html_url":"https://github.com/ales-drnz/mpv_audio_kit","commit_stats":null,"previous_names":["ales-drnz/mpv_audio_kit"],"tags_count":9,"template":false,"template_full_name":null,"purl":"pkg:github/ales-drnz/mpv_audio_kit","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ales-drnz%2Fmpv_audio_kit","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ales-drnz%2Fmpv_audio_kit/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ales-drnz%2Fmpv_audio_kit/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ales-drnz%2Fmpv_audio_kit/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/ales-drnz","download_url":"https://codeload.github.com/ales-drnz/mpv_audio_kit/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ales-drnz%2Fmpv_audio_kit/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34080026,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-08T02:00:07.615Z","response_time":111,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["audio-player","cross-platform","dsp","ffmpeg","libmpv"],"created_at":"2026-06-08T21:00:44.504Z","updated_at":"2026-06-08T21:00:48.880Z","avatar_url":"https://github.com/ales-drnz.png","language":"Dart","funding_links":["https://www.patreon.com/cw/ales_drnz","https://www.buymeacoffee.com/ales.drnz"],"categories":[],"sub_categories":[],"readme":"# mpv_audio_kit\n\n#### Audio engine for Flutter and Dart.\n\n[![](https://img.shields.io/pub/v/mpv_audio_kit.svg?style=for-the-badge\u0026logo=dart\u0026logoColor=white)](https://pub.dev/packages/mpv_audio_kit)\n[![](https://img.shields.io/badge/libmpv-v0.41.0-orange.svg?style=for-the-badge)]()\n[![](https://img.shields.io/badge/license-BSD--3--Clause-blue.svg?style=for-the-badge)](LICENSE)\n[![](https://img.shields.io/github/stars/ales-drnz/mpv_audio_kit?style=for-the-badge\u0026logo=github\u0026logoColor=white)](https://github.com/ales-drnz/mpv_audio_kit)\n[![](https://img.shields.io/discord/1485588004029333516?style=for-the-badge\u0026logo=discord\u0026logoColor=white)](https://discord.gg/g2Qf4Mq9MP)\n[![](https://img.shields.io/badge/Patreon-F96854?style=for-the-badge\u0026logo=patreon\u0026logoColor=white)](https://www.patreon.com/cw/ales_drnz)\n[![](https://img.shields.io/badge/Buy%20Me%20a%20Coffee-FFDD00?style=for-the-badge\u0026logo=buy-me-a-coffee\u0026logoColor=black)](https://www.buymeacoffee.com/ales.drnz)\n\n\u003ctable\u003e\n\u003ctr\u003e\n\u003ctd valign=\"middle\" width=\"90\"\u003e\u003cimg src=\"https://raw.githubusercontent.com/ales-drnz/mpv_audio_kit/main/imgs/mpv_audio_kit.png\" width=\"70\" alt=\"logo\"\u003e\u003c/td\u003e\n\u003ctd valign=\"middle\"\u003e\u003ccode\u003empv_audio_kit\u003c/code\u003e is an audio library built on mpv \u003ccode\u003ev0.41.0\u003c/code\u003e. It provides a dedicated background event loop, a complete DSP pipeline, and direct access to every property, making it the most capable audio library available for Flutter.\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/table\u003e\n\n---\n\n## Why did I build this?\n\nMany existing Flutter audio libraries are either built on an old version of mpv or they are simply too restrictive, hiding some cool features relative to audio processing. So I made this project to provide the most powerful and flexible audio library for Flutter and solve 3 main needs:\n\n\u003ctable\u003e\n\u003ctr\u003e\n\u003ctd valign=\"middle\" width=\"48\"\u003e\u003cimg src=\"https://raw.githubusercontent.com/ales-drnz/mpv_audio_kit/main/imgs/protocols/jellyfin.png\" width=\"32\" alt=\"Jellyfin\"\u003e\u003c/td\u003e\n\u003ctd valign=\"middle\"\u003e\u003cb\u003eJellyfin\u003c/b\u003e\u003cbr\u003efor song streaming, supporting \u003ccode\u003e.m3u8\u003c/code\u003e (\u003ca href=\"#21-supported-uri-schemes\"\u003eHLS\u003c/a\u003e) is essential when using transcoding. This is particulary handy because it enables seeking on the mpv player instead of blocking it when using \u003ccode\u003e.stream\u003c/code\u003e.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd valign=\"middle\"\u003e\u003cimg src=\"https://raw.githubusercontent.com/ales-drnz/mpv_audio_kit/main/imgs/protocols/plex.png\" width=\"32\" alt=\"Plex\"\u003e\u003c/td\u003e\n\u003ctd valign=\"middle\"\u003e\u003cb\u003ePlex\u003c/b\u003e\u003cbr\u003etranscoding in this case requires a \u003ccode\u003e/decision\u003c/code\u003e call before each stream. Plex rejects multiple parallel requests when creating playlists, so instead of relying to a local proxy server, the \u003ccode\u003eon_load\u003c/code\u003e \u003ca href=\"#12-hooks\"\u003ehook method\u003c/a\u003e resolves \u003ccode\u003e.m3u8\u003c/code\u003e or \u003ccode\u003e.mpd\u003c/code\u003e (\u003ca href=\"#21-supported-uri-schemes\"\u003eDASH\u003c/a\u003e) URLs lazily.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd valign=\"middle\"\u003e\u003cimg src=\"https://raw.githubusercontent.com/ales-drnz/svg-icons/main/png/wrench.png\" width=\"32\" alt=\"\"\u003e\u003c/td\u003e\n\u003ctd valign=\"middle\"\u003e\u003cb\u003eTotal control\u003c/b\u003e\u003cbr\u003ethis library doesn't limit features; it exposes the native engine so you can tune \u003ca href=\"#7-network-and-caching\"\u003ebuffers and network timeouts\u003c/a\u003e, \u003ca href=\"#5-audio-quality-and-dsp\"\u003eDSP filters\u003c/a\u003e and play with ffmpeg exactly how you want.\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/table\u003e\n\n---\n\n## Installation\n\nAdd `mpv_audio_kit` to your `pubspec.yaml`:\n\n```yaml\ndependencies:\n  mpv_audio_kit: ^0.3.4\n```\n\n## Platforms requirements\n\n| Platform  | Minimum | Architecture | Device | Emulator |\n| :--- | :--- | :--- | :---: | :---:\n| **Android** | 7.0 (SDK 24) | arm64-v8a, armeabi-v7a, x86_64 | ✅ | ✅ |\n| **iOS** | 15.0 | arm64, x86_64 | ✅ | ✅ |\n| **macOS** | 12.0 | arm64, x86_64 | ✅ | - |\n| **Windows**| 10 | arm64, x86_64 | ✅ | - |\n| **Linux** | Ubuntu 24.04 | aarch64, x86_64 | ✅ | - |\n\n---\n\n## Contents\n\n*   [Visuals](#visuals)\n*   [Features](#features)\n*   [Quick start](#quick-start)\n*   [Guide](#guide)\n    \u003cdetails\u003e\n    \u003csummary\u003e\u003ca href=\"#1-initialization-and-lifecycle\"\u003e\u003cb\u003e1. Initialization and lifecycle\u003c/b\u003e\u003c/a\u003e\u003c/summary\u003e\n\n    * [1.1 Global initialization](#11-global-initialization)\n    * [1.2 Creating a player](#12-creating-a-player)\n    * [1.3 Disposing a player](#13-disposing-a-player)\n\n    \u003c/details\u003e\n\n    \u003cdetails\u003e\n    \u003csummary\u003e\u003ca href=\"#2-media-sources\"\u003e\u003cb\u003e2. Media sources\u003c/b\u003e\u003c/a\u003e\u003c/summary\u003e\n\n    * [2.1 Supported URI schemes](#21-supported-uri-schemes)\n    * [2.2 HTTP headers](#22-http-headers)\n    * [2.3 Extras](#23-extras)\n    * [2.4 Demuxer options](#24-demuxer-options)\n\n    \u003c/details\u003e\n\n    \u003cdetails\u003e\n    \u003csummary\u003e\u003ca href=\"#3-playlist-management\"\u003e\u003cb\u003e3. Playlist management\u003c/b\u003e\u003c/a\u003e\u003c/summary\u003e\n\n    * [3.1 Opening a single track](#31-opening-a-single-track)\n    * [3.2 Opening multiple tracks](#32-opening-multiple-tracks)\n    * [3.3 Modifying the queue at runtime](#33-modifying-the-queue-at-runtime)\n    * [3.4 Navigation](#34-navigation)\n    * [3.5 Repeat and shuffle](#35-repeat-and-shuffle)\n    * [3.6 Chapter navigation](#36-chapter-navigation)\n\n    \u003c/details\u003e\n\n    \u003cdetails\u003e\n    \u003csummary\u003e\u003ca href=\"#4-playback-control\"\u003e\u003cb\u003e4. Playback control\u003c/b\u003e\u003c/a\u003e\u003c/summary\u003e\n\n    * [4.1 Basic controls](#41-basic-controls)\n    * [4.2 Seeking](#42-seeking)\n    * [4.3 A-B loop](#43-a-b-loop)\n    * [4.4 Speed and pitch](#44-speed-and-pitch)\n    * [4.5 Volume and mute](#45-volume-and-mute)\n    * [4.6 Audio delay](#46-audio-delay)\n    * [4.7 Resume playback (watch later)](#47-resume-playback-watch-later)\n\n    \u003c/details\u003e\n\n    \u003cdetails\u003e\n    \u003csummary\u003e\u003ca href=\"#5-audio-quality-and-dsp\"\u003e\u003cb\u003e5. Audio quality and DSP\u003c/b\u003e\u003c/a\u003e\u003c/summary\u003e\n\n    * [5.1 The AudioEffects bundle](#51-the-audioeffects-bundle)\n    * [5.2 Common effects: quick examples](#52-common-effects-quick-examples)\n    * [5.3 Available effects](#53-available-effects)\n    * [5.4 ReplayGain](#54-replaygain)\n    * [5.5 Gapless playback](#55-gapless-playback)\n\n    \u003c/details\u003e\n\n    \u003cdetails\u003e\n    \u003csummary\u003e\u003ca href=\"#6-hardware-and-routing\"\u003e\u003cb\u003e6. Hardware and routing\u003c/b\u003e\u003c/a\u003e\u003c/summary\u003e\n\n    * [6.1 Audio output driver](#61-audio-output-driver)\n    * [6.2 Exclusive mode](#62-exclusive-mode)\n    * [6.3 Device selection](#63-device-selection)\n    * [6.4 Output format](#64-output-format)\n    * [6.5 SPDIF passthrough](#65-spdif-passthrough)\n    * [6.6 Audio client name](#66-audio-client-name)\n    * [6.7 Audio track selection](#67-audio-track-selection)\n    * [6.8 Reload audio](#68-reload-audio)\n    * [6.9 Media role](#69-media-role)\n\n    \u003c/details\u003e\n\n    \u003cdetails\u003e\n    \u003csummary\u003e\u003ca href=\"#7-network-and-caching\"\u003e\u003cb\u003e7. Network and caching\u003c/b\u003e\u003c/a\u003e\u003c/summary\u003e\n\n    * [7.1 Cache configuration](#71-cache-configuration)\n    * [7.2 Demuxer memory pool](#72-demuxer-memory-pool)\n    * [7.3 Network timeout](#73-network-timeout)\n    * [7.4 TLS and SSL verification](#74-tls-and-ssl-verification)\n    * [7.5 Audio buffer](#75-audio-buffer)\n    * [7.6 Audio stream silence](#76-audio-stream-silence)\n    * [7.7 Untimed null output](#77-untimed-null-output)\n    * [7.8 Radio and live streams](#78-radio-and-live-streams)\n    * [7.9 Throttling CDNs and chunked requests](#79-throttling-cdns-and-chunked-requests)\n    * [7.10 Monitoring the demuxer cache](#710-monitoring-the-demuxer-cache)\n\n    \u003c/details\u003e\n\n    \u003cdetails\u003e\n    \u003csummary\u003e\u003ca href=\"#8-metadata-and-cover-art\"\u003e\u003cb\u003e8. Metadata and cover art\u003c/b\u003e\u003c/a\u003e\u003c/summary\u003e\n\n    * [8.1 Metadata tags](#81-metadata-tags)\n    * [8.2 Cover art](#82-cover-art)\n\n    \u003c/details\u003e\n\n    \u003cdetails\u003e\n    \u003csummary\u003e\u003ca href=\"#9-state-and-streams\"\u003e\u003cb\u003e9. State and streams\u003c/b\u003e\u003c/a\u003e\u003c/summary\u003e\n\n    * [9.1 Core streams](#91-core-streams)\n    * [9.2 Playlist and track streams](#92-playlist-and-track-streams)\n    * [9.3 Audio hardware streams](#93-audio-hardware-streams)\n    * [9.4 DSP and filter streams](#94-dsp-and-filter-streams)\n    * [9.5 Network and cache streams](#95-network-and-cache-streams)\n    * [9.6 File metadata and path streams](#96-file-metadata-and-path-streams)\n    * [9.7 Playback timing streams](#97-playback-timing-streams)\n    * [9.8 A-B loop streams](#98-a-b-loop-streams)\n    * [9.9 Cover art streams](#99-cover-art-streams)\n    * [9.10 Runtime diagnostics](#910-runtime-diagnostics)\n    * [9.11 Prefetch lifecycle stream](#911-prefetch-lifecycle-stream)\n    * [9.12 Aggregate lifecycle](#912-aggregate-lifecycle)\n    * [9.13 Complete state snapshot](#913-complete-state-snapshot)\n    * [9.14 Spectrum and PCM streams](#914-spectrum-and-pcm-streams)\n    * [9.15 Media session streams](#915-media-session-streams)\n\n    \u003c/details\u003e\n\n    \u003cdetails\u003e\n    \u003csummary\u003e\u003ca href=\"#10-raw-api\"\u003e\u003cb\u003e10. Raw API\u003c/b\u003e\u003c/a\u003e\u003c/summary\u003e\n\n    * [10.1 Read a property](#101-read-a-property)\n    * [10.2 Write a property](#102-write-a-property)\n    * [10.3 Send a command](#103-send-a-command)\n\n    \u003c/details\u003e\n\n    \u003cdetails\u003e\n    \u003csummary\u003e\u003ca href=\"#11-error-handling-and-logging\"\u003e\u003cb\u003e11. Error handling and logging\u003c/b\u003e\u003c/a\u003e\u003c/summary\u003e\n\n    * [11.1 Typed error stream](#111-typed-error-stream)\n    * [11.2 End file stream](#112-end-file-stream)\n    * [11.3 Network state](#113-network-state)\n    * [11.4 Audio output lifecycle](#114-audio-output-lifecycle)\n    * [11.5 Log streams](#115-log-streams)\n\n    \u003c/details\u003e\n\n    \u003cdetails\u003e\n    \u003csummary\u003e\u003ca href=\"#12-hooks\"\u003e\u003cb\u003e12. Hooks\u003c/b\u003e\u003c/a\u003e\u003c/summary\u003e\n\n    * [12.1 Registering a hook](#121-registering-a-hook)\n    * [12.2 Listening and continuing](#122-listening-and-continuing)\n    * [12.3 HTTP headers via hook](#123-http-headers-via-hook)\n    * [12.4 Lazy URL resolution](#124-lazy-url-resolution)\n\n    \u003c/details\u003e\n\n    \u003cdetails\u003e\n    \u003csummary\u003e\u003ca href=\"#13-visualizer-waveform-and-spectrum\"\u003e\u003cb\u003e13. Visualizer, Waveform and Spectrum\u003c/b\u003e\u003c/a\u003e\u003c/summary\u003e\n\n    * [13.1 Subscribing to the spectrum stream](#131-subscribing-to-the-spectrum-stream)\n    * [13.2 Configuring the pipeline](#132-configuring-the-pipeline)\n    * [13.3 Raw PCM stream](#133-raw-pcm-stream)\n    * [13.4 Per-filter PCM taps](#134-per-filter-pcm-taps)\n    * [13.5 Waveform](#135-waveform)\n\n    \u003c/details\u003e\n\n    \u003cdetails\u003e\n    \u003csummary\u003e\u003ca href=\"#14-os-media-session\"\u003e\u003cb\u003e14. OS media session\u003c/b\u003e\u003c/a\u003e\u003c/summary\u003e\n\n    * [14.1 Enabling the session](#141-enabling-the-session)\n    * [14.2 Overriding metadata](#142-overriding-metadata)\n    * [14.3 Reacting to OS commands](#143-reacting-to-os-commands)\n    * [14.4 Capabilities, intervals and speeds](#144-capabilities-intervals-and-speeds)\n    * [14.5 Audio interruptions](#145-audio-interruptions)\n    * [14.6 App identity (desktop)](#146-app-identity-desktop)\n\n    \u003c/details\u003e\n*   [Permissions](#permissions)\n*   [Troubleshooting](#troubleshooting)\n*   [Project background](#project-background)\n\n---\n\n## Visuals\n\n\u003ctable\u003e\n\u003ctr\u003e\n\u003ctd valign=\"middle\" width=\"90\"\u003e\u003cimg src=\"https://raw.githubusercontent.com/ales-drnz/mpv_audio_kit/main/imgs/mpv_studio.png\" width=\"70\" alt=\"MPV Studio\"\u003e\u003c/td\u003e\n\u003ctd valign=\"middle\"\u003eThe screenshots below are from \u003cb\u003e\u003ca href=\"https://github.com/ales-drnz/mpv_studio\"\u003eMPV Studio\u003c/a\u003e\u003c/b\u003e, the standalone showcase app built on \u003ccode\u003empv_audio_kit\u003c/code\u003e. The bundled \u003ccode\u003eexample/\u003c/code\u003e is a deliberately minimal single-file demo.\n\u003c/tr\u003e\n\u003c/table\u003e\n\n#### Playback\n\n\u003cp align=\"center\"\u003e\n  \u003cimg src=\"https://raw.githubusercontent.com/ales-drnz/mpv_audio_kit/main/imgs/playback.gif\" width=\"100%\"\u003e\n\u003c/p\u003e\n\n#### Queue\n\n\u003cp align=\"center\"\u003e\n  \u003cimg src=\"https://raw.githubusercontent.com/ales-drnz/mpv_audio_kit/main/imgs/queue.gif\" width=\"100%\"\u003e\n\u003c/p\u003e\n\n#### Stream\n\n\u003cp align=\"center\"\u003e\n  \u003cimg src=\"https://raw.githubusercontent.com/ales-drnz/mpv_audio_kit/main/imgs/stream.gif\" width=\"100%\"\u003e\n\u003c/p\u003e\n\n#### Effects\n\n\u003cp align=\"center\"\u003e\n  \u003cimg src=\"https://raw.githubusercontent.com/ales-drnz/mpv_audio_kit/main/imgs/effects.gif\" width=\"100%\"\u003e\n\u003c/p\u003e\n\n#### Settings\n\n\u003cp align=\"center\"\u003e\n  \u003cimg src=\"https://raw.githubusercontent.com/ales-drnz/mpv_audio_kit/main/imgs/settings.gif\" width=\"100%\"\u003e\n\u003c/p\u003e\n\n#### Console\n\n\u003cp align=\"center\"\u003e\n  \u003cimg src=\"https://raw.githubusercontent.com/ales-drnz/mpv_audio_kit/main/imgs/console.gif\" width=\"100%\"\u003e\n\u003c/p\u003e\n\n---\n\n## Features\n\n\u003ctable\u003e\n\u003ctr\u003e\n\u003ctd valign=\"middle\" width=\"48\"\u003e\u003cimg src=\"https://raw.githubusercontent.com/ales-drnz/svg-icons/main/png/zap.png\" width=\"32\"\u003e\u003c/td\u003e\n\u003ctd valign=\"middle\" width=\"45%\"\u003e\u003cb\u003eNon-blocking\u003c/b\u003e\u003cbr\u003empv events run in a background isolate; the UI thread stays free.\u003c/td\u003e\n\u003ctd valign=\"middle\" width=\"48\"\u003e\u003cimg src=\"https://raw.githubusercontent.com/ales-drnz/svg-icons/main/png/shield-check.png\" width=\"32\"\u003e\u003c/td\u003e\n\u003ctd valign=\"middle\" width=\"45%\"\u003e\u003cb\u003eType-safe API\u003c/b\u003e\u003cbr\u003etyped enums, sealed selectors, \u003ccode\u003e*Settings\u003c/code\u003e bundles. No stringly-typed setters.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd valign=\"middle\"\u003e\u003cimg src=\"https://raw.githubusercontent.com/ales-drnz/svg-icons/main/png/activity.png\" width=\"32\"\u003e\u003c/td\u003e\n\u003ctd valign=\"middle\"\u003e\u003cb\u003eReactive state\u003c/b\u003e\u003cbr\u003esynchronous \u003ca href=\"#913-complete-state-snapshot\"\u003e\u003ccode\u003estate\u003c/code\u003e\u003c/a\u003e snapshot, \u003ca href=\"#9-state-and-streams\"\u003e90+ observable streams\u003c/a\u003e covering every mpv property.\u003c/td\u003e\n\u003ctd valign=\"middle\"\u003e\u003cimg src=\"https://raw.githubusercontent.com/ales-drnz/svg-icons/main/png/music.png\" width=\"32\"\u003e\u003c/td\u003e\n\u003ctd valign=\"middle\"\u003e\u003cb\u003eGapless playback\u003c/b\u003e\u003cbr\u003eseamless track transitions with an observable \u003ca href=\"#911-prefetch-lifecycle-stream\"\u003eprefetch lifecycle\u003c/a\u003e.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd valign=\"middle\"\u003e\u003cimg src=\"https://raw.githubusercontent.com/ales-drnz/svg-icons/main/png/sliders-horizontal.png\" width=\"32\"\u003e\u003c/td\u003e\n\u003ctd valign=\"middle\"\u003e\u003cb\u003eDSP pipeline\u003c/b\u003e\u003cbr\u003eone \u003ca href=\"#5-audio-quality-and-dsp\"\u003e\u003ccode\u003eAudioEffects\u003c/code\u003e\u003c/a\u003e bundle covering 18-band graphic EQ, compressor, loudness, pitch and tempo, bass and treble, stereo width, headphone crossfeed, silence trim, plus any custom \u003ccode\u003e--af\u003c/code\u003e filter.\u003c/td\u003e\n\u003ctd valign=\"middle\"\u003e\u003cimg src=\"https://raw.githubusercontent.com/ales-drnz/svg-icons/main/png/audio-lines.png\" width=\"32\"\u003e\u003c/td\u003e\n\u003ctd valign=\"middle\"\u003e\u003cb\u003eVisualizer\u003c/b\u003e\u003cbr\u003ereal-time \u003ca href=\"#13-visualizer-waveform-and-spectrum\"\u003eFFT spectrum + raw PCM streams\u003c/a\u003e with log-spaced bands and asymmetric smoothing.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd valign=\"middle\"\u003e\u003cimg src=\"https://raw.githubusercontent.com/ales-drnz/svg-icons/main/png/scale.png\" width=\"32\"\u003e\u003c/td\u003e\n\u003ctd valign=\"middle\"\u003e\u003cb\u003eReplayGain\u003c/b\u003e\u003cbr\u003etrack and album normalization, preamp, fallback gain.\u003c/td\u003e\n\u003ctd valign=\"middle\"\u003e\u003cimg src=\"https://raw.githubusercontent.com/ales-drnz/svg-icons/main/png/list-plus.png\" width=\"32\"\u003e\u003c/td\u003e\n\u003ctd valign=\"middle\"\u003e\u003cb\u003eDynamic playlist\u003c/b\u003e\u003cbr\u003eadd, remove, move, replace mid-playback; \u003ca href=\"#36-chapter-navigation\"\u003echapters\u003c/a\u003e and \u003ca href=\"#43-a-b-loop\"\u003eA-B loop\u003c/a\u003e.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd valign=\"middle\"\u003e\u003cimg src=\"https://raw.githubusercontent.com/ales-drnz/svg-icons/main/png/layers.png\" width=\"32\"\u003e\u003c/td\u003e\n\u003ctd valign=\"middle\"\u003e\u003cb\u003eMulti-track audio\u003c/b\u003e\u003cbr\u003etyped \u003ca href=\"#67-audio-track-selection\"\u003etrack selection\u003c/a\u003e for multilingual containers (MKV, MP4) with codec, language, and gain metadata per track.\u003c/td\u003e\n\u003ctd valign=\"middle\"\u003e\u003cimg src=\"https://raw.githubusercontent.com/ales-drnz/svg-icons/main/png/cpu.png\" width=\"32\"\u003e\u003c/td\u003e\n\u003ctd valign=\"middle\"\u003e\u003cb\u003eHardware control\u003c/b\u003e\u003cbr\u003e\u003ca href=\"#62-exclusive-mode\"\u003eexclusive mode\u003c/a\u003e, \u003ca href=\"#63-device-selection\"\u003edevice selection\u003c/a\u003e, \u003ca href=\"#64-output-format\"\u003ebit-perfect sample-rate and format\u003c/a\u003e, \u003ca href=\"#65-spdif-passthrough\"\u003eSPDIF passthrough\u003c/a\u003e.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd valign=\"middle\"\u003e\u003cimg src=\"https://raw.githubusercontent.com/ales-drnz/svg-icons/main/png/tag.png\" width=\"32\"\u003e\u003c/td\u003e\n\u003ctd valign=\"middle\"\u003e\u003cb\u003eMetadata and cover art\u003c/b\u003e\u003cbr\u003e\u003ca href=\"#82-cover-art\"\u003eembedded artwork\u003c/a\u003e as raw bytes plus a Flutter \u003ca href=\"#82-cover-art\"\u003e\u003ccode\u003eImageProvider\u003c/code\u003e\u003c/a\u003e helper, and \u003ca href=\"#81-metadata-tags\"\u003etags\u003c/a\u003e.\u003c/td\u003e\n\u003ctd valign=\"middle\"\u003e\u003cimg src=\"https://raw.githubusercontent.com/ales-drnz/svg-icons/main/png/globe.png\" width=\"32\"\u003e\u003c/td\u003e\n\u003ctd valign=\"middle\"\u003e\u003cb\u003eNetwork streams\u003c/b\u003e\u003cbr\u003eHLS, DASH, SMB, HTTP and HTTPS.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd valign=\"middle\"\u003e\u003cimg src=\"https://raw.githubusercontent.com/ales-drnz/svg-icons/main/png/package.png\" width=\"32\"\u003e\u003c/td\u003e\n\u003ctd valign=\"middle\"\u003e\u003cb\u003eCache control\u003c/b\u003e\u003cbr\u003e\u003ca href=\"#71-cache-configuration\"\u003e\u003ccode\u003eCacheSettings\u003c/code\u003e\u003c/a\u003e bundle for memory cache, disk overflow, pause-on-empty.\u003c/td\u003e\n\u003ctd valign=\"middle\"\u003e\u003cimg src=\"https://raw.githubusercontent.com/ales-drnz/svg-icons/main/png/webhook.png\" width=\"32\"\u003e\u003c/td\u003e\n\u003ctd valign=\"middle\"\u003e\u003cb\u003eHooks\u003c/b\u003e\u003cbr\u003eintercept the file-loading pipeline (also during \u003ca href=\"#12-hooks\"\u003eprefetch\u003c/a\u003e) to resolve URLs, redirect, or inject headers.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd valign=\"middle\"\u003e\u003cimg src=\"https://raw.githubusercontent.com/ales-drnz/svg-icons/main/png/triangle-alert.png\" width=\"32\"\u003e\u003c/td\u003e\n\u003ctd valign=\"middle\"\u003e\u003cb\u003eTyped errors\u003c/b\u003e\u003cbr\u003esealed \u003ca href=\"#111-typed-error-stream\"\u003e\u003ccode\u003eMpvPlayerError\u003c/code\u003e\u003c/a\u003e hierarchy plus dedicated sinks for engine errors, end-file events, AO failures, and logs.\u003c/td\u003e\n\u003ctd valign=\"middle\"\u003e\u003cimg src=\"https://raw.githubusercontent.com/ales-drnz/svg-icons/main/png/terminal.png\" width=\"32\"\u003e\u003c/td\u003e\n\u003ctd valign=\"middle\"\u003e\u003cb\u003eRaw access\u003c/b\u003e\u003cbr\u003eread or write any mpv property or command; failures surface as typed \u003ca href=\"#10-raw-api\"\u003e\u003ccode\u003eMpvException\u003c/code\u003e\u003c/a\u003e.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd valign=\"middle\"\u003e\u003cimg src=\"https://raw.githubusercontent.com/ales-drnz/svg-icons/main/png/media-session.png\" width=\"32\"\u003e\u003c/td\u003e\n\u003ctd valign=\"middle\"\u003e\u003cb\u003eOS media session\u003c/b\u003e\u003cbr\u003epublish the player to the \u003ca href=\"#14-os-media-session\"\u003esystem media controls\u003c/a\u003e. Now Playing, Control Center and lockscreen, MPRIS on Linux, SMTC on Windows, and the Android media notification.\u003c/td\u003e\n\u003ctd valign=\"middle\"\u003e\u003c/td\u003e\n\u003ctd valign=\"middle\"\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/table\u003e\n\n---\n\n## Quick start\n\n```dart\nimport 'package:flutter/material.dart';\nimport 'package:mpv_audio_kit/mpv_audio_kit.dart';\n\nvoid main() {\n  WidgetsFlutterBinding.ensureInitialized();\n  MpvAudioKit.ensureInitialized();\n  runApp(const MaterialApp(home: AudioPlayerScreen()));\n}\n\nclass AudioPlayerScreen extends StatefulWidget {\n  const AudioPlayerScreen({super.key});\n  @override\n  State\u003cAudioPlayerScreen\u003e createState() =\u003e _AudioPlayerScreenState();\n}\n\nclass _AudioPlayerScreenState extends State\u003cAudioPlayerScreen\u003e {\n  late final Player player = Player();\n\n  @override\n  void initState() {\n    super.initState();\n    player.open(Media('https://example.com/audio.mp3'));\n  }\n\n  @override\n  void dispose() {\n    player.dispose(); // fire and forget is fine inside Flutter's synchronous dispose()\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n      body: Center(\n        child: StreamBuilder\u003cDuration\u003e(\n          stream: player.stream.position,\n          builder: (context, snap) =\u003e Text('Position: ${snap.data}'),\n        ),\n      ),\n      floatingActionButton: FloatingActionButton(\n        // Bind the button to the intent axis (`playWhenReady`), not\n        // `playing`. `playing` toggles transiently during seeks.\n        onPressed: () =\u003e\n            player.state.playWhenReady ? player.pause() : player.play(),\n        child: const Icon(Icons.play_arrow),\n      ),\n    );\n  }\n}\n```\n\n---\n\n## Guide\n\n### 1. Initialization and lifecycle\n\n\u003cimg src=\"https://raw.githubusercontent.com/ales-drnz/mpv_audio_kit/main/imgs/diagrams/player_lifecycle.png\" width=\"100%\"\u003e\n\n#### 1.1 Global initialization\n\nCall `MpvAudioKit.ensureInitialized()` once at startup, before creating any `Player` instance. This registers the native backend and cleans up any handles that leaked across a Flutter Hot-Restart.\n\n```dart\nvoid main() async {\n  WidgetsFlutterBinding.ensureInitialized();\n  MpvAudioKit.ensureInitialized();\n  runApp(const MyApp());\n}\n```\n\n#### 1.2 Creating a player\n\n```dart\nfinal player = Player(\n  configuration: const PlayerConfiguration(\n    logLevel: LogLevel.info, // mpv log verbosity\n    initialVolume: 100.0, // Volume at startup (0 to 100)\n    autoPlay: true,       // Start playing automatically on open()\n  ),\n);\n```\n\nAll `PlayerConfiguration` fields are optional. Their defaults:\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003cb\u003e9 configuration fields\u003c/b\u003e (click to expand)\u003c/summary\u003e\n\n| Field | Default | Description |\n| :--- | :--- | :--- |\n| `autoPlay` | `false` | Whether `open()` starts playback immediately |\n| `initialVolume` | `100.0` | Volume at startup |\n| `logLevel` | `LogLevel.warn` | Threshold forwarded to `player.stream.log`. Typed enum: `LogLevel.off`, `.fatal`, `.error`, `.warn`, `.info`, `.v`, `.debug`, `.trace` |\n| `resumePlayback` | `true` | Restore the saved position on reopen (watch-later). Only audio-relevant props are persisted, ideal for audiobook and podcast resume |\n| `watchLaterDir` | `null` | Directory for the watch-later resume configs. Often not writable on mobile, point this at an app-writable path |\n| `forceSeekable` | `false` | Allow in-cache seeking on streams mpv reports as non-seekable (direct-HTTP or HLS audio) |\n| `hlsBitrate` | `HlsBitrate.max` | Which variant mpv selects from an adaptive HLS playlist (`HlsBitrate.no`, `.min`, or `.max`). Use `.min` to save bandwidth on metered links |\n| `normalizeDownmix` | `false` | Loudness-normalize surround content downmixed to fewer channels, avoiding clipping on 5.1→stereo |\n| `demuxerCacheDir` | `null` | Directory for the on-disk demuxer cache (companion of `CacheSettings.onDisk`). Point it at a writable path on mobile |\n\n\u003c/details\u003e\n\nThe audio client name is set after construction:\n\n```dart\nawait player.setAudioClientName('MyMusicApp');\n```\n\n#### 1.3 Disposing a player\n\nAlways call `dispose()` to release native handles.\n\n```dart\nawait player.dispose();\n```\n\n---\n\n### 2. Media sources\n\nA `Media` object wraps a URI with optional per-track metadata and HTTP configuration.\n\n```dart\n// HTTPS stream\nfinal track = Media('https://cdn.example.com/audio.flac');\n\n// Local file\nfinal local = Media('file:///home/user/music/song.flac');\n\n// Flutter asset\nfinal asset = Media('asset:///assets/audio/sample.mp3');\n\n// Android content URI (e.g. from file picker)\nfinal content = Media('content://com.android.externalstorage.documents/...');\n```\n\n#### 2.1 Supported URI schemes\n\n##### Local and app-bundled sources\n\n|  | Scheme | Description |\n| :---: | :--- | :--- |\n| \u003cimg src=\"https://raw.githubusercontent.com/ales-drnz/mpv_audio_kit/main/imgs/protocols/file.png\" width=\"32\" style=\"vertical-align: middle\" alt=\"File\"\u003e | `file://` | Local files with absolute path |\n| \u003cimg src=\"https://raw.githubusercontent.com/ales-drnz/mpv_audio_kit/main/imgs/protocols/flutter.png\" width=\"32\" style=\"vertical-align: middle\" alt=\"Flutter\"\u003e | `asset:///` | Flutter assets bundled in the app |\n| \u003cimg src=\"https://raw.githubusercontent.com/ales-drnz/mpv_audio_kit/main/imgs/protocols/android.png\" width=\"32\" style=\"vertical-align: middle\" alt=\"Android\"\u003e | `content://` | Android content provider URIs (file picker, media store) |\n\n##### Streaming sources\n\n|  | Scheme | Description |\n| :---: | :--- | :--- |\n| \u003cimg src=\"https://raw.githubusercontent.com/ales-drnz/mpv_audio_kit/main/imgs/protocols/https.png\" width=\"32\" style=\"vertical-align: middle\" alt=\"HTTPS\"\u003e | `https://`, `http://` | Network streams, live radio, etc... |\n| \u003cimg src=\"https://raw.githubusercontent.com/ales-drnz/mpv_audio_kit/main/imgs/protocols/jellyfin.png\" width=\"32\" style=\"vertical-align: middle\" alt=\"Jellyfin\"\u003e | `https://…/*.m3u8` | HTTP Live Streaming (HLS) manifest, as used by Jellyfin transcoding |\n| \u003cimg src=\"https://raw.githubusercontent.com/ales-drnz/mpv_audio_kit/main/imgs/protocols/plex.png\" width=\"32\" style=\"vertical-align: middle\" alt=\"Plex\"\u003e | `https://…/*.mpd` | Dynamic Adaptive Streaming over HTTP (DASH) manifest, as used by Plex transcoding |\n| \u003cimg src=\"https://raw.githubusercontent.com/ales-drnz/mpv_audio_kit/main/imgs/protocols/samba.png\" width=\"32\" style=\"vertical-align: middle\" alt=\"Samba\"\u003e | `smb2://` | SMB2 and SMB3 network shares |\n\n#### 2.2 HTTP headers\n\nHeaders are applied natively to the libmpv HTTP layer, without a local proxy:\n\n```dart\nfinal media = Media(\n  'https://api.example.com/stream/episode-42.mp3',\n  httpHeaders: {\n    'Authorization': 'Bearer my_token',\n    'User-Agent': 'MyApp/1.0',\n    'X-Custom-Header': 'value',\n  },\n);\nawait player.open(media);\n```\n\n#### 2.3 Extras\n\nAttach arbitrary data to a track. The player carries it through the\nplaylist so your UI can access it without a separate lookup.\n\n```dart\nfinal media = Media(\n  'https://cdn.example.com/track.mp3',\n  extras: {\n    'title': 'Track Title',\n    'artist': 'Artist Name',\n    'album': 'Album Name',\n    'duration': const Duration(minutes: 4, seconds: 12),\n    'isPodcast': true,\n  },\n);\n```\n\nAccess later via `player.state.playlist.items[index].extras`.\n\n#### 2.4 Demuxer options\n\nPass per-track options straight to libmpv's libavformat demuxer with\n`Media.demuxerLavfOptions` (applied as the file-local `demuxer-lavf-o`, scoped\nto that entry). A common use is reaching the *segment* demuxer of an HLS or DASH\nstream through the HLS demuxer's `seg_format_options` dictionary:\n\n```dart\nfinal media = Media(\n  'https://server/Audio/123/main.m3u8?…',\n  demuxerLavfOptions: {\n    // forward an option to the HLS segment demuxer\n    'seg_format_options': 'advanced_editlist=0',\n  },\n);\n```\n\nValues must not contain a comma; keys must be non-empty.\n\n---\n\n### 3. Playlist management\n\n#### 3.1 Opening a single track\n\n```dart\n// Respects PlayerConfiguration.autoPlay\nawait player.open(media);\n\n// Override auto-play for this call\nawait player.open(media, play: true);\nawait player.open(media, play: false); // Load but do not start\n```\n\n#### 3.2 Opening multiple tracks\n\n```dart\nawait player.openAll([track1, track2, track3]);\n\n// Start at a specific index\nawait player.openAll([track1, track2, track3], index: 1);\n\n// Override auto-play\nawait player.openAll([track1, track2], play: false);\n\n// Load a playlist FILE or URL (.m3u, .m3u8, .pls, .cue), its entries\n// are expanded into player.stream.playlist (internet-radio station lists,\n// remote playlists). open() loads a single entry; openPlaylistFile parses it.\nawait player.openPlaylistFile(Media('https://example.com/stations.m3u'));\n```\n\n\u003e Per-track HTTP headers from `Media.httpHeaders` are applied\n\u003e automatically to every entry, both the first and the queued ones.\n\u003e The wrapper holds an internal `on_load` hook that re-attaches them\n\u003e via `file-local-options/http-header-fields` when mpv enters the\n\u003e file-local scope for each track (the only safe moment per the mpv\n\u003e manual). You only need to register your own [§12](#12-hooks) hook\n\u003e for URL resolution, custom auth flows, etc.\n\n#### 3.3 Modifying the queue at runtime\n\n```dart\nawait player.add(newTrack);          // Append to end\nawait player.remove(0);              // Remove track at index 0\nawait player.move(5, 0);             // Move track from index 5 to index 0\nawait player.replace(2, newTrack);   // Replace track at index 2\n\nawait player.clearPlaylist();        // Remove all tracks (stops playback)\n```\n\n#### 3.4 Navigation\n\n```dart\nawait player.next();              // Skip to the next track\nawait player.previous();          // Skip to the previous track\nawait player.jump(2);             // Jump to track at index 2 (0-indexed)\n\nawait player.next(force: true);   // Past the last entry: stop playback\nawait player.nextPlaylist();      // Jump to the next source playlist\nawait player.previousPlaylist();  // …and back across concatenated playlists\n```\n\nFor internet-radio station lists and remote `.m3u` and `.pls` playlists, load\nthe playlist file itself with `openPlaylistFile` (see [§3.2](#32-opening-multiple-tracks));\nunlike `open`, it expands the entries into `player.stream.playlist`.\n\n#### 3.5 Repeat and shuffle\n\n```dart\n// Repeat modes\nawait player.setLoop(Loop.off);       // No repeat (default)\nawait player.setLoop(Loop.file);      // Loop the current track\nawait player.setLoop(Loop.playlist);  // Loop the entire playlist\n\n// Shuffle\nawait player.setShuffle(true);   // Shuffle the queue\nawait player.setShuffle(false);  // Restore original order\n```\n\n`Loop` aggregates mpv's two underlying loop properties (`loop-file`\nand `loop-playlist`) into a single mutually-exclusive choice. Subscribe\nvia `player.stream.loop` for live updates.\n\n#### 3.6 Chapter navigation\n\nFor audiobooks, podcasts, and any container that ships chapter markers:\n\n```dart\n// Subscribe to the chapter list (populated after each load)\nplayer.stream.chapters.listen((chapters) {\n  for (var i = 0; i \u003c chapters.length; i++) {\n    print('${i}. ${chapters[i].title} @ ${chapters[i].time}');\n  }\n});\n\n// Active chapter index (0-based; null when no chapter is active)\nplayer.stream.currentChapter.listen((idx) =\u003e print('chapter: $idx'));\n\n// Per-chapter metadata (mpv `chapter-metadata`)\nplayer.stream.chapterMetadata.listen((tags) =\u003e print(tags));\n\n// Jump to a chapter by index\nawait player.setChapter(2);\n```\n\n`Chapter` exposes `time` and an optional `title`. Use `state.demuxerStartTime` if you need the source-side.\n\n---\n\n### 4. Playback control\n\n#### 4.1 Basic controls\n\n```dart\nawait player.play();    // Start or resume\nawait player.pause();   // Pause\nawait player.stop();    // Stop and unload current file\n\n// Toggle pattern: bind to `playWhenReady` (the intent axis), not\n// `playing`, so the button stays stable while seeking or buffering.\nplayer.state.playWhenReady ? await player.pause() : await player.play();\n```\n\n#### 4.2 Seeking\n\n```dart\n// Seek to an absolute position\nawait player.seek(const Duration(seconds: 30));\n\n// Seek forward or backward relative to current position\nawait player.seek(const Duration(seconds: 10), relative: true);\nawait player.seek(const Duration(seconds: -5), relative: true);\n\n// Sample-accurate (vs keyframe) seeking\nawait player.seek(const Duration(seconds: 30), exact: true);\n\n// Percentage-based seeking (for progress-bar scrubbing)\nawait player.seekToPercent(50); // jump to the half-way point (0 to 100)\n\n// Undo the last seek\nawait player.revertSeek();\n```\n\nmpv uses the `absolute` seek mode by default, which works correctly on\nall formats including HLS, providing precise seeking even during\ntranscoded streams.\n\n#### 4.3 A-B loop\n\nDefine a sub-region of the current track and loop between two\ntimestamps. Useful for language-learning apps, transcript review, or\npractising a passage on repeat.\n\n```dart\n// Set the A and B markers (null disables the marker)\nawait player.setAbLoopA(const Duration(seconds: 30));\nawait player.setAbLoopB(const Duration(seconds: 45));\n\n// Limit the number of repetitions; null = infinite\nawait player.setAbLoopCount(3);\n\n// Read remaining iterations (null = no loop or infinite)\nplayer.stream.remainingAbLoops.listen((n) =\u003e print('left: $n'));\n\n// Disable\nawait player.setAbLoopA(null);\nawait player.setAbLoopB(null);\n```\n\n#### 4.4 Speed and pitch\n\n```dart\nawait player.setRate(1.5);              // 1.5× speed (0.01 to 100.0)\nawait player.setPitch(0.9);             // Lower pitch without affecting speed\nawait player.setPitchCorrection(true);  // Pitch correction when changing rate\n```\n\n`setPitchCorrection` enables mpv's `scaletempo` algorithm, which\nadjusts playback speed while preserving the original pitch.\n\nFor high-quality time-stretching that decouples pitch and tempo, use\nthe [`rubberband`](#52-common-effects-quick-examples) effect on the\nDSP bundle.\n\n#### 4.5 Volume and mute\n\nThere are two independent volume layers: the **soft** volume (mpv's own\nsoftware gain) and the **system** volume (the OS per-app mixer).\n\n```dart\n// Soft volume and mute (mpv's software gain, always available)\nawait player.setVolume(80.0);     // 0 to 100 (values above 100 amplify)\nawait player.setMute(true);\nawait player.setMute(false);\n\nawait player.setVolumeMax(150.0); // Raise the software volume ceiling\nawait player.setVolumeGain(6.0);  // Decoder-side pre-amp, in dB\n\n// Clamp the dB range setVolumeGain accepts (defaults -96 and +12)\nawait player.setVolumeGainMin(-60.0);\nawait player.setVolumeGainMax(6.0);\n```\n\nThe **system** volume and mute drive the OS per-app mixer (mpv's `ao-volume` and\n`ao-mute`), distinct from the soft volume. They are best-effort: silently\nignored (no throw) when the active audio backend doesn't expose system\nvolume, in which case `state.systemVolume` and `state.systemMute` stay `null`.\n\n```dart\nif (player.state.systemVolume != null) {        // backend supports it\n  await player.setSystemVolume(70.0);\n  await player.setSystemMute(false);\n}\n```\n\n#### 4.6 Audio delay\n\n```dart\n// Shift audio forward by 50 ms (useful for Bluetooth A2DP sync)\nawait player.setAudioDelay(const Duration(milliseconds: 50));\n\n// Shift backward by 200 ms\nawait player.setAudioDelay(const Duration(milliseconds: -200));\n```\n\n#### 4.7 Resume playback (watch later)\n\nFor audiobooks and podcasts, the player can save a resume point for the\ncurrent file and restore it the next time that file is opened. Only\naudio-relevant properties are persisted (position, speed, pitch, volume,\nmute, audio delay, the effect chain, and the selected track).\n\nRestore-on-reopen is controlled at build time by\n`PlayerConfiguration.resumePlayback` (default `true`). Point\n`watchLaterDir` at an app-writable directory on mobile; the platform\ndefault is often not writable.\n\n```dart\nfinal player = Player(\n  configuration: const PlayerConfiguration(\n    resumePlayback: true,                 // restore on reopen (default)\n    watchLaterDir: '/path/to/app/support/watch_later',\n  ),\n);\n\n// Save a resume point for the current file (e.g. on background or pause)\nawait player.writeResumeConfig();\n\n// Clear it: for the current file, or a specific one by filename\nawait player.deleteResumeConfig();\nawait player.deleteResumeConfig(filename: '/music/audiobook.m4b');\n```\n\n---\n\n### 5. Audio quality and DSP\n\nAll processing in this section runs through ffmpeg filter pipeline and\nworks on every platform.\n\n\u003cimg src=\"https://raw.githubusercontent.com/ales-drnz/mpv_audio_kit/main/imgs/diagrams/dsp_chain.png\" width=\"100%\"\u003e\n\n#### 5.1 The AudioEffects bundle\n\nThe DSP rack lives on a single immutable [`AudioEffects`] value. Every\naudio effect has its own typed `*Settings` field on the bundle, and\nthe whole rack is applied in one call via two setters on `Player`:\n\n```dart\n// Replace the whole bundle (presets, factory defaults, JSON restore):\nFuture\u003cvoid\u003e setAudioEffects(AudioEffects effects);\n\n// Mutate one or more fields with a copyWith mapper:\nFuture\u003cvoid\u003e updateAudioEffects(AudioEffects Function(AudioEffects) f);\n```\n\nEach effect carries an `enabled` flag (default off). When on, it joins\nthe audio chain; when off, it leaves the chain but keeps its parameters\nintact for the next toggle.\n\n```dart\n// Read the live bundle synchronously\nfinal fx = player.state.audioEffects;\nprint('compressor on: ${fx.acompressor.enabled} '\n      'threshold=${fx.acompressor.threshold}');\n\n// Or via stream: sub-stream a single effect with .map().distinct()\nplayer.stream.audioEffects\n  .map((e) =\u003e e.acompressor)\n  .distinct()\n  .listen((c) =\u003e print('compressor: $c'));\n```\n\nApply a multi-effect preset in one shot:\n\n```dart\nawait player.setAudioEffects(const AudioEffects(\n  acompressor: AcompressorSettings(\n    enabled: true, threshold: 0.1, ratio: 4,        // linear ratio threshold\n  ),\n  loudnorm: LoudnormSettings(enabled: true, I: -16),\n  bass: BassSettings(enabled: true, g: 3),          // bass shelf, +3 dB\n  treble: TrebleSettings(enabled: true, g: -2),     // treble shelf, -2 dB\n  rubberband: RubberbandSettings(\n    enabled: true, pitch: 1.0, tempo: 0.95,         // -5% tempo, no pitch shift\n  ),\n));\n```\n\nToggle a single stage:\n\n```dart\nawait player.updateAudioEffects((e) =\u003e e.copyWith(\n  acompressor: e.acompressor.copyWith(enabled: !e.acompressor.enabled),\n));\n```\n\nReset everything:\n\n```dart\nawait player.setAudioEffects(const AudioEffects());\n```\n\n\u003e Mixing `setAudioEffects` with raw `setRawProperty('af', ...)` writes\n\u003e is not supported. The typed bundle owns the chain, and raw writes\n\u003e are rejected. Filters without a typed equivalent go through\n\u003e `effects.custom` (a `List\u003cString\u003e` of raw entries emitted at the head\n\u003e of the chain, before any typed stage).\n\u003e\n\u003e ```dart\n\u003e // mix typed effects with a raw filter you want to inject\n\u003e await player.updateAudioEffects((e) =\u003e e.copyWith(\n\u003e   custom: ['lavfi-aeval=val(0)|val(1)'],\n\u003e   acompressor: const AcompressorSettings(enabled: true, threshold: 0.1),\n\u003e ));\n\u003e ```\n\n#### 5.2 Common effects: quick examples\n\nBelow are some audio effects with representative settings. Every\nother effect works the same way: a typed\nfield on the bundle, configured via `copyWith`.\n\n```dart\n// Dynamic-range compressor\nawait player.updateAudioEffects((e) =\u003e e.copyWith(\n  acompressor: const AcompressorSettings(\n    enabled: true,\n    threshold: 0.1,    // 0..1 linear scale\n    ratio: 4.0,\n    attack: 20.0,      // ms\n    release: 250.0,    // ms\n  ),\n));\n\n// EBU R128 loudness normalisation\nawait player.updateAudioEffects((e) =\u003e e.copyWith(\n  loudnorm: const LoudnormSettings(\n    enabled: true, I: -16, TP: -1.5, LRA: 11,\n  ),\n));\n\n// Pitch and tempo shifting (librubberband). Enums for quality presets.\nawait player.updateAudioEffects((e) =\u003e e.copyWith(\n  rubberband: const RubberbandSettings(\n    enabled: true, pitch: 1.0594, tempo: 1.0,\n    pitchq: RubberbandPitch.quality,\n  ),\n));\n\n// 18-band ISO graphic EQ. Bands keyed by their original `1b`..`18b` names.\nawait player.updateAudioEffects((e) =\u003e e.copyWith(\n  superequalizer: const SuperequalizerSettings(\n    enabled: true,\n    params: {'4b': 1.5, '5b': 2.0, '8b': 0.5, '13b': 1.0},\n  ),\n));\n\n// Bass + treble shelves (two independent shelving effects).\nawait player.updateAudioEffects((e) =\u003e e.copyWith(\n  bass: const BassSettings(enabled: true, g: 4, f: 100),\n  treble: const TrebleSettings(enabled: true, g: -2, f: 3000),\n));\n```\n\nEffects with multiple-choice parameters, rubberband's pitch quality,\naemphasis curve, equalizer filter type, expose them as Dart enums for\nIDE autocomplete and compile-time safety.\n\n#### 5.3 Available effects\n\nThe bundle ships with 86 audio effects covering compression, EQ,\ndenoising, spatialisation, modulation, and more. Each row below maps to a\n`\u003cName\u003eSettings` field on `AudioEffects` (e.g. `acompressor` →\n`AudioEffects.acompressor` of type `AcompressorSettings`). For raw\n`lavfi-*` filters outside this typed set, or for quick experimentation,\nuse `AudioEffects.custom: List\u003cString\u003e` to push entries through the head\nof the chain.\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003cb\u003eBrowse the full catalogue\u003c/b\u003e (click to expand)\u003c/summary\u003e\n\n##### Dynamics and loudness\n\n| Effect | Description |\n|---|---|\n| `acompressor` | Dynamic-range compressor |\n| `acontrast` | Simple dynamic-range compression |\n| `adrc` | Spectral dynamic-range controller |\n| `adynamicequalizer` | Signal-driven dynamic equalization |\n| `adynamicsmooth` | Dynamic smoothing of audio levels |\n| `agate` | Noise gate; silences signal below a threshold |\n| `alimiter` | Brick-wall limiter; caps the output level |\n| `apsyclip` | Psychoacoustic clipper |\n| `asoftclip` | Smooth soft-knee clipping |\n| `compand` | Compander (compress on input, expand on output) |\n| `deesser` | De-essing for sibilance reduction |\n| `drmeter` | Dynamic-range meter |\n| `dynaudnorm` | Adaptive loudness normalization |\n| `ebur128` | EBU R128 loudness scanner |\n| `loudnorm` | EBU R128 loudness normalisation |\n| `mcompand` | Multiband compander |\n| `speechnorm` | Adaptive speech-loudness normaliser |\n\n##### Equalization and tone\n\n| Effect | Description |\n|---|---|\n| `anequalizer` | High-order parametric multiband equalizer |\n| `asubboost` | Subwoofer-frequency boost |\n| `atilt` | Spectral tilt across the frequency range |\n| `bass` | Low-shelf filter (boost, cut bass) |\n| `biquad` | Generic biquad IIR filter |\n| `equalizer` | Two-pole peaking EQ band |\n| `firequalizer` | FIR equalizer with arbitrary frequency response |\n| `highshelf` | High-shelf filter |\n| `lowshelf` | Low-shelf filter |\n| `superequalizer` | 18-band ISO graphic EQ |\n| `tiltshelf` | Tilt shelf (combined low plus high shelf) |\n| `treble` | High-shelf filter (boost, cut treble) |\n\n##### Filters\n\n| Effect | Description |\n|---|---|\n| `allpass` | Two-pole all-pass filter |\n| `asubcut` | Subwoofer-frequency cut |\n| `asupercut` | High-frequency Butterworth cut |\n| `asuperpass` | High-order Butterworth band-pass |\n| `asuperstop` | High-order Butterworth band-stop |\n| `bandpass` | Two-pole Butterworth band-pass |\n| `bandreject` | Two-pole Butterworth band-reject |\n| `highpass` | High-pass at a given frequency |\n| `lowpass` | Low-pass at a given frequency |\n\n##### Pitch, tempo and time\n\n| Effect | Description |\n|---|---|\n| `afreqshift` | Shift the spectrum by a fixed offset |\n| `aphaseshift` | Shift the phase of every spectral bin |\n| `aresample` | Resample to a target sample rate and format |\n| `atempo` | Adjust tempo without changing pitch |\n| `rubberband` | High-quality independent pitch and tempo |\n\n##### Stereo, channels and spatial\n\n| Effect | Description |\n|---|---|\n| `channelmap` | Remap input channels to new positions |\n| `crossfeed` | Headphone crossfeed |\n| `dialoguenhance` | Centre-channel dialogue enhancement |\n| `earwax` | Headphone listening enhancement |\n| `extrastereo` | Increase the left-right difference signal |\n| `haas` | Haas effect (precedence-based stereo widening) |\n| `headphone` | HRTF-based binaural headphone rendering |\n| `pan` | Mix channels with explicit per-channel gains |\n| `stereotools` | Comprehensive stereo image manipulation |\n| `stereowiden` | Stereo widening by reducing common signal |\n| `surround` | Stereo-to-surround upmix |\n| `virtualbass` | Psychoacoustic bass enhancement |\n\n##### Modulation and creative\n\n| Effect | Description |\n|---|---|\n| `acrusher` | Bit-crusher (resolution and rate reduction) |\n| `aecho` | Single-tap echo and multi-tap delay |\n| `aemphasis` | RIAA, FM, disc emphasis curves |\n| `aexciter` | Harmonic exciter |\n| `aphaser` | Phaser |\n| `apulsator` | Auto-panner, tremolo hybrid |\n| `chorus` | Chorus |\n| `crystalizer` | Audio sharpening and brightener |\n| `dcshift` | DC offset shift |\n| `flanger` | Flanger |\n| `hdcd` | HDCD decoder |\n| `tremolo` | Sinusoidal amplitude modulation |\n| `vibrato` | Sinusoidal pitch modulation |\n\n##### Denoise and restoration\n\n| Effect | Description |\n|---|---|\n| `adeclick` | Click and impulse-noise removal |\n| `adeclip` | Clip-restoration |\n| `adecorrelate` | Channel decorrelation |\n| `adelay` | Per-channel delay |\n| `adenorm` | Add low-level dither to fix denormals |\n| `aderivative` | Compute the derivative of the signal |\n| `afftdn` | FFT-based broadband noise reduction |\n| `afwtdn` | Wavelet-based broadband noise reduction |\n| `anlmdn` | Non-local-means denoiser |\n| `arnndn` | RNN-based speech denoiser |\n| `compensationdelay` | Speaker and microphone delay compensation |\n\n##### Spectral, fade and routing\n\n**Spectral**\n\n| Effect | Description |\n|---|---|\n| `afftfilt` | Apply expressions in the frequency domain |\n| `aiir` | Apply an arbitrary IIR filter |\n\n**Fade, silence and padding**\n\n| Effect | Description |\n|---|---|\n| `afade` | Fade in and out |\n| `apad` | Pad with trailing silence |\n| `silenceremove` | Trim leading, trailing, inline silence |\n\n**Routing**\n\n| Effect | Description |\n|---|---|\n| `aeval` | Per-channel expression-based filter |\n| `aformat` | Constrain output format |\n\n\u003c/details\u003e\n\n#### 5.4 ReplayGain\n\n```dart\nawait player.setReplayGain(const ReplayGainSettings(\n  mode: ReplayGain.track,     // .no, .track, .album\n  preamp: 2.0,                // +2 dB on top of the RG value\n  fallback: -6.0,             // -6 dB on files without RG tags\n  clip: false,                // false = peak-limit; true = allow clipping\n));\n\n// Tweak a single field via copyWith\nawait player.setReplayGain(\n  player.state.replayGain.copyWith(mode: ReplayGain.album),\n);\n```\n\n#### 5.5 Gapless playback\n\n```dart\nawait player.setGapless(Gapless.yes);   // Full gapless. Re-uses the decoder\nawait player.setGapless(Gapless.weak);  // Gapless only on compatible formats (default)\nawait player.setGapless(Gapless.no);    // Close and re-open the AO between tracks\n```\n\nFor seamless transitions between tracks of any format, combine\n`Gapless.yes` with `setPrefetchPlaylist(true)` and observe the\n[prefetch lifecycle](#911-prefetch-lifecycle-stream).\n\n```dart\n// Pre-open the next playlist entry in the background. First audio\n// frame ready before the current track ends.\nawait player.setPrefetchPlaylist(true);\n```\n---\n\n### 6. Hardware and routing\n\n#### 6.1 Audio output driver\n\nSelect the native backend used for audio output:\n\n```dart\nawait player.setAudioDriver('wasapi');    // Windows\nawait player.setAudioDriver('coreaudio'); // macOS\nawait player.setAudioDriver('pulse');     // Linux\nawait player.setAudioDriver('alsa');      // Linux\nawait player.setAudioDriver('pipewire');  // Linux\nawait player.setAudioDriver('auto');      // Auto\n```\n\n#### 6.2 Exclusive mode\n\nBypasses the OS audio mixer and writes directly to the hardware. Eliminates software resampling and volume processing for bit-perfect output. Only available on WASAPI (Windows), ALSA (Linux) and CoreAudio (macOS):\n\n```dart\nawait player.setAudioExclusive(true);   // Request exclusive access\nawait player.setAudioExclusive(false);  // Release, return to shared mode\n```\n\n\u003e Exclusive mode locks the audio device. Always call `player.dispose()` when done, or other apps will have no sound.\n\n#### 6.3 Device selection\n\n```dart\n// Listen to available devices\nplayer.stream.audioDevices.listen((devices) {\n  for (final d in devices) {\n    print('${d.name}: ${d.description}');\n  }\n});\n\n// Switch to a specific device\nfinal devices = player.state.audioDevices;\nawait player.setAudioDevice(devices.first);\n```\n\nDevices are populated automatically by mpv when the player initializes. The `name` field is the mpv device identifier; `description` is the human-readable label.\n\n#### 6.4 Output format\n\nForce a specific output format for bit-perfect playback or DAC compatibility:\n\n```dart\n// Sample rate\nawait player.setAudioSampleRate(0);       // Auto\nawait player.setAudioSampleRate(44100);   // 44.1 kHz (CD)\nawait player.setAudioSampleRate(48000);   // 48 kHz (DVD, broadcast)\nawait player.setAudioSampleRate(88200);   // 88.2 kHz (hi-res)\nawait player.setAudioSampleRate(96000);   // 96 kHz (hi-res)\nawait player.setAudioSampleRate(192000);  // 192 kHz (studio)\nawait player.setAudioSampleRate(384000);  // 384 kHz (DXD)\n\n// Bit depth format\nawait player.setAudioFormat(Format.auto);          // mpv picks (default)\nawait player.setAudioFormat(Format.u8);            // 8-bit unsigned, interleaved\nawait player.setAudioFormat(Format.u8Planar);      // 8-bit unsigned, planar\nawait player.setAudioFormat(Format.s16);           // 16-bit signed, interleaved\nawait player.setAudioFormat(Format.s16Planar);     // 16-bit signed, planar\nawait player.setAudioFormat(Format.s32);           // 32-bit signed, interleaved\nawait player.setAudioFormat(Format.s32Planar);     // 32-bit signed, planar\nawait player.setAudioFormat(Format.s64);           // 64-bit signed, interleaved\nawait player.setAudioFormat(Format.s64Planar);     // 64-bit signed, planar\nawait player.setAudioFormat(Format.float32);       // 32-bit float, interleaved\nawait player.setAudioFormat(Format.float32Planar); // 32-bit float, planar\nawait player.setAudioFormat(Format.float64);       // 64-bit float, interleaved\nawait player.setAudioFormat(Format.float64Planar); // 64-bit float, planar\n\n// Channel layout \nawait player.setAudioChannels(Channels.auto);             // mpv picks\nawait player.setAudioChannels(Channels.autoSafe);         // mpv picks, reject multichannel unless verified\n\n// 1 channel\nawait player.setAudioChannels(Channels.mono);             // mono\nawait player.setAudioChannels(Channels.oneZero);          // 1.0 (alias of mono)\n\n// 2 channels\nawait player.setAudioChannels(Channels.stereo);           // stereo\nawait player.setAudioChannels(Channels.twoZero);          // 2.0 (alias of stereo)\nawait player.setAudioChannels(Channels.downmix);          // downmix (semantic alias of stereo)\n\n// ...                                                    // ...\n\n// 8 channels\nawait player.setAudioChannels(Channels.sevenOne);         // 7.1 canonical\nawait player.setAudioChannels(Channels.sevenOneAlsa);     // 7.1(alsa)\nawait player.setAudioChannels(Channels.sevenOneWide);     // 7.1(wide)\nawait player.setAudioChannels(Channels.sevenOneWideSide); // 7.1(wide-side)\nawait player.setAudioChannels(Channels.sevenOneTop);      // 7.1(top)\nawait player.setAudioChannels(Channels.sevenOneRear);     // 7.1(rear)\nawait player.setAudioChannels(Channels.octagonal);        // octagonal\nawait player.setAudioChannels(Channels.cube);             // cube\n\n// Cinema, immersive\nawait player.setAudioChannels(Channels.hexadecagonal);    // hexadecagonal (16ch)\nawait player.setAudioChannels(Channels.surround222);      // 22.2 (NHK, ITU-R BS.775)\n\n// Custom escape, anything mpv recognises but isn't in the named set\nawait player.setAudioChannels(\n  const Channels.custom('fl-fr-fc-bl-br-sl-sr-lfe'),\n);\n```\n\n\u003e When you force a downmix (e.g. 5.1 → stereo), set\n\u003e `PlayerConfiguration.normalizeDownmix: true` to loudness-normalize the\n\u003e result and avoid clipping. It is a build-time option (default `false`).\n\n#### 6.5 SPDIF passthrough\n\nSend compressed audio (AC3, DTS, TrueHD, …) directly to an AV receiver\nover SPDIF or HDMI:\n\n```dart\n// Home-theater Dolby + DTS-HD passthrough\nawait player.setAudioSpdif({Spdif.ac3, Spdif.eac3, Spdif.trueHd, Spdif.dtsHd});\n\n// Dolby only\nawait player.setAudioSpdif({Spdif.ac3, Spdif.eac3, Spdif.trueHd});\n\n// Disable passthrough\nawait player.setAudioSpdif({});\n```\n\n#### 6.6 Audio client name\n\nThe name shown in system audio mixers:\n\n```dart\nawait player.setAudioClientName('MyMusicApp');\n```\n\n#### 6.7 Audio track selection\n\nFor containers with multiple audio tracks (e.g. MKV, MP4 with language\nvariants), the library exposes both the inventory of tracks the\ndemuxer surfaced and the active track:\n\n```dart\n// Walk the audio inventory:\nfor (final t in player.state.tracks.where((tr) =\u003e tr.type == 'audio')) {\n  print('${t.id}: ${t.title ?? t.lang ?? \"audio\"} '\n        '(${t.codec} ${t.sampleRate} Hz ${t.channelCount}ch)');\n}\n\n// Currently selected track\nplayer.stream.currentAudioTrack.listen((track) {\n  if (track == null) return;\n  print('Now decoding track #${track.id}: ${track.title}');\n});\n\n// Switch by id\nawait player.setAudioTrack(const Track.id(2));\n\n// Defer to mpv's automatic choice (container default or first audio)\nawait player.setAudioTrack(Track.auto);\n\n// Disable audio output entirely (e.g. show only metadata + cover art)\nawait player.setAudioTrack(Track.off);\n\n// Load an external audio file as an extra selectable track on the current\n// file (a separate-language dub, a commentary track), then remove it.\nawait player.addAudioTrack(Media('commentary.mka'), title: 'Commentary');\nawait player.removeAudioTrack(const Track.id(3));\n\n// Re-scan sidecar external files (auto-loaded audio and cover art) without\n// reopening the current file.\nawait player.rescanExternalFiles();\n```\n\n`MpvTrack` ships rich per-track introspection: codec, decoder, sample\nrate, channel count, ReplayGain tags, language, default and forced\nflags, `image` and `albumArt` flags so you can skip embedded picture\nstreams when populating a track-switcher UI, plus `external` and\n`externalFilename` (whether the track was loaded from a separate file,\nand its path) and `codecProfile` (the codec profile string when the\ncontainer reports one).\n\n#### 6.8 Reload audio\n\nForce the audio output to reinitialize:\n\n```dart\nawait player.reloadAudio();\n```\n\n#### 6.9 Media role\n\nOn Linux, tag the stream's media role as \"music\" so the audio server\n(PulseAudio and PipeWire) applies the right routing and per-role volume\nprofile. On Android the audtrack and aaudio backends honour it too; it is a\nno-op elsewhere.\n\n```dart\nawait player.setAudioMediaRole(true);\n// Observe via player.stream.audioMediaRole and player.state.audioMediaRole\n```\n\n---\n\n### 7. Network and caching\n\n#### 7.1 Cache configuration\n\nThe six mpv cache properties (`cache`, `cache-secs`,\n`cache-on-disk`, `cache-pause`, `cache-pause-wait`, `cache-pause-initial`)\nare all set in one call through `setCache(CacheSettings)`:\n\n```dart\nawait player.setCache(const CacheSettings(\n  mode: Cache.yes,                 // .auto (default), .yes, .no\n  secs: Duration(seconds: 30),         // target cache duration ahead of the playhead\n  onDisk: true,                        // spill overflow cache to disk\n  pause: true,                         // auto-pause when cache runs dry\n  pauseWait: Duration(seconds: 3),     // pre-buffer required before resume\n  pauseInitial: true,                  // buffer before playback starts (and after seek)\n));\n\n// Tweak a single field via copyWith\nawait player.setCache(\n  player.state.cache.copyWith(secs: const Duration(seconds: 60)),\n);\n\n// Subscribe to live changes\nplayer.stream.cache.listen((cfg) =\u003e print('cache: ${cfg.mode} ${cfg.secs}'));\n```\n\n`pauseInitial` buffers before playback starts, and again after each seek,\nuntil the cache fills, for a smoother start on network sources (web-radio,\nHLS, Plex). When `onDisk` is set, point the on-disk cache at a writable\ndirectory with the build-time `PlayerConfiguration.demuxerCacheDir` (it\ndefaults to mpv's location, which is often not writable on mobile).\n\n#### 7.2 Demuxer memory pool\n\nThe demuxer is the component that reads and parses the media container (MP4, MKV, OGG, etc.) before the audio decoder processes it:\n\n```dart\n// Maximum bytes the demuxer is allowed to cache ahead (default: 150 MiB)\nawait player.setDemuxerMaxBytes(50 * 1024 * 1024); // 50 MiB\n\n// Maximum bytes for the seekback buffer (default: 50 MiB)\nawait player.setDemuxerMaxBackBytes(20 * 1024 * 1024);\n\n// How far ahead the demuxer should read (default: 1s)\nawait player.setDemuxerReadaheadSecs(const Duration(seconds: 5));\n```\n\nFor radio streams or live content where seeking is not needed, reduce the back buffer to zero to save memory:\n\n```dart\nawait player.setDemuxerMaxBackBytes(0);\n```\n\n#### 7.3 Network timeout\n\n```dart\nawait player.setNetworkTimeout(const Duration(seconds: 10)); // Fail after 10 seconds of no data\n```\n\n#### 7.4 TLS and SSL verification\n\n```dart\nawait player.setTlsVerify(true); // Enable; uses the bundled CA pem\n```\n\n#### 7.5 Audio buffer\n\nThe hardware audio buffer. Lower values reduce latency, higher values improve stability under load:\n\n```dart\nawait player.setAudioBuffer(const Duration(milliseconds: 100));  // 100 ms (low latency)\nawait player.setAudioBuffer(const Duration(milliseconds: 500));  // 500 ms (stable on slow hardware)\n```\n\n#### 7.6 Audio stream silence\n\nKeep audio hardware active even when playback is paused, to eliminate click or pop on resume:\n\n```dart\nawait player.setAudioStreamSilence(true);\n```\n\n\u003e Note on iOS: the audio driver in this case is never released, so after an iOS interruption (phone call, other app audio) it stays suspended and playback can't continue.\n\n#### 7.7 Untimed null output\n\nWhen using the `null` audio driver (e.g. for server-side processing or testing without a sound device), this makes the null output run as fast as possible instead of at real time:\n\n```dart\nawait player.setAudioNullUntimed(true);\n```\n\n#### 7.8 Radio and live streams\n\nFor radio, disable caching and cache-pause to minimize latency:\n\n```dart\nawait player.open(Media('https://stream.radio.example.com/live.mp3'));\nawait player.setCache(const CacheSettings(mode: Cache.no, pause: false));\nawait player.setNetworkTimeout(const Duration(seconds: 10));\n```\n\nFor HLS streams (like Jellyfin transcoding), the default cache settings work well. Mpv handles HLS natively and provides precise seeking even on transcoded streams:\n\n```dart\nawait player.open(Media(\n  'https://jellyfin.example.com/audio/stream.m3u8',\n  httpHeaders: {'Authorization': 'MediaBrowser Token=\"...\"'},\n));\n```\n\nTwo build-time `PlayerConfiguration` knobs tune streaming behaviour:\n\n```dart\nfinal player = Player(\n  configuration: const PlayerConfiguration(\n    // Allow in-cache seeking on streams mpv reports as non-seekable\n    // (direct-HTTP or HLS audio). Default false.\n    forceSeekable: true,\n    // Which rendition mpv picks from an adaptive HLS playlist. Default\n    // .max; use .min to save bandwidth on metered links.\n    hlsBitrate: HlsBitrate.min,\n  ),\n);\n```\n\n#### 7.9 Throttling CDNs and chunked requests\n\nSome CDNs, notably progressive YouTube and `googlevideo` audio, rate-limit a single open-ended HTTP range request (the whole rest of the file) to a crawl. Playback is fine from the start but **freezes after a seek**, once the buffered audio drains and the throttled connection can't refill it.\n\nOpt in to bounded range requests for that source via `Media.httpChunkSize`, the same technique yt-dlp uses (`--http-chunk-size`). Each request stays below the CDN's threshold, so it keeps serving at full speed:\n\n```dart\nawait player.open(Media(\n  googlevideoUrl,\n  httpChunkSize: 8 * 1024 * 1024, // 8 MiB chunks\n));\n```\n\nIt is opt-in and scoped to that exact track. Leave it `null` (the default) for fast, trusted servers (Plex, Jellyfin, your own library) where one large request buffers fastest. Must be a positive byte count when set.\n\n#### 7.10 Monitoring the demuxer cache\n\n`player.stream.demuxerCacheState` (and `player.state.demuxerCacheState`)\nsurface a structured `DemuxerCacheState` snapshot for streaming sources:\nthe buffered time ranges, the raw download rate, and the eof-cached,\nbof-cached, and underrun flags. It is empty for directly-seekable local files.\n\nThe `seekableRanges` are what you render as the downloaded (buffered)\nregions of a network seek bar:\n\n```dart\nplayer.stream.demuxerCacheState.listen((cache) {\n  for (final r in cache.seekableRanges) {\n    // Each CacheRange is a [start, end] Duration window already buffered.\n    print('buffered ${r.start} … ${r.end}');\n  }\n  if (cache.underrun) {\n    // The cache ran dry, show a buffering spinner.\n  }\n});\n```\n\n---\n\n### 8. Metadata and cover art\n\n#### 8.1 Metadata tags\n\n```dart\nplayer.stream.metadata.listen((tags) {\n  final title = tags['title'];\n  final artist = tags['artist'];\n  final album = tags['album'];\n  final date = tags['date'];\n  final trackNumber = tags['track'];\n  print('Now playing: $title, $artist');\n});\n\n// Synchronous access\nfinal meta = player.state.metadata;\n```\n\nCommon tag keys (case as returned by mpv): `title`, `artist`, `album`, `album_artist`, `date`, `track`, `disc`, `genre`, `comment`, `composer`.\n\n#### 8.2 Cover art\n\n\u003cimg src=\"https://raw.githubusercontent.com/ales-drnz/mpv_audio_kit/main/imgs/diagrams/cover_art_flow.png\" width=\"100%\"\u003e\n\nEmbedded cover art is exposed as **raw codec bytes** plus a few\nFlutter conveniences, on a synchronous-state + reactive-stream pair:\n\n```dart\n// Synchronous read: peek at the current track's cover\nfinal art = player.state.coverArt;\nif (art != null) {\n  print('Format: ${art.extension}, ${art.bytes.length} bytes');\n}\n\n// Reactive: emits on every file load, null when no cover is embedded\nplayer.stream.coverArt.listen((art) {\n  if (art != null) updateUi(art.image);\n});\n```\n\nThe [`CoverArt`] type carries the bytes plus a few helpers:\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003cb\u003e5 members\u003c/b\u003e (click to expand)\u003c/summary\u003e\n\n| Member | Kind | Description |\n| :--- | :--- | :--- |\n| `bytes` | `final Uint8List` | Raw codec bytes |\n| `mimeType` | `final String` | `'image/png'`, `'image/jpeg'`, `'image/webp'`, `'image/bmp'`, `'image/gif'` |\n| `image` | getter `ImageProvider` | Ready to drop into Flutter `Image(image: …)` |\n| `extension` | getter `String` | `'png'`, `'jpg'`, `'webp'`, `'bmp'`, `'gif'` |\n| `isPng`, `isJpeg`, `isWebp`, `isBmp`, `isGif` | getter `bool` | MIME-type predicates |\n\n\u003c/details\u003e\n\n##### In a Flutter widget\n\n```dart\nStreamBuilder\u003cCoverArt?\u003e(\n  stream: player.stream.coverArt,\n  builder: (ctx, snap) {\n    final art = snap.data;\n    if (art == null) {\n      return const Icon(Icons.music_note, size: 96);\n    }\n    return Image(\n      image: art.image,         // MemoryImage backed by the raw bytes\n      fit: BoxFit.cover,\n    );\n  },\n)\n```\n\n##### Saving the cover to disk\n\n```dart\nfinal art = player.state.coverArt;\nif (art != null) {\n  await File('${dir.path}/cover.${art.extension}').writeAsBytes(art.bytes);\n}\n```\n\n##### Lifecycle\n\n- `stream.coverArt` emits **once per `open()` call**, on file load,\n  before playback starts.\n- The emitted value is `null` when the file has no embedded picture.\n  The stream emits the `null` (rather than skipping the file) so a UI\n  bound to it clears the previous cover on every track change.\n- `state.coverArt` mirrors the latest stream emit synchronously.\n- No re-encoding, no thumbnail generation. The bytes are exactly what\n  the demuxer pulled out of the file.\n\n##### External cover files\n\nIf you want mpv to *also* look for a `cover.jpg` sitting\nnext to the audio file on disk:\n\n```dart\nawait player.setCoverArtAuto(Cover.no);     // library default (disabled)\nawait player.setCoverArtAuto(Cover.exact);  // match the audio filename\nawait player.setCoverArtAuto(Cover.fuzzy);  // any image in the same folder\nawait player.setCoverArtAuto(Cover.all);    // any image, even loosely matched\n```\n\nThe library defaults to `no` (mpv's own default is `exact`) so\nunrelated images can't sneak in. Switch to `exact` or `fuzzy` for a\nlocal-file player that wants disk-side artwork.\n\n---\n\n### 9. State and streams\n\n`mpv_audio_kit` exposes all player state in two complementary ways:\n\n- **`player.state`**: a synchronous, immutable snapshot of the current state. Safe to read from anywhere.\n- **`player.stream`**: reactive streams that emit on every change. Use with `StreamBuilder` or `.listen()`.\n\n#### 9.1 Core streams\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003cb\u003e17 streams\u003c/b\u003e (click to expand)\u003c/summary\u003e\n\n| Stream | Type | Notes |\n| :--- | :--- | :--- |\n| `playWhenReady` | `bool` | User intent to play (the play-pause axis). Set by `play`, `pause`, `open` and `stop`; **stable across seeks and buffering**, so bind your play-pause button (and the OS media controls already do) to this, not to `playing`. |\n| `playing` | `bool` | `true` only while audio is actually being produced; tracks mpv's `core-idle` (inverted). **Toggles transiently during seeks and buffering**, so use for a spinner, not a play-pause button. \"Actually emitting audio\" is `playWhenReady \u0026\u0026 playing`. |\n| `completed` | `bool` | `true` once the current track reaches natural EOF. |\n| `eofReached` | `bool` | mpv's `eof-reached`; `true` while paused at the end of a file with `keep-open=yes`. |\n| `position` | `Duration` | Current playhead, throttled to ~30 Hz. |\n| `duration` | `Duration` | Total duration of the current file; `Duration.zero` for live streams. |\n| `seekCompleted` | `void` | Fires once per `loadfile` or seek when mpv re-initialises (PLAYBACK_RESTART). Use as the authoritative \"file ready\" signal. |\n| `buffering` | `bool` | `true` between `start-file` and `file-loaded`. |\n| `buffer` | `Duration` | Absolute timestamp the demuxer has buffered up to. |\n| `bufferDuration` | `Duration` | Headroom ahead of the playhead (`demuxer-cache-duration`). |\n| `bufferingPercentage` | `double` (0-100) | Wrapper-computed cache fill against `state.cache.secs`. |\n| `volume` | `double` | 0-100; values above 100 amplify. |\n| `mute` | `bool` | |\n| `rate` | `double` | Playback speed multiplier. |\n| `pitch` | `double` | Pitch multiplier. |\n| `pitchCorrection` | `bool` | Whether `scaletempo` is engaged. |\n| `audioDelay` | `Duration` | Audio offset relative to video (sub-millisecond precision is rounded). |\n\n\u003c/details\u003e\n\n#### 9.2 Playlist and track streams\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003cb\u003e8 streams\u003c/b\u003e (click to expand)\u003c/summary\u003e\n\n| Stream | Type | Setter |\n| :--- | :--- | :--- |\n| `playlist` | `Playlist` | `open`, `openAll`, `add`, `remove`, `move`, `replace`, `clearPlaylist` |\n| `loop` | `Loop` | `setLoop` |\n| `shuffle` | `bool` | `setShuffle` |\n| `prefetchPlaylist` | `bool` | `setPrefetchPlaylist` |\n| `tracks` | `List\u003cMpvTrack\u003e` | _(observed; populated by demuxer)_ |\n| `currentAudioTrack` | `MpvTrack?` | `setAudioTrack` |\n| `chapters` | `List\u003cChapter\u003e` | _(observed; populated by demuxer)_ |\n| `currentChapter` | `int?` | `setChapter` |\n\n\u003c/details\u003e\n\n#### 9.3 Audio hardware streams\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003cb\u003e23 streams\u003c/b\u003e (click to expand)\u003c/summary\u003e\n\n| Stream | Type | Setter |\n| :--- | :--- | :--- |\n| `audioDevice` | `Device` | `setAudioDevice` |\n| `audioDevices` | `List\u003cDevice\u003e` | _(read-only)_ |\n| `audioParams` | `AudioParams` | _(decoder; observed)_ |\n| `audioOutParams` | `AudioParams` | _(hardware; observed)_ |\n| `audioBitrate` | `double?` | _(observed)_ |\n| `audioOutputState` | `AudioOutputState` | _(see [§11.4](#114-audio-output-lifecycle))_ |\n| `audioDriver` | `String` | `setAudioDriver` |\n| `audioExclusive` | `bool` | `setAudioExclusive` |\n| `audioBuffer` | `Duration` | `setAudioBuffer` |\n| `audioStreamSilence` | `bool` | `setAudioStreamSilence` |\n| `audioNullUntimed` | `bool` | `setAudioNullUntimed` |\n| `audioSpdif` | `Set\u003cSpdif\u003e` | `setAudioSpdif` |\n| `volumeMax` | `double` | `setVolumeMax` |\n| `volumeGain` | `double` | `setVolumeGain` |\n| `volumeGainMin` | `double` | `setVolumeGainMin` |\n| `volumeGainMax` | `double` | `setVolumeGainMax` |\n| `systemVolume` | `double?` | `setSystemVolume` (OS mixer; `null` when unsupported by the active AO) |\n| `systemMute` | `bool?` | `setSystemMute` (OS mixer; `null` when unsupported by the active AO) |\n| `audioSampleRate` | `int` | `setAudioSampleRate` |\n| `audioFormat` | `Format` | `setAudioFormat` |\n| `audioChannels` | `Channels` | `setAudioChannels` |\n| `audioClientName` | `String` | `setAudioClientName` |\n| `audioMediaRole` | `bool` | `setAudioMediaRole` (reports a \"music\" role to PulseAudio and PipeWire) |\n\n\u003c/details\u003e\n\n`AudioParams` carries: `format`, `sampleRate`, `channels`,\n`channelCount`, `hrChannels`, `codec`, `codecName`.\n\n#### 9.4 DSP and filter streams\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003cb\u003e3 streams\u003c/b\u003e (click to expand)\u003c/summary\u003e\n\n| Stream | Type | Setter |\n| :--- | :--- | :--- |\n| `audioEffects` | `AudioEffects` | `setAudioEffects`, `updateAudioEffects` |\n| `replayGain` | `ReplayGainSettings` | `setReplayGain` |\n| `gapless` | `Gapless` | `setGapless` |\n\n\u003c/details\u003e\n\n#### 9.5 Network and cache streams\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003cb\u003e11 streams\u003c/b\u003e (click to expand)\u003c/summary\u003e\n\n| Stream | Type | Setter |\n| :--- | :--- | :--- |\n| `cache` | `CacheSettings` | `setCache` |\n| `networkTimeout` | `Duration` | `setNetworkTimeout` |\n| `tlsVerify` | `bool` | `setTlsVerify` |\n| `pausedForCache` | `bool` | _(observed; auto-pause signal)_ |\n| `demuxerViaNetwork` | `bool` | _(observed)_ |\n| `demuxerCacheState` | `DemuxerCacheState` | _(observed; buffered ranges, raw input rate, eof-cached, bof-cached, underrun)_ |\n| `cacheSpeed` | `double` (bytes per second) | _(observed)_ |\n| `cacheBufferingState` | `int` (0-100) | _(observed)_ |\n| `demuxerMaxBytes` | `int` | `setDemuxerMaxBytes` |\n| `demuxerMaxBackBytes` | `int` | `setDemuxerMaxBackBytes` |\n| `demuxerReadaheadSecs` | `Duration` | `setDemuxerReadaheadSecs` |\n\n\u003c/details\u003e\n\n#### 9.6 File metadata and path streams\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003cb\u003e12 streams\u003c/b\u003e (click to expand)\u003c/summary\u003e\n\n| Stream | Type | mpv property |\n| :--- | :--- | :--- |\n| `metadata` | `Map\u003cString, String\u003e` | `metadata` |\n| `mediaTitle` | `String` | `media-title` (falls back to filename when no `title` tag) |\n| `fileFormat` | `String` | `file-format` |\n| `fileSize` | `int` | `file-size` |\n| `path` | `String` | `path` (canonicalised, post-redirect) |\n| `filename` | `String` | `filename` (no directory) |\n| `streamPath` | `String` | `stream-path` (URI as originally requested) |\n| `streamOpenFilename` | `String` | `stream-open-filename` (URI as opened post-redirect) |\n| `playlistPath` | `String` | `playlist-path` (source `.m3u` or `.pls`; empty when not loaded via a playlist) |\n| `seekable` | `bool` | `seekable` |\n| `partiallySeekable` | `bool` | `partially-seekable` (HLS or DASH window) |\n| `demuxerIdle` | `bool` | `demuxer-cache-idle` |\n\n\u003c/details\u003e\n\n#### 9.7 Playback timing streams\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003cb\u003e3 streams\u003c/b\u003e (click to expand)\u003c/summary\u003e\n\n| Stream | Type | Notes |\n| :--- | :--- | :--- |\n| `audioPts` | `Duration` | mpv's `audio-pts`; per-frame timestamp including AO latency. More granular than [`position`](#91-core-streams). |\n| `timeRemaining` | `Duration` | Wall-clock time to EOF, **ignoring** playback rate. |\n| `playtimeRemaining` | `Duration` | Time to EOF **adjusted** for playback rate. |\n\n\u003c/details\u003e\n\n#### 9.8 A-B loop streams\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003cb\u003e4 streams\u003c/b\u003e (click to expand)\u003c/summary\u003e\n\n| Stream | Type | Setter |\n| :--- | :--- | :--- |\n| `abLoopA` | `Duration?` (`null` = disabled) | `setAbLoopA` |\n| `abLoopB` | `Duration?` (`null` = disabled) | `setAbLoopB` |\n| `abLoopCount` | `int?` (`null` = infinite) | `setAbLoopCount` |\n| `remainingAbLoops` | `int?` (`null` when no loop or infinite) | _(observed; counts down)_ |\n\n\u003c/details\u003e\n\n#### 9.9 Cover art streams\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003cb\u003e2 streams\u003c/b\u003e (click to expand)\u003c/summary\u003e\n\n| Stream | Type | Setter |\n| :--- | :--- | :--- |\n| `coverArt` | `CoverArt?` (one emit per file load) | _(observed; from embedded picture)_ |\n| `coverArtAuto` | `Cover` | `setCoverArtAuto` |\n\n\u003c/details\u003e\n\n#### 9.10 Runtime diagnostics\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003cb\u003e8 streams\u003c/b\u003e (click to expand)\u003c/summary\u003e\n\n| Stream | Type | mpv property |\n| :--- | :--- | :--- |\n| `seeking` | `bool` | `seeking` (UI gate against concurrent seeks) |\n| `percentPos` | `double` (0-100) | `percent-pos` |\n| `currentDemuxer` | `String` | `current-demuxer` |\n| `currentAo` | `String` | `current-ao` |\n| `demuxerStartTime` | `Duration` | `demuxer-start-time` (initial timestamp offset) |\n| `chapterMetadata` | `Map\u003cString, String\u003e` | `chapter-metadata` (per-chapter tags) |\n| `mpvVersion` | `String` | `mpv-version` |\n| `ffmpegVersion` | `String` | `ffmpeg-version` |\n\n\u003c/details\u003e\n\n#### 9.11 Prefetch lifecycle stream\n\nmpv pre-opens the next playlist entry in the background to make the\ntransition between tracks gapless. The wrapper exposes a typed stream\nso you can drive a \"Prefetching…\" UI, verify gapless, or log\nwarnings when a prefetch is dropped without parsing log lines.\n\n\u003cimg src=\"https://raw.githubusercontent.com/ales-drnz/mpv_audio_kit/main/imgs/diagrams/mpv_prefetch_state.png\" width=\"100%\"\u003e\n\n```dart\nplayer.stream.prefetchState.listen((state) {\n  switch (state) {\n    case MpvPrefetchState.idle:\n      // No background prefetch in progress.\n    case MpvPrefetchState.loading:\n      // The opener thread is creating the demuxer for the next item\n      // and the secondary cache is filling.\n      showIndicator('Prefetching…');\n    case MpvPrefetchState.ready:\n      // Secondary demuxer is open AND idle (cache-secs reached,\n      // no segment fetches outstanding). Gapless is armed.\n      showIndicator('Ready');\n    case MpvPrefetchState.used:\n      // Edge-trigger: the track just transitioned gaplessly.\n      // Fires once and then drops back to `idle`.\n      showIndicator('Using prefetched');\n    case MpvPrefetchState.failed:\n      // Edge-trigger: the opener thread failed (network error,\n      // unsupported codec, on_load hook abort).\n      showIndicator('Prefetch failed');\n  }\n});\n```\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003cb\u003e5 prefetch states\u003c/b\u003e (click to expand)\u003c/summary\u003e\n\n| State | When it fires | Notes |\n| :--- | :--- | :--- |\n| `idle` | Default; after every cancel or drop | Also fires right after `used` and `failed` so they read as one-shot transients |\n| `loading` | Opener thread running | Persists until the demuxer is open and the reader goes idle |\n| `ready` | Secondary demuxer open + reader idle | Gapless is armed |\n| `used` | Track transitioned via the prefetched stream | Edge-triggered; pairs with the subsequent `idle` |\n| `failed` | Opener thread error | Edge-triggered; pairs with the subsequent `idle` |\n\n\u003c/details\u003e\n\nFor a determinate progress bar instead of a spinner, pair the state with\n`player.stream.prefetchCacheDuration`: how much of the next track is\nalready buffered ahead, as a `Duration`. Divide it by your configured\ncache target for a percentage; it emits `Duration.zero` while no prefetch\nis in flight.\n\n```dart\nplayer.stream.prefetchCacheDuration.listen((buffered) {\n  const target = Duration(seconds: 30); // your cache target\n  final pct = (buffered.inMilliseconds / target.inMilliseconds)\n      .clamp(0.0, 1.0);\n  showPrefetchProgress(pct); // 0.0 → 1.0\n});\n```\n\n#### 9.12 Aggregate lifecycle\n\n`player.stream.playbackState` collapses the four underlying flags\n(`playing`, `buffering`, `completed`, `pausedForCache`) plus\n`duration` into a single mutually-exclusive `MpvPlaybackState` enum,\nideal when the UI wants one indicator instead of three.\n\n```dart\nplayer.stream.playbackState.listen((phase) {\n  switch (phase) {\n    case MpvPlaybackState.idle:       // No file loaded\n    case MpvPlaybackState.loading:    // File is opening (demuxer and decoder init)\n    case MpvPlaybackState.buffering:  // Mid-playback network stall\n    case MpvPlaybackState.playing:    // Producing audio\n    case MpvPlaybackState.paused:     // File loaded, audio paused\n    case MpvPlaybackState.completed:  // Reached natural EOF\n  }\n});\n```\n\n#### 9.13 Complete state snapshot\n\n`player.state` mirrors every stream above. Use it for one-shot reads\ninside event handlers and `build()` methods:\n\n```dart\nfinal s = player.state;\nprint(s.playing);                                // bool\nprint(s.position);                               // Duration\nprint(s.duration);                               // Duration\nprint(s.volume);                                 // double\nprint(s.buffer);                                 // Duration\nprint(s.playlist.items[s.playlist.index].uri);   // String\nprint(s.metadata['title']);                      // String?\nprint(s.audioParams.codec);                      // String?\nprint(s.audioEffects.acompressor.threshold);     // double (linear ratio)\nprint(s.cache.secs);                             // Duration\nprint(s.replayGain.preamp);                      // double\nprint(s.tracks.where((t) =\u003e t.type == 'audio')); // Iterable\u003cMpvTrack\u003e\nprint(s.chapters);                               // List\u003cChapter\u003e\nprint(s.audioOutputState);                       // AudioOutputState\nprint(s.mpvVersion);                             // e.g. '0.41.0'\nprint(s.ffmpegVersion);                          // e.g. '8.1.1'\n```\n\n#### 9.14 Spectrum and PCM streams\n\nTwo real-time streams expose the audio currently flowing through the\noutput. See [§13](#13-visualizer-waveform-and-spectrum) for the full\nconfiguration surface, the math behind the pipeline, and a reference\nvisualizer.\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003cb\u003e2 streams\u003c/b\u003e (click to expand)\u003c/summary\u003e\n\n| Stream                | Type                | What it carries |\n|-----------------------|---------------------|-----------------|\n| `stream.fft`          | `Stream\u003cFftFrame\u003e`  | Smoothed FFT bands plus raw bins, ready for a bar visualizer. |\n| `stream.pcm`          | `Stream\u003cPcmFrame\u003e`  | Raw post-DSP samples, ready for a waveform and VU. |\n\n\u003c/details\u003e\n\nBoth streams are lazy (poll loop runs only while subscribed) and\nshare the same upstream tap, so subscribing to both costs only the\nduplicate FFT computation.\n\n```dart\nplayer.stream.fft.listen((frame) {\n  // 64 bands, each in [0, 1]. Paint directly:\n  for (var i = 0; i \u003c frame.bands.length; i++) {\n    paintBar(i, frame.bands[i]);\n  }\n});\n```\n\n#### 9.15 Media session streams\n\nTwo streams expose the OS media session. See\n[§14](#14-os-media-session) for the full setup and command surface.\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003cb\u003e2 streams\u003c/b\u003e (click to expand)\u003c/summary\u003e\n\n| Stream                          | Type                            | What it carries |\n|---------------------------------|---------------------------------|-----------------|\n| `stream.mediaSession`           | `Stream\u003cMediaSession?\u003e`         | The active session config (or `null` when none is published); mirrors `state.mediaSession`. |\n| `stream.mediaSessionCommands`   | `Stream\u003cMediaSessionCommand\u003e`   | Transport commands the OS sends back (play, pause, next, seek and so on). Auto-applied to the player; surfaced here for analytics or interception. |\n\n\u003c/details\u003e\n\n---\n\n### 10. Raw API\n\nFor anything not covered by the typed API, you can access mpv directly.\n\n#### 10.1 Read a property\n\nReturns `null` if the property doesn't exist or the call fails.\n\n```dart\nfinal String? value = await player.getRawProperty('audio-codec');\nfinal String? samplerate = await player.getRawProperty('audio-params/samplerate');\n```\n\n#### 10.2 Write a property\n\nThrows `MpvException` if libmpv rejects the write (typo, out-of-range\nvalue, …). Carries `name`, mpv `code`, and the human-readable\n`message` from `mpv_error_string`.\n\n```dart\ntry {\n  await player.setRawProperty('audio-samplerate', '96000');\n  await player.setRawProperty('audio-channels', 'stereo');\n} on MpvException catch (e) {\n  print('mpv rejected ${e.name}: ${e.message} (code=${e.code})');\n}\n```\n\n#### 10.3 Send a command\n\nSame `MpvException` contract on rejection.\n\n```dart\nawait player.sendRawCommand(['af', 'add', 'lavfi-aresample=48000']);\nawait player.sendRawCommand(['playlist-shuffle']);\nawait player.sendRawCommand(['ao-reload']);\n```\n\nAny command or property from the\n[mpv documentation](https://mpv.io/manual/master/) is accessible\nthrough these methods.\n\n\u003e Prefer the typed setters (`setVolume`, `setCache`,\n\u003e `setReplayGain`, …) when they cover your use case. They update\n\u003e `state` immediately, while the raw escape hatches have to wait for\n\u003e mpv to confirm the change.\n\n---\n\n### 11. Error handling and logging\n\n#### 11.1 Typed error stream\n\nThe error stream emits `MpvPlayerError`, a sealed class with two subtypes that let you distinguish between playback failures and informational engine errors:\n\n\u003cimg src=\"https://raw.githubusercontent.com/ales-drnz/mpv_audio_kit/main/imgs/diagrams/error_streams.png\" width=\"100%\"\u003e\n\n```dart\nplayer.stream.error.listen((error) {\n  switch (error) {\n    case MpvEndFileError():\n      // Playback ended due to an error (e.g. network timeout, file not found).\n      print('End-file error: reason=${error.reason}, code=${error.code}');\n      print('  isLoadingError: ${error.isLoadingError}');\n      print('  isAudioOutputError: ${error.isAudioOutputError}');\n      print('  isFormatError: ${error.isFormatError}');\n    case MpvLogError():\n      // An mpv subsystem logged at error or fatal level (e.g. codec issue).\n      // Does NOT necessarily mean playback has stopped.\n      print('Log error [${error.prefix}] ${error.level}: ${error.message}');\n  }\n});\n```\n\n`MpvEndFileError`, emitted when `MPV_EVENT_END_FILE` fires with a non-zero error code:\n- `reason`: a `MpvEndFileReason` enum (`eof`, `stop`, `quit`, `error`, `redirect`)\n- `code`: the raw mpv error code (e.g. `-13` for `MPV_ERROR_LOADING_FAILED`)\n- `isLoadingError`: `true` for network or file loading failures\n- `isAudioOutputError`: `true` when the audio output driver failed to initialize\n- `isFormatError`: `true` when the file format is unrecognizable or has no audio\n\n`MpvLogError`, emitted when mpv logs at `error` or `fatal` level:\n- `prefix`: the mpv subsystem (e.g. `'ffmpeg'`, `'ao'`, `'demux'`)\n- `level`: `LogLevel` (`LogLevel.error` or `LogLevel.fatal`)\n- `text`: the raw log line from the mpv subsystem\n- `message`: getter returning `'[prefix] level.mpvValue: text'`\n\n\u003e Network note: per the mpv documentation, a network disconnection mid-stream\n\u003e may report as `MpvEndFileReason.eof` rather than `MpvEndFileReason.error`.\n\u003e Use `player.stream.endFile` and compare position vs duration for reliable detection (see §11.2).\n\n#### 11.2 End file stream\n\n`player.stream.endFile` emits an `MpvFileEndedEvent` for **every** file-end, not just errors. This is the only way to detect premature EOFs caused by network disconnections, which mpv reports as `reason: eof` with no error code:\n\n```dart\nplayer.stream.endFile.listen((event) {\n  if (event.reason == MpvEndFileReason.eof) {\n    final pos = player.state.position;\n    final dur = player.state.duration;\n    if (dur \u003e Duration.zero \u0026\u0026 (dur - pos).inSeconds \u003e 5) {\n      print('Premature EOF, likely a network drop');\n    }\n  }\n});\n```\n\n`MpvFileEndedEvent` fields:\n- `reason`: a `MpvEndFileReason` enum value\n- `error`: the raw mpv error code (non-zero only when `reason == MpvEndFileReason.error`)\n\n#### 11.3 Network state\n\nTwo dedicated streams for monitoring network conditions:\n\n```dart\n// True when playback is paused because the cache ran empty (network stall).\n// This is the authoritative signal. Prefer it over interpreting error events.\nplayer.stream.pausedForCache.listen((paused) {\n  if (paused) showBufferingIndicator();\n});\n\n// True when the current stream is being read via a network protocol.\n// Useful for deciding whether an error is likely network-related.\nplayer.stream.demuxerViaNetwork.listen((isNetwork) {\n  print('Network stream: $isNetwork');\n});\n```\n\nBoth are also available synchronously via `player.state.pausedForCache` and `player.state.demuxerViaNetwork`.\n\n#### 11.4 Audio output lifecycle\n\nmpv exposes the audio output's lifecycle as a typed stream. Read it\nto drive a \"Connecting…\" UI on slow backends, or to detect a silent\nplayer without polling format params.\n\n```dart\nplayer.stream.audioOutputState.listen((state) {\n  switch (state) {\n    case AudioOutputState.closed:        // No AO active\n    case AudioOutputState.initializing:  // ao_init_best in flight\n    case AudioOutputState.active:        // AO open, producing samples\n    case AudioOutputState.failed:        // ao_init_best returned NULL\n  }\n});\n```\n\nThe library also surfaces a typed `MpvLogError` on `stream.error` the\nmoment the AO transitions to `failed`, so you don't need a separate\nlistener for the \"no sound\" case.\n\n#### 11.5 Log streams\n\nTwo streams keep engine and library messages disjoint. Route them to\ndifferent sinks (e.g. show only `log` in a debug overlay while\nforwarding `internalLog` to crash reporting).\n\n```dart\n// mpv engine messages: ffmpeg, demux, ao, cplayer, …\nplayer.stream.log.listen((entry) {\n  // MpvLogEntry has: prefix (String), level (LogLevel), text (String)\n  if (entry.level == LogLevel.error || entry.level == LogLevel.fatal) {\n    print('[${entry.level.mpvValue}] ${entry.prefix}: ${entry.text}');\n  }\n});\n\n// library-side diagnostics: JSON parse warnings, hook timeouts,\n// resolution errors. Always carries prefix: 'mpv_audio_kit'.\nplayer.stream.internalLog.listen((entry) {\n  print('[wrapper:${entry.level.mpvValue}] ${entry.text}');\n});\n```\n\n`PlayerConfiguration.logLevel` sets the initial verbosity (`LogLevel.warn`\nfor production, `LogLevel.debug` or `.v` for development, `LogLevel.off` to\nsilence the engine). Change it at runtime with `setLogLevel`:\n\n```dart\nawait player.setLogLevel(LogLevel.debug); // more detail on demand\nawait player.setLogLevel(LogLevel.off);   // silence the engine\n```\n\n---\n\n### 12. Hooks\n\nHooks intercept mpv's file-loading pipeline before a stream is opened. Use them to lazily resolve URLs, inject per-file HTTP headers, or redirect to a different source without a local proxy server:\n\n\u003cimg src=\"https://raw.githubusercontent.com/ales-drnz/mpv_audio_kit/main/imgs/diagrams/on_load_hook_sequence.png\" width=\"100%\"\u003e\n\n\u003e The library already registers an internal `on_load` hook so\n\u003e `Media.httpHeaders` are applied automatically to every track via\n\u003e `file-local-options/http-header-fields`. You only need to register\n\u003e your own hook when you want to handle URL resolution, redirects, or\n\u003e any other per-load logic on top of that.\n\n#### 12.1 Registering a hook\n\nCall `registerHook` **once** after creating the player (before any `open` call).\n\n```dart\nplayer.registerHook(Hook.load);\n```\n\nYou can add a safety timeout. If `continueHook` isn't called within the given duration, the library auto-continues to prevent mpv from stalling indefinitely (e.g. due to an unhandled exception):\n\n```dart\nplayer.registerHook(Hook.load, timeout: const Duration(seconds: 10));\n```\n\nThe full set of mpv lifecycle hooks:\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003cb\u003e6 lifecycle hooks\u003c/b\u003e (click to expand)\u003c/summary\u003e\n\n| Hook | When it fires |\n| :--- | :--- |\n| `Hook.beforeStartFile` | Before any per-file work begins (drains stale property changes) |\n| `Hook.load` | Before a stream is opened. Redirect the URL or attach per-file headers |\n| `Hook.loadFail` | After a stream failed to open. Useful for fallback URLs |\n| `Hook.preloaded` | File open, demuxer ready, before track selection and decoder init |\n| `Hook.unload` | Before a file is closed. Cleanup hook tied to the current file |\n| `Hook.afterEndFile` | After a file finished and was fully unloaded |\n\n\u003c/details\u003e\n\n\u003e Hooks fire during prefetch too. When mpv pre-opens the next playlist entry to enable gapless transitions, `on_load` is invoked for that track too, so custom URL schemes (e.g. `plex-transcode://` → resolved HLS URL) are resolved for every track, including the one being prefetched in the background. Your listener is called once per track regardless of whether playback is active or prefetching, and `setRawProperty('stream-open-filename', …)` accepts hook-driven rewrites in either context.\n\n#### 12.2 Listening and continuing\n\nSubscribe to `player.stream.hook` and call `continueHook` when processing is done. You must always call `continueHook`, even on error, otherwise mpv stalls indefinitely:\n\n```dart\nplayer.stream.hook.listen((event) async {\n  if (event.hook == Hook.load) {\n    final url = await player.getRawProperty('stream-open-filename') ?? '';\n\n    try {\n      if (url.startsWith('my-scheme://')) {\n        // Redirect to a real URL\n        final resolved = await myResolver(url);\n        await player.setRawProperty('stream-open-filename', resolved.url);\n\n        // Inject per-file HTTP headers (direct HTTP only; for HLS use URL query params)\n        if (resolved.headers.isNotEmpty) {\n          final headerString = resolved.headers.entries\n              .map((e) =\u003e '${e.key}: ${e.value}')\n              .join(',');\n          await player.setRawProperty(\n            'file-local-options/http-header-fields',\n            headerString,\n          );\n        }\n      }\n    } finally {\n      player.continueHook(event.id); // always call\n    }\n  } else {\n    player.continueHook(event.id);\n  }\n});\n```\n\n#### 12.3 HTTP headers via hook\n\n`file-local-options/http-header-fields` sets headers only for the current file. They are applied at the mpv and libmpv layer and work correctly for direct HTTP streams.\n\nImportant note for HLS streams: when mpv opens an HLS playlist, the actual segment downloads are handled directly by ffmpeg's lavf, which does not inherit `http-header-fields` set via the hook. If your server requires authentication on the HLS segments, embed the credentials in the URL as query parameters instead:\n\n```dart\n// ✅ Correct for HLS: auth in the URL, visible to ffmpeg's lavf\nplayer.setRawProperty(\n  'stream-open-filename',\n  'https://server/stream/playlist.m3u8?token=abc123',\n);\n\n// ⚠️ Works for direct HTTP streams only; ignored by ffmpeg's lavf for HLS sub-requests\nplayer.setRawProperty('file-local-options/http-header-fields', 'Authorization: Bearer abc123');\n```\n\n#### 12.4 Lazy URL resolution\n\nWhen building a playlist with `Future.wait`, all `getStreamUrl` calls run in parallel. If your server rejects concurrent session creation (as Plex does for transcoding), store the session parameters and return a placeholder URL (e.g. `my-scheme://session-id`). The `on_load` hook fires **sequentially** as mpv opens each track, so resolution calls never overlap:\n\n```dart\n// Building the queue. No real API calls yet\nfinal medias = await Future.wait(tracks.map((t) async {\n  final url = await service.getStreamUrl(t.id); // returns \"my-scheme://abc\"\n  return Media(url);\n}));\nawait player.openAll(medias);\n\n// When mpv reaches each track, the hook resolves it on demand:\n// on_load → myResolver(\"my-scheme://abc\") → /decision + start.m3u8 URL\n```\n\n---\n\n### 13. Visualizer, Waveform and Spectrum\n\nA real-time FFT spectrum and a raw PCM stream are exposed straight\noff the audio output, captured *post-DSP* (after volume, EQ,\ncompressor: what you actually hear). Drive a `CustomPainter`\nvisualizer with `bands`, build a VU meter with `samples`, or run any\ncustom feature extraction on top:\n\n\u003cimg src=\"https://raw.githubusercontent.com/ales-drnz/mpv_audio_kit/main/imgs/diagrams/spectrum_pipeline.png\" width=\"100%\"\u003e\n\nThe pipeline is lazy: the FFT runs only while\n`Player.stream.fft` (or `Player.stream.pcm`) has at least one\nlistener. On the last cancel the timer stops and the FFT memory is\nfreed.\n\n#### 13.1 Subscribing to the spectrum stream\n\n`Player.stream.fft` emits an [FftFrame] on a fixed interval\n(default 30 fps).\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003cb\u003e6 fields per frame\u003c/b\u003e (click to expand)\u003c/summary\u003e\n\n| Field         | Type                | What it is |\n|---------------|---------------------|------------|\n| `bins`        | `Float32List`       | Raw FFT magnitude per linear-frequency bin, normalised `[0, 1]`. Length = `fftSize / 2`. |\n| `bands`       | `Float32List`       | Smoothed log-spaced perceptual bands ready for a bar visualizer. Length = `bandCount`. |\n| `timestamp`   | `Duration`          | Playback position the samples correspond to. |\n| `sampleRate`  | `int`               | Hz of the AO output. |\n| `bandLowHz`   | `double`            | Lower edge of the band axis. |\n| `bandHighHz`  | `double`            | Upper edge of the band axis (clamped to Nyquist). |\n\n\u003c/details\u003e\n\n```dart\nplayer.stream.fft.listen((frame) {\n  // 64 bands ready to paint:\n  for (var i = 0; i \u003c frame.bands.length; i++) {\n    final h = frame.bands[i] * canvasHeight;\n    // draw bar i with height h …\n  }\n});\n```\n\nUse `bands` for visualizers, the band-level asymmetric EMA (fast\nattack, slow release) makes painting them directly produce the\n\"bouncy\" feel without any client-side animation logic. Use `bins`\nfor custom remappings (mel, Bark, constant-Q, peak detection).\n\n#### 13.2 Configuring the pipeline\n\nTune the FFT and the visual smoothing through `SpectrumSettings`,\napplied in one call via `Player.setSpectrum`, or one field at a time\nwith `Player.updateSpectrum` (copyWith mapper).\n\n```dart\n// 60 fps with 128 bands and a tighter dB window:\nawait player.setSpectrum(const SpectrumSettings(\n  fftSize: 2048,\n  bandCount: 128,\n  emitInterval: Duration(milliseconds: 16),\n  minDb: -90,\n  maxDb: -20,\n));\n\n// Mutate one field, e.g. UI slider on attack smoothing:\nawait player.updateSpectrum((s) =\u003e s.copyWith(attackSmoothing: 0.7));\n```\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003cb\u003e10 fields\u003c/b\u003e (click to expand)\u003c/summary\u003e\n\n| Field             | Default        | Range or choice |\n|-------------------|----------------|----------------|\n| `fftSize`         | 2048           | Power of 2: 256, 512, 1024, 2048, 4096 |\n| `bandCount`       | 64             | Typical 32-128 |\n| `bandLowHz`       | 20.0           | Bottom of human hearing |\n| `bandHighHz`      | 20000.0        | Clamped to Nyquist (`sampleRate / 2`) |\n| `window`          | `WindowFunction.hann` | Hann, Blackman-Harris, Rectangular |\n| `emitInterval`    | 33 ms (~30 fps)| 8-67 ms (~120-15 fps) |\n| `attackSmoothing` | 0.5            | EMA when band rises; higher = snappier |\n| `releaseSmoothing`| 0.1            | EMA when band falls; lower = slower decay |\n| `minDb`, `maxDb`  | -100, -30      | dB clip range mapped to `[0, 1]`. Matches Web Audio API `AnalyserNode.minDecibels` and `maxDecibels` defaults. |\n| `overlapFactor`   | 4              | Power of 2: 1 (no overlap), 2, 4 (75 %, default), 8, 16 |\n\n\u003c/details\u003e\n\nThe pipeline reallocates FFT memory only on changes that require it,\nso flipping settings on every UI tick is cheap.\n\nPause behaviour. When playback pauses, the AO ring stops\nreceiving samples and the pipeline stops emitting. The last\n[FftFrame] is \"frozen\": the consumer holds the displayed state\nuntil playback resumes. To fade the visualizer to zero on pause,\nanimate the held frame client-side.\n\n#### 13.3 Raw PCM stream\n\n`Player.stream.pcm` emits [PcmFrame]s on the same cadence as\n`fft`, carrying the raw post-DSP samples instead of the\nfrequency-domain transform. Use it for time-domain visualisations:\noscilloscope-style waveforms, accurate VU and peak meters, custom feature\nextractors that need amplitude.\n\n```dart\ndouble peak = 0;\nplayer.stream.pcm.listen((frame) {\n  for (final s in frame.samples) {\n    if (s.abs() \u003e peak) peak = s.abs();\n  }\n  peak *= 0.92; // decay\n  vuController.value = peak;\n});\n```\n\n`samples` is a `Float32List` of interleaved channels (`L, R, L, R…`).\nUse `frame.samplesPerChannel` to compute the per-channel sample\ncount.\n\n#### 13.4 Per-filter PCM taps\n\n`player.stream.tap(filter, side: ...)` captures raw audio samples at\neither side of a single filter in the `af` chain. The slot is named\nthrough the typed `AudioEffect` enum, the same 86 values surfaced as\n`*Settings` fields on `AudioEffects`, so every reachable filter is\nchosen at compile time. The side is one of `TapSide.pre` (before the\nfilter's DSP runs) or `TapSide.post` (after).\n\n```dart\n// Two-curve display: input vs output of a single filter.\nplayer.stream\n    .tap(AudioEffect.equalizer, side: TapSide.pre)\n    .listen((pcm) =\u003e paintInputWaveform(pcm.samples));\nplayer.stream\n    .tap(AudioEffect.equalizer, side: TapSide.post)\n    .listen((pcm) =\u003e paintOutputWaveform(pcm.samples));\n```\n\nFor frequency-domain bands run the samples through `BandProcessor`,\nthe same FFT, windowing and smoothing the library uses for the global\nvisualizer, so per-filter and global curves pulse with the exact same\nballistic:\n\n```dart\nfinal processor = BandProcessor(player.spectrumSettings);\n// Track the global config so the local FFT stays in lock-step.\nfinal cfgSub = player.stream.spectrum.listen(processor.setSettings);\nfinal tapSub = player.stream\n    .tap(AudioEffect.equalizer, side: TapSide.post)\n    .listen((pcm) {\n      final fft = processor.process(pcm);\n      if (fft != null) painter.bands = fft.bands;\n    });\n```\n\n`AudioEffectsX.active` cross-links the singular `AudioEffect` enum\nwith the `AudioEffects` bundle, yielding the enum value for every slot\nflagged `enabled: true`, so you can iterate the live rack:\n\n```dart\nfor (final f in player.state.audioEffects.active) {\n  player.stream.tap(f, side: TapSide.post).listen(_paint);\n}\n```\n\nThe taps are lazy in the same way the global pipeline is: the\nmatching engine hook activates only while at least one listener is\nattached and tears down on the last cancel.\n\n#### 13.5 Waveform\n\nA mono min and max envelope of the loaded track is exposed via\n[PlayerStream.waveform], computed in the background on a worker thread:\n~2000 min and max bins, enough to paint a waveform overview or a\nwaveform-style seekbar.\n\nFor local files the whole envelope arrives in a single emit. For streams\nthat can't be decoded ahead of time (network or transcode sources) it\n**grows progressively**, re-emitting as playback advances. The\n`wave.filled` flag (one byte per bin: `1` covered, `0` not yet) marks\nwhich bins hold real data, so a renderer can draw the un-analysed ones as\na baseline instead of a misleading flat-zero spike. Local files arrive\nfully covered.\n\nThe stream is **listener-gated**: the analyzer runs only while\nsomething is subscribed to `player.stream.waveform`, and costs\nnothing when nobody listens. No configuration, no opt-in setter,\njust listen.\n\n```dart\nplayer.stream.waveform.listen((wave) {\n  if (wave == null) return; // null on track change, until the first bins land\n  // wave.min and wave.max: Float32List, range [-1, +1], wave.bins long.\n  // Bin i spans [i / wave.bins * wave.duration, (i + 1) / … ).\n  for (var i = 0; i \u003c wave.bins; i++) {\n    if (wave.filled[i] == 0) continue; // not yet analysed, draw a baseline\n    // draw a vertical bar from wave.min[i] to wave.max[i]\n  }\n});\n```\n\n---\n\n### 14. OS media session\n\nPublish the player to the operating system's media controls (the\n**Now Playing** panel, Control Center and lockscreen on iOS and macOS,\nMPRIS on Linux, SMTC on Windows, and the media notification on Android)\nand receive the transport commands the OS sends back.\n\nEverything goes through one setter, `Player.setMediaSession`. Pass\n`null` to remove the entry. Only one `Player` per process may own the\nsession at a time (enabling a second throws `StateError`); `dispose()`\nreleases it automatically.\n\n\u003cimg src=\"https://raw.githubusercontent.com/ales-drnz/mpv_audio_kit/main/imgs/diagrams/media_session_flow.png\" width=\"100%\"\u003e\n\n#### 14.1 Enabling the session\n\nTitle, artist, album, artwork and duration are derived from the playing\nfile automatically, you don't have to push anything.\n\n```dart\nawait player.setMediaSession(const MediaSession());  // enable\nawait player.setMediaSession(null);                  // remove\n```\n\n#### 14.2 Overriding metadata\n\nEach metadata field is an override: `null` keeps mpv's value, a value\ntakes over, an empty string forces a blank. Use `copyWith` to change one\nfield on the fly.\n\n```dart\nawait player.setMediaSession(\n  (player.state.mediaSession ?? const MediaSession())\n      .copyWith(title: 'Custom title', artist: 'Custom artist'),\n);\n```\n\nArtwork is a typed choice on the `artwork` field:\n\n```dart\nMediaSessionArtwork.embedded            // default, the file's embedded cover\nMediaSessionArtwork.custom(myCoverArt)  // your own image, ignoring the file cover\nMediaSessionArtwork.uri(myArtUrl)       // a URL the OS fetches itself (only the URL crosses the channel)\nMediaSessionArtwork.none                // no artwork\n```\n\n#### 14.3 Reacting to OS commands\n\nCommands are **auto-applied** to the player (lockscreen play → `play()`,\nscrub → `seek()`, …) and also surfaced on\n`Player.stream.mediaSessionCommands` for analytics or interception:\n\n```dart\nplayer.stream.mediaSessionCommands.listen((command) {\n  switch (command) {\n    case MediaSessionCommandSeekTo(:final position):\n      log('scrubbed to $position');\n    case MediaSessionCommandNext():\n      // next and previous drive mpv's playlist when one is loaded; with a\n      // single track they only emit here, so advance your own queue.\n      myQueue.next();\n    case _:\n      break;\n  }\n});\n```\n\n#### 14.4 Capabilities, intervals and speeds\n\n`actions` advertises the controls the OS may surface. What actually renders\nis **platform-gated, not just space-gated**: each native media UI draws only a\nsubset (and a lockscreen typically shows 3 to 4 of those). Advertising an\naction a platform doesn't render is harmless: it simply isn't drawn. The full\nenum also includes `MediaAction.stop` and `MediaAction.like` (a favourite or star).\n\n```dart\nconst MediaSession(\n  actions: {\n    MediaAction.play, MediaAction.pause, MediaAction.playPause,\n    MediaAction.next, MediaAction.previous, MediaAction.seek,\n    MediaAction.setRepeatMode, MediaAction.setShuffle,\n  },\n  fastForwardInterval: Duration(seconds: 30),     // skip-forward button\n  rewindInterval: Duration(seconds: 15),          // skip-back button\n  supportedPlaybackRates: [1.0, 1.25, 1.5, 2.0],  // speed picker\n  autoApplyPlaylistNavigation: false,             // handle next and prev yourself\n);\n```\n\nSet `autoApplyPlaylistNavigation: false` to handle the next and previous buttons\nyourself, e.g. a ±30s skip on a multi-file audiobook, instead of the package\nswitching track. The command still arrives on\n[`stream.mediaSessionCommands`](#915-media-session-streams).\n\n**Which actions each platform's native media UI actually draws:**\n\n| Action | iOS | macOS | Android | Windows | Linux¹ |\n|---|:--:|:--:|:--:|:--:|:--:|\n| `play`, `pause`, `playPause` | ✓ | ✓ | ✓ | ✓ | ✓ |\n| `next`, `previous` | ✓ | ✓ | ✓ | ✓ | ✓ |\n| `seek` (scrubber) | ✓ | ✓ | ✓ | ✓ | KDE |\n| `fastForward`, `rewind` | ✓ | ✓ | ✓ | ✓ | ✗ |\n| `stop` | ✗ | ✗ | ✗ | ✓ | ✗ |\n| `setRepeatMode`, `setShuffle` | ✗ | ✗ | ✓ | ✗² | KDE |\n| `setPlaybackRate` | ✗ | ✗ | ✗ | ✗² | KDE |\n| `like` | ✓ | ✗ | ✓ | ✗ | ✗ |\n\n¹ Linux depends on the desktop's MPRIS consumer, KDE Plasma draws\nseek, repeat, shuffle, and rate toggles, GNOME's built-in popup shows only\nprev, play-pause, and next.\n² Windows SMTC exposes repeat, shuffle, and rate as settable *properties* but the\nnative flyout draws no toggle for them.\n\n#### 14.5 Audio interruptions\n\n`interruptionPolicy` decides what","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fales-drnz%2Fmpv_audio_kit","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fales-drnz%2Fmpv_audio_kit","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fales-drnz%2Fmpv_audio_kit/lists"}