Pour jouer de l'audio en arrière-plan avec Capacitor, vous avez besoin de deux éléments natifs : le audio mode arrière-plan sur iOS, et un mediaPlayback service de premier plan sur Android. Ajoutez des contrôles sur l'écran de verrouillage via une session média, et votre application de musique, de podcast ou de méditation continue de jouer lorsque l'utilisateur verrouille son téléphone ou change d'application. Ce guide présente les deux configurations sur Capacitor 8, avec @capgo/capacitor-native-audio pour la lecture native et @capgo/capacitor-media-session pour les contrôles système.
Pourquoi l'audio s'arrête en arrière-plan
Une application Capacitor est un shell natif autour d'une WebView et suit donc les règles de cycle de vie de chaque plateforme :
| iOS | Android | |
|---|---|---|
| Comportement en arrière-plan | L'app est suspendue en quelques secondes | L'activité est en pause, les timers de la WebView sont bridés, le processus peut être tué |
| Ce qui maintient l'audio actif | UIBackgroundModes contient audio, catégorie de session audio playback |
Un service de premier plan de type mediaPlayback avec une notification |
| Bouton silencieux | playback la catégorie l'ignore, ambient la respecte |
Aucun équivalent |
| Contrôles de l’écran verrouillé | MPNowPlayingInfoCenter et MPRemoteCommandCenter |
MediaSession avec une notification de style média |
| Règle du store | Doit diffuser un contenu audible attendu par l’utilisateur (directive 2.5.4) | Le type de service de premier plan doit correspondre à l’usage réel, déclaré dans Play Console |
Aucune des deux plateformes ne fournit d’audio en arrière-plan par défaut, et corriger un seul côté est la cause la plus fréquente des signalements de bug « fonctionne sur iPhone, s’arrête sur Android ».
Choisissez votre approche de lecture
Il existe deux bonnes architectures :
- Lecture native avec
@capgo/capacitor-native-audio: l’audio est décodé par AVFoundation sur iOS et Media3 ExoPlayer sur Android. Idéal pour les lecteurs de musique, les livres audio, les apps de méditation et tout ce que le WebView risque de brider. Prend en charge les fichiers locaux, les URL distantes, les flux HLS, les fondus, la vitesse de lecture et les métadonnées Now Playing. - HTML
<audio>, ce qui@capgo/capacitor-media-session: conservez votre lecteur web existant et laissez le plugin publier les métadonnées et les contrôles vers le système d'exploitation. Sur Android, il gère également le service en premier plan pendant la lecture. Idéal si vous avez déjà un lecteur web ou si vous avez besoin d'un seul chemin code pour le web et le natif.
@capgo/capacitor-native-audio |
<audio> + media session |
|
|---|---|---|
| Moteur de lecture | Natif | WebView |
| Support web | Oui (repli sur l'audio HTML) | Oui |
| Métadonnées de l'écran de verrouillage | showNotification + notificationMetadata |
setMetadata |
| Service en premier plan Android | Vous en démarrez un | Le plugin le démarre pendant la lecture ou la pause |
| Événements de contrôl’à distance | playbackState listener |
setActionHandler |
| SFX précis à faible latence | Oui | Limité |
Configurer iOS pour l'audio en arrière-plan
Ouvrez ios/App/App/Info.plist et ajoutez le mode en arrière-plan. Vous pouvez aussi cocher « Audio, AirPlay, and Picture in Picture » sous Signing & Capabilities, Background Modes dans Xcode, ce qui écrit la même clé :
<key>UIBackgroundModes</key>
<array>
<string>audio</string>
</array>
La catégorie de session audio compte aussi. @capgo/capacitor-native-audio le définit pour vous : avec showNotification: true il utilise .playback avec le mode par défaut, qui interrompt les autres apps (Spotify se met en pause) et fait de votre app l'app Now Playing. Avec showNotification: false il utilise .playback par mixWithOthers, ce qui convient aux effets sonores mais n'affiche pas les contrôles sur l'écran de verrouillage. Choisissez-en un par app, pas par piste.
Si vous diffusez via <audio>, WebKit active la session de lecture dès que le média démarre. Veillez à ce que la lecture soit lancée par un geste utilisateur la première fois, sinon iOS la bloquera.
Compilez avec Xcode 26 ou une version ultérieure avant l'upload sur App Store Connect : c'est obligatoire depuis avril 2026. Build Capgo peut le faire dans le cloud si vous travaillez sous Windows ou Linux.
Configurer Android pour l'audio en arrière-plan
Ajoutez les autorisations à 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 est obligatoire pour les apps ciblant Android 14 (API 34) ou version supérieure, et Capacitor 8 cible API 36. Sans cela, le démarrage du service lève une SecurityException. Lors de la publication, Play Console vous demande de déclarer le type de service en premier plan et de le justifier. « Lecture de médias initiée par l’utilisateur » est l’usage accepté.
POST_NOTIFICATIONS est une permission d’exécution sur Android 13+. Les notifications de session média sont exemptées de la permission de notification et s’affichent toujours dans le panneau, mais toute autre notification publiée par votre service en a besoin.
Android 12+ bloque le démarrage d’un service en premier plan lorsque l’application est en arrière-plan. Lancez-le lorsque l’utilisateur appuie sur lecture, pendant que votre activité est visible.
Option A : laisser le plugin media session gérer le service
@capgo/capacitor-media-session déclare un mediaPlayback service dans son propre manifest et le démarre lorsque vous définissez l’état de lecture sur playing ou paused. Il l’arrête lorsque l’état repasse à none et que l’application est en arrière-plan. Si vous voulez que le service tourne pendant toute la session de l’application, configurez le plugin dans capacitor.config.ts:
plugins: {
MediaSession: {
foregroundService: 'always',
},
},
The default, starting only during playback, is what most apps want.
Option B : démarrer votre propre foreground service
@capgo/capacitor-native-audio gère la lecture, le focus et la notification média, mais ne crée pas le service en premier plan. Si vous l’utilisez sans le plugin de session média, ajoutez un petit service et un plugin local à votre projet 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 dans le même 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();
}
}
Enregistrez-le dans MainActivity.java avant super.onCreate, et déclarez le service dans le manifeste :
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" />
Depuis 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';
Lire l'audio nativement avec @capgo/capacitor-native-audio
bun add @capgo/capacitor-native-audio
bunx cap sync
Configurez une fois au démarrage, puis préchargez et jouez :
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 accepte un chemin relatif à vos assets web (audio/intro.mp3), un file:// URL, un https:// URL ou un flux HLS .m3u8 . Pour tout autre type d'asset que ceux du bundle, définissez isUrl: true. Si votre hôte audio exige une authentification, passez headers.
Créer une playlist qui avance toute seule
Le complete l’événement se déclenche à la fin d’une piste, même si l’écran est verrouillé, car le lecteur natif continue de fonctionner :
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();
}
});
Gardez le service en premier plan actif entre les pistes. Si vous l’arrêtez après une piste puis essayez de le relancer en arrière-plan, Android 12+ lève ForegroundServiceStartNotAllowedException.
Synchroniser votre UI avec l'écran de verrouillage
Les utilisateurs appuieront sur pause depuis l'écran de verrouillage, via leurs écouteurs ou depuis la notification. Écoutez playbackState et utilisez le lecteur natif comme source de vérité :
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
});
Rafraîchissez aussi l'état quand l'application revient au premier plan, en utilisant isPlaying et getCurrentTime, car la WebView a pu être mise en pause pendant l'émission des événements.
Utilisez un élément audio HTML avec les contrôles Media Session
Si vous avez déjà un lecteur web, conservez-l’et ajoutez le 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,
});
});
Appelez setPositionState au maximum une ou deux fois par seconde en production. L'écran de verrouillage interpole entre les mises à jour, et saturer le bridge avec des timeupdate événements épuise la batterie.
Gérer les interruptions et les changements de route
Les appels téléphoniques, Siri, les alarmes et d'autres applications interrompent la lecture. L'audio natif se met en pause lors d'une interruption et, sur iOS, reprend lorsque le système l'autorise. Deux autres cas méritent votre attention :
- Casque débranché: users expect playback to pause. On Android, register a receiver for
AudioManager.ACTION_AUDIO_BECOMING_NOISYdans votre service si votre lecteur ne se met pas déjà en pause. Sur iOS, écoutez les changements de route avec@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. pause: les invites de navigation atténuent votre audio. Les applications de parole doivent plutôt mettre en pause, car une voix atténuée est difficile à suivre.
Tester la lecture en arrière-plan
Les simulateurs mentent sur le comportement en arrière-plan. Testez sur de vrais appareils :
- Lancez la lecture, verrouillez l’écran, attendez deux minutes. L’audio doit continuer à jouer.
- Utilisez les commandes lecture, pause, suivant et seek sur l’écran verrouillé, puis déverrouillez et vérifiez votre UI.
- Lancez la lecture, passez à une autre application qui joue de l’audio, puis revenez.
- Passez un appel téléphonique ou un FaceTime audio pendant la lecture.
- Sur Android, activez l'économiseur de batterie et testez sur un appareil Samsung ou Xiaomi. Certains fabricants tuent agressivement les applications en arrière-plan. Un foreground service survit, une simple tâche en arrière-plan non.
- Sur Android 13+, refusez la permission de notification et confirmez que la lecture fonctionne toujours.
- Laissez une playlist jouer plusieurs morceaux avec l'écran éteint.
adb shell dumpsys activity services | grep -A3 PlaybackService indique si votre service tourne au premier plan.
Troubleshooting
L'audio s'arrête après environ 30 secondes sur iOS: UIBackgroundModes manque audio, ou la catégorie de session est ambient ou soloAmbient. Vérifiez Info.plist dans l'app compilée, pas seulement dans votre code source.
L'audio s'arrête après quelques minutes sur Android: aucun service au premier plan, ou le service n'est pas de type mediaPlayback. Also check OEM battery optimization settings.
SecurityException: Starting FGS with type mediaPlayback ... requires permissions: ajouter FOREGROUND_SERVICE_MEDIA_PLAYBACK.
ForegroundServiceStartNotAllowedException: vous avez démarré le service alors que l'app était en arrière-plan. Lancez-le lors de l'appui de l'utilisateur sur lecture.
Aucun contrôle sur l'écran de verrouillage sous iOS: showNotification est false (session mixable), ou aucune métadonnée n'a été définie. Seule l'app qui possède une session non mixable devient l'app Now Playing.
Deux notifications sous Android: vous avez activé showNotification dans native audio et a également publié sa propre notification de service. Gardez la notification de service minimale, ou utilisez uniquement le service du plugin media session.
Other music apps keep playing over yours: vous avez configuré une session mixable. Définissez focus: true et showNotification: true.
Déployez les modifications du lecteur plus rapidement
La logique de file d'attente, l'interface, les métadonnées et l'analytique de votre lecteur sont en JavaScript. Une fois la configuration native ci-dessus intégrée dans un build store, les live updates Capgo vous permet de pousser des correctifs vers ce code sans nouveau cycle de revue. Le documentation native audio et documentation media session couvrez toutes les options, et si votre application enregistre aussi de l'audio, consultez le plugin d'enregistrement audio Capacitor.