diff options
| author | Christophe Besson <cbesson@gmail.com> | 2026-08-26 15:09:20 +0200 |
|---|---|---|
| committer | Christophe Besson <cbesson@gmail.com> | 2026-08-26 15:09:20 +0200 |
| commit | db7f81fd08742847f3ebea061c75530b7b31b934 (patch) | |
| tree | 14b308686860eea0a9c12c47c39f8a68a1bba376 /packages/meshbay-node | |
| parent | 7126fd3c265ba75d77b449bfd0f83f5f3e584b74 (diff) | |
| parent | 59d9f50bf41b2b38b7c95f8da52b698dff923c2d (diff) | |
| download | meshbay-db7f81fd08742847f3ebea061c75530b7b31b934.tar.gz | |
Merge branch 'debug/webrtc-lock-resume'
WebRTC transport dies silently after an extended mobile screen lock
(confirmed live via client-side trace + node logs): ICE goes
disconnected -> failed within ~10s of each other on both ends, but the
DataChannel's readyState stays "open" throughout, so nothing failed fast —
every request just sat out its own timeout, matching the reported symptom
(poster spinners, blocked chat, dead new streams, stuck music).
- Automatic reconnect on WebRTC "failed": capped exponential backoff,
redoes the full signaling handshake, wakes immediately on
visibilitychange instead of waiting out a throttled backoff timer.
- Fixed two real bugs the reconnect work exposed: the signaling POST to
the hub kept using the token captured at construction, never the fresh
one fetched per reconnect attempt (401 loop, no possible recovery); and
connect() re-armed a diagnostic listener/interval on every attempt
without disposing of the previous one.
- pipelinedDownload retries a lost chunk instead of aborting the whole
transfer — covers Files downloads, video poster/thumbnail fetches, and
music-player.js's blob-based track download.
- music-player.js: don't throw "Transport not connected" while a
reconnect is already landing (waitForReconnect); prefetch depth now
adapts to network type (5 tracks ahead on Wi-Fi, 3 on cellular or
unrecognized — Firefox/Safari included, where the detection API is
simply absent).
- video-player.js: onReconnected reissues the existing seek-to-current-time
path, so a mid-stream reconnect looks like an ordinary seek rather than
a dead player; holds a Screen Wake Lock unconditionally while open.
- New opt-in (off by default) user preference: keep the screen on during
audio playback, for whoever wants to trade battery for sidestepping the
screen-lock gap entirely — off by default because the ordinary
expectation (matching Spotify/Deezer) is that the phone locks on its
own while listening.
- hub: /app and / now serve Cache-Control: no-store — the SPA shell had no
cache header at all, so a browser that cached it heuristically could
keep re-serving an old build (old ASSET_V, old JS) through any number of
reloads or pull-to-refreshes.
Verified against real production use across many rounds (demo groups,
actual mobile screen-lock testing) rather than synthetic reproduction
alone. 430 hub tests + 650 node tests passing throughout.
Diffstat (limited to 'packages/meshbay-node')
| -rw-r--r-- | packages/meshbay-node/src/meshbay_node/transport/webrtc_server.py | 39 |
1 files changed, 39 insertions, 0 deletions
diff --git a/packages/meshbay-node/src/meshbay_node/transport/webrtc_server.py b/packages/meshbay-node/src/meshbay_node/transport/webrtc_server.py index 6709fbc..724527b 100644 --- a/packages/meshbay-node/src/meshbay_node/transport/webrtc_server.py +++ b/packages/meshbay-node/src/meshbay_node/transport/webrtc_server.py @@ -199,6 +199,20 @@ def _pack(obj: dict) -> bytes: return struct.pack(">I", len(data)) + data +# Opt-in, off by default: a per-session heartbeat log (message count, time +# since the last message, ICE state) and ICE-state-change logging, on top of +# the connectionstatechange logging that already runs unconditionally. Added +# while chasing a report of the browser side going unresponsive after a +# mobile screen lock; --log-level DEBUG was not the right knob for this, +# since it is already used for the per-message request/response tracing +# every group index lookup produces, and turning that on for days of normal +# operation just to catch one intermittent session is not viable. Set +# MESHBAY_WEBRTC_TRACE=1 in the node's environment for the duration of a +# debugging session. +_WEBRTC_TRACE = os.environ.get("MESHBAY_WEBRTC_TRACE") == "1" +_WEBRTC_TRACE_INTERVAL_S = 30.0 + + class _DataChannelBuffer: """ Accumulate DataChannel messages and extract length-prefixed msgpack. @@ -295,6 +309,9 @@ class WebRTCPeerSession: self._nonce_client: bytes = b"" self._admin_ops: dict[str, dict] = {} # op_id → pending admin operation self._uploads: dict[str, dict] = {} # filename → {next_index, bytes} + # Diagnostics only (_WEBRTC_TRACE): when the last DataChannel message + # arrived, so the heartbeat can report silence duration. + self._last_msg_at: float = 0.0 def _setup_channel(self, channel: RTCDataChannel) -> None: self._channel = channel @@ -305,6 +322,7 @@ class WebRTCPeerSession: if isinstance(message, str): message = message.encode() self._msg_count += 1 + self._last_msg_at = time.monotonic() if self._msg_count <= 3: log.info("WebRTC data received: %d bytes, msg #%d (peer=%s)", len(message), self._msg_count, self._peer_id) @@ -312,6 +330,22 @@ class WebRTCPeerSession: for msg in self._buffer.messages(): self._handle_message(msg) + if _WEBRTC_TRACE: + self._spawn(self._trace_heartbeat()) + + async def _trace_heartbeat(self) -> None: + """Diagnostics only (_WEBRTC_TRACE): periodic proof-of-life for this + session, so a gap in these lines pinpoints when the node stopped + hearing from a peer that (from its own side) may still look connected.""" + while True: + await asyncio.sleep(_WEBRTC_TRACE_INTERVAL_S) + silence = time.monotonic() - self._last_msg_at if self._last_msg_at else -1 + log.info( + "WebRTC heartbeat peer=%s msgs=%d silence=%.0fs pc=%s ice=%s", + self._peer_id, self._msg_count, silence, + self._pc.connectionState, self._pc.iceConnectionState, + ) + def _handle_message(self, msg: dict) -> None: mtype = msg.get("type") log.debug("WebRTC recv: %s", mtype) @@ -4353,6 +4387,11 @@ class WebRTCTransport: log.info("WebRTC DataChannel opened: %s (peer=%s)", channel.label, peer_id) session._setup_channel(channel) + if _WEBRTC_TRACE: + @pc.on("iceconnectionstatechange") + def on_ice_state_change(): + log.info("WebRTC ICE state: %s (peer=%s)", pc.iceConnectionState, peer_id) + @pc.on("connectionstatechange") async def on_state_change(): state = pc.connectionState |