nextcloud / nextcloud/spreed

Cellular call failure when two phones use cell network with talk app

Open
#19,197 4 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

bug Needs triage
Dominant language
JavaScript
Stars
2.2k
Forks
586
Avg merge
18h 27m
Merged PRs (30d)
333

Description

Call freezes after ~3 seconds when BOTH participants use the native mobile apps on a direct P2P connection

Summary

In a 1:1 call between two mobile devices on cellular, media flows correctly in both
directions for ~2–4 seconds and then stops permanently. The call is never torn down by
the server; signalling stays healthy throughout.

The failure occurs only when both participants use the native Talk apps. Replacing
either endpoint with the Talk web client — same devices, same networks, same
call topology, same selected ICE candidate pair type — makes the problem disappear
completely.

The negotiated media path in the failing case is a direct peer-to-peer IPv6
host-to-host candidate pair
. A TURN server is configured and reachable, but it is not
in the media path, and I have verified server-side that it never carries the media.

Environment

Component Version
Nextcloud Server 34.0.3
Talk (spreed) 24.0.4
Talk iOS 24.0.2 (current on the App Store; no update offered)
Talk Android 24.0.4
Signalling Internal (no High Performance Backend) — signaling_mode and signaling_servers both empty
TURN coturn 4.6.3, IPv4-only
Devices iPhone (iOS) and Samsung Galaxy S22 Ultra (Android)
Network Both devices on the same MVNO cellular plan, Wi-Fi disabled on both. Both receive public IPv6.

Steps to reproduce

  1. Two mobile devices, both on cellular only (Wi-Fi off), both with public IPv6.
  2. Place a 1:1 Talk call between them using the native Talk apps on both ends.
  3. Call connects; audio and video work.
  4. After ~2–4 seconds one side's outbound media stops. ~5 s later the other side stops too (consistent with ICE consent-freshness expiry).

Reproduced on every attempt — audio-only and video calls alike.

Expected vs actual

Expected: media continues, or ICE fails over to the configured TURN relay.

Actual: media stops permanently. No recovery, and no failover to the relay occurs.

The decisive evidence: same path, different client

Android side iOS side Media path Result
Talk app Talk app direct P2P ❌ freezes ~3 s
Chrome (desktop, tethered to the Android) Talk app direct IPv6 host↔host ✅ works
Chrome (on the Android itself) Talk app direct IPv6 host↔host ✅ works
Talk app Safari (web client) direct P2P ✅ works
Talk app Talk app IPv4 TURN relay ✅ works

Neither app is broken in isolation: the Android app works against Safari, the iOS app
works against Chrome, and both work against each other when the path is the TURN relay.
Only the app-to-app + direct-P2P combination fails.

Two details tighten this considerably:

  • In the "Chrome on the Android" test, the Android's own IPv6 address is in the same /64 as in the failing app-to-app tests.
  • The iOS device presented the identical IPv6 address (same interface identifier) in both the working and failing runs.

So the same IPv6 candidate pair, between the same two devices on the same carrier, works
under a browser and fails under the app.

Client-side data (rtcstats, working browser calls)

Nominated pair, Chrome on Android → Talk iOS app:

PAIR nominated=true
  local  host/udp  2600:xxxx:xxxx:xxxx:....:51849
  remote host/udp  2600:xxxx:xxxx:xxxx:....:51052
  bytesSent = 7,846,357   bytesReceived = 313,354   rtt = 0.051

Nominated pair, Chrome on desktop → Talk iOS app:

PAIR nominated=true
  local  host/udp  2600:xxxx:xxxx:xxxx:....:52084
  remote host/udp  2600:xxxx:xxxx:xxxx:....:60775
  bytesSent = 2,671,406   bytesReceived = 1,468,851   rtt = 0.087

In both cases the IPv4 TURN relay pair is present and succeeded, but carries
bytesSent=0, bytesReceived=0 — the relay is available and simply loses candidate
selection to the direct IPv6 pair, as expected.

I have no equivalent client-side data for the failing case, because the native mobile
apps do not expose a WebRTC stats dump. **That is the main gap in this report, and a
built-in way to export **rtcstats from the mobile apps would have made this far easier

Server-side observations during failing calls

Captured with tcpdump on the TURN host, covering UDP/TCP 3478 and the relay port range:

  • No media ever traverses the TURN server. Peak throughput ~1.3 KB/s, largest packet 396 bytes — STUN connectivity checks and TURN control only, no RTP.
  • coturn logs zero errors across every call: no auth failures, no quota, no refused allocations. Permissions are correctly created for the peers.
  • Every call ends with a client-side DELETE .../call/<token>; the server never terminates a call.
  • Talk signalling requests are all HTTP 200/304 with no 5xx, and continue normally on both devices after media has stopped.

Two observations that may help pinpoint the client behaviour:

  1. The iOS app stops communicating with the TURN server ~0.87 s into the call, on
    both its UDP and its TCP socket, 82 ms apart, and never resumes — while continuing
    Talk signalling normally. This is consistent with it nominating the direct pair and
    ceasing to service its relay candidates.

  2. The abandoned TURN-over-TCP connection is not closed — no FIN, no REFRESH
    with lifetime=0. It is simply left. The server then retransmits into a black hole
    for minutes. Across two consecutive calls the socket accepted a byte-identical
    396 acknowledged bytes before going silent:

    call 1 call 2.
    bytes_sent 4752 4488
    bytes_acked 396 396
    bytes_retrans 3960 3696
    RTO backoff 10 9

    Leaving the TCP allocation dangling rather than closing it looks like a resource leak
    on the server side, independent of the main issue.

Ruled out

Server load; bandwidth, packet size and path MTU (the stalled segment is only 396 bytes,
and audio-only calls at ~40 kbps fail identically); mesh size (genuine 1:1, room
type=1); NAT traversal and the TURN port-forward (verified end to end with a
hand-rolled TURN client); carrier CGNAT filtering; iOS Lockdown Mode (off); iCloud
Private Relay (not subscribed); iOS Low Data Mode (not present on this plan); VPN or DNS
profiles (none installed); Talk app versions (an iPad on the identical iOS build 24.0.2
holds calls fine).

Workaround

Have either participant join from the web client instead of the native app. Only one
side needs to change.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

No source file or test entry point is identified in the report. Start by locating the native iOS and Android WebRTC call and ICE candidate-handling entry points, then reproduce the direct IPv6 app-to-app case alongside the working browser case. Done means native app calls keep media flowing or correctly fail over to the configured TURN relay, with a regression test or captured diagnostic evidence.

Written by the indexing model from the issue text.

Assessment

Tech stack
android, ios
Domain
audio-video-rtc, mobile-dev
Issue type
Bug
Difficulty
5/5
Estimated time
Over a week
Activity status
Active
Clarity
Needs clarification
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.