◉RORK LABEN
●EXPO — EAS Observe がネイティブクラッシュも記録(10/07)。SDK 57 は 57.0.21 以降で確認できます●SDK 58 — SDK 58 Beta は 9/15 公開。stable の日付はまだ未確認です●RN 0.88 — React Native 0.88.x の正式リリース予定は 10/12、残り3日●Q&A — expo-widgets が本番ビルドだけ真っ黒になる、という問いが出ています●RORK — 09-29 に GPT-6.1 Sol を追加。Pro・Max プランで利用できます●NEW — クライアントのアプリを作る前に決める、公開アカウントの持ち主●EXPO — EAS Observe がネイティブクラッシュも記録(10/07)。SDK 57 は 57.0.21 以降で確認できます●SDK 58 — SDK 58 Beta は 9/15 公開。stable の日付はまだ未確認です●RN 0.88 — React Native 0.88.x の正式リリース予定は 10/12、残り3日●Q&A — expo-widgets が本番ビルドだけ真っ黒になる、という問いが出ています●RORK — 09-29 に GPT-6.1 Sol を追加。Pro・Max プランで利用できます●NEW — クライアントのアプリを作る前に決める、公開アカウントの持ち主
記事一覧/開発ツール
⬡ 開発ツール/2026-07-02上級

Rork のビデオ通話が『つながっているのに片方だけ聞こえない』とき — WebRTC の沈黙する失敗を計測して切り分ける運用メモ

Rork で作ったビデオ通話が本番でだけ片方向音声になる・地下鉄で静かに切れる問題を、WebRTC の getStats で計測して切り分ける運用メモ。ICE 再起動・TURN リレー確認・後始末の実装まで。

Rork577WebRTC4ビデオ通話ICETURNgetStatsReact Native238

✦ プレミアム記事

個人開発でビデオ通話機能を初めてアプリに組み込んだとき、私自身が一番長く悩まされたのは「クラッシュしないバグ」でした。通話は始まる。相手の顔も映る。接続状態は connected と出ている。なのに、片方だけ声が届かない。テスト機の Wi-Fi 同士では毎回成功するので、原因の場所すら分からないまま数日が過ぎました。

WebRTC の厄介さは、失敗が例外として飛んでこないことにあります。try/catch は通り抜け、ログにはエラーが出ず、UI 上は「通話中」のまま静かに劣化します。ここでは、Rork で生成したビデオ通話アプリを実機とセルラー回線で運用してきて、実際に効いた「沈黙する失敗を数値にする」やり方を、症状ごとにまとめます。派手な新機能の話ではなく、リリース後に効いてくる地味な計測と後始末の話です。

症状を先に分類する — 「つながらない」は3種類ある

現場で報告される「通話が変」は、原因の層がまったく違う3つに分かれます。ここを混ぜると、TURN の設定をいじるべき場面でカメラのパーミッションを疑う、といった遠回りが起きます。

症状疑うべき層最初に見る指標
映像も音声も一切来ない(特定回線だけ)ICE / TURN 到達性candidate-pair の state と relay 使用
片方向だけ音が出ないトラック追加 or 受信側の再生inbound-rtp の bytesReceived
数分後に静かに固まる・カクつく回線切替 / パケットロスiceConnectionState と packetsLost

この切り分けの共通言語が RTCPeerConnection.getStats() です。推測でコードをいじる前に、まずここから数字を出します。

沈黙する失敗を数値にする監視クラス

getStats() は生の統計の集合で、そのまま眺めても使いにくいものです。私は「今この通話は健全か」を一言で返す薄い監視層を必ず挟むようにしました。ポイントは、映像・音声それぞれの受信バイト数が増えているか、リレー経由になっていないか、往復遅延とロス率がしきい値を超えていないかを、同じ場所で観測することです。

type CallHealth = {
  audioInbound: number;   // 受信音声バイト(増えていないと片方向の疑い)
  videoInbound: number;
  usingRelay: boolean;    // TURN リレー経由か
  rtt: number;            // 往復遅延(秒)
  lossRate: number;       // 映像パケットロス率
  iceState: string;
};
 
class CallStatsMonitor {
  private pc: RTCPeerConnection;
  private prevAudio = 0;
  private prevVideo = 0;
 
  constructor(pc: RTCPeerConnection) {
    this.pc = pc;
  }
 
