◉RORK LABEN
●GPT-6.1 SOL — Rork のモデルメニューに追加(09-29)。Low/Medium/High を選べ、Pro・Max に含まれます●SONNET 5.5 — UI づくりが得意で、Sonnet 5 より出力が速いモデルが選べます(09-28)●SDK 58 — Beta が 09/15 から。stable の案内はまだなく、Expo Go でのプロジェクト実行は 09/03 からログインが必要です●WIDGET — iOS のウィジェットが本番ビルドだけ真っ黒になる原因を追った報告が出ています●OBSERVE — EAS Observe がネイティブクラッシュも記録します(SDK 57.0.21 以降・10/07)●NEW — 起動広告が、背面から戻った回にしか出ていませんでした。「前面に戻った」合図が、…●GPT-6.1 SOL — Rork のモデルメニューに追加(09-29)。Low/Medium/High を選べ、Pro・Max に含まれます●SONNET 5.5 — UI づくりが得意で、Sonnet 5 より出力が速いモデルが選べます(09-28)●SDK 58 — Beta が 09/15 から。stable の案内はまだなく、Expo Go でのプロジェクト実行は 09/03 からログインが必要です●WIDGET — iOS のウィジェットが本番ビルドだけ真っ黒になる原因を追った報告が出ています●OBSERVE — EAS Observe がネイティブクラッシュも記録します(SDK 57.0.21 以降・10/07)●NEW — 起動広告が、背面から戻った回にしか出ていませんでした。「前面に戻った」合図が、…
記事一覧/開発ツール
⬡ 開発ツール/2026-08-31中級

画面ロックから3分後、expo-audio の環境音は静かに止まっていました

expo-audio の音が画面ロックで止まる原因を、ビルド設定・オーディオセッション・ロック画面連携の三層に分けて切り分けます。Android で約3分後に止まるのは、ドキュメントに書かれた止まり方です。

expo-audio4バックグラウンド再生4React Native239Expo219Android55

個人開発で、睡眠向けの環境音アプリを長く運用しています。ユーザーは雨の音を再生して、そのまま眠ります。画面は1分もせずロックされ、音だけが朝まで残る——「画面が消えてからが本番」という性質のアプリです。

expo-av が廃止された流れを受けて expo-audio へ移行した検証ビルドを Android 実機で試したとき、妙な止まり方に出会いました。画面をロックした直後は鳴り続けているのに、3分ほど経ってから、エラーも出さずに音だけが消えるのです。

即座に止まるなら設定漏れを疑えます。しかし「しばらく鳴ってから止まる」は、原因の見当がつけにくい挙動です。調べてみると、これはバグでも端末の癖でもなく、Expo の公式ドキュメントに明記されている止まり方でした。止まるまでの経緯と、設定を三層に分けて塞いだ手順を残しておきます。

expo-av の背景再生の知識は、expo-audio に持ち越せません

前提の整理から始めます。expo-av は SDK 52 で非推奨となり、公式ドキュメントでは SDK 55 での削除が案内されていました。Rork が生成するアプリは Expo を基盤にしているため、新しい SDK でビルドし直す限り、音まわりは expo-audio へ移ることになります。

expo-av の時代、バックグラウンド再生の要は staysActiveInBackground という一つの真偽値でした。setAudioModeAsync にこれを渡せば、おおむね話は済んでいました。

expo-audio の AudioMode には、このフィールドが存在しません。名前で探して見つからず、それらしい shouldPlayInBackground を true にして満足する——私がまさにその順路をたどりました。iOS ではそれで動きます。そして Android では、3分で止まります。

設定が「一つの真偽値」から「三つの層」に分かれたこと。これが今回の落とし穴の正体です。

三層の全体像——どの層が欠けると、どう止まるか

先に地図を示します。expo-audio のバックグラウンド再生は、次の三層がすべて揃って初めて成立します。

層設定する場所設定内容欠けたときの症状
第一層:ビルド設定app.json の config pluginenableBackgroundPlayback(既定 true)iOS はロックで即停止。Android は前面サービス未宣言
第二層:オーディオセッション実行時の setAudioModeAsyncshouldPlayInBackground: true(既定 false)両 OS ともロックや背景移行で即停止
第三層:ロック画面連携実行時の setActiveForLockScreenAndroid では実質必須(iOS は任意)Android のみ、約3分後に停止

