Skip to content

Commit f94fb89

Browse files
authored
feat(llc): Added participants count in CallState (#790)
1 parent 19c73b0 commit f94fb89

File tree

9 files changed

+135
-16
lines changed

9 files changed

+135
-16
lines changed

dogfooding/lib/widgets/call_duration_title.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ class _CallDurationTitleState extends State<CallDurationTitle> {
2727
void initState() {
2828
super.initState();
2929

30-
widget.call.get().then((value) {
30+
widget.call.get(watch: false).then((value) {
3131
_startedAt = value.foldOrNull(
3232
success: (callData) =>
3333
callData.data.metadata.session.startedAt ?? DateTime.now()) ??

packages/stream_video/CHANGELOG.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,29 @@
1+
## Unreleased
2+
3+
This release introduces a major rework of the join/reconnect flow in the Call class to support Reconnect V2, enhancing reconnection handling across various scenarios. Most updates are within the internals of the Call class, though some changes are outward-facing, including a few breaking changes.
4+
5+
🔄 Changed
6+
* `Call.reject()` method will now always call `Call.leave()` method internally.
7+
8+
🚧 Breaking changes
9+
* Removed the deprecated `Call.joinLobby()` method.
10+
* The `maxDuration` and `maxParticipants` parameters of `Call.getOrCreate()` are now combined into the `StreamLimitsSettings? limits` parameter.
11+
12+
🔄 Dependency updates
13+
* Updated Firebase dependencies to resolve Xcode 16 build issues.
14+
15+
✅ Added
16+
* Added the `registerPushDevice` optional parameter (default is `true`) to the `StreamVideo.connect()` method,allowing the prevention of automatic push token registration.
17+
* Added `participantCount` and `anonymousParticipantCount` to `CallState` reflecting the current number of participants in the call.
18+
* Introduced the `watch` parameter to `Call.get()` and `Call.getOrCreate()` methods (default is `true`). When set to `true`, this enables the `Call` to listen for coordinator events and update its state accordingly, even before the call is joined (`Call.join()`).
19+
* Added support for `targetResolution` setting set on the Dashboard to determine the max resolution the video stream.
20+
21+
🐞 Fixed
22+
* Automatic push token registration by `StreamVideo` now stores registered token in `SharedPreferences`, performing an API call only when the token changes.
23+
* Fixed premature ringing termination issues.
24+
* Resolved issues where ringing would not end when the caller terminates the call in an app-terminated state.
25+
* Fixed issue with call not ending in some cases when only one participant is left and `dropIfAloneInRingingFlow` is set to `true`.
26+
127
## 0.5.5
228

329
🐞 Fixed

packages/stream_video/lib/src/call/call.dart

Lines changed: 62 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,9 @@ class Call {
255255
return _connectOptionsOverride ?? _connectOptions;
256256
}
257257

258+
/// It is better to pass the [connectOptions] to [join] method,
259+
/// setting it directly has to be done carefully. Depending on the moment in the call lifecycle,
260+
/// it might be overwritten by default configuration or it might be too late to apply the changes.
258261
set connectOptions(CallConnectOptions connectOptions) {
259262
if (state.value.status is CallStatusConnected) {
260263
_logger.w(
@@ -296,6 +299,7 @@ class Call {
296299
}
297300

298301
void _observeEvents() {
302+
_subscriptions.cancel(_idCoordEvents);
299303
_subscriptions.add(
300304
_idCoordEvents,
301305
_coordinatorClient.events.on<CoordinatorCallEvent>((event) async {
@@ -312,7 +316,7 @@ class Call {
312316
(status) {
313317
if (status == InternetStatus.disconnected) {
314318
_logger.d(() => '[observeReconnectEvents] network disconnected');
315-
_awaitNetworkAvailableFuture = awaitNetworkAvailable();
319+
_awaitNetworkAvailableFuture = _awaitNetworkAvailable();
316320
_reconnect(SfuReconnectionStrategy.fast);
317321
}
318322
},
@@ -412,11 +416,22 @@ class Call {
412416
}),
413417
);
414418
return _stateManager.coordinatorCallReaction(event);
419+
case CoordinatorCallSessionParticipantCountUpdatedEvent _:
420+
if (state.value.status.isConnected || state.value.status.isJoined) {
421+
return;
422+
}
423+
424+
return _stateManager.setParticipantsCount(
425+
totalCount:
426+
event.participantsCountByRole.values.fold(0, (a, b) => a + b),
427+
anonymousCount: event.anonymousParticipantCount,
428+
);
415429
default:
416430
break;
417431
}
418432
}
419433

434+
/// Accepts the incoming call.
420435
Future<Result<None>> accept() async {
421436
final state = this.state.value;
422437
_logger.i(() => '[accept] state: $state');
@@ -451,6 +466,7 @@ class Call {
451466
return result;
452467
}
453468

469+
/// Rejects the incoming call.
454470
Future<Result<None>> reject({CallRejectReason? reason}) async {
455471
final state = this.state.value;
456472
_logger.i(() => '[reject] state: $state');
@@ -473,6 +489,7 @@ class Call {
473489
return result;
474490
}
475491

492+
/// Ends the call for all participants.
476493
Future<Result<None>> end() async {
477494
final state = this.state.value;
478495
_logger.d(() => '[end] status: ${state.status}');
@@ -492,6 +509,9 @@ class Call {
492509
return result;
493510
}
494511

512+
/// Joins the call.
513+
///
514+
/// - [connectOptions]: optional initial call configuration
495515
Future<Result<None>> join({
496516
CallConnectOptions? connectOptions,
497517
}) async {
@@ -744,8 +764,6 @@ class Call {
744764
return Result.success(credentials);
745765
}
746766

747-
/// Allows you to create a new call with the given parameters
748-
/// and joins the call immediately.
749767
Future<Result<CallJoinedData>> _performJoinCallRequest({
750768
bool create = false,
751769
bool video = false,
@@ -865,7 +883,7 @@ class Call {
865883
_idSessionStats,
866884
session.stats.listen((stats) {
867885
_stats.emit(stats);
868-
processStats(stats);
886+
_processStats(stats);
869887
}),
870888
);
871889

@@ -900,7 +918,7 @@ class Call {
900918
);
901919
}
902920

903-
void processStats(CallStats stats) {
921+
void _processStats(CallStats stats) {
904922
var publisherStats =
905923
state.value.publisherStats ?? PeerConnectionStats.empty();
906924
var subscriberStats =
@@ -1006,6 +1024,11 @@ class Call {
10061024
_stateManager.callPreferences.dropIfAloneInRingingFlow) {
10071025
await leave();
10081026
}
1027+
} else if (sfuEvent is SfuHealthCheckResponseEvent) {
1028+
_stateManager.setParticipantsCount(
1029+
totalCount: sfuEvent.participantCount.total,
1030+
anonymousCount: sfuEvent.participantCount.anonymous,
1031+
);
10091032
}
10101033

10111034
if (sfuEvent is SfuSocketDisconnected) {
@@ -1144,7 +1167,7 @@ class Call {
11441167
);
11451168
}
11461169

1147-
Future<InternetStatus> awaitNetworkAvailable() async {
1170+
Future<InternetStatus> _awaitNetworkAvailable() async {
11481171
_logger.v(() => '[awaitNetworkAwailable] starting timer');
11491172
final fastReconnectTimer = Timer(_fastReconnectDeadline, () {
11501173
_logger.w(() => '[awaitNetworkAwailable] too late for fast reconnect');
@@ -1201,6 +1224,9 @@ class Call {
12011224
return const Result.success(none);
12021225
}
12031226

1227+
/// Leaves the call.
1228+
///
1229+
/// - [reason]: optional reason for leaving the call
12041230
Future<Result<None>> leave({DisconnectReason? reason}) async {
12051231
final state = this.state.value;
12061232
_logger.i(() => '[leave] state: $state');
@@ -1250,6 +1276,10 @@ class Call {
12501276
return [...?_session?.getTracks(trackIdPrefix)];
12511277
}
12521278

1279+
/// Takes a picture of a VideoTrack at highest possible resolution
1280+
///
1281+
/// - [participant]: the participant whose track to take a screenshot of
1282+
/// - [trackType]: optional type of track to take a screenshot of, defaults to [SfuTrackType.video]
12531283
Future<ByteBuffer?> takeScreenshot(
12541284
CallParticipantState participant, {
12551285
SfuTrackType? trackType,
@@ -1470,6 +1500,7 @@ class Call {
14701500
});
14711501
}
14721502

1503+
/// Adds members to the current call.
14731504
Future<Result<None>> addMembers(List<UserInfo> users) {
14741505
return _coordinatorClient.addMembers(
14751506
callCid: callCid,
@@ -1479,6 +1510,7 @@ class Call {
14791510
);
14801511
}
14811512

1513+
/// Removes members from the current call.
14821514
Future<Result<None>> removeMembers(List<String> userIds) {
14831515
return _coordinatorClient.removeMembers(
14841516
callCid: callCid,
@@ -1499,19 +1531,29 @@ class Call {
14991531
);
15001532
}
15011533

1502-
/// Receives a call information. You can then use
1503-
/// the [CallReceivedData] in order to create a [Call] object.
1534+
/// Loads the information about the call.
1535+
///
1536+
/// - [ringing]: If `true`, sends a VoIP notification, triggering the native call screen on iOS and Android.
1537+
/// - [notify]: If `true`, sends a standard push notification.
1538+
/// - [video]: Marks the call as a video call if `true`; otherwise, audio-only.
1539+
/// - [watch]: If `true`, listens to coordinator events and updates call state accordingly.
1540+
/// - [membersLimit]: Sets the total number of members to return as part of the response.
15041541
Future<Result<CallReceivedData>> get({
15051542
int? membersLimit,
15061543
bool ringing = false,
15071544
bool notify = false,
15081545
bool video = false,
1546+
bool watch = true,
15091547
}) async {
15101548
_logger.d(
15111549
() => '[get] cid: $callCid, membersLimit: $membersLimit'
15121550
', ringing: $ringing, notify: $notify, video: $video',
15131551
);
15141552

1553+
if (watch) {
1554+
_observeEvents();
1555+
}
1556+
15151557
final response = await _coordinatorClient.getCall(
15161558
callCid: callCid,
15171559
membersLimit: membersLimit,
@@ -1538,14 +1580,19 @@ class Call {
15381580
);
15391581
}
15401582

1541-
/// Receives a call or creates it with given information. You can then use
1542-
/// the [CallReceivedOrCreatedData] in order to create a [Call] object.
1583+
/// Loads the information about the call and creates it if it doesn't exist.
1584+
///
1585+
/// - [ringing]: If `true`, sends a VoIP notification, triggering the native call screen on iOS and Android.
1586+
/// - [notify]: If `true`, sends a standard push notification.
1587+
/// - [video]: Marks the call as a video call if `true`; otherwise, audio-only.
1588+
/// - [watch]: If `true`, listens to coordinator events and updates call state accordingly.
15431589
Future<Result<CallReceivedOrCreatedData>> getOrCreate({
15441590
List<String> memberIds = const [],
15451591
bool ringing = false,
15461592
bool video = false,
1547-
String? team,
1593+
bool watch = true,
15481594
bool? notify,
1595+
String? team,
15491596
DateTime? startsAt,
15501597
StreamBackstageSettings? backstage,
15511598
StreamLimitsSettings? limits,
@@ -1556,6 +1603,10 @@ class Call {
15561603
'memberIds: $memberIds',
15571604
);
15581605

1606+
if (watch) {
1607+
_observeEvents();
1608+
}
1609+
15591610
if (ringing) {
15601611
await _streamVideo.state.setOutgoingCall(this);
15611612
}

packages/stream_video/lib/src/call/state/mixins/state_participant_mixin.dart

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -289,4 +289,14 @@ mixin StateParticipantMixin on StateNotifier<CallState> {
289289
}).toList(),
290290
);
291291
}
292+
293+
void setParticipantsCount({
294+
required int totalCount,
295+
required int anonymousCount,
296+
}) {
297+
state = state.copyWith(
298+
participantCount: totalCount,
299+
anonymousParticipantCount: anonymousCount,
300+
);
301+
}
292302
}

packages/stream_video/lib/src/call_state.dart

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ class CallState extends Equatable {
4141
localStats: null,
4242
latencyHistory: const [],
4343
blockedUserIds: const [],
44+
participantCount: 0,
45+
anonymousParticipantCount: 0,
4446
custom: const {},
4547
);
4648
}
@@ -76,6 +78,8 @@ class CallState extends Equatable {
7678
required this.localStats,
7779
required this.latencyHistory,
7880
required this.blockedUserIds,
81+
required this.participantCount,
82+
required this.anonymousParticipantCount,
7983
required this.custom,
8084
});
8185

@@ -108,6 +112,8 @@ class CallState extends Equatable {
108112
final LocalStats? localStats;
109113
final List<int> latencyHistory;
110114
final List<String> blockedUserIds;
115+
final int participantCount;
116+
final int anonymousParticipantCount;
111117
final Map<String, Object> custom;
112118

113119
String get callId => callCid.id;
@@ -154,6 +160,8 @@ class CallState extends Equatable {
154160
LocalStats? localStats,
155161
List<int>? latencyHistory,
156162
List<String>? blockedUserIds,
163+
int? participantCount,
164+
int? anonymousParticipantCount,
157165
Map<String, Object>? custom,
158166
}) {
159167
return CallState._(
@@ -186,6 +194,9 @@ class CallState extends Equatable {
186194
localStats: localStats ?? this.localStats,
187195
latencyHistory: latencyHistory ?? this.latencyHistory,
188196
blockedUserIds: blockedUserIds ?? this.blockedUserIds,
197+
participantCount: participantCount ?? this.participantCount,
198+
anonymousParticipantCount:
199+
anonymousParticipantCount ?? this.anonymousParticipantCount,
189200
custom: custom ?? this.custom,
190201
);
191202
}
@@ -244,6 +255,8 @@ class CallState extends Equatable {
244255
localStats,
245256
latencyHistory,
246257
blockedUserIds,
258+
participantCount,
259+
anonymousParticipantCount,
247260
custom,
248261
];
249262

packages/stream_video/lib/src/coordinator/open_api/open_api_extensions.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -374,6 +374,7 @@ extension CallSessionResponseExt on open.CallSessionResponse {
374374
endedAt: endedAt,
375375
liveStartedAt: liveStartedAt,
376376
liveEndedAt: liveEndedAt,
377+
timerEndsAt: timerEndsAt,
377378
);
378379
}
379380
}

packages/stream_video/lib/src/models/call_session_data.dart

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ class CallSessionData with EquatableMixin {
1616
this.liveStartedAt,
1717
this.endedAt,
1818
this.liveEndedAt,
19+
this.timerEndsAt,
1920
});
2021

2122
final String id;
@@ -27,6 +28,7 @@ class CallSessionData with EquatableMixin {
2728
final DateTime? endedAt;
2829
final DateTime? liveStartedAt;
2930
final DateTime? liveEndedAt;
31+
final DateTime? timerEndsAt;
3032

3133
@override
3234
List<Object?> get props => [
@@ -35,6 +37,7 @@ class CallSessionData with EquatableMixin {
3537
liveStartedAt,
3638
liveStartedAt,
3739
liveEndedAt,
40+
timerEndsAt,
3841
];
3942
}
4043

packages/stream_video/lib/src/stream_video.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -766,7 +766,7 @@ class StreamVideo extends Disposable {
766766
callType: callType,
767767
id: id,
768768
);
769-
final callResult = await call.get();
769+
final callResult = await call.get(watch: false);
770770

771771
return callResult.fold(
772772
failure: (failure) {

0 commit comments

Comments
 (0)