Untuk memutar audio di latar belakang di Capacitor, Anda membutuhkan dua komponen native: audio mode background di iOS, dan mediaPlayback foreground service di Android. Tambahkan kontrol layar kunci lewat media session, dan aplikasi musik, podcast, atau meditasi Anda akan tetap berjalan saat pengguna mengunci ponsel atau berpindah aplikasi. Panduan ini menunjukkan kedua setup di Capacitor 8, dengan @capgo/capacitor-native-audio untuk pemutaran native dan @capgo/capacitor-media-session untuk kontrol sistem.
Mengapa audio berhenti di background
Aplikasi Capacitor adalah shell native yang membungkus WebView, jadi mengikuti aturan lifecycle tiap platform:
| iOS | Android | |
|---|---|---|
| Apa yang terjadi di background | App ditangguhkan dalam hitungan detik | Activity dijeda, timer WebView diturunkan, proses bisa dihentikan |
| Yang menjaga audio tetap hidup | UIBackgroundModes berisi audio, kategori audio session playback |
Foreground service bertipe mediaPlayback dengan notifikasi |
| Sakelar senyap | playback kategori mengabaikannya, ambient mematuhinya |
Tidak ada padanan |
| Kontrol lock screen | MPNowPlayingInfoCenter dan MPRemoteCommandCenter |
MediaSession dengan notifikasi gaya media |
| Aturan store | Harus memutar konten audio yang diharapkan pengguna (pedoman 2.5.4) | Tipe foreground service harus sesuai dengan penggunaan sebenarnya, dideklarasikan di Play Console |
Kedua platform tidak menyediakan audio latar belakang secara default, dan hanya memperbaiki satu sisi adalah alasan paling umum untuk laporan bug “berfungsi di iPhone, berhenti di Android”.
Pilih pendekatan playback Anda
Ada dua arsitektur yang baik:
- Pemutaran native dengan
@capgo/capacitor-native-audio: audio didekode oleh AVFoundation di iOS dan Media3 ExoPlayer di Android. Terbaik untuk pemutar musik, buku audio, aplikasi meditasi, dan apa pun di mana WebView mungkin dibatasi. Mendukung file lokal, URL remote, stream HLS, fades, rate, dan metadata Now Playing. - HTML
<audio>plus@capgo/capacitor-media-session: pertahankan web player yang sudah ada dan biarkan plugin menerbitkan metadata serta kontrol ke OS. Di Android, plugin ini juga menjalankan foreground service selama sesi diputar. Paling cocok jika Anda sudah memiliki web player atau membutuhkan satu jalur code untuk web dan native.
@capgo/capacitor-native-audio |
<audio> + media session |
|
|---|---|---|
| Mesin pemutaran | Native | WebView |
| Dukungan Web | Ya (fallback audio HTML) | Ya |
| Metadata layar kunci | showNotification + notificationMetadata |
setMetadata |
| Foreground service Android | Anda memulai satu | Plugin memulainya saat diputar atau dijeda |
| Event remote control | playbackState listener |
setActionHandler |
| SFX presisi dengan latensi rendah | Ya | Terbatas |
Konfigurasi iOS untuk audio background
Buka ios/App/App/Info.plist dan tambahkan background mode. Anda juga bisa mencentang “Audio, AirPlay, and Picture in Picture” di Signing & Capabilities, Background Modes di Xcode, yang menulis key yang sama:
<key>UIBackgroundModes</key>
<array>
<string>audio</string>
</array>
Kategori sesi audio juga penting. @capgo/capacitor-native-audio mengaturnya untuk Anda: dengan showNotification: true menggunakannya .playback dengan mode default, yang akan mengganggu aplikasi lain (Spotify akan jeda) dan menjadikan aplikasi Anda sebagai aplikasi Now Playing. Dengan showNotification: false menggunakannya .playback dengan mixWithOthers, yang cocok untuk efek suara tetapi tidak menampilkan kontrol di layar kunci. Pilih salah satu per aplikasi, bukan per track.
Jika Anda memutar melalui <audio>, WebKit mengaktifkan sesi playback saat media dimulai. Pastikan playback dimulai dari user gesture untuk pertama kalinya, kalau tidak iOS akan memblokirnya.
Build dengan Xcode 26 atau lebih baru sebelum upload ke App Store Connect, karena ini sudah diwajibkan sejak April 2026. Build Capgo bisa dilakukan di cloud jika Anda menggunakan Windows atau Linux.
Konfigurasi Android untuk audio background
Tambahkan permission ke 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 wajib untuk aplikasi dengan target Android 14 (API 34) atau lebih tinggi, dan Capacitor 8 menargetkan API 36. Tanpanya, memulai service akan melempar SecurityException. Saat Anda publish, Play Console meminta Anda mendeklarasikan tipe foreground service dan alasannya. “Media playback the user started” adalah penggunaan yang diterima.
POST_NOTIFICATIONS adalah runtime permission pada Android 13+. Notifikasi media session dikecualikan dari permission notifikasi dan tetap muncul di shade, tetapi notifikasi lain yang diposting service Anda membutuhkannya.
Android 12+ memblokir memulai foreground service saat aplikasi di background. Mulai service saat user menekan play, selagi activity Anda masih terlihat.
Opsi A: biarkan plugin media session menjalankan service
@capgo/capacitor-media-session mendeklarasikan mediaPlayback service di manifest-nya sendiri dan memulainya saat Anda mengatur status playback menjadi playing atau paused. Plugin menghentikannya saat status kembali ke none dan aplikasi berada di background. Jika Anda ingin service berjalan selama seluruh sesi aplikasi, atur konfigurasi plugin di capacitor.config.ts:
plugins: {
MediaSession: {
foregroundService: 'always',
},
},
Default, yang hanya dimulai saat playback, adalah yang diinginkan sebagian besar aplikasi.
Opsi B: jalankan foreground service sendiri
@capgo/capacitor-native-audio menangani playback, focus, dan notifikasi media, tetapi tidak membuat foreground service. Jika Anda menggunakannya tanpa media session plugin, tambahkan service kecil dan plugin lokal ke proyek Android Anda.
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 di paket yang sama:
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();
}
}
Daftarkan di MainActivity.java sebelum super.onCreate, dan deklarasikan service di 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" />
Dari 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';
Putar audio secara native dengan @capgo/capacitor-native-audio
bun add @capgo/capacitor-native-audio
bunx cap sync
Konfigurasi sekali saat startup, lalu preload dan putar:
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 menerima path relatif terhadap web assets Anda (audio/intro.mp3), sebuah file:// URL, sebuah https:// URL atau HLS .m3u8 stream. Untuk apa pun selain asset yang dibundel, setel isUrl: true. Jika host audio Anda memerlukan auth, pass headers.
Buat playlist yang berjalan otomatis
Panduan complete event dipicu saat track berakhir, bahkan saat layar terkunci, karena native player tetap berjalan:
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();
}
});
Jalankan foreground service di antara track. Jika Anda menghentikannya setelah satu track dan mencoba memulainya lagi saat di background, Android 12+ akan melempar ForegroundServiceStartNotAllowedException.
Sinkronkan UI Anda dengan lock screen
User akan menekan pause di lock screen, lewat headphone, atau di notifikasi. Dengarkan playbackState dan jadikan native player sebagai source of truth:
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
});
Refresh juga state saat app kembali ke foreground, dengan menggunakan isPlaying dan getCurrentTime, karena WebView mungkin sedang dalam kondisi pause saat event dipancarkan.
Gunakan elemen audio HTML dengan media session controls
Jika sudah punya web player, tetap gunakan dan tambahkan plugin ini:
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,
});
});
Panggil setPositionState maksimal satu atau dua kali per detik di produksi. Lock screen akan melakukan interpolasi antar update, dan membanjiri bridge dengan timeupdate event akan memboroskan baterai.
Tangani interupsi dan perubahan rute
Panggilan telepon, Siri, alarm, dan aplikasi lain dapat menginterupsi playback. Native audio akan pause saat ada interupsi dan, di iOS, resume saat sistem mengizinkannya. Ada dua kasus lain yang perlu Anda perhatikan:
- Headphone dicabut: pengguna mengharapkan playback di-pause. Di Android, daftarkan receiver untuk
AudioManager.ACTION_AUDIO_BECOMING_NOISYdi dalam service Anda jika player belum otomatis pause. Di iOS, pantau perubahan route dengan@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. menjeda audio: prompt navigasi akan men-duck audio Anda. Aplikasi berbasis suara sebaiknya pause saja, karena suara yang dikecilkan sulit diikuti.
Uji background playback
Simulator tidak bisa dipercaya soal perilaku background. Uji di perangkat asli:
- Mulai playback, kunci layar, tunggu dua menit. Audio harus tetap berjalan.
- Gunakan kontrol play, pause, next, dan seek di lock screen, lalu buka kunci layar dan pastikan UI Anda sesuai.
- Putar audio, pindah ke aplikasi lain yang memutar audio, lalu kembali lagi.
- Mulai panggilan telepon atau FaceTime audio saat playback berjalan.
- Di Android, aktifkan penghemat baterai dan tes di perangkat Samsung atau Xiaomi. Beberapa OEM mematikan aplikasi background secara agresif. Foreground service tetap bertahan, tapi background task biasa tidak.
- Di Android 13+, tolak izin notifikasi dan pastikan playback tetap berjalan.
- Biarkan playlist berjalan melewati beberapa track dengan layar mati.
adb shell dumpsys activity services | grep -A3 PlaybackService menunjukkan apakah service Anda sedang berjalan di foreground.
Pemecahan Masalah
Audio berhenti setelah sekitar 30 detik di iOS: UIBackgroundModes lacks audio, atau session category-nya adalah ambient atau soloAmbient. Periksa Info.plist di aplikasi yang sudah di-build, bukan hanya di source Anda.
Audio berhenti setelah beberapa menit di Android: tidak ada foreground service, atau service-nya bukan bertipe mediaPlayback. Juga periksa pengaturan optimasi baterai OEM.
SecurityException: Starting FGS with type mediaPlayback ... requires permissions: tambah FOREGROUND_SERVICE_MEDIA_PLAYBACK.
ForegroundServiceStartNotAllowedException: Anda memulai service saat aplikasi berada di background. Mulailah saat user menekan tombol play.
Tidak ada kontrol lock screen di iOS: showNotification adalah false (sesi mixable), atau metadata tidak diset. Hanya aplikasi yang memiliki sesi non-mixable yang menjadi aplikasi Now Playing.
Dua notifikasi di Android: kamu sudah mengaktifkan showNotification di native audio dan juga memposting notifikasi layanan milikmu sendiri. Jaga agar notifikasi layanan tetap minimal, atau gunakan hanya layanan dari plugin media session.
Aplikasi musik lain tetap diputar di atas aplikasimu: Anda telah mengonfigurasi sesi yang dapat dicampur. Atur focus: true dan showNotification: true.
Rilis perubahan player lebih cepat
Logika antrean, UI, metadata, dan analitik untuk player-mu ada di JavaScript. Setelah setup native di atas masuk ke build store, Live updates Capgo memungkinkanmu mendorong perbaikan ke code tersebut tanpa siklus review baru. Yang dokumentasi native audio dan dokumentasi media session mencakup semua opsi, dan jika aplikasi Anda juga merekam audio, lihat Capacitor plugin perekam audio.