この表は、症状からの逆引きに使えます。ロックした瞬間に止まるなら第一層か第二層。しばらく鳴ってから止まるなら第三層。「3分」という数字が、どの層を疑うべきかを教えてくれるわけです。

第一層と第二層——ビルド時の宣言と、実行時のセッション設定

第一層は config plugin です。app.json に次のように書きます。

{
  "expo": {
    "plugins": [
      ["expo-audio", { "enableBackgroundPlayback": true }]
    ]
  }
}

これが Android 側に FOREGROUND_SERVICE と FOREGROUND_SERVICE_MEDIA_PLAYBACK の2つの権限と、メディア再生用の前面サービス(AudioControlsService)の宣言を追加し、iOS 側に UIBackgroundModes の audio を追加してくれます。ネイティブ設定を手で触る必要はありません。

注意したいのは、enableBackgroundPlayback の既定値が true であることです。つまりこの層は「何もしなくても通っている」ことが多いのです。一方で第二層の shouldPlayInBackground の既定値は false です。ビルド設定は既定で有効、実行時設定は既定で無効——二つの層で既定値の向きが逆なので、「マニフェストには全部入っているのに止まる」という混乱が起きやすくなっています。

第二層は、再生を始める前に一度だけ呼ぶセッション設定です。

import { setAudioModeAsync } from 'expo-audio';
 
await setAudioModeAsync({
  playsInSilentMode: true,      // iOS のサイレントスイッチ下でも再生する
  shouldPlayInBackground: true, // 背景再生の本体。既定は false
  interruptionMode: 'doNotMix', // ロック画面操作を使うなら必須
});

interruptionMode の既定は 'mixWithOthers' ですが、ロック画面のコントロールを正しく自分のプレイヤーに結び付けるには 'doNotMix' が必要だとドキュメントに明記されています。また、かつての interruptionModeAndroid は非推奨になり、interruptionMode が両プラットフォームに効くよう統合されました。移行時に古い記事を参照すると、ここでも名前の食い違いに当たります。

第三層——Android は setActiveForLockScreen がないと約3分で止まります

ここが今回の本題です。Expo のドキュメントには、Android について「持続的なバックグラウンド再生には setActiveForLockScreen でロック画面コントロールを有効にする必要がある。さもなければ約3分で音声が止まる(OS の制限)」という趣旨の記述があります。

私のビルドが3分で止まったのは、この一文の通りの挙動でした。実装としては、再生開始のタイミングで setActiveForLockScreen を呼びます。

player.loop = true;
player.setActiveForLockScreen(true, {
  title: '雨の音',
  artist: 'Ambient Sounds',
});
player.play();

この呼び出しがメディア通知を出し、その通知に紐づく前面サービスが再生を生かし続けます。通知シェードにメディア通知が出ていることが、「止まらないこと」の目に見える担保です。逆に言えば、ロック前に通知が出ていなければ、その再生は3分の砂時計の上にあります。

補足として、ロック画面を操作できるプレイヤーは同時に1つだけです(公式にも同じ記述があります)。環境音アプリのように複数の音源を切り替える構成では、アクティブなプレイヤーを切り替えるたびに呼び直すか、updateLockScreenMetadata で表示だけ更新する設計になります。

動作した最小構成と、私が検証に3分を組み込んだ理由

三層をまとめた最小構成です。

import { useAudioPlayer, setAudioModeAsync } from 'expo-audio';
import { useEffect } from 'react';
import { Button } from 'react-native';
 
const rainSound = require('./assets/rain.mp3');
 
export function AmbientPlayer() {
  const player = useAudioPlayer(rainSound);
 
  useEffect(() => {
    setAudioModeAsync({
      playsInSilentMode: true,
      shouldPlayInBackground: true,
      interruptionMode: 'doNotMix',
    });
  }, []);
 
  const start = () => {
    player.loop = true;
    player.setActiveForLockScreen(true, {
      title: '雨の音',
      artist: 'Ambient Sounds',
    });
    player.play();
  };
 
  return <Button title="再生" onPress={start} />;
}

