nextcloud / nextcloud/talk-ios
Call ends when a participants update does not contain the own session
- Dominant language
- Swift
- Stars
- 230
- Forks
- 121
- Avg merge
- 1d 6h
- Merged PRs (30d)
- 38
Description
## Steps to reproduce
1. Run Talk against an external signaling server that can send a `participants` `update` whose `users` array does not list every session currently in the room.
2. Join a call from the iOS app and stay in it.
3. Let such an update arrive.
The easiest way to get one today is a clustered nextcloud-spreed-signaling setup: when an internal client joins the room on a node that has not received the room's user list, that node publishes an update to the whole cluster containing only its own internal session. A recording server is the common way to reach it, so in practice the reproduction is "start a call recording while an iOS client is in the call". The server side of that is discussed in strukturag/nextcloud-spreed-signaling#1305; this report is only about what the app does with the message.
The message looks like this, with the own session absent:
```json
{"type":"event","event":{"target":"participants","type":"update",
"update":{"roomid":"...","users":[
{"sessionId":"","inCall":3,"internal":true,"lastPing":1789551046}]}}}
```
### Expected behaviour
A session that is not mentioned in a participants update keeps the state it was last given. This is the documented intent — the signaling maintainer put it as "if a session is not included in an event from the signaling server, the client will keep the information received earlier and not assume the session to be disconnected" ([comment](https://github.com/strukturag/nextcloud-spreed-signaling/issues/1305#issuecomment-5675974515)) — and it is what the web client does: `usersChanged` only writes into `usersInCallMapping` and never removes from it.
### Actual behaviour
The app hangs up. From the user's side the call simply ends and the chat says they left it.
## Where it happens
Read on `main` at b5d5071e9873c50cb225a5837eab4689f64bbeff.
The update is unwrapped and the `users` array handed to the delegate as-is:
https://github.com/nextcloud/talk-ios/blob/b5d5071e9873c50cb225a5837eab4689f64bbeff/NextcloudTalk/WebRTC/NCExternalSignalingController.swift#L714-L727
and the call controller passes it into `processUsersInRoom`:
https://github.com/nextcloud/talk-ios/blob/b5d5071e9873c50cb225a5837eab4689f64bbeff/NextcloudTalk/Calls/NCCallController.swift#L1458-L1472
That is the same function that handles the `usersInRoom` message from internal signaling, which really is a full authoritative list:
https://github.com/nextcloud/talk-ios/blob/b5d5071e9873c50cb225a5837eab4689f64bbeff/NextcloudTalk/Calls/NCCallController.swift#L1504-L1511
There are exactly these two callers, and nothing distinguishes them — `processUsersInRoom` treats every list it is given as complete:
```swift
let leftSessions = Set(oldSessions).subtracting(currentSessions)
...
for sessionId in leftSessions {
// Hang up call if user sessionId is no longer in the call
// Could be because a moderator "ended the call for everyone"
if sessionId == self.signalingSessionId {
self.delegate?.callControllerWants(toHangUpCall: self)
return
```
https://github.com/nextcloud/talk-ios/blob/b5d5071e9873c50cb225a5837eab4689f64bbeff/NextcloudTalk/Calls/NCCallController.swift#L1908-L1917
and the delegate does end the call:
https://github.com/nextcloud/talk-ios/blob/b5d5071e9873c50cb225a5837eab4689f64bbeff/NextcloudTalk/Calls/CallViewController.swift#L1386-L1390
The guards earlier in the function do not help here. `isLeavingCall` is false, and the `previousUserInCall == 0` branch is skipped because the user was in the call before the update — `getInCallSessions` only assigns `self.userInCall` when it sees the own session, and the own session is exactly what is missing. The `all` / `incall` special case above the call site does not apply either, since these updates carry no `all` key.
For this particular update it is worse than a partial list would normally be, because `getInCallSessions` skips entries carrying `internal: true`:
https://github.com/nextcloud/talk-ios/blob/b5d5071e9873c50cb225a5837eab4689f64bbeff/NextcloudTalk/Calls/NCCallController.swift#L1974-L1977
The update contains one entry and that entry is internal, so `currentSessions` comes out empty and every known session, own included, lands in `leftSessions`.
## Why it is worth fixing beyond the clustered case
strukturag/nextcloud-spreed-signaling#1175 plans to send only the changed sessions to users, including in non-clustered setups. After that, an update that does not mention the reader's own session becomes the normal case rather than a clustering artefact, and every one of them would end the call on iOS.
## Android had the same bug
nextcloud/talk-android#5452 is the same symptom from a user's side — call dropped when a participant joins, custom signaling server, "You left the call" in the chat. The fix in nextcloud/talk-android@481ed682 separates the two cases: `onUsersInRoom` still treats absent participants as gone, `onParticipantsUpdate` moves them to unchanged instead. Comparing the blob of `CallParticipantList.java` across tags, that first shipped in 24.0.0; 23.0.1 and earlier still carry the old file.
The same split would apply here: keep `processUsersInRoom` as it is for the internal-signaling caller, and give the external-signaling caller a path that merges the entries it receives into the known set rather than replacing it, so that only sessions explicitly reported as no longer in the call are moved to `leftSessions`.
I am happy to open a pull request if you would like it in that shape, or another one if you prefer.
## Device information
**Device:** not specific to one device — this is in the handling of a signaling message. In the deployment where we hit it the sessions that were dropped were the phones while browsers stayed in the call, but I do not have the individual handset and app versions recorded and would rather not guess at them. The code above is current `main`.
**iOS version:** as above
**Talk version:** as above
## Server information
**Nextcloud version:** 34.0.3
**Talk version:** 24.0.4
**Custom Signaling server configured:** yes, nextcloud-spreed-signaling v2.1.1, four nodes in one cluster behind a load balancer, one recording server per node
**Custom TURN server configured:** yes, though not relevant here — no media is involved, the call ends while it is still connected
**Custom STUN server configured:** no
### Server log (data/nextcloud.log)
Nothing shows up there. The server and the signaling server both consider the client to have left the call voluntarily, because that is what the app told them.
Contributor guide
Research direction
Start with processUsersInRoom in NextcloudTalk/Calls/NCCallController.swift, then trace its callers in NCCallController.swift and NCExternalSignalingController.swift. Compare the external participants update path with the internal usersInRoom path and the Android fix cited in the issue; done means an update omitting the local session no longer ends the call while genuinely absent sessions remain handled correctly.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- swift
- Domain
- audio-video-rtc, mobile-dev
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 78/100