feat(video_calls): repeated calls after call termination fix

This commit is contained in:
misha
2026-10-15 15:21:38 +03:00
parent 1901248539
commit 4ef174ebde
5 changed files with 74 additions and 4 deletions

View File

@@ -406,7 +406,11 @@ class VideoCallManager {
Future<void> endCall() async { Future<void> endCall() async {
final uuid = _snapshot.conversationUuid; final uuid = _snapshot.conversationUuid;
final shouldNotifyBackend = !_callEndedNotified && uuid.isNotEmpty; final shouldNotifyBackend = shouldNotifyCallEnded(
alreadyNotified: _callEndedNotified,
hasConversation: uuid.isNotEmpty,
peerStillInRoom: _roomHasRemoteUser(),
);
if (uuid.isNotEmpty) { if (uuid.isNotEmpty) {
_callEndedNotified = true; _callEndedNotified = true;
} }
@@ -1099,6 +1103,14 @@ class VideoCallManager {
return false; return false;
} }
bool _roomHasRemoteUser() {
final room = _room;
if (room == null) return false;
return videoCallRoomStillHasRemoteUser(
room.remoteParticipants.values.map((participant) => participant.identity),
);
}
bool get _canHandleCallAudio => bool get _canHandleCallAudio =>
_callStageEntered && _callStageEntered &&
!_liveKitDisposed && !_liveKitDisposed &&

View File

@@ -541,9 +541,20 @@ class VideoCallSessionCubit extends BaseCubit<VideoCallSessionState> {
} }
Future<void> endCall() async { Future<void> endCall() async {
await _screenWakeLock.release(); _suppressDisposeSideEffects = true;
await _systemPipCoordinator.disarm(); try {
await _videoCallManager.endCall(); await _screenWakeLock.release();
await _systemPipCoordinator.disarm();
await _videoCallManager.endCall();
if (isClosed) return;
// The idle snapshot is ignored while the call screen is still visible,
// so hang up has to clear the session before the route pops. Otherwise
// dispose keeps the call and the next token request rings both users.
emit(const VideoCallSessionState());
await _restoreSystemUiAfterCall();
} finally {
_suppressDisposeSideEffects = false;
}
} }
void expandLocal() { void expandLocal() {

View File

@@ -18,6 +18,18 @@ bool shouldForceEndCallAfterPeerLeft({
return !videoCallRoomStillHasRemoteUser(remoteIdentities); return !videoCallRoomStillHasRemoteUser(remoteIdentities);
} }
/// The participant who taps hang up must not report the end while the other
/// person is still in the room. That report and the remaining client's report
/// both see an empty room and each one rings the other user again.
bool shouldNotifyCallEnded({
required bool alreadyNotified,
required bool hasConversation,
required bool peerStillInRoom,
}) {
if (!hasConversation || alreadyNotified || peerStillInRoom) return false;
return true;
}
bool shouldForceEndCallAfterRoomDisconnect({ bool shouldForceEndCallAfterRoomDisconnect({
required bool callStageEntered, required bool callStageEntered,
required bool sessionConnected, required bool sessionConnected,

View File

@@ -2,6 +2,8 @@ import 'package:eatvio_app/features/video_calls/managers/video_call_manager.dart
import 'package:eatvio_app/features/video_calls/services/video_call_foreground_coordinator.dart'; import 'package:eatvio_app/features/video_calls/services/video_call_foreground_coordinator.dart';
import 'package:eatvio_app/features/video_calls/services/video_call_lifecycle_binding.dart'; import 'package:eatvio_app/features/video_calls/services/video_call_lifecycle_binding.dart';
import 'package:eatvio_app/features/video_calls/repositories/video_call_repository.dart'; import 'package:eatvio_app/features/video_calls/repositories/video_call_repository.dart';
import 'dart:collection';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:livekit_client/livekit_client.dart' as lk; import 'package:livekit_client/livekit_client.dart' as lk;
@@ -53,6 +55,9 @@ void main() {
mockRoom = MockRoom(); mockRoom = MockRoom();
when(mockRoom.connectionState).thenReturn(lk.ConnectionState.connected); when(mockRoom.connectionState).thenReturn(lk.ConnectionState.connected);
when(mockRoom.localParticipant).thenReturn(null); when(mockRoom.localParticipant).thenReturn(null);
when(
mockRoom.remoteParticipants,
).thenReturn(UnmodifiableMapView(<String, lk.RemoteParticipant>{}));
}); });
tearDown(() { tearDown(() {

View File

@@ -66,6 +66,36 @@ void main() {
); );
}); });
test('does not report the end while the other user is still in the room', () {
expect(
shouldNotifyCallEnded(
alreadyNotified: false,
hasConversation: true,
peerStillInRoom: true,
),
isFalse,
);
});
test('reports the end when this client is the last participant', () {
expect(
shouldNotifyCallEnded(
alreadyNotified: false,
hasConversation: true,
peerStillInRoom: false,
),
isTrue,
);
expect(
shouldNotifyCallEnded(
alreadyNotified: true,
hasConversation: true,
peerStillInRoom: false,
),
isFalse,
);
});
test('keeps a connection-loss screen when the link drops', () { test('keeps a connection-loss screen when the link drops', () {
expect( expect(
shouldForceEndCallAfterRoomDisconnect( shouldForceEndCallAfterRoomDisconnect(