RORK LABJP
BUILD — Rork Max runs real Macs in the cloud loaded with Xcode and the iOS SDK, writing SwiftUI, compiling, reading the errors and building again. That loop, not the code generation, is what lifts the outputNATIVE — What comes out is pure Swift and SwiftUI, not React Native. Reaching AR, Metal graphics and widgets that React Native cannot touch is the real gap between this and other buildersPLATFORMS — Coverage spans iPhone, iPad, Apple Watch, Apple TV and Vision Pro, plus iMessage. Worth a look if you want to start from a watch app or an extension rather than a phone screenCOMPANION — The Rork Companion app lets you check a generated build on a real iPhone without a paid Apple Developer account, lowering the bar for trying a first project end to endPRICING — Free to start, paid plans from $25 a month, and Rork Max on the $200 Max plan. Worth working out up front how many projects it takes to earn that backDEADLINE — From August 31, 2026, Google Play requires target API level 36 or higher for new apps and updates alike. Ten days out, and the targetSdkVersion of what you generate is yours to verifyBUILD — Rork Max runs real Macs in the cloud loaded with Xcode and the iOS SDK, writing SwiftUI, compiling, reading the errors and building again. That loop, not the code generation, is what lifts the outputNATIVE — What comes out is pure Swift and SwiftUI, not React Native. Reaching AR, Metal graphics and widgets that React Native cannot touch is the real gap between this and other buildersPLATFORMS — Coverage spans iPhone, iPad, Apple Watch, Apple TV and Vision Pro, plus iMessage. Worth a look if you want to start from a watch app or an extension rather than a phone screenCOMPANION — The Rork Companion app lets you check a generated build on a real iPhone without a paid Apple Developer account, lowering the bar for trying a first project end to endPRICING — Free to start, paid plans from $25 a month, and Rork Max on the $200 Max plan. Worth working out up front how many projects it takes to earn that backDEADLINE — From August 31, 2026, Google Play requires target API level 36 or higher for new apps and updates alike. Ten days out, and the targetSdkVersion of what you generate is yours to verify
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.

Rork540WebRTC4video callICETURNgetStatsReact Native227

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-08-14
Find the native edits expo prebuild will erase before you upgrade to SDK 57
Expo SDK 57 makes expo prebuild clear and regenerate ios and android by default. Here is how to audit your hand edits first, move them into config plugins, and why 57.0.9 matters for Reanimated apps.
Dev Tools2026-08-10
Switching to Signed URLs Killed My Image Cache — Decoupling expo-image Keys from the URL
Signed URLs rewrite their query string on every expiry, so a URL-keyed cache never hits. Here is how to derive a stable key and drive expo-image's writeToCacheAsync and readFromCacheAsync yourself, with measured results.
📚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 →