跳过主要内容

如何在Capacitor中在后台播放音频

在 Capacitor: iOS 后台音频模式、Android 前台服务、锁屏控制、播放列表和中断中播放音频。

文章来源

马丁·多纳迪厄

作者

瓦莱里亚

审阅者

乔丹

编辑

在 Capacitor 中播放背景音频

要在 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报告的最常见原因。

选择播放方式

有两种好的架构:

  1. 原生播放 @capgo/capacitor-native-audio:音频由iOS上的AVFoundation和Android上的Media3 ExoPlayer解码。适合音乐播放器、有声读物应用、冥想应用和WebView可能会被限制的任何应用。它支持本地文件、远程URL、HLS流、淡入、淡出、速度和正在播放的元数据。
  2. 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 &amp; 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.暂停:导航提示会降低音量。 语音应用应该暂停,因为降低的语音难以跟随。

测试背景播放

模拟器会欺骗你关于背景行为。 在真实设备上测试:

  1. 开始播放,锁屏,等待两分钟。 音频必须继续播放。
  2. 使用锁屏播放,暂停,下一首和快进控制,然后解锁并检查你的UI是否匹配。
  3. 播放,切换到另一个播放音频的应用,然后回来。
  4. 开始一个电话或FaceTime音频播放。
  5. 在Android上,启用电池节省并在Samsung或Xiaomi设备上测试。 一些OEM会激进地杀死背景应用。 前台服务会存活,普通背景任务不会。
  6. 在 Android 13+ 上,拒绝通知权限并确认播放仍然有效。
  7. 让播放列表在屏幕关闭时播放几个歌曲。

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音频录音插件.

为Capacitor应用提供即时更新

当有web层bug时,通过Capgo快速部署修复,而不是等待几天的app store审批。用户在后台接收更新,而原生变化仍在正常审批路径中。

最新博客

Capgo为您提供创建真正专业移动应用所需的最佳见解。