◉RORK LABEN
●GPT-6.1 SOL — Rork のモデルメニューに追加(09-29)。Low/Medium/High を選べ、Pro・Max に含まれます●SONNET 5.5 — UI づくりが得意で、Sonnet 5 より出力が速いモデルが選べます(09-28)●SONNET 4.5 — Claude API での提供終了は 11/30、残り26日。API 連携を足したアプリは移行先の確認を●SDK 58 — React Native 0.88 に更新されました。Android のリリース版で R8 が既定オンになり、iOS は SceneDelegate を生成します●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)●SONNET 4.5 — Claude API での提供終了は 11/30、残り26日。API 連携を足したアプリは移行先の確認を●SDK 58 — React Native 0.88 に更新されました。Android のリリース版で R8 が既定オンになり、iOS は SceneDelegate を生成します●OBSERVE — EAS Observe がネイティブクラッシュも記録します(SDK 57.0.21 以降・10/07)●NEW — 起動広告が、背面から戻った回にしか出ていませんでした。「前面に戻った」合図が、…
記事一覧/開発ツール
⬡ 開発ツール/2026-11-04初級

ストリーミング音声の再生が始まるまでの数秒を、読み込み表示と時間切れで受け止める

再生ボタンを押しても音が出るまで間が空く。その数秒を不具合に見せないための状態の持ち方を、expo-audio の isLoaded と isBuffering、400ms の遅延表示、8秒の時間切れで組み立てます。

expo-audio6ストリーミング4読み込み表示タイムアウト4React Native243Rork612

再生ボタンを押したのに、音が出ませんでした。

指を離して一拍おいても、画面は何も変わりませんでした。

ヒーリング音源アプリの再生画面を整えていた夜、私は自分のアプリでこの「間」を見て、胃のあたりが少し重くなりました。バグではありません。回線が細いところで、音声ファイルの先頭を受け取るまでに時間がかかっているだけでした。けれど画面が無反応だと、使う方には壊れているのと区別がつきません。ボタンを二度三度と押され、そのたびに再生要求が重なっていきました。

最初にお伝えしたいのは、直すべきは通信ではなく、待っているあいだの画面の状態だということです。

待ち時間を「状態」として持つ

再生ボタンを押してから音が出るまでの時間は、私たちの側では短くできません。そこで、待っている最中を画面の状態として明示的に持ちます。私は次の4つに分けています。

状態意味ボタンの見た目
idleまだ押されていない再生アイコン
waiting押された。音はまだ回転表示(押せない)
playing音が出ている一時停止アイコン
failed時間切れまたは読み込み失敗やり直しの案内

ポイントは waiting を playing と別に持つことです。「再生中のつもり」の状態が長いと、二重押しの原因になります。

expo-audio の状態から waiting を判定する

expo-audio の useAudioPlayerStatus は、プレイヤーの状態をオブジェクトで返します。この中の isLoaded(読み込み完了か)、isBuffering(バッファ待ちか)、playing(再生中か)を使います。フィールドの名前と意味は、お使いの SDK のバージョンの型定義でご確認ください。

まず、状態を決める小さな関数を分けておきます。画面から切り離しておくと、読み返すときに迷いません。

// 何を解決するか: 「押したのに音が出ない間」を画面の状態として決める
type PlayUiState = 'idle' | 'waiting' | 'playing' | 'failed';
 
export function decidePlayUiState(args: {
  requested: boolean;   // 利用者が再生を押したか
  playing: boolean;     // status.playing
  failed: boolean;      // 時間切れ・読み込み失敗
}): PlayUiState {
  if (args.failed) return 'failed';
  if (args.playing) return 'playing';
  if (args.requested) return 'waiting'; // 押された後、音が出るまで
  return 'idle';
}

requested を自分で持つのが肝です。プレイヤー側の isBuffering だけに頼ると、押した瞬間はまだ false の瞬間があり、その一拍で画面が無反応に見えます。押した事実を先に記録しておけば、押した瞬間から waiting にできます。

400ms 待ってから表示する

回転表示をすぐに出すと、回線が良いときには一瞬ちらついて、かえって落ち着かない画面になります。私は 400ms を過ぎても音が出ないときだけ、回転表示を出すようにしています。この 400ms は計測値ではなく、私の好みで置いた設計値です。