  async sample(): Promise<CallHealth> {
    const stats = await this.pc.getStats();
    let audioInbound = 0, videoInbound = 0;
    let packetsLost = 0, packetsReceived = 0, rtt = 0;
    let usingRelay = false;
 
    stats.forEach((r: any) => {
      if (r.type === 'inbound-rtp' && r.kind === 'audio') {
        audioInbound = r.bytesReceived || 0;
      }
      if (r.type === 'inbound-rtp' && r.kind === 'video') {
        videoInbound = r.bytesReceived || 0;
        packetsLost = r.packetsLost || 0;
        packetsReceived = r.packetsReceived || 0;
      }
      if (r.type === 'candidate-pair' && r.state === 'succeeded' && r.nominated) {
        rtt = r.currentRoundTripTime || 0;
        // 選ばれた経路のローカル候補が relay なら TURN 経由
        const local = stats.get(r.localCandidateId);
        if (local && local.candidateType === 'relay') usingRelay = true;
      }
    });
 
    const lossRate = packetsReceived > 0
      ? packetsLost / (packetsLost + packetsReceived) : 0;
 
    return {
      audioInbound, videoInbound, usingRelay, rtt, lossRate,
      iceState: this.pc.iceConnectionState,
    };
  }
 
  // 前回サンプルとの差分で「音声が本当に流れているか」を判定する
  isAudioFlowing(h: CallHealth): boolean {
    const delta = h.audioInbound - this.prevAudio;
    this.prevAudio = h.audioInbound;
    return delta > 0;
  }
}

ここでの肝は、瞬間値ではなく差分を見ることです。bytesReceived は累積値なので、「今の値が正か」ではなく「前回より増えたか」で判定しないと、通話開始直後の一瞬の値に騙されます。片方向音声のバグを、私は最終的にこの1行(delta > 0)で毎回再現できるようにしてから、ようやく原因にたどり着けました。

✦

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

この記事の続きを読む

この先には、実装コードやベンチマーク結果など、実務でお役に立てる内容をご用意しています。このサイトは広告を掲載しておらず、サーバーや開発にかかる費用はメンバーの皆様のご支援で成り立っています。もしお役に立てていましたら、ご支援いただけますと大変ありがたいです。

この記事で得られること
✦片方向音声・接続断の『沈黙する失敗』を getStats で数値として捕まえる監視クラスの実装
✦地下鉄やWi-Fi切替で落ちる通話を ICE 再起動と指数バックオフで静かに復旧させる設計
✦TURN リレーが本当に効いているかを candidate-pair から確認し、セルラーだけ失敗する事故を防ぐ手順
Stripe による安全な決済 · いつでもキャンセル可能
✦

この記事を購入する

この先の内容をすべてお読みいただけます。一度のご購入で、いつでも何度でもアクセスできます。このサイトは広告を掲載しておらず、皆さまのご支援がサーバー費用などの運営を支えています。

または
メンバーシップなら全記事が読み放題 →
シェア

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

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

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

関連記事

⬡ 開発ツール2026-09-04
EAS の secret は「アプリに入れない」設定ではありません — 接頭辞と可視性を別々に決める
EXPO_PUBLIC_ の接頭辞は成果物に入るかを決め、EAS の可視性は誰が読めるかを決めます。二つを重ねたときに OTA 更新でだけ値が消える筋道と、出荷前に自分の手で確かめる手順をまとめました。
⬡ 開発ツール2026-08-22
一括置換は全ファイル成功しました。壊れたのは、消さなかった行のほうです
生成コードに一括置換をかけると、一致した行ではなく隣の行が壊れます。運用中のプロジェクトで実際に起きた破損と、置換の不変条件を検査する20行のガードスクリプトをまとめました。
⬡ 開発ツール2026-08-14
Expo SDK 57 に上げる前に、prebuild で消えるネイティブ変更を洗い出す
Expo SDK 57 では expo prebuild が既定で ios と android を消してから再生成します。手編集を上げる前に洗い出す方法、config plugin への移し替え、57.0.9 で解消したメモリ回帰までを実務の順序でまとめました。
📚RECOMMENDED BOOKS
大規模言語モデル入門
山田育矢
LLM開発
生成AIプロンプトエンジニアリング入門
我妻幸長
プロンプト
Claude CodeによるAI駆動開発入門
平川知秀
AI駆動開発
※ アフィリエイトリンクを含みます