Saltar al contenido principal

Cómo reproducir audio en segundo plano en Capacitor

Reproducir audio en segundo plano en Capacitor: modo de audio en segundo plano de iOS, servicio en primer plano de Android, controles en la pantalla de bloqueo, listas de reproducción e interrupciones.

Audio en segundo plano con controles en pantalla de bloqueo en una app Capacitor

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:

  1. 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.
  2. 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 &amp; 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_NOISY en 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:

  1. Inicia la reproducción, bloquea la pantalla y espera dos minutos. El audio debe seguir sonando.
  2. Usa los controles de play, pause, next y seek de la pantalla de bloqueo; luego desbloquea y verifica que tu UI coincida.
  3. Reproduce audio, cambia a otra app que también reproduzca audio y luego regresa.
  4. Inicia una llamada telefónica o de FaceTime durante la reproducción.
  5. 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.
  6. En Android 13+, deniega el permiso de notificaciones y confirma que la reproducción sigue funcionando.
  7. 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.

Live updates para apps Capacitor

Cuando un bug de la capa web está en producción, publica el fix a través de Capgo en lugar de esperar días a la aprobación de la app store. Los usuarios reciben la actualización en segundo plano mientras los cambios nativos siguen el flujo de revisión habitual.

Lo último de nuestro blog

Capgo te da la información clave que necesitas para crear una app móvil realmente profesional.