import { useEffect, useRef, useState } from 'react';
import { useAudioPlayer, useAudioPlayerStatus } from 'expo-audio';
 
const SHOW_SPINNER_AFTER_MS = 400; // ちらつき防止
const GIVE_UP_AFTER_MS = 8000;     // 時間切れ
 
export function useStreamingPlayback(url: string) {
  const player = useAudioPlayer({ uri: url });
  const status = useAudioPlayerStatus(player);
  const [requested, setRequested] = useState(false);
  const [showSpinner, setShowSpinner] = useState(false);
  const [failed, setFailed] = useState(false);
  const timers = useRef<ReturnType<typeof setTimeout>[]>([]);
 
  const clearTimers = () => {
    timers.current.forEach(clearTimeout);
    timers.current = [];
  };
 
  const start = () => {
    if (requested) return;            // 二重押しを無視
    setFailed(false);
    setRequested(true);
    player.play();
    timers.current.push(setTimeout(() => setShowSpinner(true), SHOW_SPINNER_AFTER_MS));
    timers.current.push(setTimeout(() => setFailed(true), GIVE_UP_AFTER_MS));
  };
 
  // 音が出た・または失敗したらタイマーを片付ける
  useEffect(() => {
    if (status.playing || failed) {
      clearTimers();
      setShowSpinner(false);
      if (failed) setRequested(false); // やり直せる状態へ戻す
    }
  }, [status.playing, failed]);
 
  useEffect(() => clearTimers, []);   // 画面を離れたら必ず片付ける
 
  return { requested, showSpinner, failed, playing: status.playing, start };
}

このコードで私が気をつけたのは三点です。start の先頭で二重押しを止めること。音が出たらタイマーを片付けること。画面を離れるときにも片付けること。最後の一つを忘れると、離れた画面の setState が後から走ります。

8秒で見切りをつけ、やり直しを渡す

待ち続けさせないために、時間切れを置きます。8秒という数字も設計値です。短すぎると、混み合った回線の方を失敗扱いにしてしまい、長すぎると、無反応と区別がつきません。

失敗したときに出す文言は、原因を断定しません。「読み込みに時間がかかっています。もう一度お試しください」と、次の行動を一つだけ示します。通信が悪いのか、配信側なのか、画面からは分からないからです。

私はこの画面で、時間切れのあとに自動で再試行しないことにしました。自動再試行を入れた版では、回線が不安定な場所で再生要求が裏で積み重なり、しばらくして複数の音が重なって鳴ったことがありました。いまは、再試行は利用者が押した一回だけにしています。待たせる時間は決めても、やり直しの引き金は利用者に渡します。 この線引きだけは、画面を作り直すたびに残しています。

落とし穴

次にやること

まず、お使いの再生ボタンに requested を一つ足して、押した瞬間に見た目が変わるかどうかを確かめてみてください。回線を意図的に細くした状態(端末の通信制限や機内モードの切り替え)で試すと、「無反応に見える間」がよく分かります。私もまずそこから直しました。

シェア

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

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

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

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

関連記事

⬡ 開発ツール2026-05-05
Rork のポッドキャストアプリを本番運用へ — expo-audio への移行・割り込みからの復帰・ダウンロード再開
Rork のポッドキャストアプリを実運用に耐えさせるための実装メモ。SDK 55 で消える expo-av からの移行、着信割り込みからの復帰、中断されたダウンロードの再開、iCloud バックアップと容量予算の設計まで、動くコードで扱います。
⬡ 開発ツール2026-10-25
効果音が40回目で鳴らなくなる——expo-audio の createAudioPlayer は remove() だけでは片づきません
expo-audio で再生のたびに createAudioPlayer を作ると、Android では約40回で音が止まり、didJustFinish も届かなくなります。remove() と release() の役割の違い、作り方を変える三つの選択肢、止まったときの見張り方を整理します。
⬡ 開発ツール2026-09-04
EAS の secret は「アプリに入れない」設定ではありません — 接頭辞と可視性を別々に決める
EXPO_PUBLIC_ の接頭辞は成果物に入るかを決め、EAS の可視性は誰が読めるかを決めます。二つを重ねたときに OTA 更新でだけ値が消える筋道と、出荷前に自分の手で確かめる手順をまとめました。