要在 Capacitor 中播放背景音频,您需要两个本机组件: audio iOS 上的背景模式,以及 mediaPlayback Android 上的前台服务。通过媒体会话添加锁屏控制,使您的音乐、播客或冥想应用在用户锁定手机或切换应用时继续播放。这篇指南将在 Capacitor 8 上展示两种设置。 @capgo/capacitor-native-audio 用于原生播放和 @capgo/capacitor-media-session 在系统控制中。
为什么背景音乐会停止
一个 Capacitor 应用程序是每个平台的生命周期规则的native shell around a WebView,所以它遵循:
| iOS | Android | |
|---|---|---|
| 在后台发生什么 | 应用程序在几秒内被挂起 | 活动被暂停,WebView定时器减速,进程可以被杀死 |
| 什么让音频保持活跃 | UIBackgroundModes 包含 audio,音频会话类别 playback |
A前台服务类型 mediaPlayback 带有通知 |
| 静音switch | playback 类别忽略它 ambient 遵守它 |
无等效 |
| 锁屏控制 | MPNowPlayingInfoCenter 和 MPRemoteCommandCenter |
MediaSession 带有媒体样式通知 |
| 商店规则 | 必须播放用户期望的可听内容(指南2.5.4) | 前台服务类型必须与实际用途匹配,声明在Play Console |
两者平台都没有提供背景音频的默认支持,仅修复一方是“在iPhone上工作,停止在Android上”的bug报告的最常见原因。
选择播放方式
有两种好的架构:
- 原生播放
@capgo/capacitor-native-audio:音频由iOS上的AVFoundation和Android上的Media3 ExoPlayer解码。适合音乐播放器、有声读物应用、冥想应用和WebView可能会被限制的任何应用。它支持本地文件、远程URL、HLS流、淡入、淡出、速度和正在播放的元数据。 - HTML
<audio>+@capgo/capacitor-media-session:保留您的现有Web播放器并让插件发布元数据和控制到操作系统。 在Android上,它还会在会话播放时运行前台服务。 当您已经有一个Web播放器或需要一个单独的code路径时,最佳选择。
@capgo/capacitor-native-audio |
<audio> +媒体会话 |
|
|---|---|---|
| 播放引擎 | 原生 | Web视图 |
| Web 支持 | 是的(HTML 音频 fallback) | 是 |
| 背景音乐 | showNotification + notificationMetadata |
setMetadata |
| 锁屏元数据 | Android 前台服务 | 您启动一个 |
| 插件在播放或暂停时启动它 | playbackState 远程控制事件 |
setActionHandler |
| 监听器 | 是 | 精确低延迟 SFX |
Configure iOS for background audio
Open ios/App/App/Info.plist 并添加后台模式。您还可以在Xcode的Signing & Capabilities,Background Modes下勾选“音频,AirPlay和画面在画面中”,这会为您写入相同的密钥:
<key>UIBackgroundModes</key>
<array>
<string>audio</string>
</array>
音频会话类别也很重要。 @capgo/capacitor-native-audio 设置它会为您设置:使用 showNotification: true 它使用 .playback 使用默认模式,会中断其他应用(Spotify会暂停)并将您的应用设置为当前播放应用。使用 showNotification: false 它使用 .playback 使用 mixWithOthers,适合音效,但不会显示锁屏控件。每个应用只选择一个,不要每个音频文件都选择。
如果您在后台播放 <audio>,WebKit会在媒体开始时激活播放会话。确保第一次播放是由用户手势触发,否则iOS会阻止它。
使用 Xcode 26 或更高版本在 App Store Connect 上上传之前,自 2026 年 4 月起已成为必需项。 Capgo 构建 如果您使用 Windows 或 Linux,可以在云端完成。
配置 Android 背景音频
添加权限到 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 对于目标 Android 14 (API 34) 或更高版本的应用,Capacitor 8 目标 API 36。没有它,启动服务会抛出 SecurityException当您发布时,Play Console 会要求您声明前台服务类型并为其辩护。 "用户启动的媒体播放" 是接受的用途。
POST_NOTIFICATIONS 是 Android 13+ 上的运行时权限。媒体会话通知免于通知权限,但仍然在阴影中显示,但您的服务发布的任何其他通知都需要它。
Android 12+ 阻止在应用程序处于后台时启动前台服务。请在用户按下播放键时启动它,直到您的活动可见。
选项 A:让媒体会话插件运行服务
@capgo/capacitor-media-session 在其自己的清单中声明 mediaPlayback 在设置播放状态为时启动 playing 或 paused. 当状态回退到时,它会停止它 none 和应用程序在后台。如果您希望服务在整个应用程序会话期间运行,请在 capacitor.config.ts:
plugins: {
MediaSession: {
foregroundService: 'always',
},
},
的默认值,仅在播放时启动,是大多数应用程序想要的。
选项B:启动自己的前台服务
@capgo/capacitor-native-audio 处理播放,焦点和媒体通知,但它不会创建前台服务。如果您使用它而不使用媒体会话插件,请在您的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 在同一个包中:
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();
}
}
在 MainActivity.java 前 super.onCreate在开发新功能之前创建一个问题并讨论
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" />
,并在清单中声明服务:
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';
Play audio natively with @capgo/capacitor-native-audio
bun add @capgo/capacitor-native-audio
bunx cap sync
在启动时配置一次,然后预加载并播放:
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 接受一个相对于您的Web资产的路径(audio/intro.mp3),一个 file:// URL,一个 https:// URL 或一个HLS .m3u8 流。对于任何不是捆绑资产的内容,请设置 isUrl: true。如果您的音频主机需要身份验证,请传递 headers.
构建一个自动前进的播放列表
The complete 在音频之间保持前台服务运行。如果您在一个音频结束后停止它,然后尝试在后台再次启动它,Android 12+会
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();
}
});
保持您的UI与锁屏同步 ForegroundServiceStartNotAllowedException.
__CAPGO_KEEP_0__
用户会在锁屏、耳机或通知中按暂停,继续 playbackState 并将原生播放器视为真实来源:
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
});
当应用返回前台时,使用 isPlaying 并 getCurrentTime刷新状态,因为WebView可能在事件被发送时被暂停。
使用HTML音频元素与媒体会话控制
如果您已经有一个Web播放器,请保留它并添加插件:
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,
});
});
Call setPositionState 最多一次或两次,锁屏会在更新之间进行插值,而向桥接器发送 timeupdate 事件会浪费电池。
处理中断和路由变化
电话呼叫、Siri、闹钟和其他应用会中断播放。原生音频在中断时暂停,并在iOS中在系统允许时恢复。另外两个情况需要您的关注:
- 耳机拔出:用户期望播放暂停。 在Android上,注册一个接收器来
AudioManager.ACTION_AUDIO_BECOMING_NOISY在你的服务中,如果你的播放器没有暂停。 在iOS上,监听路由变化@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'
});
- 降低音量vs.暂停:导航提示会降低音量。 语音应用应该暂停,因为降低的语音难以跟随。
测试背景播放
模拟器会欺骗你关于背景行为。 在真实设备上测试:
- 开始播放,锁屏,等待两分钟。 音频必须继续播放。
- 使用锁屏播放,暂停,下一首和快进控制,然后解锁并检查你的UI是否匹配。
- 播放,切换到另一个播放音频的应用,然后回来。
- 开始一个电话或FaceTime音频播放。
- 在Android上,启用电池节省并在Samsung或Xiaomi设备上测试。 一些OEM会激进地杀死背景应用。 前台服务会存活,普通背景任务不会。
- 在 Android 13+ 上,拒绝通知权限并确认播放仍然有效。
- 让播放列表在屏幕关闭时播放几个歌曲。
adb shell dumpsys activity services | grep -A3 PlaybackService 显示您的服务是否在前台运行。
故障排除
iOS 上音频在 30 秒后停止。: UIBackgroundModes 缺乏 audio或会话类别是 ambient 或 soloAmbient。检查 Info.plist 在构建的应用程序中,而不是仅在源代码中。
Android 上音频在几分钟后停止。:没有前台服务,或者服务类型不是 mediaPlayback也检查 OEM 电池优化设置。
SecurityException: Starting FGS with type mediaPlayback ... requires permissions: 添加。 FOREGROUND_SERVICE_MEDIA_PLAYBACK.
ForegroundServiceStartNotAllowedException: 您在应用程序处于后台时启动了服务。请在用户点击播放时启动它。
iOS 上没有锁屏控制。: showNotification 是 false(可混合会话),或未设置元数据。只有拥有非可混合会话的应用程序才能成为当前播放应用程序。
Android 上有两个通知。: 您启用了。 showNotification 在原生音频中也发布了自己的服务通知。请保持服务通知简洁,或仅使用媒体会话插件的服务。
其他音乐应用程序可以在您的应用程序播放时继续播放。: 您配置了可混合会话。请设置。 focus: true 并且 showNotification: true.
快速部署播放器更新
JavaScript中,队列逻辑、UI、元数据和分析都在您的播放器中。 一旦原生设置在商店构建中, Capgo实时更新 让您在不需要新的审查周期的情况下将修复推送到该code。 让您在没有新审查周期的情况下将修复推送到__CAPGO_KEEP_0__。 原生音频文档 和 如果您的应用程序还记录音频,请参阅 Capacitor音频录音插件.