メインコンテンツへスキップ

Capacitorでバックグラウンドでオーディオを再生する方法

Capacitor でバックグラウンドでオーディオを再生する: iOS のオーディオバックグラウンドモード、Android のフォアグラウンドサービス、ロック画面コントロール、プレイリスト、割り込み。

Capacitor アプリでロック画面コントロール付きのバックグラウンドオーディオ再生

バックグラウンドでオーディオを再生するには、Capacitor では2つのネイティブ要素が必要です: audio iOS のバックグラウンドモードと、 mediaPlayback Android のフォアグラウンドサービスです。メディアセッション経由でロック画面コントロールを追加すると、ユーザーが端末をロックしたりアプリを切り替えたりしても、音楽、ポッドキャスト、瞑想アプリの再生が続きます。このガイドでは、Capacitor 8 での両方のセットアップを、 @capgo/capacitor-native-audio ネイティブ再生用と @capgo/capacitor-media-session システムコントロール用に。

バックグラウンドでオーディオが止まる理由

Capacitor アプリは WebView を囲むネイティブシェルなので、各プラットフォームのライフサイクルルールに従います:

iOSと Androidでは
バックグラウンドで何が起きるか 数秒でアプリがサスペンドされる Activityが一時停止し、WebViewのタイマーがスロットリングされ、プロセスがkillされる可能性がある
オーディオを維持するもの UIBackgroundModes を含む audio、オーディオセッションカテゴリ playback タイプのフォアグラウンドサービス mediaPlayback 通知付きの
サイレントスイッチ playback カテゴリは無視します、 ambient それに従います 該当なし
ロック画面コントロール MPNowPlayingInfoCenter と MPRemoteCommandCenter MediaSession メディアスタイルの通知付き
ストアのルール ユーザーが期待する音声を再生すること (ガイドライン 2.5.4) Foreground service typeは、Play Consoleで宣言した実際の用途と一致している必要があります

どちらのプラットフォームもデフォルトではバックグラウンドオーディオを提供しません。片方だけを修正してしまうことが、「iPhoneでは動くのにAndroidでは止まる」というバグ報告の最も一般的な原因です。

再生方式を選択

適したアーキテクチャは2つあります:

  1. Native playback(プラグインを使用) @capgo/capacitor-native-audio: iOSではAVFoundation、AndroidではMedia3 ExoPlayerがオーディオをデコードします。音楽プレイヤー、オーディオブック、瞑想アプリ、WebViewがスロットリングされる可能性があるアプリに最適です。ローカルファイル、リモートURL、HLSストリーム、フェード、再生速度、Now Playingメタデータに対応しています。
  2. HTML <audio> と @capgo/capacitor-media-session: 既存のWebプレイヤーを維持し、プラグインがメタデータとコントロールをOSに公開します。Androidでは、セッション再生中にフォアグラウンドサービスも実行します。Webプレイヤーが既に存在する場合、またはWebとネイティブで単一の code パスが必要な場合に最適です。
@capgo/capacitor-native-audio <audio> + メディアセッション
再生エンジン ネイティブ WebView
Web対応 対応(HTML audioフォールバック) はい
ロック画面のメタデータ showNotification + notificationMetadata setMetadata
Androidフォアグラウンドサービス 自分で開始する 再生中または一時停止中にプラグインが開始
リモートコントロールイベント playbackState リスナー setActionHandler
低遅延で正確なSFX はい 制限あり

iOSのバックグラウンドオーディオを設定する

開く ios/App/App/Info.plist を追加して、バックグラウンドモードを有効にします。Xcode の「Signing & Capabilities」>「Background Modes」で「Audio, AirPlay, and Picture in Picture」にチェックを入れると、同じキーが書き込まれます:

<key>UIBackgroundModes</key>
<array>
  <string>audio</string>
</array>

オーディオセッションのカテゴリも重要です。 @capgo/capacitor-native-audio が設定します: showNotification: true では、これを使います .playback デフォルトモードでは、他のアプリを中断し(Spotify が一時停止します)、アプリを Now Playing アプリにします。 showNotification: false では、これを使います .playback を使用して mixWithOthers。これは効果音には適していますが、ロック画面のコントロールは表示されません。トラックごとではなく、アプリごとに1つ選んでください。

