Per riprodurre audio in background in Capacitor, servono due componenti native: audio background mode su iOS e un mediaPlayback foreground service su Android. Aggiungi controlli sulla lock screen tramite una media session, così la tua app di musica, podcast o meditazione continuerà a riprodursi quando l'utente blocca il telefono o cambia app. Questa guida mostra entrambi i setup su Capacitor 8, con @capgo/capacitor-native-audio per la riproduzione nativa e @capgo/capacitor-media-session per i controlli di sistema.
Perché l'audio si interrompe in background
Un'app Capacitor è un guscio nativo attorno a una WebView, quindi segue le regole del ciclo di vita di ogni piattaforma:
| iOS | Android | |
|---|---|---|
| Cosa succede in background | L'app viene sospesa in pochi secondi | L'attività è in pausa, i timer della WebView vengono limitati e il processo può essere terminato |
| Cosa mantiene l'audio attivo | UIBackgroundModes contiene audio, categoria della sessione audio playback |
Un foreground service di tipo mediaPlayback con una notifica |
| Interruttore silenzioso | playback la categoria lo ignora, ambient la rispetta |
Nessun equivalente |
| Controlli sulla lock screen | MPNowPlayingInfoCenter e MPRemoteCommandCenter |
MediaSession con una notifica in stile media |
| Regola dello Store | Deve riprodurre contenuti audio attesi dall'utente (guideline 2.5.4) | Il tipo di foreground service deve corrispondere all'uso reale dichiarato in Play Console |
Nessuna delle due piattaforme offre l'audio in background di default, e sistemare solo un lato è la causa più comune delle segnalazioni di bug “funziona su iPhone, si ferma su Android”.
Scegli il tuo approccio di riproduzione
Ci sono due buone architetture:
- Riproduzione nativa con
@capgo/capacitor-native-audio: l'audio viene decodificato da AVFoundation su iOS e Media3 ExoPlayer su Android. Ideale per player musicali, audiolibri, app di meditazione e qualsiasi scenario in cui la WebView potrebbe subire throttling. Supporta file locali, URL remoti, stream HLS, fade, velocità e metadati Now Playing. - HTML
<audio>più@capgo/capacitor-media-session: mantieni il tuo web player esistente e lascia che il plugin pubblichi metadati e controlli al sistema operativo. Su Android esegue anche il foreground service mentre la sessione è in riproduzione. Ideale quando hai già un web player o ti serve un unico percorso code per web e nativo.
@capgo/capacitor-native-audio |
<audio> + sessione media |
|
|---|---|---|
| Motore di riproduzione | Nativo | WebView |
| Supporto web | Sì (fallback audio HTML) | Sì |
| Metadati lock screen | showNotification + notificationMetadata |
setMetadata |
| Servizio in primo piano Android | Ne avvii uno | Il plugin lo avvia in riproduzione o in pausa |
| Eventi di controllo remoto | playbackState listener |
setActionHandler |
| SFX precisi a bassa latenza | Sì | Limitato |
Configura iOS per l'audio in background
Apri ios/App/App/Info.plist e aggiungi la background mode. Puoi anche selezionare “Audio, AirPlay, and Picture in Picture” sotto Signing & Capabilities, Background Modes in Xcode, che scrive la stessa chiave:
<key>UIBackgroundModes</key>
<array>
<string>audio</string>
</array>
Anche la categoria della sessione audio è importante. @capgo/capacitor-native-audio la imposta per te: con showNotification: true utilizza .playback con la modalità predefinita, che interrompe le altre app (Spotify va in pausa) e rende la tua app quella in riproduzione. Con showNotification: false utilizza .playback con mixWithOthers, che è ideale per gli effetti sonori ma non mostra i controlli sulla lock screen. Scegline una per app, non per traccia.
Se riproduci tramite <audio>, WebKit attiva la sessione di riproduzione all'avvio del media. Assicurati che la riproduzione parta da un gesto dell'utente la prima volta, altrimenti iOS la bloccherà.
Esegui il build con Xcode 26 o versioni successive prima di caricare su App Store Connect, come richiesto da aprile 2026. Build Capgo puoi farlo nel cloud se lavori su Windows o Linux.
Configura Android per l'audio in background
Aggiungi i permessi a android/app/src/main/AndroidManifest.xml:
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
FOREGROUND_SERVICE_MEDIA_PLAYBACK è obbligatorio per le app che puntano ad Android 14 (API 34) o versioni superiori, e Capacitor 8 punta a API 36. Senza di esso, l'avvio del servizio genera un SecurityException. Quando pubblichi, Play Console ti chiederà di dichiarare il tipo di foreground service e giustificarlo. “Media playback the user started” è l'uso accettato.
POST_NOTIFICATIONS è un permesso runtime su Android 13+. Le notifiche della media session sono esenti dal permesso notifiche e appaiono comunque nel centro notifiche, ma qualsiasi altra notifica inviata dal servizio ne ha bisogno.
Android 12+ blocca l'avvio di un foreground service mentre l'app è in background. Avvialo quando l'utente preme play, mentre la tua activity è visibile.
Opzione A: lascia che il plugin media session gestisca il servizio
@capgo/capacitor-media-session dichiara un mediaPlayback service nel proprio manifest e lo avvia quando imposti lo stato di riproduzione su playing o paused. Lo interrompe quando lo stato torna a none e l'app è in background. Se vuoi che il service resti attivo per tutta la sessione, imposta la config del plugin in capacitor.config.ts:
plugins: {
MediaSession: {
foregroundService: 'always',
},
},
The default, starting only during playback, is what most apps want.
Opzione B: avvia il tuo foreground service
@capgo/capacitor-native-audio gestisce la riproduzione, il focus e la notifica media, ma non crea il foreground service. Se lo usi senza il plugin media session, aggiungi un piccolo service e un plugin locale al tuo progetto Android.
android/app/src/main/java/com/example/app/PlaybackService.java:
package com.example.app;
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.Service;
import android.content.Intent;
import android.content.pm.ServiceInfo;
import android.os.Build;
import android.os.IBinder;
import androidx.core.app.NotificationCompat;
public class PlaybackService extends Service {
private static final String CHANNEL_ID = "playback";
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
NotificationChannel channel = new NotificationChannel(
CHANNEL_ID, "Playback", NotificationManager.IMPORTANCE_LOW);
getSystemService(NotificationManager.class).createNotificationChannel(channel);
}
Notification notification = new NotificationCompat.Builder(this, CHANNEL_ID)
.setContentTitle("Playing audio")
.setSmallIcon(R.mipmap.ic_launcher)
.setOngoing(true)
.build();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
startForeground(1, notification, ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK);
} else {
startForeground(1, notification);
}
return START_NOT_STICKY;
}
@Override
public IBinder onBind(Intent intent) {
return null;
}
}
PlaybackServicePlugin.java nello stesso package:
package com.example.app;
import android.content.Intent;
import androidx.core.content.ContextCompat;
import com.getcapacitor.Plugin;
import com.getcapacitor.PluginCall;
import com.getcapacitor.PluginMethod;
import com.getcapacitor.annotation.CapacitorPlugin;
@CapacitorPlugin(name = "PlaybackService")
public class PlaybackServicePlugin extends Plugin {
@PluginMethod
public void start(PluginCall call) {
ContextCompat.startForegroundService(getContext(), new Intent(getContext(), PlaybackService.class));
call.resolve();
}
@PluginMethod
public void stop(PluginCall call) {
getContext().stopService(new Intent(getContext(), PlaybackService.class));
call.resolve();
}
}
Registralo in MainActivity.java prima super.onCreate, e dichiara il service nel manifest:
public class MainActivity extends BridgeActivity {
@Override
public void onCreate(Bundle savedInstanceState) {
registerPlugin(PlaybackServicePlugin.class);
super.onCreate(savedInstanceState);
}
}
<service
android:name=".PlaybackService"
android:exported="false"
android:foregroundServiceType="mediaPlayback" />
Da TypeScript:
import { Capacitor, registerPlugin } from '@capacitor/core';
interface PlaybackServicePlugin {
start(): Promise<void>;
stop(): Promise<void>;
}
export const PlaybackService = registerPlugin<PlaybackServicePlugin>('PlaybackService');
export const isAndroid = Capacitor.getPlatform() === 'android';
Riproduci audio nativamente con @capgo/capacitor-native-audio
bun add @capgo/capacitor-native-audio
bunx cap sync
Configura una volta all'avvio, poi carica e riproduci:
import { NativeAudio } from '@capgo/capacitor-native-audio';
import { PlaybackService, isAndroid } from './playback-service';
await NativeAudio.configure({
focus: true, // request audio focus, pause others
background: true, // keep playing in the background
backgroundPlayback: true, // Android: skip auto pause on background
showNotification: true, // lock screen and notification controls
});
export async function playEpisode(id: string, url: string, title: string, artwork: string) {
await NativeAudio.preload({
assetId: id,
assetPath: url,
isUrl: true,
notificationMetadata: {
title,
artist: 'My Podcast',
artworkUrl: artwork,
},
});
if (isAndroid) await PlaybackService.start();
await NativeAudio.play({ assetId: id });
}
export async function stopEpisode(id: string) {
await NativeAudio.stop({ assetId: id });
await NativeAudio.unload({ assetId: id });
if (isAndroid) await PlaybackService.stop();
}
assetPath accetta un percorso relativo ai tuoi web assets (audio/intro.mp3), un file:// URL, un https:// URL o uno stream .m3u8 HLS. Per qualsiasi cosa diversa da un asset incluso nel bundle, imposta isUrl: true. Se l'host audio richiede auth, passa headers.
Crea una playlist che avanza automaticamente
Il complete l'evento scatta alla fine di un brano, anche con lo schermo bloccato, perché il player nativo continua a girare:
const queue = [
{ id: 'ep1', url: 'https://cdn.example.com/ep1.mp3', title: 'Episode 1' },
{ id: 'ep2', url: 'https://cdn.example.com/ep2.mp3', title: 'Episode 2' },
];
let index = 0;
await NativeAudio.addListener('complete', async ({ assetId }) => {
await NativeAudio.unload({ assetId });
index += 1;
if (index < queue.length) {
const next = queue[index];
await NativeAudio.preload({
assetId: next.id,
assetPath: next.url,
isUrl: true,
notificationMetadata: { title: next.title, artist: 'My Podcast' },
});
await NativeAudio.play({ assetId: next.id });
} else if (isAndroid) {
await PlaybackService.stop();
}
});
Mantieni attivo il foreground service tra i brani. Se lo fermi dopo un brano e provi a riavviarlo in background, Android 12+ restituisce ForegroundServiceStartNotAllowedException.
Sincronizza la UI con la lock screen
Gli utenti premeranno pausa dalla lock screen, dalle cuffie o dalle notifiche. Ascolta playbackState e usa il player nativo come unica fonte di verità:
await NativeAudio.addListener('playbackState', (event) => {
// event.state: 'playing' | 'paused' | 'stopped'
// event.reason: 'play', 'pause', 'remotePlay', 'complete', ...
store.setPlaying(event.isPlaying);
if (event.currentTime !== undefined) store.setPosition(event.currentTime);
});
await NativeAudio.addListener('currentTime', ({ currentTime }) => {
store.setPosition(currentTime); // every 100 ms while playing
});
Aggiorna lo stato anche quando l'app torna in foreground, usando isPlaying e getCurrentTime, poiché la WebView potrebbe essersi messa in pausa durante l'emissione degli eventi.
Usa un elemento audio HTML con i media session controls
Se hai già un web player, mantienilo e aggiungi il plugin:
bun add @capgo/capacitor-media-session
bunx cap sync
import { MediaSession } from '@capgo/capacitor-media-session';
const audio = new Audio();
audio.preload = 'auto';
export async function play(track: { url: string; title: string; artist: string; cover: string }) {
audio.src = track.url;
await audio.play();
await MediaSession.setMetadata({
title: track.title,
artist: track.artist,
artwork: [{ src: track.cover, sizes: '512x512', type: 'image/jpeg' }],
});
await MediaSession.setPlaybackState({ playbackState: 'playing' });
}
await MediaSession.setActionHandler({ action: 'play' }, async () => {
await audio.play();
await MediaSession.setPlaybackState({ playbackState: 'playing' });
});
await MediaSession.setActionHandler({ action: 'pause' }, async () => {
audio.pause();
await MediaSession.setPlaybackState({ playbackState: 'paused' });
});
await MediaSession.setActionHandler({ action: 'seekto' }, (details) => {
if (details.seekTime != null) audio.currentTime = details.seekTime;
});
await MediaSession.setActionHandler({ action: 'nexttrack' }, () => playNext());
audio.addEventListener('timeupdate', () => {
MediaSession.setPositionState({
duration: audio.duration || 0,
position: audio.currentTime,
playbackRate: audio.playbackRate,
});
});
Chiama setPositionState al massimo una o due volte al secondo in produzione. La lock screen interpola gli aggiornamenti, e inondare il bridge con timeupdate gli eventi spreca batteria.
Gestisci interruzioni e cambi di rotta
Chiamate, Siri, sveglie e altre app interrompono la riproduzione. Native Audio si mette in pausa durante l'interruzione e, su iOS, riprende quando il sistema lo richiede. Ci sono altri due casi da gestire:
- Cuffie scollegate: gli utenti si aspettano che la riproduzione si interrompa. Su Android, registra un receiver per
AudioManager.ACTION_AUDIO_BECOMING_NOISYnel tuo service se il player non si mette già in pausa. Su iOS, ascolta i cambi di route con@capgo/capacitor-audio-session:
import { AudioSession, RouteChangeReasons } from '@capgo/capacitor-audio-session';
await AudioSession.addListener('routeChanged', (reason) => {
if (reason === RouteChangeReasons.OLD_DEVICE_UNAVAILABLE) {
pausePlayback();
}
});
await AudioSession.addListener('interruption', (type) => {
// 'began' or 'ended'
});
- Ducking vs. pausa: i prompt di navigazione abbassano il volume dell'audio. Le app con parlato dovrebbero invece mettere in pausa, dato che un parlato a volume ridotto è difficile da seguire.
Testa la riproduzione in background
I simulatori mentono sul comportamento in background. Testa su dispositivi reali:
- Avvia la riproduzione, blocca lo schermo e attendi due minuti. L'audio deve continuare a suonare.
- Usa i controlli play, pausa, avanti e seek dalla schermata di blocco, poi sblocca e verifica che la UI corrisponda.
- Avvia la riproduzione, passa a un'altra app che riproduce audio e poi torna.
- Avvia una chiamata o un FaceTime audio durante la riproduzione.
- Su Android, attiva il risparmio energetico e testa su un dispositivo Samsung o Xiaomi. Alcuni OEM chiudono le app in background in modo aggressivo. Un foreground service sopravvive, un semplice task in background no.
- Su Android 13+, nega il permesso per le notifiche e conferma che la riproduzione funzioni ancora.
- Lascia che una playlist vada avanti per diversi brani con lo schermo spento.
adb shell dumpsys activity services | grep -A3 PlaybackService mostra se il tuo service è in esecuzione in foreground.
Risoluzione problemi
L'audio si interrompe dopo circa 30 secondi su iOS: UIBackgroundModes manca audio, o la session category è ambient o soloAmbient. Controlla Info.plist nell'app compilata, non solo nel tuo codice sorgente.
L'audio si interrompe dopo pochi minuti su Android: nessun foreground service, o il service non è di tipo mediaPlayback. Controlla anche le impostazioni di ottimizzazione batteria OEM.
SecurityException: Starting FGS with type mediaPlayback ... requires permissions: aggiungi FOREGROUND_SERVICE_MEDIA_PLAYBACK.
ForegroundServiceStartNotAllowedException: hai avviato il service mentre l'app era in background. Avvialo al tap di play dell'utente.
Nessun controllo sulla lock screen su iOS: showNotification è false (sessione mixable), o non è stata impostata alcuna metadata. Solo l'app con sessione non-mixable diventa l'app Now Playing.
Due notifiche su Android: hai abilitato showNotification in native audio e hai anche postato la tua notifica di service. Mantieni la notifica del service minimale, o usa solo il service del plugin media session.
Altre app musicali continuano a suonare sopra la tua: hai configurato una sessione mixable. Imposta focus: true e showNotification: true.
Rilascia aggiornamenti al player più velocemente
Gestisci logica, UI, metadati e analytics del player direttamente in JavaScript. Una volta completato il setup nativo nel build dello store, Live updates di Capgo puoi inviare fix a quel code senza attendere un nuovo ciclo di revisione. La documentazione audio nativa e documentazione media session copre ogni opzione; se la tua app registra anche audio, consulta il plugin Capacitor audio recorder.