[{"data":1,"prerenderedAt":27},["ShallowReactive",2],{"articolo-riproduzione-audio-in-flutter-con-just-audio-player-playlist-e-background":3,"comments-article-riproduzione-audio-in-flutter-con-just-audio-player-playlist-e-background":26},{"id":4,"title":5,"slug":6,"excerpt":7,"body":8,"cover_image":9,"cover_remote_url":10,"cover_credit":11,"video_url":15,"status":16,"published_at":17,"meta_title":18,"meta_description":19,"category":20,"author":24},92,"Riproduzione audio in Flutter con just_audio: player, playlist e background","riproduzione-audio-in-flutter-con-just-audio-player-playlist-e-background","Guida pratica a just_audio: dalla riproduzione di un singolo file alla gestione di playlist, seek bar reattiva, streaming con caching, controlli in background con notifica di sistema e gestione delle interruzioni audio.","## Perché just_audio\n\nQuando un'app deve riprodurre musica, podcast o semplici effetti sonori, la scelta del pacchetto giusto fa la differenza tra qualche riga di codice e settimane di lavoro sui dettagli nativi. `just_audio` è oggi la libreria di riferimento per l'audio in Flutter: supporta Android, iOS, macOS, Web, Windows e Linux, gestisce lo streaming HTTP, HLS e DASH, espone tutto lo stato del player tramite `Stream` e si integra con `just_audio_background` per i controlli nella notifica di sistema e nella lock screen.\n\nIn questa guida costruiamo un player completo partendo dalle basi, evidenziando gli errori più comuni: memory leak, seek bar che scatta, audio che continua a suonare quando arriva una telefonata.\n\n## Installazione e configurazione delle piattaforme\n\n```yaml\ndependencies:\n  just_audio: ^0.10.0\n  just_audio_background: ^0.0.1-beta.16\n  audio_session: ^0.2.0\n  rxdart: ^0.28.0\n```\n\n### Android\n\nServe il permesso di rete e, se usi `just_audio_background`, la dichiarazione del servizio. In `android\u002Fapp\u002Fsrc\u002Fmain\u002FAndroidManifest.xml`:\n\n```xml\n\u003Cuses-permission android:name=\"android.permission.INTERNET\"\u002F>\n\u003Cuses-permission android:name=\"android.permission.WAKE_LOCK\"\u002F>\n\u003Cuses-permission android:name=\"android.permission.FOREGROUND_SERVICE\"\u002F>\n\u003Cuses-permission android:name=\"android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK\"\u002F>\n\n\u003Capplication ...>\n  \u003Cactivity android:name=\".MainActivity\" ... \u002F>\n\n  \u003Cservice\n      android:name=\"com.ryanheise.audioservice.AudioService\"\n      android:foregroundServiceType=\"mediaPlayback\"\n      android:exported=\"true\">\n    \u003Cintent-filter>\n      \u003Caction android:name=\"android.media.browse.MediaBrowserService\"\u002F>\n    \u003C\u002Fintent-filter>\n  \u003C\u002Fservice>\n\n  \u003Creceiver\n      android:name=\"com.ryanheise.audioservice.MediaButtonReceiver\"\n      android:exported=\"true\">\n    \u003Cintent-filter>\n      \u003Caction android:name=\"android.intent.action.MEDIA_BUTTON\"\u002F>\n    \u003C\u002Fintent-filter>\n  \u003C\u002Freceiver>\n\u003C\u002Fapplication>\n```\n\n> Nota: se usi `just_audio_background`, la tua `MainActivity` deve estendere `AudioServiceActivity` (Kotlin: `class MainActivity: AudioServiceActivity()`).\n\n### iOS\n\nIn `ios\u002FRunner\u002FInfo.plist` abilita la modalità background audio:\n\n```xml\n\u003Ckey>UIBackgroundModes\u003C\u002Fkey>\n\u003Carray>\n  \u003Cstring>audio\u003C\u002Fstring>\n\u003C\u002Farray>\n```\n\nSe carichi file da URL non HTTPS, ricordati di configurare `NSAppTransportSecurity`.\n\n## Il player minimo\n\nL'API di base è estremamente compatta:\n\n```dart\nimport 'package:just_audio\u002Fjust_audio.dart';\n\nfinal player = AudioPlayer();\n\nFuture\u003Cvoid> start() async {\n  await player.setUrl('https:\u002F\u002Fexample.com\u002Ftrack.mp3');\n  await player.play(); \u002F\u002F ritorna quando la riproduzione termina\n}\n```\n\nDue punti fondamentali che generano confusione:\n\n- `play()` **non** è un semplice \"avvia\": restituisce un `Future` che si completa quando la traccia finisce (o quando viene messa in pausa\u002Ffermata). Non fare `await player.play()` dentro un `onPressed` se poi devi aggiornare la UI subito dopo.\n- `AudioPlayer` va sempre rilasciato con `dispose()`, altrimenti resta un player nativo attivo che consuma memoria e batteria.\n\n```dart\nclass PlayerPage extends StatefulWidget {\n  const PlayerPage({super.key});\n  @override\n  State\u003CPlayerPage> createState() => _PlayerPageState();\n}\n\nclass _PlayerPageState extends State\u003CPlayerPage> {\n  late final AudioPlayer _player;\n\n  @override\n  void initState() {\n    super.initState();\n    _player = AudioPlayer();\n    _init();\n  }\n\n  Future\u003Cvoid> _init() async {\n    try {\n      await _player.setAudioSource(\n        AudioSource.uri(Uri.parse('https:\u002F\u002Fexample.com\u002Ftrack.mp3')),\n      );\n    } on PlayerException catch (e) {\n      debugPrint('Errore di caricamento: ${e.message}');\n    } on PlayerInterruptedException catch (_) {\n      \u002F\u002F Il caricamento è stato annullato da una nuova richiesta\n    }\n  }\n\n  @override\n  void dispose() {\n    _player.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) => const SizedBox.shrink();\n}\n```\n\n## Le sorgenti audio\n\n`AudioSource` astrae la provenienza del contenuto:\n\n```dart\n\u002F\u002F File remoto (mp3, m4a, HLS, DASH...)\nAudioSource.uri(Uri.parse('https:\u002F\u002Fexample.com\u002Fstream.m3u8'));\n\n\u002F\u002F Asset incluso nel bundle\nAudioSource.asset('assets\u002Faudio\u002Fjingle.mp3');\n\n\u002F\u002F File locale\nAudioSource.file('\u002Fstorage\u002Femulated\u002F0\u002FMusic\u002Fsong.mp3');\n\n\u002F\u002F Streaming con cache su disco: scarica mentre riproduce e riusa il file\nLockCachingAudioSource(Uri.parse('https:\u002F\u002Fexample.com\u002Fpodcast.mp3'));\n\n\u002F\u002F Solo una porzione della traccia\nClippingAudioSource(\n  child: AudioSource.uri(Uri.parse('https:\u002F\u002Fexample.com\u002Ftrack.mp3')),\n  start: const Duration(seconds: 30),\n  end: const Duration(seconds: 60),\n);\n```\n\n`LockCachingAudioSource` è particolarmente utile per i podcast: la seconda riproduzione parte istantaneamente e non consuma dati.\n\n## Stato del player: gli stream da conoscere\n\nTutto lo stato è esposto come `Stream`, quindi si integra bene con `StreamBuilder` o con qualsiasi state manager.\n\n| Stream | Contenuto |\n| --- | --- |\n| `playerStateStream` | `playing` (bool) + `processingState` |\n| `positionStream` | posizione corrente, aggiornata frequentemente |\n| `bufferedPositionStream` | quanto è stato bufferizzato |\n| `durationStream` | durata della traccia corrente (nullable) |\n| `currentIndexStream` | indice nella playlist |\n| `sequenceStateStream` | stato completo: sequenza, indice, shuffle |\n| `volumeStream`, `speedStream` | volume e velocità |\n\n`ProcessingState` vale `idle`, `loading`, `buffering`, `ready` o `completed`: è la base per capire se mostrare uno spinner o il pulsante play.\n\n```dart\nStreamBuilder\u003CPlayerState>(\n  stream: _player.playerStateStream,\n  builder: (context, snapshot) {\n    final state = snapshot.data;\n    final processing = state?.processingState;\n    final playing = state?.playing ?? false;\n\n    if (processing == ProcessingState.loading ||\n        processing == ProcessingState.buffering) {\n      return const SizedBox(\n        width: 48,\n        height: 48,\n        child: CircularProgressIndicator(),\n      );\n    }\n\n    if (!playing) {\n      return IconButton(\n        iconSize: 48,\n        icon: const Icon(Icons.play_arrow),\n        onPressed: _player.play, \u002F\u002F niente await\n      );\n    }\n\n    if (processing != ProcessingState.completed) {\n      return IconButton(\n        iconSize: 48,\n        icon: const Icon(Icons.pause),\n        onPressed: _player.pause,\n      );\n    }\n\n    return IconButton(\n      iconSize: 48,\n      icon: const Icon(Icons.replay),\n      onPressed: () => _player.seek(Duration.zero),\n    );\n  },\n)\n```\n\n## Una seek bar che non scatta\n\nL'errore classico è costruire la barra di avanzamento su `positionStream` e aggiornare lo slider mentre l'utente lo sta trascinando: il risultato è un cursore che \"rimbalza\". La soluzione è combinare i tre stream rilevanti in un unico modello e mantenere un valore di drag locale.\n\n```dart\nimport 'package:rxdart\u002Frxdart.dart';\n\nclass PositionData {\n  const PositionData(this.position, this.buffered, this.duration);\n  final Duration position;\n  final Duration buffered;\n  final Duration duration;\n}\n\nStream\u003CPositionData> positionDataStream(AudioPlayer player) =>\n    Rx.combineLatest3\u003CDuration, Duration, Duration?, PositionData>(\n      player.positionStream,\n      player.bufferedPositionStream,\n      player.durationStream,\n      (position, buffered, duration) =>\n          PositionData(position, buffered, duration ?? Duration.zero),\n    );\n```\n\nE il widget:\n\n```dart\nclass SeekBar extends StatefulWidget {\n  const SeekBar({\n    super.key,\n    required this.data,\n    required this.onChangeEnd,\n  });\n\n  final PositionData data;\n  final ValueChanged\u003CDuration> onChangeEnd;\n\n  @override\n  State\u003CSeekBar> createState() => _SeekBarState();\n}\n\nclass _SeekBarState extends State\u003CSeekBar> {\n  double? _dragValue;\n\n  @override\n  Widget build(BuildContext context) {\n    final max = widget.data.duration.inMilliseconds.toDouble();\n    final current = _dragValue ??\n        widget.data.position.inMilliseconds.toDouble().clamp(0, max);\n\n    return Slider(\n      min: 0,\n      max: max == 0 ? 1 : max,\n      value: current.toDouble(),\n      onChanged: (value) => setState(() => _dragValue = value),\n      onChangeEnd: (value) {\n        widget.onChangeEnd(Duration(milliseconds: value.round()));\n        setState(() => _dragValue = null);\n      },\n    );\n  }\n}\n```\n\nMentre l'utente trascina, `_dragValue` ha la precedenza; al rilascio si esegue il `seek` e si torna a seguire lo stream.\n\n## Playlist, shuffle e loop\n\nDalla versione 0.10 le playlist si gestiscono direttamente sul player con `setAudioSources`, `addAudioSource`, `removeAudioSourceAt` e `moveAudioSource` (la vecchia `ConcatenatingAudioSource` è deprecata ma ancora funzionante nei progetti esistenti).\n\n```dart\nawait _player.setAudioSources(\n  [\n    AudioSource.uri(Uri.parse('https:\u002F\u002Fexample.com\u002F1.mp3')),\n    AudioSource.uri(Uri.parse('https:\u002F\u002Fexample.com\u002F2.mp3')),\n    AudioSource.asset('assets\u002Faudio\u002F3.mp3'),\n  ],\n  initialIndex: 0,\n  initialPosition: Duration.zero,\n);\n\nawait _player.setLoopMode(LoopMode.all);   \u002F\u002F off | one | all\nawait _player.setShuffleModeEnabled(true);\n\n\u002F\u002F Navigazione\nawait _player.seekToNext();\nawait _player.seekToPrevious();\nawait _player.seek(Duration.zero, index: 2);\n```\n\nPer sapere quale elemento è in riproduzione, `sequenceStateStream` restituisce tutto il contesto:\n\n```dart\nStreamBuilder\u003CSequenceState>(\n  stream: _player.sequenceStateStream,\n  builder: (context, snapshot) {\n    final state = snapshot.data;\n    final source = state?.currentSource;\n    final metadata = source?.tag as MediaItem?;\n    return Text(metadata?.title ?? 'Nessuna traccia');\n  },\n)\n```\n\n## Controlli in background e notifica di sistema\n\nPer far continuare la riproduzione con l'app in background e mostrare i controlli nella notifica, nella lock screen e sui dispositivi Bluetooth, il modo più rapido è `just_audio_background`.\n\n```dart\nimport 'package:just_audio_background\u002Fjust_audio_background.dart';\n\nFuture\u003Cvoid> main() async {\n  WidgetsFlutterBinding.ensureInitialized();\n\n  await JustAudioBackground.init(\n    androidNotificationChannelId: 'it.example.audio.channel',\n    androidNotificationChannelName: 'Riproduzione audio',\n    androidNotificationOngoing: true,\n    androidStopForegroundOnPause: true,\n  );\n\n  runApp(const MyApp());\n}\n```\n\nDa quel momento ogni `AudioSource` deve portare un `tag` di tipo `MediaItem`, che alimenta i metadati mostrati dal sistema:\n\n```dart\nAudioSource.uri(\n  Uri.parse('https:\u002F\u002Fexample.com\u002Fpodcast-ep1.mp3'),\n  tag: MediaItem(\n    id: 'ep1',\n    title: 'Episodio 1 — Flutter e l\\'audio',\n    album: 'Il podcast di esempio',\n    artist: 'Redazione',\n    duration: const Duration(minutes: 42),\n    artUri: Uri.parse('https:\u002F\u002Fexample.com\u002Fcover.jpg'),\n  ),\n);\n```\n\nSe l'app ha bisogno di logica più sofisticata (code dinamiche, integrazione con Android Auto, download offline), la strada corretta è il pacchetto `audio_service` con un `BaseAudioHandler` personalizzato, di cui `just_audio_background` è una versione semplificata.\n\n## Gestire interruzioni, cuffie e ducking\n\nUn player serio deve reagire a telefonate, notifiche di altre app e alla rimozione delle cuffie. Se ne occupa `audio_session`:\n\n```dart\nimport 'package:audio_session\u002Faudio_session.dart';\n\nFuture\u003Cvoid> configureSession(AudioPlayer player) async {\n  final session = await AudioSession.instance;\n  await session.configure(const AudioSessionConfiguration.music());\n\n  \u002F\u002F Cuffie staccate: metti in pausa (comportamento atteso dagli utenti)\n  session.becomingNoisyEventStream.listen((_) => player.pause());\n\n  session.interruptionEventStream.listen((event) {\n    if (event.begin) {\n      switch (event.type) {\n        case AudioInterruptionType.duck:\n          player.setVolume(0.3);\n        case AudioInterruptionType.pause:\n        case AudioInterruptionType.unknown:\n          player.pause();\n      }\n    } else {\n      switch (event.type) {\n        case AudioInterruptionType.duck:\n          player.setVolume(1.0);\n        case AudioInterruptionType.pause:\n          player.play();\n        case AudioInterruptionType.unknown:\n          break;\n      }\n    }\n  });\n}\n```\n\n`just_audio_background` configura già una sessione di default: in quel caso limitati ad ascoltare gli eventi senza riconfigurare, per non sovrascrivere le impostazioni.\n\n## Incapsulare il player in un servizio\n\nEsporre `AudioPlayer` direttamente ai widget rende difficile testare e cambiare implementazione. Meglio un servizio con un'API di dominio, registrabile in `get_it` o esposto con un provider:\n\n```dart\nclass AudioRepository {\n  AudioRepository(this._player);\n\n  final AudioPlayer _player;\n\n  Stream\u003Cbool> get isPlaying => _player.playingStream;\n  Stream\u003CPositionData> get position => positionDataStream(_player);\n  Stream\u003Cint?> get currentIndex => _player.currentIndexStream;\n\n  Future\u003Cvoid> loadQueue(List\u003CTrack> tracks, {int startAt = 0}) async {\n    await _player.setAudioSources(\n      tracks.map(_toSource).toList(),\n      initialIndex: startAt,\n    );\n  }\n\n  AudioSource _toSource(Track track) => AudioSource.uri(\n        Uri.parse(track.url),\n        tag: MediaItem(\n          id: track.id,\n          title: track.title,\n          artist: track.artist,\n          artUri: Uri.tryParse(track.coverUrl),\n        ),\n      );\n\n  Future\u003Cvoid> play() => _player.play();\n  Future\u003Cvoid> pause() => _player.pause();\n  Future\u003Cvoid> seek(Duration position) => _player.seek(position);\n  Future\u003Cvoid> setSpeed(double speed) => _player.setSpeed(speed);\n\n  Future\u003Cvoid> dispose() => _player.dispose();\n}\n```\n\nCon un'interfaccia del genere i widget non sanno nulla di `just_audio`, e in test puoi sostituire il repository con un fake.\n\n## Funzionalità extra utili\n\n- **Velocità di riproduzione**: `player.setSpeed(1.5)` — indispensabile per i podcast; su Android e iOS il pitch resta corretto.\n- **Volume e bilanciamento**: `setVolume(0..1)`, e su alcune piattaforme `setPitch`.\n- **Preload della traccia successiva**: gestito automaticamente nelle playlist, con gap-less playback sui formati supportati.\n- **Effetti audio**: su Android e iOS puoi collegare `AudioPipeline` con equalizzatore e loudness enhancer (`AndroidEqualizer`, `AndroidLoudnessEnhancer`).\n- **Timer di spegnimento**: basta un `Timer` che chiami `pause()` combinato con un fade sul volume.\n\n```dart\nfinal equalizer = AndroidEqualizer();\nfinal player = AudioPlayer(\n  audioPipeline: AudioPipeline(androidAudioEffects: [equalizer]),\n);\nawait equalizer.setEnabled(true);\n```\n\n## Errori frequenti da evitare\n\n1. **Creare un `AudioPlayer` per ogni widget**: usa una singola istanza condivisa a livello di app, o al massimo una per contesto (musica di sottofondo vs effetti).\n2. **Dimenticare `dispose()`**: su Android il player nativo continua a girare e la notifica resta appesa.\n3. **Chiamare `setUrl` a ogni rebuild**: il caricamento va fatto in `initState` o nel servizio, mai in `build`.\n4. **Ignorare le eccezioni**: `PlayerException` (errore della piattaforma, con codice) e `PlayerInterruptedException` (caricamento annullato) vanno gestite separatamente, altrimenti l'app mostra spinner infiniti.\n5. **Usare `setState` sul `positionStream`**: un aggiornamento ogni ~200 ms che ricostruisce l'intera pagina è uno spreco; isola la seek bar in uno `StreamBuilder` dedicato.\n6. **Testare solo in foreground**: verifica sempre il comportamento a schermo bloccato, con telefonata in arrivo e con le cuffie Bluetooth.\n\n## Conclusione\n\n`just_audio` copre praticamente ogni esigenza di riproduzione audio in Flutter con un'API dichiarativa e reattiva. Il pattern vincente è sempre lo stesso: un'unica istanza del player incapsulata in un servizio, la UI costruita su stream ben separati, `just_audio_background` per i controlli di sistema e `audio_session` per convivere educatamente con le altre app. Con questi quattro elementi si passa da un semplice \"play\" a un'esperienza di ascolto che gli utenti percepiscono come nativa.","https:\u002F\u002Fflutter.it\u002Fstorage\u002Farticles\u002F8c69a3c3-1473-402e-90ea-c2af3371ead9.jpg","https:\u002F\u002Fimages.unsplash.com\u002Fphoto-1665396697388-378154e2b937?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w5NzA2NTJ8MHwxfHJhbmRvbXx8fHx8fHx8fDE3ODg5MjY1MTJ8&ixlib=rb-4.1.0&q=80&w=1080",{"name":12,"author_url":13,"photo_url":14},"BandLab","https:\u002F\u002Funsplash.com\u002F@bandlab_music","https:\u002F\u002Funsplash.com\u002Fphotos\u002Fa-person-looking-at-the-phone-S6RtzAL_b9Q",null,"published","2026-09-09T04:01:52+00:00","Audio in Flutter con just_audio: guida pratica","Come riprodurre audio in Flutter con just_audio: player, playlist, seek bar reattiva, streaming con cache, controlli in background e gestione delle interruzioni.",{"id":21,"name":22,"slug":23},1,"Guide","guide",{"id":21,"name":25},"Flutter Bot",[],1789120577727]