Documents WebRTC remote connection investigation and findings. Feature on hold due to one-way channel bug. See docs for details. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
13 KiB
WebRTC Data Channel Disconnect Fixes
Document Purpose
Track all fixes attempted for the WebRTC data channel disconnection issues in Ensemble's remote connection feature. This document should be referenced when debugging future disconnection problems.
Problem Summary
The WebRTC data channel between the Ensemble mobile app and Music Assistant server can fail in several ways:
- One-way failure: Data channel appears "open" but only sends work, receives stop
- Silent disconnect: No error events fire, channel just stops working
- ICE failure: ICE connection fails, properly detected but causes disconnect
- Signaling disconnect: Signaling WebSocket closes, causing ICE/WebRTC to fail
Key Files
| File | Purpose |
|---|---|
lib/services/remote/webrtc_connection.dart |
WebRTC peer connection, data channel management |
lib/services/remote/remote_bridge.dart |
WebSocket bridge, health monitoring, reconnection logic |
lib/services/remote/signaling_client.dart |
Signaling server WebSocket connection |
Implemented Fixes (Chronological)
Fix 1: Data Channel Keepalive (Original)
Problem: Data channel could silently die without any error events firing.
Solution: Added periodic keepalive ping/pong on the data channel itself.
Code (webrtc_connection.dart):
_keepaliveTimer: Runs every 10 seconds_checkKeepalive(): Checks time since last data received_sendKeepalivePing(): Sends JSON ping message_recordDataReceived(): Called on every message receipt
Constants:
static const _keepaliveInterval = Duration(seconds: 10);
static const _keepaliveTimeout = Duration(seconds: 20); // Reduced from 30
static const _maxKeepaliveMisses = 2; // Reduced from 3
Fix 2: ICE Recovery
Problem: ICE connection could temporarily disconnect and might auto-recover.
Solution: Added ICE recovery timer before declaring failure.
Code (webrtc_connection.dart):
_startIceRecovery(): Starts 5-second timer when ICE disconnects_cancelIceRecovery(): Cancels if ICE recovers on its own_attemptIceRestart(): CallsrestartIce()if recovery timer expires
Fix 3: Flow Control (Buffer Overflow Prevention)
Problem: Fire-and-forget sends could overflow the 16MB SCTP buffer, causing silent channel death.
Solution: Added send-side flow control with buffering.
Code (webrtc_connection.dart):
static const _maxBufferedAmount = 1048576; // 1MB threshold
static const _bufferLowThreshold = 65536; // 64KB resume threshold
bool _sendPaused = false;
final _sendQueue = <String>[];
Modified sendRaw():
- Checks
bufferedAmountbefore sending - Queues messages if buffer > 1MB
onBufferedAmountLowcallback drains queue
Fix 4: Health Check Stale Detection
Problem: Health check could see "30s since last data" but not trigger reconnection.
Solution: Reduced stale detection threshold from 45s to 30s.
Code (remote_bridge.dart):
if (_state == RemoteBridgeState.connected && timeSinceLastData > 30 && !_isReconnecting) {
// Force reconnection
}
Fix 5: Reduced Keepalive Timeouts
Problem: 30s timeout × 3 misses = 90s minimum before detection.
Solution: Reduced to 20s timeout × 2 misses = ~40s detection.
Fix 6: Recv Stagnation Detection
Problem: One-way failure where recv stops but sent continues not detected quickly enough.
Solution: Track recv count across health checks. If recv stuck while sent increases for 2 consecutive checks (20s), force reconnection.
Code (remote_bridge.dart):
// Track across health checks
int _lastHealthCheckRecvCount = 0;
int _lastHealthCheckSentCount = 0;
int _recvStagnationCount = 0;
// In health check:
if (recvStuck && sentIncreased) {
_recvStagnationCount++;
if (_recvStagnationCount >= 2) {
// Force reconnection - channel is one-way
}
}
Detection time: ~20 seconds (2 × 10s health check interval)
Log Patterns to Watch For
Healthy Connection
Health check - API sent:93 recv:88 ... lastRecv:0s ago
sentandrecvboth increasinglastRecvlow (< 5s typically)
One-Way Failure (CRITICAL)
Health check - API sent:206 recv:163 ... lastRecv:30s ago
Health check - API sent:218 recv:163 ... lastRecv:40s ago
sentincreases,recvSTUCKlastRecvkeeps increasing- Data channel still shows "open"
Signaling Disconnect
WebRTC: Signaling disconnected
... (6-16 seconds later) ...
WebRTC: ICE connection state: RTCIceConnectionStateDisconnected
WebRTC: ICE connection state: RTCIceConnectionStateFailed
Network Failure (DNS)
Signaling: WebSocket closed
... (reconnection attempt) ...
Signaling: Failed to connect: SocketException: Failed host lookup: 'signaling.music-assistant.io'
This indicates the phone lost network connectivity entirely.
Reconnection Storm (Signaling Instability)
Signaling: Connecting to wss://signaling.music-assistant.io/ws
Signaling: Sending connect-request
Signaling: WebSocket closed ← Closes within 100-500ms
WebRTC: Signaling disconnected
RemoteBridge: Scheduling reconnect attempt X/10 in Ys
Rapid reconnection loop - signaling never stays connected long enough.
Flow Control Engaged
WebRTC: ⚠️ Buffer full (1234567 bytes) or paused - queueing message
WebRTC: Buffer drained to 12345 bytes - draining 5 queued messages
WebRTC: ✅ Send queue drained (5 messages) - resuming normal sends
Keepalive Detection
WebRTC: ⚠️ Keepalive timeout! No data for 22s (missed: 1/2)
WebRTC: ❌ Data channel appears dead - triggering reconnection
Known Issues / Limitations
1. flutter_webrtc Limitations
RTCDataChannel.statemay show "open" when actually dead (Issue #843)bufferedAmountmay not be accurate on all platformsonBufferedAmountLowcallback support varies
2. SCTP Receive Window
- If server sends faster than client processes, receive window fills
- This causes server-side to stop sending, client sees no data
- No way to monitor receive-side buffer from client
3. TURN Server Issues
- aiortc (server-side) has known issues with TURN + DataChannel (#1156)
- Data channel may fail silently when using TURN relay
4. Network Transitions
- WiFi → Cellular or vice versa can cause ICE to fail
- App backgrounding on mobile can close WebSocket/WebRTC
Root Cause Categories
Category A: Network Issues (Not Code Bugs)
These are environmental issues, not bugs in our code:
- DNS failure:
Failed host lookup- Phone lost internet - WiFi/Cellular switch: Network changed, connections dropped
- Server unreachable: Signaling server down or blocked
- Firewall/NAT issues: TURN relay needed but unavailable
Evidence: Signaling WebSocket closes, DNS lookups fail, reconnection attempts fail repeatedly.
Category B: WebRTC/SCTP Issues (Partial Code Control)
These can be mitigated but not fully prevented:
- One-way channel death: Channel appears open but data stops flowing
- Buffer overflow: Too much data sent, SCTP buffer exceeded
- ICE failure: Network path lost, no recovery possible
Evidence: Data channel shows "open" but recv stuck, lastRecv increasing.
Category C: Code Bugs (Fixable)
Issues in our implementation:
- Detection too slow: Timeouts too long, misses too many
- Recovery not triggered: State checks wrong, reconnection not started
- Resource leaks: Timers not cancelled, connections not cleaned up
Evidence: Logs show problem but no action taken, or wrong action taken.
Debugging Checklist
When investigating a disconnect:
-
Find the last successful response:
grep "WebRTC API → WS" logs.txt | head -5 -
Check health check progression:
grep "Health check" logs.txtLook for
recvgetting stuck whilesentincreases. -
Check for signaling issues:
grep -i "signaling" logs.txt -
Check ICE state changes:
grep "ICE connection state" logs.txt -
Check for keepalive detection:
grep -i "keepalive" logs.txt -
Check buffer status:
grep -E "(buffer|queue|paused)" logs.txt
Potential Future Improvements
1. Server-Side Backpressure
MA protocol doesn't have explicit backpressure. Could add:
- Client-side request throttling
- Application-level ACKs for critical messages
2. More Aggressive Detection
- Track recv count across health checks, detect stagnation
- Shorter keepalive interval (5s instead of 10s)
3. Proactive Health Signals
- Monitor
bufferedAmounttrend (increasing = problem) - Track round-trip latency of keepalive pings
4. Connection Quality Metrics
- Expose packet loss, jitter from WebRTC stats
- Use to predict imminent failure
Test Scenarios
| Scenario | How to Test | Expected Behavior |
|---|---|---|
| Normal usage | Browse library, play music | No warnings |
| Heavy sync | Pull-to-refresh library | Buffer may queue, should drain |
| Long session | Leave connected 2+ hours | Should stay connected |
| Rapid navigation | Quick tab switching | Brief queue usage, recovers |
| Network change | WiFi → Cellular | Should detect and reconnect |
| App background | Background for 5+ min | May disconnect, should reconnect |
Observed Failure Patterns
Pattern 1: One-Way Death After Library Sync
Observed: 2026-01-23 (multiple times)
17:13:06.175 - Last response (playlists sync)
17:13:06.264 - Local batch save
-- 5 seconds silence --
17:13:11.005 - Send players/all → NO RESPONSE
17:13:13.914 - ICE state: completed (looks healthy!)
17:13:28 - Keepalive detects timeout
17:13:38 - Reconnection triggered
Characteristics:
- Often happens after library sync (large batch responses)
- ICE shows "completed" even though data channel is dead
- Signaling ping/pong continues working
- Only data channel receive is affected
Hypothesis:
- Server-side SCTP buffer exhausted from sending large responses?
- Client receive window full, server blocks?
- flutter_webrtc onMessage callback stops firing?
Pattern 2: Reconnection Storm / Signaling Instability
Observed: 2026-01-23 17:30-17:31
17:31:05.093 - Signaling: Connecting
17:31:05.216 - Signaling: Sending connect-request
17:31:05.218 - Signaling: WebSocket closed (123ms later!)
17:31:05.218 - WebRTC: Signaling disconnected
Characteristics:
- Signaling WebSocket closes almost immediately after connect
- Multiple rapid reconnection attempts (storm)
- Connection never stable long enough for data flow
- Often happens after network instability
- Artist detail, albums etc don't load because connection keeps failing
Possible Causes:
- Network instability (WiFi/cellular issues)
- Signaling server rate limiting
- DNS resolution failures
- Server rejecting rapid reconnection attempts
Side Effects:
- UI shows errors like "Not connected to Music Assistant server"
- Artist/album detail screens fail to load
- Players list empty or stale
Pattern 3: API Channel Dies While Sendspin Lives
Observed: 2026-01-23 17:53
17:53:35.755 - Send album_tracks request → NO RESPONSE
17:53:40.596 - Health check: recv stuck at 197, sent increased
17:53:55.204 - Sendspin channel RECEIVES server/time response!
17:54:02.790 - Keepalive detects dead API channel
Characteristics:
- API data channel (ma-api) stops receiving
- Sendspin data channel continues working normally
- Same WebRTC peer connection, different data channels
- This proves it's NOT a network issue - Sendspin traffic flows fine
Hypothesis:
- flutter_webrtc bug where
onMessagecallback stops firing for one channel - Possible memory/GC issue with the callback
- Server-side might be sending to wrong channel?
Key Evidence:
- Recv count stays at 197 for API
- Sendspin recv count increases (7→8)
- Both channels use same peer connection
Version History
| Date | Build | Changes |
|---|---|---|
| 2026-01-23 | ~v49 | Initial flow control implementation |
| 2026-01-23 | ~v49 | Reduced keepalive timeout 30→20s, misses 3→2 |
| 2026-01-23 | ~v49 | Reduced health check stale threshold 45→30s |
| 2026-01-23 | ~v49 | Fixed excessive "queue drained" logging |
| 2026-01-23 | ~v49 | Created WEBRTC_DISCONNECT_FIXES.md documentation |
| 2026-01-23 | ~v49 | Added recv stagnation detection (20s detection) |
| 2026-01-23 | ~v49 | Documented Pattern 2: Reconnection storm / signaling instability |
| 2026-01-23 | ~v49 | Fixed race condition in _scheduleReconnect (multiple simultaneous calls) |
| 2026-01-23 | ~v49 | Added WebSocket close code/reason logging to signaling |
| 2026-01-23 | ~v49 | CRITICAL FIX: _reconnectWebRTC() was returning immediately due to _isReconnecting already being true |
| 2026-01-23 | ~v49 | Fixed old WebRTC callbacks firing during reconnection cleanup (clear callbacks before disconnect) |
| 2026-01-23 | ~v49 | Fixed onStateChanged handler calling _handleWebRTCDisconnection during active reconnection |
References
- flutter_webrtc Issue #843: Data channel state unreliable
- aiortc Issue #1156: TURN + DataChannel bugs
- WebRTC SCTP: Max buffer is 16MB, recommended threshold 1MB
- Music Assistant remote protocol: No explicit flow control