RORK LABEN
BUILD — Rork Maxがネイティブ Swift アプリを生成し、React Nativeでは届きにくい領域に踏み込めますPLATFORM — Rork MaxはiPhone・iPad・Apple Watch・Apple TV・Vision Pro・iMessageに対応しますNATIVE — HealthKit・Core ML・NFC・Dynamic Island・Live Activitiesなどのネイティブ機能が使えますTEST — ブラウザ内ストリーミングiOSシミュレータで、XcodeやMacなしでテストできますDEPLOY — ビルド・証明書・App Store申請までの自動化で公開までを簡素化しますPRICE — 無料で開始でき、有料プランは$25/月〜、Rork Maxは$200/月ですBUILD — Rork Maxがネイティブ Swift アプリを生成し、React Nativeでは届きにくい領域に踏み込めますPLATFORM — Rork MaxはiPhone・iPad・Apple Watch・Apple TV・Vision Pro・iMessageに対応しますNATIVE — HealthKit・Core ML・NFC・Dynamic Island・Live Activitiesなどのネイティブ機能が使えますTEST — ブラウザ内ストリーミングiOSシミュレータで、XcodeやMacなしでテストできますDEPLOY — ビルド・証明書・App Store申請までの自動化で公開までを簡素化しますPRICE — 無料で開始でき、有料プランは$25/月〜、Rork Maxは$200/月です
記事一覧/開発ツール
開発ツール/2026-07-02上級

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

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

Rork480WebRTC4ビデオ通話ICETURNgetStatsReact Native192

プレミアム記事

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

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

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

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

症状疑うべき層最初に見る指標
映像も音声も一切来ない(特定回線だけ)ICE / TURN 到達性candidate-pair の staterelay 使用
片方向だけ音が出ないトラック追加 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/月 または ¥1,480 の永久アクセス
メンバーシップを見る →

関連記事

開発ツール2026-05-01
Rork × CallKit + PushKit で VoIP 通話アプリを実装する — Native 着信 UI と専用 Push の本番運用
Rork で作った通話アプリに CallKit + PushKit を組み込み、ネイティブ着信 UI と VoIP 専用 Push を本番運用する手順を、トークン管理・audio session・審査リスクまで含めて解説します。
開発ツール2026-06-29
Rork のテキスト入力にキーボード上のツールバーを足す — iOS の InputAccessoryView と Android の自前バーを1つの部品に揃える
Rork が生成した React Native アプリのテキスト入力に、キーボードの上へ固定する「完了」やクイック挿入ボタンのツールバーを足す方法です。iOS の InputAccessoryView と、Android で自前に組むバーを、1つの再利用コンポーネントへ畳む実装を実コードで残します。
開発ツール2026-06-29
New Architecture へ移行したら本番だけ画面がカクつくとき — interop 層への静かなフォールバックを計測して切り分ける運用メモ
New Architecture へ切り替えた Rork アプリが、開発中は快適なのにリリースビルドの実機でだけ一覧スクロールがカクつく。原因は旧式ネイティブモジュールが interop 層へ静かにフォールバックしていたことでした。検知の計測と段階的な切り戻しの実務メモです。
📚RECOMMENDED BOOKS
大規模言語モデル入門
山田育矢
LLM開発
生成AIプロンプトエンジニアリング入門
我妻幸長
プロンプト
Claude CodeによるAI駆動開発入門
平川知秀
AI駆動開発
※ アフィリエイトリンクを含みます
もっと見る →