Para reproducir audio en segundo plano en Capacitor, necesitas dos componentes nativos: el audio modo en segundo plano en iOS, y un mediaPlayback servicio en primer plano en Android. Añade controles en la pantalla de bloqueo mediante una media session, y tu app de música, podcast o meditación seguirá reproduciéndose cuando el usuario bloquee el teléfono o cambie de app. Esta guía muestra ambas configuraciones en Capacitor 8, con @capgo/capacitor-native-audio para la reproducción nativa y @capgo/capacitor-media-session para los controles del sistema.
Por qué el audio se detiene en segundo plano
Una app de Capacitor es una shell nativa alrededor de un WebView, por lo que sigue las reglas de ciclo de vida de cada plataforma:
| iOS | Android | |
|---|---|---|
| Qué ocurre en segundo plano | La app se suspende en segundos | La actividad se pausa, los temporizadores del WebView se limitan y el proceso puede matarse |
| Qué mantiene el audio activo | UIBackgroundModes contiene audio, categoría de sesión de audio playback |
Un servicio en primer plano de tipo mediaPlayback con una notificación |
| Interruptor de silencio | playback la categoría lo ignora, ambient lo respeta |
Sin equivalente |
| Controles de la pantalla de bloqueo | MPNowPlayingInfoCenter y MPRemoteCommandCenter |
MediaSession con una notificación de estilo multimedia |
| Regla de la tienda | Debe reproducir contenido audible que el usuario espera (directriz 2.5.4) | El tipo de foreground service debe coincidir con el uso real y declararse en Play Console |
Ninguna plataforma ofrece audio en segundo plano por defecto, y arreglar solo un lado es la causa más común de los reportes de "funciona en iPhone, falla en Android".
Elige tu enfoque de reproducción
Hay dos buenas arquitecturas:
- Reproducción nativa con
@capgo/capacitor-native-audio: el audio lo decodifica AVFoundation en iOS y Media3 ExoPlayer en Android. Ideal para reproductores de música, audiolibros, apps de meditación y todo lo que pueda sufrir throttling en el WebView. Soporta archivos locales, URLs remotas, streams HLS, fades, rate y metadatos de Now Playing. - HTML
<audio>además de@capgo/capacitor-media-session: mantén tu reproductor web actual y deja que el plugin publique metadatos y controles en el SO. En Android también ejecuta el foreground service mientras la sesión se reproduce. Ideal si ya tienes un reproductor web o necesitas un único code path para web y nativo.
@capgo/capacitor-native-audio |
<audio> + sesión de medios |
|
|---|---|---|
| Motor de reproducción | Nativo | WebView |
| Soporte web | Sí (fallback de audio HTML) | Sí |
| Metadatos de la pantalla de bloqueo | showNotification + notificationMetadata |
setMetadata |
| Servicio en primer plano de Android | Tú lo inicias | El plugin lo inicia durante la reproducción o en pausa |
| Eventos de control remoto | playbackState listener |
setActionHandler |
| SFX precisos de baja latencia | Sí | Limitado |
Configura iOS para audio en segundo plano
Abrir ios/App/App/Info.plist y añade el modo en segundo plano. También puedes marcar “Audio, AirPlay, and Picture in Picture” en Signing & Capabilities, Background Modes de Xcode, lo que escribe la misma clave:
<key>UIBackgroundModes</key>
<array>
<string>audio</string>
</array>
La categoría de la sesión de audio también es importante. @capgo/capacitor-native-audio lo configura por ti: con showNotification: true usa .playback con el modo predeterminado, que interrumpe otras apps (Spotify se pausa) y convierte tu app en la app Now Playing. Con showNotification: false usa .playback con mixWithOthers, que es adecuado para efectos de sonido pero no muestra controles en la pantalla de bloqueo. Elige uno por app, no por pista.
Si reproduces a través de <audio>, WebKit activa la sesión de reproducción cuando empieza el contenido multimedia. Asegúrate de que la primera vez la reproducción se inicie con un gesto del usuario, o iOS la bloqueará.
Realiza el build con Xcode 26 o superior antes de subirlo a App Store Connect, requisito obligatorio desde abril de 2026. Build de Capgo puedes hacerlo en la nube si trabajas en Windows o Linux.
Configura Android para audio en segundo plano
Añade los permisos 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 es obligatorio para apps con Android 14 (API 34) o superior, y Capacitor 8 apunta a API 36. Sin esto, al iniciar el servicio se lanza un SecurityException. Al publicar, Play Console te pedirá que declares el tipo de foreground service y lo justifiques. “Media playback the user started” es el uso aceptado.
POST_NOTIFICATIONS es un permiso de runtime en Android 13+. Las notificaciones de media session están exentas del permiso de notificaciones y se siguen mostrando en el panel, pero cualquier otra notificación que publique tu servicio lo necesita.
Android 12+ bloquea el inicio de un foreground service mientras la app está en segundo plano. Inícialo cuando el usuario pulse play, con tu activity visible.
Opción A: deja que el plugin de media session ejecute el servicio
@capgo/capacitor-media-session declara un mediaPlayback servicio en su propio manifiesto y lo inicia cuando estableces el estado de reproducción en playing o paused. Lo detiene cuando el estado vuelve a none y la app está en segundo plano. Si quieres que el servicio se ejecute durante toda la sesión de la app, configura el plugin en capacitor.config.ts:
plugins: {
MediaSession: {
foregroundService: 'always',
},
},
The default, starting only during playback, is what most apps want.
Opción B: inicia tu propio servicio en primer plano
@capgo/capacitor-native-audio gestiona la reproducción, el foco y la notificación multimedia, pero no crea el servicio en primer plano. Si lo usas sin el plugin de sesión multimedia, añade un servicio pequeño y un plugin local a tu proyecto de 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 en el mismo paquete:
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();
}
}
Regístralo en MainActivity.java antes super.onCreate, y declara el servicio en el manifiesto:
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" />
Desde 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';
Reproduce audio de forma nativa con @capgo/capacitor-native-audio
bun add @capgo/capacitor-native-audio
bunx cap sync
Configura una vez al inicio, luego precarga y reproduce:
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 acepta una ruta relativa a tus assets web (audio/intro.mp3), una file:// URL, una https:// URL o un stream HLS .m3u8 . Para cualquier cosa que no sea un asset empaquetado, configura isUrl: true. Si tu host de audio requiere autenticación, pasa headers.
Crea una playlist que avance automáticamente
El complete el evento se dispara cuando termina una pista, incluso con la pantalla bloqueada, porque el reproductor nativo sigue ejecutándose:
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();
}
});
Mantén el servicio en primer plano activo entre pistas. Si lo detienes después de una pista e intentas reiniciarlo en segundo plano, Android 12+ lanza ForegroundServiceStartNotAllowedException.
Mantén tu UI sincronizada con la pantalla de bloqueo
Los usuarios presionarán pausa en la pantalla de bloqueo, en los auriculares o en la notificación. Escucha playbackState y trata al reproductor nativo como la fuente de verdad:
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
});
También actualiza el estado cuando la app vuelve al primer plano, usando isPlaying y getCurrentTime, ya que el WebView pudo haberse pausado mientras se emitían eventos.
Usa un elemento de audio HTML con controles de media session
Si ya tienes un reproductor web, consérvalo y añade el 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,
});
});
Llama a setPositionState como máximo una o dos veces por segundo en producción. La pantalla de bloqueo interpola entre actualizaciones, y saturar el bridge con timeupdate eventos desperdicia batería.
Gestiona interrupciones y cambios de ruta
Llamadas, Siri, alarmas y otras apps interrumpen la reproducción. El audio nativo se pausa cuando hay una interrupción y, en iOS, se reanuda cuando el sistema lo indica. Hay dos casos más que requieren tu atención:
- Desconexión de auriculares: los usuarios esperan que la reproducción se pause. En Android, registra un receiver para
AudioManager.ACTION_AUDIO_BECOMING_NOISYen tu servicio si tu reproductor no se pausa automáticamente. En iOS, escucha los cambios de ruta 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. pausar: las indicaciones de navegación bajan el volumen de tu audio. Las apps de voz deberían pausar en su lugar, ya que el habla atenuada es difícil de seguir.
Prueba la reproducción en segundo plano
Simulators lie about background behavior. Test on real devices:
- Inicia la reproducción, bloquea la pantalla y espera dos minutos. El audio debe seguir sonando.
- Usa los controles de play, pause, next y seek de la pantalla de bloqueo; luego desbloquea y verifica que tu UI coincida.
- Reproduce audio, cambia a otra app que también reproduzca audio y luego regresa.
- Inicia una llamada telefónica o de FaceTime durante la reproducción.
- En Android, activa el ahorro de batería y prueba en un dispositivo Samsung o Xiaomi. Algunos OEMs matan las apps en segundo plano agresivamente. Un servicio en primer plano sobrevive, una tarea en segundo plano simple no.
- En Android 13+, deniega el permiso de notificaciones y confirma que la reproducción sigue funcionando.
- Deja que una lista de reproducción pase por varias pistas con la pantalla apagada.
adb shell dumpsys activity services | grep -A3 PlaybackService muestra si tu servicio está ejecutándose en primer plano.
Solución de problemas
El audio se detiene después de unos 30 segundos en iOS: UIBackgroundModes carece de audio, o la categoría de sesión es ambient o soloAmbient. Revisa Info.plist en la app compilada, no solo en tu código fuente.
El audio se detiene a los pocos minutos en Android: no hay servicio en primer plano, o el servicio no es de tipo mediaPlayback. Revisa también los ajustes de optimización de batería del OEM.
SecurityException: Starting FGS with type mediaPlayback ... requires permissions: añadir FOREGROUND_SERVICE_MEDIA_PLAYBACK.
ForegroundServiceStartNotAllowedException: iniciaste el servicio con la app en segundo plano. Inícialo cuando el usuario toque reproducir.
Sin controles en la pantalla de bloqueo en iOS: showNotification es false (sesión mixable), o no se establecieron metadatos. Solo la app con una sesión no mixable se convierte en la app Now Playing.
Dos notificaciones en Android: habilitaste showNotification en native audio y también publicaste tu propia notificación de servicio. Mantén la notificación de servicio al mínimo o usa solo el servicio del plugin de media session.
Otras apps de música siguen sonando sobre la tuya: configuraste una sesión mixable. Establece focus: true y showNotification: true.
Publica cambios del reproductor más rápido
La lógica de cola, la UI, los metadatos y la analítica de tu reproductor viven en JavaScript. Una vez que la configuración nativa anterior está en un build de la tienda, actualizaciones Live Update de Capgo te permite enviar correcciones a ese code sin un nuevo ciclo de revisión. El docs de native audio y docs de media session cubre todas las opciones, y si tu app también graba audio, consulta el plugin de grabadora de audio de Capacitor.