RORK LABJP
BUILD — Rork Max generates native Swift apps, reaching areas React Native struggles to touchPLATFORM — Rork Max supports iPhone, iPad, Apple Watch, Apple TV, Vision Pro, and iMessageNATIVE — Tap native features like HealthKit, Core ML, NFC, Dynamic Island, and Live ActivitiesTEST — A browser-based streaming iOS simulator lets you test without Xcode or a MacDEPLOY — Automated builds, certificates, and App Store submission simplify shippingPRICE — Start free; paid plans begin at $25/month and Rork Max is $200/monthBUILD — Rork Max generates native Swift apps, reaching areas React Native struggles to touchPLATFORM — Rork Max supports iPhone, iPad, Apple Watch, Apple TV, Vision Pro, and iMessageNATIVE — Tap native features like HealthKit, Core ML, NFC, Dynamic Island, and Live ActivitiesTEST — A browser-based streaming iOS simulator lets you test without Xcode or a MacDEPLOY — Automated builds, certificates, and App Store submission simplify shippingPRICE — Start free; paid plans begin at $25/month and Rork Max is $200/month
Articles/Dev Tools
Dev Tools/2026-07-02Advanced

When a Rork Video Call Is 'Connected' but Only One Side Can Hear — Field Notes on Instrumenting WebRTC's Silent Failures

Rork video calls that go one-way or quietly drop on the subway, diagnosed by measuring WebRTC with getStats instead of guessing. Covers ICE restart, verifying TURN relay, and clean teardown.

Rork481WebRTC4video callICETURNgetStatsReact Native192

Premium Article

As an indie developer, the bug that cost me the most time on my first app with video calling wasn't a crash. The call would start. The other person's face appeared. The connection state read connected. And yet only one side could hear the other. Between two phones on the same Wi-Fi it succeeded every single time, so for days I couldn't even locate where the problem lived.

That is the trap with WebRTC: failure rarely arrives as an exception. Your try/catch never fires, the logs stay clean, and the UI keeps saying "in call" while the experience quietly degrades. What follows is what actually worked once I started running Rork-generated calling apps on real devices and cellular networks — a symptom-by-symptom account of making silent failures visible. It isn't about shiny features; it's about the unglamorous measurement and cleanup that only start to matter after launch.

Sort the symptoms first — "it doesn't connect" is really three problems

Field reports of "the call is weird" split into three problems whose root causes sit in completely different layers. Blur them together and you'll suspect a camera permission when you should be editing TURN config.

SymptomLayer to suspectFirst metric to read
No video or audio at all (specific networks only)ICE / TURN reachabilitycandidate-pair state and relay use
Audio flows one way onlyTrack attach or receiver playbackinbound-rtp bytesReceived
Quietly freezes or stutters after minutesNetwork handoff / packet lossiceConnectionState and packetsLost

The shared vocabulary for this triage is RTCPeerConnection.getStats(). Before touching code on a hunch, I get numbers out of it first.

A monitor class that turns silent failure into numbers

getStats() returns a bag of raw reports that is awkward to eyeball. I now always insert a thin monitoring layer that answers one question — "is this call healthy right now?" The point is to observe, in one place, whether inbound bytes for audio and video are actually increasing, whether the path went through a relay, and whether round-trip time and loss have crossed a threshold.

type CallHealth = {
  audioInbound: number;   // received audio bytes (flat = one-way suspicion)
  videoInbound: number;
  usingRelay: boolean;    // routed through a TURN relay?
  rtt: number;            // round-trip time (seconds)
  lossRate: number;       // video packet loss rate
  iceState: string;
};
 
class CallStatsMonitor {
  private pc: RTCPeerConnection;
  private prevAudio = 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;
        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,
    };
  }
 
  // Judge "is audio really flowing" from the delta, not the raw value
  isAudioFlowing(h: CallHealth): boolean {
    const delta = h.audioInbound - this.prevAudio;
    this.prevAudio = h.audioInbound;
    return delta > 0;
  }
}

The crucial part is reading the delta rather than the instantaneous value. bytesReceived is cumulative, so you must ask "did it grow since last time," not "is the current value non-zero" — otherwise a momentary reading right after the call starts fools you. Once I could reliably reproduce the one-way audio bug with this single line (delta > 0), I finally reached the actual cause.

Thank you for reading this far.

Continue Reading

What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.

WHAT YOU'LL LEARN
A monitor class that turns one-way audio and quiet disconnects into numbers via getStats deltas
ICE-restart recovery with grace periods and exponential backoff for subway and Wi-Fi handoffs
A procedure to confirm TURN relay actually works, so calls don't silently fail on cellular only
Secure payment via Stripe · Cancel anytime

Unlock This Article

Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.

or
Unlock all articles with Membership →
Share

Thank You for Reading

Rork Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • Copy-paste ready implementation code
  • New advanced guides published daily
  • $5/mo or $10 for lifetime access
View Membership →

Related Articles

Dev Tools2026-05-01
Implementing CallKit + PushKit in Rork: Native Call UI and VoIP Push for Voice & Video Apps
A practical, production-grade walkthrough for wiring CallKit and PushKit into a Rork-built voice or video calling app — covering token lifecycle, audio sessions, App Store review risks, and the WebRTC handoff.
Dev Tools2026-06-29
Adding a keyboard toolbar to Rork text inputs — unifying iOS InputAccessoryView and an Android bar into one component
How to add a toolbar pinned above the keyboard — a Done button or quick-insert actions — to the React Native app Rork generates. The iOS InputAccessoryView and a hand-built Android bar, folded into one reusable component, with working code.
Dev Tools2026-06-29
When a New Architecture Migration Only Janks in Release Builds — Field Notes on Catching Silent Interop-Layer Fallback
A Rork app on the New Architecture scrolled fine in development but stuttered only in release builds on real devices. The cause: a legacy native module quietly falling back to the interop layer. Field notes on measuring it and rolling out a fix without reverting the whole app.
📚RECOMMENDED BOOKS
Build a Large Language Model (From Scratch)
Sebastian Raschka
LLM Dev
Prompt Engineering for LLMs
Berryman & Ziegler
Prompting
AI Engineering
Chip Huyen
AI Eng
* Contains affiliate links
See all →