検証の手順も、この件を経て一つ増やしました。Android 実機で再生を始め、画面をロックし、通知シェードにメディア通知が出ていることを確かめたうえで、3分以上そのまま聴く。シミュレータの30秒確認では、この問題は永遠に見つかりません。

環境音アプリにとって、3分は致命的な数字です。ユーザーがいちばん無防備な、眠りかけの時間に音が消えます。しかもレビューに書かれるのは「音が止まる」だけで、3分という手がかりは書かれません。

最初のうち、私は検証を「ロックして30秒聴く」で済ませていました。結果は芳しくなく、リリース後にしか気づけない種類の不具合を、手元で見逃していたのだと思います。いまは次のような時刻つきのメモを取っています。

経過時間確認することここで止まるなら
ロック直後音が鳴っているか/通知シェードにメディア通知があるか第一層・第二層(設定の欠け)
1〜2分通知が残っているか通知が消えているなら第三層の呼び出し順序を疑う
3分を過ぎた時点音が続いているかここで止まるなら第三層(setActiveForLockScreen 未呼び出し)
10分音が続いているか、通知が残っているか端末の省電力設定など、別要因の切り分けへ

一点だけ、確かめきれていないことがあります。Android 13 以降は通知の権限状態によって通知シェードの見え方が変わりうるため、私の検証は通知を許可した状態で行っています。通知を拒否した端末での挙動は未検証ですので、気になる方は実機で確かめていただければ幸いです。

音が鳴っていることではなく、3分後も鳴っていることを、出荷の条件にしています。 この線引きは、睡眠向けの音源を扱うかぎり、変えないつもりです。

ループの継ぎ目の途切れやサイレントモードでの無音など、環境音まわりの別の落とし穴は Rork で癒し系アプリのアンビエントサウンドを実装して詰まった3つのことにまとめています。また、ロック中の再生が確保できた次の段階——スリープタイマーとフェードアウトの精度——については、プレミアム記事の Rorkの癒しアプリにスリープタイマーを付けたら、フェードアウトが実際の音とズレた話で、再生位置駆動の設計として詳しく扱っています。

まずは実機で3分、聴いてみてください。そこが通れば、朝まで鳴り続ける土台はできています。

シェア

お読みいただきありがとうございます

Rork Lab は広告なしで運営しており、サーバー費用などの運営コストはメンバーシップのご支援で賄っています。実装コード・ベンチマーク・本番設計パターンなど、実務でお役立ていただける記事を毎日更新しています。もし読んでよかったと感じていただけましたら、ぜひご覧ください。

  • ✦コピー&ペーストで使える実装コード付き
  • ✦毎日新しい上級ガイドを追加
  • ✦¥580/月 または ¥1,480 の永久アクセス
メンバーシップを見る →

もしこの記事がお役に立ちましたら、チップ(¥150)で応援いただけると大変励みになります。広告なしでの運営を続けるため、皆さまのご支援が大きな力になっています。

関連記事

⬡ 開発ツール2026-09-10
画面を暗くする機能を足したら、アプリを閉じたあとも iPhone が暗いままでした
expo-brightness の setBrightnessAsync は、Android では現在のアクティビティにだけ効き、iOS では端末の明るさそのものを変えます。戻す責任が片側にしかない理由と、AppState で後始末をするフックの実装をまとめました。
⬡ 開発ツール2026-05-05
Rork のポッドキャストアプリを本番運用へ — expo-audio への移行・割り込みからの復帰・ダウンロード再開
Rork のポッドキャストアプリを実運用に耐えさせるための実装メモ。SDK 55 で消える expo-av からの移行、着信割り込みからの復帰、中断されたダウンロードの再開、iCloud バックアップと容量予算の設計まで、動くコードで扱います。
⬡ 開発ツール2026-10-07
日本語の見出しだけ太字にならない — Android で fontWeight が効かないときの切り分けと直し方
Rork で作ったアプリで、iPhone では太字なのに Android では日本語の見出しが細いままになる症状の切り分け記事です。原因の見分け方、fontFamily を重みごとに分ける直し方、確認用コードを載せています。