を介して再生する場合 <audio>、メディア再生が始まると WebKit が再生セッションを有効にします。初回は必ずユーザー操作を起点に再生を開始してください。そうしないと iOS にブロックされます。

2026年4月以降、App Store Connect にアップロードする前に Xcode 26 以降でビルドすることが必須です。 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 以降では、アプリがバックグラウンドにある状態でフォアグラウンドサービスを開始できません。ユーザーが再生を押したとき、Activity が表示されているうちに開始してください。

オプション A: media session プラグインにサービスを実行させる

@capgo/capacitor-media-session は次を宣言します: mediaPlayback 独自のmanifestでサービスを宣言し、再生状態を次に設定すると起動します: 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" />

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';

@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.

自動で進むプレイリストを作成しましょう

この complete ネイティブプレイヤーが動作し続けるため、画面ロック中でもトラック終了時にイベントが発火します:

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();
  }
});

トラック間もフォアグラウンドサービスを実行し続けてください。1曲終了後に停止し、バックグラウンドで再開しようとすると、Android 12以降ではエラーをスローします ForegroundServiceStartNotAllowedException.

ロック画面とUIの状態を同期させる

ユーザーはロック画面、ヘッドフォン、通知から一時停止を押します。以下をリッスンしてください: playbackState そしてネイティブプレイヤーを 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
});

アプリがフォアグラウンドに戻ったときも、次を使って状態を更新します isPlaying と getCurrentTimeイベントの送信中にWebViewが一時停止していた可能性があるためです。

Media Sessionコントロール付きのHTML audio要素を使う

既存の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,
  });
});

呼び出す setPositionState 本番環境では最大でも1秒に1〜2回にしてください。ロック画面は更新の間を補間するので、 timeupdate ブリッジに大量のイベントを流すとバッテリーを消耗します。

割り込みやルート変更への対応

電話、Siri、アラーム、その他のアプリが再生を中断します。Native audioは中断時に一時停止し、iOSではシステムが再開すべきと判断したタイミングで再開します。さらに次の2つのケースに注意が必要です:

  • ヘッドホンが外れたとき:ユーザーは再生が一時停止することを期待します。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'
});
  • Ducking(音量低下)か一時停止か:ナビゲーションのプロンプトでオーディオがダッキングされます。音声コンテンツ系アプリでは、音量を下げるのではなく一時停止すべきです。音量が下がると聞き取りにくいためです。

バックグラウンド再生のテスト

シミュレーターはバックグラウンド動作を正しく再現しません。実機でテストしてください:

  1. 再生を開始して画面をロックし、2分待ちます。オーディオが再生され続ける必要があります。
  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(mixable session)か、メタデータが設定されていません。non-mixable session を所有するアプリだけが Now Playing アプリになります。

Android で通知が 2 つ表示される: 有効にしています showNotification を native audio で有効にし、さらに独自のサービス通知も発行しています。サービス通知は最小限にするか、media session プラグインのサービスのみを使用してください。

他の音楽アプリが自分のアプリの音に重なって再生され続ける: mixable session を設定しています。設定してください focus: true と showNotification: true.

プレイヤーの変更をより速くデプロイ

プレイヤーのキューロジック、UI、メタデータ、アナリティクスはすべて JavaScript に実装されます。上記のネイティブ設定がストアビルドに含まれていれば、 Capgo Live Updates そのcodeに修正をプッシュでき、新しいレビューサイクルは不要です。 ネイティブオーディオのドキュメント と メディアセッションのドキュメント すべてのオプションを網羅しています。アプリで音声録音も行う場合は、 Capacitor オーディオレコーダープラグイン.

Capacitor アプリ向けの Live Updates

Web レイヤーのバグが本番で発生したら、App Store の審査を何日も待たずに Capgo で修正を配信しましょう。ネイティブの変更は通常の審査フローに残したまま、ユーザーはバックグラウンドでアップデートを受け取れます。

最新のブログ記事

Capgoは、真にプロフェッショナルなモバイルアプリを作るために必要な最高のインサイトを提供します。