Skip to content

Commit f82e7e3

Browse files
Brazolrenefloor
andauthored
feat: allow multiple simultaneous active calls (#977)
* allow multiple simultaneous active calls * linter fix * dogfooding fix * added backwards compatibility * linter fix * Update packages/stream_video/lib/src/core/client_state.dart Co-authored-by: Rene Floor <[email protected]> * tweak * added tests for active call functionality --------- Co-authored-by: Rene Floor <[email protected]>
1 parent fdf7a6d commit f82e7e3

26 files changed

+1613
-467
lines changed

dogfooding/lib/screens/home_screen.dart

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,10 @@ class _HomeScreenState extends State<HomeScreen> {
5959
case ServiceType.call:
6060
call.reject(reason: CallRejectReason.cancel());
6161
case ServiceType.screenSharing:
62-
StreamVideoFlutterBackground.stopService(ServiceType.screenSharing);
62+
StreamVideoFlutterBackground.stopService(
63+
ServiceType.screenSharing,
64+
callCid: call.callCid.value,
65+
);
6366
call.setScreenShareEnabled(enabled: false);
6467
}
6568
},

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

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -276,8 +276,8 @@ class Call {
276276
String get id => state.value.callId;
277277
StreamCallCid get callCid => state.value.callCid;
278278
StreamCallType get type => state.value.callType;
279-
bool get isActiveCall =>
280-
_streamVideo.state.activeCall.valueOrNull?.callCid == callCid;
279+
bool get isActiveCall => _streamVideo.state.activeCalls.value
280+
.any((call) => call.callCid == callCid);
281281

282282
StateEmitter<CallState> get state => _stateManager.callStateStream;
283283
Stream<Duration> get callDurationStream => _stateManager.durationStream;
@@ -541,11 +541,13 @@ class Call {
541541
await _streamVideo.state.setOutgoingCall(null);
542542
}
543543

544-
final activeCall = _streamVideo.state.activeCall.valueOrNull;
545-
if (activeCall != null && activeCall.callCid != callCid) {
546-
_logger.i(() => '[accept] canceling another active call: $activeCall');
547-
await activeCall.leave(reason: DisconnectReason.ended());
548-
await _streamVideo.state.setActiveCall(null);
544+
if (!_streamVideo.options.allowMultipleActiveCalls) {
545+
final activeCall = _streamVideo.activeCall;
546+
if (activeCall != null && activeCall.callCid != callCid) {
547+
_logger.i(() => '[accept] canceling another active call: $activeCall');
548+
await activeCall.leave(reason: DisconnectReason.replaced());
549+
await _streamVideo.state.removeActiveCall(activeCall);
550+
}
549551
}
550552

551553
final result = await _coordinatorClient.acceptCall(cid: state.callCid);
@@ -614,7 +616,8 @@ class Call {
614616
return const Result.success(none);
615617
}
616618

617-
if (_streamVideo.state.activeCall.valueOrNull?.callCid == callCid) {
619+
if (_streamVideo.state.activeCalls.value
620+
.any((call) => call.callCid == callCid)) {
618621
_logger.w(
619622
() => '[join] rejected (a call with the same cid is in progress)',
620623
);
@@ -1419,9 +1422,7 @@ class Call {
14191422
await _session?.dispose();
14201423
await dynascaleManager.dispose();
14211424

1422-
if (_streamVideo.state.activeCall.valueOrNull?.callCid == callCid) {
1423-
await _streamVideo.state.setActiveCall(null);
1424-
}
1425+
await _streamVideo.state.removeActiveCall(this);
14251426

14261427
if (_streamVideo.state.outgoingCall.valueOrNull?.callCid == callCid) {
14271428
await _streamVideo.state.setOutgoingCall(null);
@@ -2334,7 +2335,7 @@ class Call {
23342335

23352336
// Set multitasking camera access for iOS
23362337
final multitaskingResult = await setMultitaskingCameraAccessEnabled(
2337-
enabled && !_streamVideo.muteVideoWhenInBackground,
2338+
enabled && !_streamVideo.options.muteVideoWhenInBackground,
23382339
);
23392340

23402341
_stateManager.participantSetCameraEnabled(

packages/stream_video/lib/src/core/client_state.dart

Lines changed: 57 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
import 'package:collection/collection.dart';
2+
3+
import '../../stream_video.dart' show DisconnectReason, StreamVideoOptions;
14
import '../call/call.dart';
25
import '../models/user.dart';
36
import '../state_emitter.dart';
@@ -16,7 +19,11 @@ abstract class ClientState {
1619
/// Emits StreamVideo connection changes.
1720
StateEmitter<ConnectionState> get connection;
1821

19-
/// Sets when a call being joined.
22+
/// Emits a list of active calls.
23+
StateEmitter<List<Call>> get activeCalls;
24+
25+
/// Emits an active call.
26+
/// Will only emit if options.allowMultipleActiveCalls is set to false, use activeCalls otherwise.
2027
StateEmitter<Call?> get activeCall;
2128

2229
/// Emits when a call was created by another user with ringing set as True.
@@ -25,23 +32,38 @@ abstract class ClientState {
2532
/// Emits when a call was created by current user with ringing set as True.
2633
StateEmitter<Call?> get outgoingCall;
2734

35+
/// Sets the call as a current outgoing call.
2836
Future<void> setOutgoingCall(Call? call);
37+
38+
/// Set the active call. Will end the currently active call if `options.allowMultipleActiveCalls` is `false.
39+
/// When `allowMultipleActiveCalls` is `true` calling this with `call: null` will have no effect.
40+
/// Otherwise the call is added to the list of active calls.
2941
Future<void> setActiveCall(Call? call);
42+
43+
/// Removes the call from the list of active calls.
44+
/// It won't `leave` the call, just removes it from the list.
45+
Future<void> removeActiveCall(Call call);
3046
}
3147

3248
class MutableClientState implements ClientState {
33-
MutableClientState(User user)
49+
MutableClientState(User user, this.options)
3450
: user = MutableStateEmitterImpl(user),
51+
activeCalls = MutableStateEmitterImpl([]),
3552
activeCall = MutableStateEmitterImpl(null),
3653
incomingCall = MutableStateEmitterImpl(null),
3754
outgoingCall = MutableStateEmitterImpl(null),
3855
connection = MutableStateEmitterImpl(
3956
ConnectionState.disconnected(user.id),
4057
);
4158

59+
final StreamVideoOptions options;
60+
4261
@override
4362
final MutableStateEmitter<User> user;
4463

64+
@override
65+
final MutableStateEmitter<List<Call>> activeCalls;
66+
4567
@override
4668
final MutableStateEmitter<Call?> activeCall;
4769

@@ -58,22 +80,49 @@ class MutableClientState implements ClientState {
5880
User get currentUser => user.value;
5981

6082
Future<void> clear() async {
61-
activeCall.value = null;
83+
activeCalls.value = [];
6284
outgoingCall.value = null;
6385
connection.value = ConnectionState.disconnected(user.value.id);
6486
}
6587

66-
Call? getActiveCall() => activeCall.valueOrNull;
88+
Call? getActiveCall() {
89+
if (options.allowMultipleActiveCalls) {
90+
throw Exception(
91+
'Multiple active calls are enabled, use getActiveCalls instead',
92+
);
93+
}
94+
95+
return activeCalls.value.firstOrNull;
96+
}
97+
98+
List<Call> getActiveCalls() => activeCalls.value;
6799
Call? getOutgoingCall() => outgoingCall.valueOrNull;
68100

69101
@override
70102
Future<void> setActiveCall(Call? call) async {
71-
final currentlyActiveCall = activeCall.valueOrNull;
72-
if (currentlyActiveCall != null && call != null) {
73-
await currentlyActiveCall.leave();
103+
if (!options.allowMultipleActiveCalls) {
104+
final currentlyActiveCall = activeCalls.value.firstOrNull;
105+
if (currentlyActiveCall != null) {
106+
await currentlyActiveCall.leave(reason: DisconnectReason.replaced());
107+
}
108+
109+
activeCall.value = call;
110+
activeCalls.value = call == null ? [] : [call];
111+
} else if (call != null) {
112+
activeCalls.value = [...activeCalls.value, call];
113+
}
114+
}
115+
116+
@override
117+
Future<void> removeActiveCall(Call call) async {
118+
if (!options.allowMultipleActiveCalls &&
119+
activeCall.value?.callCid == call.callCid) {
120+
activeCall.value = null;
74121
}
75122

76-
activeCall.value = call;
123+
activeCalls.value = [
124+
...activeCalls.value.where((it) => it.callCid != call.callCid),
125+
];
77126
}
78127

79128
@override

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

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,10 @@ abstract class DisconnectReason extends Equatable {
2727
return DisconnectReasonEnded();
2828
}
2929

30+
factory DisconnectReason.replaced() {
31+
return DisconnectReasonReplaced();
32+
}
33+
3034
factory DisconnectReason.lastParticipantLeft() {
3135
return DisconnectReasonLastParticipantLeft();
3236
}
@@ -112,6 +116,22 @@ class DisconnectReasonEnded extends DisconnectReason {
112116
}
113117
}
114118

119+
class DisconnectReasonReplaced extends DisconnectReason {
120+
factory DisconnectReasonReplaced() {
121+
return _instance;
122+
}
123+
124+
const DisconnectReasonReplaced._internal();
125+
126+
static const DisconnectReasonReplaced _instance =
127+
DisconnectReasonReplaced._internal();
128+
129+
@override
130+
String toString() {
131+
return 'Replaced';
132+
}
133+
}
134+
115135
class DisconnectReasonLastParticipantLeft extends DisconnectReason {
116136
factory DisconnectReasonLastParticipantLeft() {
117137
return _instance;

0 commit comments

Comments
 (0)