diff --git a/src/views/tinode-web.jsx b/src/views/tinode-web.jsx index 1651048f..3f2d917b 100644 --- a/src/views/tinode-web.jsx +++ b/src/views/tinode-web.jsx @@ -2511,48 +2511,56 @@ class TinodeWeb extends React.Component { } } - handleCallRinging(callTopic, callSeq) { + // All of the call-signaling senders below (ringing/hang-up/offer/ice-candidate/answer) go through + // this one helper rather than calling topic.videoCall() directly. handleCallHangup in particular is + // called from several unmount/teardown paths outside the normal call flow (messages-view.jsx's + // componentWillUnmount and its topic-switch handler in componentSetup both call it via the + // onCallHangup prop, on top of the ring-timeout path and CallInvite's own cleanup here) -- including + // ones that fire *after* a logout has already torn down the connection: handleLogout() calls + // this.tinode.disconnect() and only afterwards resets state to the blank/logged-out screen, and that + // state reset is what unmounts messages-view.jsx and trips its componentWillUnmount -> handleCallHangup + // call. Real repro: click a call button (real or scripted handleStartVideoCall/handleStartAudioCall), + // then immediately log out -- topic.videoCall() ends up calling the vendored SDK's + // Connection.sendText() on an already-closed socket, which throws synchronously ("Websocket is not + // connected"). Thrown from componentWillUnmount, React has no error boundary for it and unmounts the + // entire app to a blank white page. None of these are anything the peer can still receive over a dead + // connection anyway (and for hang-up specifically, the server's own establishment-timeout already + // cleans up an abandoned call-in-progress independently), so skip sending entirely once disconnected + // instead of letting it throw -- the other four signals are currently unreachable in practice (no real + // call can get established while "call invite always 400s" -- see handleCallInvite -- is unfixed) but + // share the identical topic.videoCall() shape and would hit the exact same crash the moment that's + // fixed and real in-progress calls (offer/ice-candidate/answer exchange, or a mid-call logout) become + // possible. + sendCallSignal(callTopic, evt, callSeq, payload) { const topic = this.tinode.getTopic(callTopic); - if (!topic) { + if (!topic || !this.tinode.isConnected()) { return; } + try { + topic.videoCall(evt, callSeq, payload); + } catch (err) { + console.warn(`Failed to send call signal '${evt}'`, err); + } + } - topic.videoCall('ringing', callSeq); + handleCallRinging(callTopic, callSeq) { + this.sendCallSignal(callTopic, 'ringing', callSeq); } handleCallHangup(callTopic, callSeq) { - const topic = this.tinode.getTopic(callTopic); - if (!topic) { - return; - } - - topic.videoCall('hang-up', callSeq); + this.sendCallSignal(callTopic, 'hang-up', callSeq); } handleCallSendOffer(callTopic, callSeq, sdp) { - const topic = this.tinode.getTopic(callTopic); - if (!topic) { - return; - } - - topic.videoCall('offer', callSeq, sdp); + this.sendCallSignal(callTopic, 'offer', callSeq, sdp); } handleCallIceCandidate(callTopic, callSeq, candidate) { - const topic = this.tinode.getTopic(callTopic); - if (!topic) { - return; - } - - topic.videoCall('ice-candidate', callSeq, candidate); + this.sendCallSignal(callTopic, 'ice-candidate', callSeq, candidate); } - handleCallSendAnswer(callTopic, callSeq, sdp) { - const topic = this.tinode.getTopic(callTopic); - if (!topic) { - return; - } - topic.videoCall('answer', callSeq, sdp); + handleCallSendAnswer(callTopic, callSeq, sdp) { + this.sendCallSignal(callTopic, 'answer', callSeq, sdp); } handleCallClose() {