Um Audio im Hintergrund in Capacitor abzuspielen, benötigst du zwei native Komponenten: die audio Hintergrundmodus auf iOS und eine mediaPlayback Foreground-Service auf Android. Füge Sperrbildschirm-Steuerungen über eine Media Session hinzu, damit deine Musik-, Podcast- oder Meditations-App weiterläuft, wenn der Nutzer das Handy sperrt oder die App wechselt. Dieser Guide zeigt beide Setups auf Capacitor 8, mit @capgo/capacitor-native-audio für natives Playback und @capgo/capacitor-media-session für System-Steuerungen.
Warum Audio im Hintergrund stoppt
Eine Capacitor App ist eine native Shell um eine WebView und folgt daher den Lifecycle-Regeln der jeweiligen Plattform:
| iOS | Android | |
|---|---|---|
| Was im Hintergrund passiert | App wird innerhalb von Sekunden suspendiert | Activity wird pausiert, WebView-Timer werden gedrosselt, Prozess kann beendet werden |
| Was Audio am Laufen hält | UIBackgroundModes enthält audio, Audio-Session-Kategorie playback |
Ein Foreground-Service vom Typ mediaPlayback mit einer Benachrichtigung |
| Stummschalter | playback Kategorie ignoriert ihn, ambient beachtet ihn |
Kein Äquivalent |
| Sperrbildschirm-Steuerung | MPNowPlayingInfoCenter und MPRemoteCommandCenter |
MediaSession mit einer MediaStyle-Benachrichtigung |
| Store-Richtlinie | Muss hörbare Inhalte abspielen, die der Nutzer erwartet (Guideline 2.5.4) | Foreground service type must match real use, declared in Play Console |
Keine der beiden Plattformen bietet standardmäßig Hintergrund-Audio, und nur eine Seite zu fixen ist der häufigste Grund für Bug-Reports wie „läuft auf dem iPhone, stoppt auf Android“.
Wähle deinen Wiedergabe-Ansatz
Es gibt zwei gute Architekturen:
- Native Wiedergabe mit
@capgo/capacitor-native-audio: Audio wird auf iOS von AVFoundation und auf Android von Media3 ExoPlayer dekodiert. Ideal für Musikplayer, Hörbücher, Meditations-Apps und alles, bei dem der WebView gedrosselt werden könnte. Unterstützt lokale Dateien, Remote-URLs, HLS-Streams, Fades, Wiedergaberate und Now-Playing-Metadaten. - HTML
<audio>sowie@capgo/capacitor-media-session: Behalte deinen bestehenden Web-Player und lass das Plugin Metadaten und Steuerelemente an das OS veröffentlichen. Auf Android startet es außerdem den Foreground-Service, während die Session läuft. Ideal, wenn du bereits einen Web-Player hast oder einen einzigen code-Pfad für Web und Native brauchst.
@capgo/capacitor-native-audio |
<audio> + Media Session |
|
|---|---|---|
| Wiedergabe-Engine | Native | WebView- |
| Web-Support | Ja (HTML-Audio-Fallback) | Ja |
| Sperrbildschirm-Metadaten | showNotification + notificationMetadata |
setMetadata |
| Android Foreground Service | Du startest einen | Plugin startet ihn während der Wiedergabe oder Pause |
| Remote-Control-Events | playbackState Listener |
setActionHandler |
| Präzise Low-Latency-SFX | Ja | Eingeschränkt |
iOS für Hintergrund-Audio konfigurieren
Öffne ios/App/App/Info.plist und den Background Mode hinzufügen. Alternativ kannst du in Xcode unter Signing & Capabilities > Background Modes „Audio, AirPlay, and Picture in Picture“ aktivieren, wodurch derselbe Key gesetzt wird:
<key>UIBackgroundModes</key>
<array>
<string>audio</string>
</array>
Auch die Audio Session Category ist wichtig. @capgo/capacitor-native-audio setzt sie für dich: mit showNotification: true es nutzt .playback mit dem Standard-Modus, der andere Apps unterbricht (Spotify pausiert) und deine App zur „Now Playing“-App macht. Mit showNotification: false es nutzt .playback mit mixWithOthers, was für Soundeffekte ideal ist, aber keine Sperrbildschirm-Steuerung anzeigt. Wähle eine Einstellung pro App, nicht pro Track.
If you play through <audio>, WebKit aktiviert die Playback-Session, sobald die Wiedergabe startet. Stelle sicher, dass die Wiedergabe beim ersten Mal durch eine Benutzerinteraktion gestartet wird, sonst blockiert iOS sie.
Baue mit Xcode 26 oder neuer, bevor du zu App Store Connect hochlädst – das ist seit April 2026 Pflicht. Capgo Build kannst du in der Cloud erledigen, wenn du unter Windows oder Linux arbeitest.
Android für Background-Audio konfigurieren
Füge die Berechtigungen zu 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 ist für Apps mit Android 14 (API 34) oder höher zwingend erforderlich, und Capacitor 8 zielt auf API 36 ab. Ohne diese wird beim Starten des Service ein SecurityException. Beim Veröffentlichen fordert dich die Play Console auf, den Foreground-Service-Typ zu deklarieren und zu begründen. „Media playback the user started“ ist der akzeptierte Anwendungsfall.
POST_NOTIFICATIONS ist eine Runtime-Permission unter Android 13+. Media-Session-Benachrichtigungen sind von der Notification-Permission ausgenommen und erscheinen weiterhin in der Benachrichtigungsleiste, aber jede andere Benachrichtigung, die dein Service postet, braucht sie.
Android 12+ blockiert das Starten eines Foreground-Service, während die App im Hintergrund ist. Starte ihn, wenn der User auf Play drückt, solange deine Activity sichtbar ist.
Option A: Das Media-Session-Plugin den Service ausführen lassen
@capgo/capacitor-media-session deklariert einen mediaPlayback Service in seinem eigenen Manifest und startet es, wenn Sie den Wiedergabestatus auf playing oder paused. Es stoppt es, wenn der Zustand zurückkehrt zu none und die App im Hintergrund ist. Wenn der Service für die gesamte App-Session laufen soll, setze die Plugin-Konfiguration in capacitor.config.ts:
plugins: {
MediaSession: {
foregroundService: 'always',
},
},
Der Standard, nur während der Wiedergabe zu starten, ist das, was die meisten Apps wollen.
Option B: Starte deinen eigenen Foreground-Service
@capgo/capacitor-native-audio verarbeitet Wiedergabe, Fokus und die Medien-Benachrichtigung, erstellt aber keinen Foreground-Service. Wenn du es ohne das Media-Session-Plugin verwendest, füge einen kleinen Service und ein lokales Plugin zu deinem Android-Projekt hinzu.
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 im selben Paket:
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();
}
}
Registriere es in MainActivity.java bevor super.onCreate, und deklariere den Service im 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" />
Von TypeScript aus:
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';
Audio nativ mit @capgo/capacitor-native-audio abspielen
bun add @capgo/capacitor-native-audio
bunx cap sync
Einmalig beim Start konfigurieren, dann vorladen und abspielen:
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 akzeptiert einen Pfad relativ zu deinen Web-Assets (audio/intro.mp3), eine file:// URL, eine https:// URL oder einen HLS- .m3u8 Stream. Für alles außer einem gebündelten Asset, setze isUrl: true. Wenn dein Audio-Host Auth benötigt, übergebe headers.
Erstelle eine Playlist, die von selbst weiterläuft
Der complete Event wird ausgelöst, wenn ein Track endet, auch wenn der Bildschirm gesperrt ist, da der native Player weiterläuft:
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();
}
});
Halte den Foreground-Service zwischen den Tracks aktiv. Wenn du ihn nach einem Track stoppst und versuchst, ihn im Hintergrund neu zu starten, wirft Android 12+ eine ForegroundServiceStartNotAllowedException.
Halte deine UI mit dem Sperrbildschirm synchron
Nutzer drücken Pause auf dem Sperrbildschirm, an den Kopfhörern oder in der Benachrichtigung. Höre auf playbackState und den nativen Player als Single Source of Truth behandeln:
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
});
Aktualisiere den State auch, wenn die App in den Vordergrund zurückkehrt, mit isPlaying und getCurrentTime, da der WebView während der Ereignisemission pausiert haben kann.
Verwenden Sie ein HTML-Audioelement mit Media Session Controls
Wenn du bereits einen Web-Player hast, behalte ihn bei und füge das Plugin hinzu:
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,
});
});
Rufe setPositionState in Produktion höchstens ein- bis zweimal pro Sekunde. Der Lockscreen interpoliert zwischen den Updates, und das Überschwemmen der Bridge mit timeupdate Events verschwendet Akku.
Unterbrechungen und Routenwechsel abfangen
Telefonate, Siri, Wecker und andere Apps unterbrechen die Wiedergabe. Native Audio pausiert bei Unterbrechung und setzt auf iOS fort, wenn das System es anzeigt. Zwei weitere Fälle erfordern deine Aufmerksamkeit:
- Kopfhörer getrennt: Nutzer erwarten, dass die Wiedergabe pausiert. Auf Android registrieren Sie einen Receiver für
AudioManager.ACTION_AUDIO_BECOMING_NOISYin Ihrem Service, falls Ihr Player nicht bereits pausiert. Auf iOS lauschen Sie auf Route-Änderungen mit@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. Pausieren: Navigationsansagen ducken Ihr Audio. Apps mit gesprochenem Inhalt sollten stattdessen pausieren, da gedämpftes Sprechschwer zu folgen ist.
Background-Playback testen
Simulatoren lügen über das Hintergrundverhalten. Testen Sie auf echten Geräten:
- Wiedergabe starten, Bildschirm sperren, zwei Minuten warten. Die Audio-Wiedergabe muss weiterlaufen.
- Verwenden Sie die Lockscreen-Steuerungen für Play, Pause, Next und Seek, entsperren Sie dann und prüfen Sie, ob Ihre UI übereinstimmt.
- Abspielen, zu einer anderen App wechseln, die Audio abspielt, und dann zurückkehren.
- Starten Sie während der Wiedergabe einen Anruf oder FaceTime-Audio.
- Aktivieren Sie auf Android den Energiesparmodus und testen Sie auf einem Samsung- oder Xiaomi-Gerät. Einige OEMs beenden Hintergrund-Apps aggressiv. Ein Foreground-Service überlebt, eine normale Hintergrund-Task nicht.
- Auf Android 13+ die Benachrichtigungsberechtigung verweigern und prüfen, ob die Wiedergabe weiterhin funktioniert.
- Lass eine Playlist über mehrere Tracks laufen, während der Bildschirm aus ist.
adb shell dumpsys activity services | grep -A3 PlaybackService zeigt an, ob dein Service im Vordergrund läuft.
Fehlerbehebung
Audio stoppt nach etwa 30 Sekunden auf iOS: UIBackgroundModes fehlt audio, oder die Session-Kategorie ist ambient oder soloAmbient. Prüfe Info.plist in der gebauten App, nicht nur in deinem Quellcode.
Audio stoppt nach wenigen Minuten auf Android: kein Foreground-Service, oder der Service ist nicht vom Typ mediaPlayback. Prüfe auch die Akku-Optimierungseinstellungen des Herstellers.
SecurityException: Starting FGS with type mediaPlayback ... requires permissions: hinzufügen FOREGROUND_SERVICE_MEDIA_PLAYBACK.
ForegroundServiceStartNotAllowedException: Du hast den Service gestartet, während die App im Hintergrund war. Starte ihn beim Play-Tap des Nutzers.
Keine Lockscreen-Steuerung auf iOS: showNotification ist false (mixable session) oder es wurden keine Metadaten gesetzt. Nur die App, die eine non-mixable session besitzt, wird zur Now Playing App.
Zwei Benachrichtigungen auf Android: du hast aktiviert showNotification in native audio aktiviert und zusätzlich eine eigene Service-Benachrichtigung gesendet. Halte die Service-Benachrichtigung minimal oder nutze nur den Service des media session Plugins.
Andere Musik-Apps spielen über deine hinweg: Du hast eine mixable session konfiguriert. Setze focus: true und showNotification: true.
Player-Updates schneller ausrollen
Queue-Logik, UI, Metadaten und Analytics für deinen Player liegen in JavaScript. Sobald das native Setup oben in einem Store-Build ist, Capgo Live Updates let you push fixes to that code without a new review cycle. The Dokumentation zu nativem Audio und Dokumentation zu Media Sessions decken alle Optionen ab. Wenn Ihre App auch Audio aufzeichnet, siehe die Capacitor Audio-Recorder-Plugin.