connect method
- CallService calls
Starts the CallService.heartbeat subscription indicating that this OngoingCall is ready to connect to a media server.
No-op if already connected.
Implementation
void connect(CallService calls) {
Log.debug('connect($calls)', '$runtimeType');
_participated = true;
if (connected || callChatItemId == null || deviceId == null) {
return;
}
CallMemberId id = CallMemberId(_me.userId, deviceId);
members[_me]?.id = id;
members.move(_me, id);
_me = id;
connected = true;
_heartbeat?.cancel();
_heartbeat = calls
.heartbeat(callChatItemId!, deviceId!)
.listen(
(e) async {
switch (e.kind) {
case ChatCallEventsKind.initialized:
Log.debug('heartbeat(): ${e.kind}', '$runtimeType');
break;
case ChatCallEventsKind.chatCall:
Log.debug('heartbeat(): ${e.kind}', '$runtimeType');
final node = e as ChatCallEventsChatCall;
_handToggles.clear();
if (node.call.finishReason != null) {
// Call is already ended, so remove it.
calls.remove(chatId.value);
calls.removeCredentials(node.call.chatId, node.call.id);
} else {
call.value = node.call;
call.refresh();
if (state.value == OngoingCallState.local) {
state.value =
node.call.conversationStartedAt == null
? OngoingCallState.pending
: OngoingCallState.joining;
}
final ChatMembersDialed? dialed = node.call.dialed;
if (dialed is ChatMembersDialedConcrete) {
// Remove the members, who are not connected and still
// redialing, that are missing from the [dialed].
members.removeWhere(
(_, v) =>
v.isConnected.isFalse &&
v.isDialing.isTrue &&
dialed.members.none(
(e) => e.user.id == v.id.userId,
) &&
node.call.members.none(
(e) => e.user.id == v.id.userId,
),
);
for (final ChatMember m in dialed.members) {
_addDialing(m.user.id);
}
} else if (dialed == null) {
// Remove the members, who are not connected and still
// redialing, since no one is [dialed].
members.removeWhere(
(_, v) =>
v.isConnected.isFalse &&
v.isDialing.isTrue &&
node.call.members.none(
(e) => e.user.id == v.id.userId,
),
);
}
// Subscribes to the [RxChat.members] changes, adding the dialed
// users.
//
// Additionally handles the case, when [dialed] are
// [ChatMembersDialedAll], since we need to have a [RxChat] to
// retrieve the whole list of users this way.
Future<void> redialAndResubscribe(RxChat? v) async {
if (!connected || v == null) {
// [OngoingCall] might have been disposed or disconnected
// while this [Future] was executing.
return;
}
// Add the redialed members of the call to the [members].
if (dialed is ChatMembersDialedAll &&
v.chat.value.membersCount <= v.members.perPage) {
if (v.members.length < v.chat.value.membersCount) {
await v.members.around();
}
// Check if [ChatCall.dialed] is still [ChatMembersDialedAll].
if (call.value?.dialed is ChatMembersDialedAll) {
final Iterable<RxUser> dialings = v.members.values
.map((e) => e.user)
.where(
(e) =>
e.id != me.id.userId &&
dialed.answeredMembers.none(
(a) => a.user.id == e.id,
),
);
// Remove the members, who are not connected and still
// redialing, that are missing from the [dialings].
members.removeWhere(
(_, v) =>
v.isConnected.isFalse &&
v.isDialing.isTrue &&
dialings.none((e) => e.id == v.id.userId) &&
node.call.members.none(
(e) => e.user.id == v.id.userId,
),
);
for (final RxUser e in dialings) {
_addDialing(e.id);
}
}
}
}
// Retrieve the [RxChat] to subscribe to its [RxChat.members]
// changes, so that added users are displayed as dialed right
// away.
final FutureOr<RxChat?> chatOrFuture = calls.getChat(
chatId.value,
);
if (chatOrFuture is RxChat?) {
redialAndResubscribe(chatOrFuture);
} else {
chatOrFuture.then(redialAndResubscribe);
}
members[_me]?.isHandRaised.value =
node.call.members
.firstWhereOrNull((e) => e.user.id == _me.userId)
?.handRaised ??
false;
}
break;
case ChatCallEventsKind.event:
final versioned = (e as ChatCallEventsEvent).event;
Log.debug(
'heartbeat(ChatCallEventsEvent): ${versioned.events.map((e) => e.kind)}',
'$runtimeType($id)',
);
for (final ChatCallEvent event in versioned.events) {
switch (event.kind) {
case ChatCallEventKind.roomReady:
final node = event as EventChatCallRoomReady;
if (!_background) {
await _joinRoom(node.joinLink);
}
state.value = OngoingCallState.active;
break;
case ChatCallEventKind.finished:
final node = event as EventChatCallFinished;
if (node.chatId == chatId.value) {
calls.removeCredentials(node.call.chatId, node.call.id);
calls.remove(chatId.value);
}
break;
case ChatCallEventKind.memberLeft:
final node = event as EventChatCallMemberLeft;
if (me.id.userId == node.user.id &&
me.id.deviceId == node.deviceId) {
calls.remove(chatId.value);
}
final CallMemberId id = CallMemberId(
node.user.id,
node.deviceId,
);
if (members[id]?.isConnected.value == false) {
members.remove(id)?.dispose();
}
if (members.keys.none((e) => e.userId == node.user.id)) {
call.value?.members.removeWhere(
(e) => e.user.id == node.user.id,
);
}
break;
case ChatCallEventKind.memberJoined:
final node = event as EventChatCallMemberJoined;
final CallMemberId redialedId = CallMemberId(
node.user.id,
null,
);
final CallMemberId id = CallMemberId(
node.user.id,
node.deviceId,
);
final CallMember? redialed = members[redialedId];
if (redialed != null) {
redialed.id = id;
redialed.isDialing.value = false;
redialed.joinedAt.value = node.at;
members.move(redialedId, id);
}
final CallMember? member = members[id];
if (member == null) {
members[id] = CallMember(
id,
null,
joinedAt: node.at,
isHandRaised:
call.value?.members
.firstWhereOrNull(
(e) => e.user.id == id.userId,
)
?.handRaised ??
false,
isConnected: false,
);
} else {
member.joinedAt.value = node.at;
}
break;
case ChatCallEventKind.handLowered:
final node = event as EventChatCallHandLowered;
// Ignore the event, if it's our hand and is already lowered.
if (node.user.id == _me.userId &&
_handToggles.firstOrNull == false) {
_handToggles.removeAt(0);
} else {
for (MapEntry<CallMemberId, CallMember> m in members
.entries
.where((e) => e.key.userId == node.user.id)) {
m.value.isHandRaised.value = false;
}
}
for (ChatCallMember m in (call.value?.members ?? [])
.where((e) => e.user.id == node.user.id)) {
m.handRaised = false;
}
break;
case ChatCallEventKind.handRaised:
final node = event as EventChatCallHandRaised;
// Ignore the event, if it's our hand and is already raised.
if (node.user.id == _me.userId &&
_handToggles.firstOrNull == true) {
_handToggles.removeAt(0);
} else {
for (MapEntry<CallMemberId, CallMember> m in members
.entries
.where((e) => e.key.userId == node.user.id)) {
m.value.isHandRaised.value = true;
}
}
for (ChatCallMember m in (call.value?.members ?? [])
.where((e) => e.user.id == node.user.id)) {
m.handRaised = true;
}
break;
case ChatCallEventKind.declined:
final node = event as EventChatCallDeclined;
final CallMemberId id = CallMemberId(node.user.id, null);
if (members[id]?.isConnected.value == false) {
members.remove(id)?.dispose();
}
break;
case ChatCallEventKind.callMoved:
final node = event as EventChatCallMoved;
chatId.value = node.newChatId;
call.value = node.newCall;
connected = false;
connect(calls);
calls.moveCall(
chatId: node.chatId,
newChatId: node.newChatId,
callId: node.callId,
newCallId: node.newCallId,
);
break;
case ChatCallEventKind.redialed:
final node = event as EventChatCallMemberRedialed;
_addDialing(node.user.id);
break;
case ChatCallEventKind.answerTimeoutPassed:
final node = event as EventChatCallAnswerTimeoutPassed;
if (node.user?.id != null) {
final CallMemberId id = CallMemberId(
node.user!.id,
null,
);
if (members[id]?.isConnected.value == false) {
members.remove(id)?.dispose();
}
} else {
call.value?.dialed = null;
members.removeWhere((k, v) {
if (k.deviceId == null && v.isConnected.isFalse) {
v.dispose();
return true;
}
return false;
});
}
break;
case ChatCallEventKind.conversationStarted:
// TODO: Implement [EventChatCallConversationStarted].
break;
case ChatCallEventKind.undialed:
final node = event as EventChatCallMemberUndialed;
final CallMemberId id = CallMemberId(node.user.id, null);
if (members[id]?.isConnected.value == false) {
members.remove(id)?.dispose();
}
break;
}
}
break;
}
},
onError: (e) {
if (e is! ResubscriptionRequiredException) {
throw e;
}
},
);
}