Skip to content

chore: added members to call state #940

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Apr 29, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 19 additions & 8 deletions packages/stream_video/lib/src/call/call.dart
Original file line number Diff line number Diff line change
Expand Up @@ -458,14 +458,6 @@ class Call {
return _stateManager.coordinatorCallBroadcastingStopped(event);
case StreamCallBroadcastingFailedEvent _:
return _stateManager.coordinatorCallBroadcastingFailed(event);
case StreamCallRingingEvent _:
return _stateManager.callMetadataChanged(event.metadata);
case StreamCallMissedEvent _:
return _stateManager.callMetadataChanged(event.metadata);
case StreamCallSessionEndedEvent _:
return _stateManager.callMetadataChanged(event.metadata);
case StreamCallSessionStartedEvent _:
return _stateManager.callMetadataChanged(event.metadata);
case StreamCallUpdatedEvent _:
return _stateManager.callMetadataChanged(
event.metadata,
Expand Down Expand Up @@ -493,6 +485,25 @@ class Call {
event.participantsCountByRole.values.fold(0, (a, b) => a + b),
anonymousCount: event.anonymousParticipantCount,
);
case StreamCallMemberAddedEvent _:
return _stateManager.coordinatorCallMemberAdded(event);
case StreamCallMemberRemovedEvent _:
return _stateManager.coordinatorCallMemberRemoved(event);
case StreamCallMemberUpdatedEvent _:
return _stateManager.coordinatorCallMemberUpdated(event);
case StreamCallUserBlockedEvent _:
return _stateManager.coordinatorCallUserBlocked(event);
case StreamCallUserUnblockedEvent _:
return _stateManager.coordinatorCallUserUnblocked(event);
case StreamCallRingingEvent _:
return _stateManager.callMetadataChanged(event.metadata);
case StreamCallMissedEvent _:
return _stateManager.callMetadataChanged(event.metadata);
case StreamCallSessionEndedEvent _:
return _stateManager.callMetadataChanged(event.metadata);
case StreamCallSessionStartedEvent _:
return _stateManager.callMetadataChanged(event.metadata);

default:
break;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,14 @@ import 'package:state_notifier/state_notifier.dart';

import '../../../call_state.dart';
import '../../../logger/impl/tagged_logger.dart';
import '../../../models/call_member_state.dart';
import '../../../models/call_metadata.dart';
import '../../../models/call_participant_state.dart';
import '../../../models/call_reaction.dart';
import '../../../models/call_status.dart';
import '../../../models/disconnect_reason.dart';
import '../../call_events.dart';
import '../../call_reject_reason.dart';

final _logger = taggedLogger(tag: 'SV:CoordNotifier');

Expand All @@ -35,10 +37,11 @@ mixin StateCoordinatorMixin on StateNotifier<CallState> {
return;
}

final participant = state.callParticipants.firstWhereOrNull((participant) {
return participant.userId == event.acceptedByUserId;
final member = state.callMembers.firstWhereOrNull((member) {
return member.userId == event.acceptedByUserId;
});
if (participant == null) {

if (member == null) {
_logger.w(
() =>
'[coordinatorUpdateCallAccepted] rejected (accepted by non-Member)',
Expand All @@ -47,13 +50,20 @@ mixin StateCoordinatorMixin on StateNotifier<CallState> {
return;
}

state = state
.copyFromMetadata(
event.metadata,
)
.copyWith(
status: CallStatus.outgoing(acceptedByCallee: true),
final members = state.callMembers.map((m) {
if (m.userId == event.acceptedByUserId) {
return m.copyWith(
callAcceptedAt: event.createdAt,
);
} else {
return m;
}
}).toList();

state = state.copyWith(
status: CallStatus.outgoing(acceptedByCallee: true),
callMembers: members,
);
}

void coordinatorCallRejected(
Expand All @@ -69,44 +79,61 @@ mixin StateCoordinatorMixin on StateNotifier<CallState> {
return;
}

final participantIndex = state.callParticipants.indexWhere((participant) {
return participant.userId == event.rejectedByUserId;
});
final rejectedBy = event.metadata.session.rejectedBy;

if (participantIndex == -1) {
_logger.w(
() => '[coordinatorCallRejected] rejected '
'(by unknown user): ${event.rejectedByUserId}',
);
return;
}
final members = state.callMembers.map((m) {
if (m.userId == event.rejectedByUserId) {
return m.copyWith(
callRejectedAt: event.createdAt,
);
} else {
return m;
}
}).toList();

if (state.createdByMe) {
final everyoneElseRejected = state.callMembers
.where((m) => m.userId != state.currentUserId)
.every((m) => rejectedBy.keys.contains(m.userId));

final callParticipants = [...state.callParticipants];
final removed = callParticipants.removeAt(participantIndex);

if (removed.userId == state.currentUserId ||
callParticipants.hasSingle(state.currentUserId)) {
state = state
.copyFromMetadata(
event.metadata,
)
.copyWith(
status: CallStatus.disconnected(
DisconnectReason.rejected(
byUserId: removed.userId,
),
if (everyoneElseRejected) {
_logger.d(
() => '[coordinatorCallRejected] everyone rejected, disconnecting',
);
state = state.copyWith(
status: CallStatus.disconnected(
DisconnectReason.rejected(
byUserId: event.rejectedByUserId,
reason: CallRejectReason.custom('ring: everyone rejected'),
),
sessionId: '',
callParticipants: callParticipants,
);
}
state = state
.copyFromMetadata(
event.metadata,
)
.copyWith(
callParticipants: callParticipants,
),
sessionId: '',
callParticipants: const [],
callMembers: members,
);
return;
}
} else {
if (rejectedBy.keys.contains(state.createdByUserId)) {
_logger.d(
() => '[coordinatorCallRejected] creator rejected, disconnecting',
);
state = state.copyWith(
status: CallStatus.disconnected(
DisconnectReason.rejected(
byUserId: event.rejectedByUserId,
reason: CallRejectReason.custom('ring: creator rejected'),
),
),
sessionId: '',
callParticipants: const [],
callMembers: members,
);
return;
}
}

state = state.copyWith(callMembers: members);
}

void coordinatorCallEnded(
Expand Down Expand Up @@ -426,10 +453,68 @@ mixin StateCoordinatorMixin on StateNotifier<CallState> {
callParticipants: newParticipants,
);
}
}

extension on List<CallParticipantState> {
bool hasSingle(String userId) {
return length == 1 && firstOrNull?.userId == userId;
void coordinatorCallMemberAdded(
StreamCallMemberAddedEvent event,
) {
state = state.copyWith(
callMembers: [
...state.callMembers,
...event.members.map(
(member) {
final user = event.metadata.users.values.firstWhereOrNull((user) {
return user.id == member.userId;
});
return CallMemberState.fromCallMember(member, user);
},
),
],
);
}

void coordinatorCallMemberRemoved(
StreamCallMemberRemovedEvent event,
) {
state = state.copyWith(
callMembers: state.callMembers
.where((member) => !event.removedMemberIds.contains(member.userId))
.toList(),
);
}

void coordinatorCallMemberUpdated(
StreamCallMemberUpdatedEvent event,
) {
state = state.copyWith(
callMembers: state.callMembers.map((member) {
final updatedMember =
event.members.firstWhereOrNull((m) => m.userId == member.userId);
if (updatedMember != null) {
return member.copyWith(
roles: updatedMember.roles,
custom: updatedMember.custom,
);
} else {
return member;
}
}).toList(),
);
}

void coordinatorCallUserBlocked(StreamCallUserBlockedEvent event) {
state = state.copyWith(
blockedUserIds: [
...state.blockedUserIds,
event.user.id,
],
);
}

void coordinatorCallUserUnblocked(StreamCallUserUnblockedEvent event) {
state = state.copyWith(
blockedUserIds: state.blockedUserIds
.where((userId) => userId != event.user.id)
.toList(),
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import 'package:state_notifier/state_notifier.dart';
import '../../../call_state.dart';
import '../../../errors/video_error.dart';
import '../../../logger/impl/tagged_logger.dart';
import '../../../models/call_member_state.dart';
import '../../../models/call_received_data.dart';
import '../../../models/models.dart';
import '../../../sfu/data/models/sfu_error.dart';
Expand Down Expand Up @@ -64,7 +65,6 @@ mixin StateLifecycleMixin on StateNotifier<CallState> {
', notify: $notify, state: $state',
);

final status = data.toCallStatus(state: state, ringing: ringing);
state = state.copyWith(
status: data.toCallStatus(state: state, ringing: ringing),
isBackstage: data.metadata.details.backstage,
Expand All @@ -83,12 +83,10 @@ mixin StateLifecycleMixin on StateNotifier<CallState> {
rtmpIngress: data.metadata.details.rtmpIngress,
settings: data.metadata.settings,
ownCapabilities: data.metadata.details.ownCapabilities.toList(),
callParticipants: data.metadata.toCallParticipants(
state,
fromMembers: !status.isConnected && !status.isReconnecting,
),
callParticipants: data.metadata.toCallParticipants(state),
liveStartedAt: data.metadata.session.liveStartedAt,
liveEndedAt: data.metadata.session.liveEndedAt,
callMembers: data.metadata.toCallMembers(),
);
}

Expand All @@ -105,10 +103,7 @@ mixin StateLifecycleMixin on StateNotifier<CallState> {
)
.copyWith(
status: data.toCallStatus(state: state, ringing: ringing),
callParticipants: data.metadata.toCallParticipants(
state,
fromMembers: true,
),
callParticipants: data.metadata.toCallParticipants(state),
isRingingFlow: ringing,
audioOutputDevice: callConnectOptions.audioOutputDevice,
audioInputDevice: callConnectOptions.audioInputDevice,
Expand All @@ -128,10 +123,7 @@ mixin StateLifecycleMixin on StateNotifier<CallState> {
status: data.toCallStatus(state: state),
isRingingFlow: data.ringing,
ownCapabilities: data.metadata.details.ownCapabilities.toList(),
callParticipants: data.metadata.toCallParticipants(
state,
fromMembers: true,
),
callParticipants: data.metadata.toCallParticipants(state),
);
}

Expand All @@ -156,10 +148,7 @@ mixin StateLifecycleMixin on StateNotifier<CallState> {
.copyWith(
status: status,
ownCapabilities: data.metadata.details.ownCapabilities.toList(),
callParticipants: data.metadata.toCallParticipants(
state,
fromMembers: true,
),
callParticipants: data.metadata.toCallParticipants(state),
audioOutputDevice: callConnectOptions?.audioOutputDevice,
audioInputDevice: callConnectOptions?.audioInputDevice,
videoInputDevice: callConnectOptions?.videoInputDevice,
Expand Down Expand Up @@ -289,36 +278,23 @@ mixin StateLifecycleMixin on StateNotifier<CallState> {
}

extension on CallMetadata {
List<CallParticipantState> toCallParticipants(
CallState state, {
bool fromMembers = false,
}) {
List<CallParticipantState> toCallParticipants(CallState state) {
final result = <CallParticipantState>[];

final participantsData = fromMembers
? members.values
.map((e) => (userId: e.userId, userSessionId: null))
.toList()
: session.participants.values
.map((e) => (userId: e.userId, userSessionId: e.userSessionId))
.toList();

for (final participant in participantsData) {
for (final participant in session.participants.values) {
final userId = participant.userId;
final sessionId = participant.userSessionId;
final member = members[userId];
final user = users[userId];
final currentState = state.callParticipants.firstWhereOrNull(
(it) =>
it.userId == userId &&
(sessionId == null || it.sessionId == sessionId),
(it) => it.userId == userId && it.sessionId == sessionId,
);
final isLocal = state.currentUserId == userId &&
(sessionId == null || state.sessionId == sessionId);

final isLocal =
state.currentUserId == userId && state.sessionId == sessionId;

result.add(
currentState?.copyWith(
roles: member?.roles ?? user?.roles ?? [],
roles: user?.roles ?? [participant.role],
name: user?.name ?? '',
custom: user?.custom ?? {},
image: user?.image ?? '',
Expand All @@ -328,11 +304,11 @@ extension on CallMetadata {
) ??
CallParticipantState(
userId: userId,
roles: member?.roles ?? user?.roles ?? [],
roles: user?.roles ?? [participant.role],
name: user?.name ?? '',
custom: user?.custom ?? {},
image: user?.image ?? '',
sessionId: participant.userSessionId ?? '',
sessionId: participant.userSessionId,
trackIdPrefix: '',
isLocal: isLocal,
isOnline: !isLocal,
Expand Down
Loading