forked from eatvio/eatvio_app
feat(video_calls): ios mic fix, android call fix. feat(dev_tools): base url caching
This commit is contained in:
@@ -1,8 +1,12 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:eatvio_app/core/app_initializer.dart';
|
||||
import 'package:eatvio_app/core/cubit/marketing/marketing_cubit.dart';
|
||||
import 'package:eatvio_app/core/env_config.dart';
|
||||
import 'package:eatvio_app/core/internet_connection_cubit/internet_connection_cubit.dart';
|
||||
import 'package:eatvio_app/core/locator_service.dart';
|
||||
import 'package:eatvio_app/core/prefs.dart';
|
||||
import 'package:eatvio_app/core/prefs_keys.dart';
|
||||
import 'package:eatvio_app/core/views/cubit/views_cubit.dart';
|
||||
import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/new_messages_cubit/new_messages_cubit.dart';
|
||||
import 'package:eatvio_app/features/notifications/presentation/notifications_cubit/notifications_cubit.dart';
|
||||
@@ -31,20 +35,50 @@ class AppConfig {
|
||||
|
||||
void setBaseUrl(String baseUrls) {
|
||||
_baseUrl = baseUrls;
|
||||
_persistBaseUrl(baseUrls);
|
||||
}
|
||||
|
||||
void _persistBaseUrl(String baseUrl) {
|
||||
if (!sl.isRegistered<Prefs>()) {
|
||||
return;
|
||||
}
|
||||
final normalized = baseUrl.trim();
|
||||
if (normalized.isEmpty) {
|
||||
unawaited(sl<Prefs>().remove(PrefsKeys.customBaseUrl));
|
||||
return;
|
||||
}
|
||||
unawaited(sl<Prefs>().setString(PrefsKeys.customBaseUrl, normalized));
|
||||
}
|
||||
|
||||
/// Список глобальных BlocProvider'ов
|
||||
/// Используются lazy: true для отложенной инициализации, чтобы не блокировать старт приложения
|
||||
List<BlocProvider> get globalBlocProviders => [
|
||||
BlocProvider<VideoCallSessionCubit>.value(value: sl<VideoCallSessionCubit>()),
|
||||
BlocProvider<NewMessagesCubit>(lazy: true, create: (context) => sl<NewMessagesCubit>()),
|
||||
BlocProvider<NotificationsCubit>(lazy: true, create: (context) => sl<NotificationsCubit>()),
|
||||
BlocProvider<VideoCallSessionCubit>.value(
|
||||
value: sl<VideoCallSessionCubit>(),
|
||||
),
|
||||
BlocProvider<NewMessagesCubit>(
|
||||
lazy: true,
|
||||
create: (context) => sl<NewMessagesCubit>(),
|
||||
),
|
||||
BlocProvider<NotificationsCubit>(
|
||||
lazy: true,
|
||||
create: (context) => sl<NotificationsCubit>(),
|
||||
),
|
||||
BlocProvider<FeedCubit>(lazy: true, create: (context) => sl<FeedCubit>()),
|
||||
BlocProvider<ProfileAvatarCubit>(lazy: true, create: (context) => sl<ProfileAvatarCubit>()),
|
||||
BlocProvider<ProfileAvatarCubit>(
|
||||
lazy: true,
|
||||
create: (context) => sl<ProfileAvatarCubit>(),
|
||||
),
|
||||
BlocProvider<ChatsCubit>(lazy: true, create: (context) => sl<ChatsCubit>()),
|
||||
BlocProvider<ViewsCubit>(lazy: true, create: (context) => sl<ViewsCubit>()),
|
||||
BlocProvider<InternetConnectionCubit>(lazy: false, create: (context) => sl<InternetConnectionCubit>()),
|
||||
BlocProvider<MarketingCubit>(lazy: true, create: (context) => sl<MarketingCubit>()),
|
||||
BlocProvider<InternetConnectionCubit>(
|
||||
lazy: false,
|
||||
create: (context) => sl<InternetConnectionCubit>(),
|
||||
),
|
||||
BlocProvider<MarketingCubit>(
|
||||
lazy: true,
|
||||
create: (context) => sl<MarketingCubit>(),
|
||||
),
|
||||
];
|
||||
|
||||
/// Делегаты локализации
|
||||
@@ -56,5 +90,7 @@ class AppConfig {
|
||||
];
|
||||
|
||||
/// Поддерживаемые локали
|
||||
List<Locale> get supportedLocales => [const Locale.fromSubtags(languageCode: 'ru')];
|
||||
List<Locale> get supportedLocales => [
|
||||
const Locale.fromSubtags(languageCode: 'ru'),
|
||||
];
|
||||
}
|
||||
|
||||
16
lib/core/cached_base_url.dart
Normal file
16
lib/core/cached_base_url.dart
Normal file
@@ -0,0 +1,16 @@
|
||||
import 'package:eatvio_app/core/app_config.dart';
|
||||
import 'package:eatvio_app/core/locator_service.dart';
|
||||
import 'package:eatvio_app/core/prefs.dart';
|
||||
import 'package:eatvio_app/core/prefs_keys.dart';
|
||||
|
||||
/// Подставляет сохранённый base URL до создания HTTP-клиента.
|
||||
void applyCachedBaseUrl() {
|
||||
if (!sl.isRegistered<Prefs>() || !sl.isRegistered<AppConfig>()) {
|
||||
return;
|
||||
}
|
||||
final cached = sl<Prefs>().getString(PrefsKeys.customBaseUrl)?.trim();
|
||||
if (cached == null || cached.isEmpty) {
|
||||
return;
|
||||
}
|
||||
sl<AppConfig>().setBaseUrl(cached);
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'package:eatvio_app/core/app_initializer.dart';
|
||||
import 'package:eatvio_app/core/cached_base_url.dart';
|
||||
import 'package:eatvio_app/core/di/app_config_registration.dart';
|
||||
import 'package:eatvio_app/core/di/cubit_registrations.dart';
|
||||
import 'package:eatvio_app/core/di/external_registrations.dart';
|
||||
@@ -21,6 +22,8 @@ Future<void> init({required AppBuildType buildType}) async {
|
||||
|
||||
// Ожидание инициализации асинхронных зависимостей
|
||||
await sl.getAsync<Prefs>();
|
||||
// До создания Dio и любых запросов к бэкенду.
|
||||
applyCachedBaseUrl();
|
||||
|
||||
// Регистрация сервисов
|
||||
ServiceRegistrations.registerAll();
|
||||
@@ -32,5 +35,7 @@ Future<void> init({required AppBuildType buildType}) async {
|
||||
CubitRegistrations.registerAll();
|
||||
|
||||
// Регистрация WebSocket менеджера
|
||||
sl.registerLazySingleton<WsManager>(() => WsManager(wsService: sl(), errorReporter: sl()));
|
||||
sl.registerLazySingleton<WsManager>(
|
||||
() => WsManager(wsService: sl(), errorReporter: sl()),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -65,7 +65,11 @@ class PushNavigationHandler {
|
||||
'PushNavigationHandler: dismiss incoming call UI, source=$source',
|
||||
name: 'PushNavigationHandler',
|
||||
);
|
||||
unawaited(VideoCallEndedPushHandler.dismissIncomingCallUi());
|
||||
unawaited(
|
||||
VideoCallEndedPushHandler.dismissIncomingCallUi(
|
||||
conversationUuid: endedPayload.conversationUuid,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -1 +1,8 @@
|
||||
enum PrefsKeys { hasAnonymousTokenBeenSent, welcomeScreenWasShowed, registrationDate, marketingBottomSheetHasBeenShown, isEmailVerificationSkipped }
|
||||
enum PrefsKeys {
|
||||
hasAnonymousTokenBeenSent,
|
||||
welcomeScreenWasShowed,
|
||||
registrationDate,
|
||||
marketingBottomSheetHasBeenShown,
|
||||
isEmailVerificationSkipped,
|
||||
customBaseUrl,
|
||||
}
|
||||
|
||||
@@ -231,6 +231,9 @@ class ChatsCubit extends Cubit<ChatsState> {
|
||||
final kind = VideoCallChatSignal.kindOf(message);
|
||||
if (kind == VideoCallChatSignalKind.ended) {
|
||||
unawaited(_dismissIncomingVideoCallUi());
|
||||
unawaited(
|
||||
VideoCallEndedPushHandler.endActiveCallIfMatches(message.roomId),
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (isOwn || kind != VideoCallChatSignalKind.started) return;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:eatvio_app/core/app_router.dart';
|
||||
import 'package:eatvio_app/core/device/device_hw_id_provider.dart';
|
||||
@@ -8,6 +9,7 @@ import 'package:eatvio_app/features/video_calls/managers/video_call_manager_snap
|
||||
import 'package:eatvio_app/features/video_calls/services/video_call_audio_session_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_peer_departure.dart';
|
||||
import 'package:eatvio_app/features/video_calls/services/video_call_notification_navigator.dart';
|
||||
import 'package:eatvio_app/features/video_calls/services/video_call_task_handler.dart';
|
||||
import 'package:eatvio_app/features/video_calls/presentation/bloc/video_call_session/video_call_session_state.dart';
|
||||
@@ -37,6 +39,12 @@ class VideoCallManager {
|
||||
_audioSession =
|
||||
audioSessionCoordinator ?? VideoCallAudioSessionCoordinator() {
|
||||
FlutterForegroundTask.addTaskDataCallback(_onForegroundTaskData);
|
||||
if (Platform.isIOS) {
|
||||
// LiveKit rewrites the audio session whenever a remote audio track
|
||||
// appears. That second configuration stops the microphone and does
|
||||
// not start it again.
|
||||
lk.Hardware.instance.setAutomaticConfigurationEnabled(enable: false);
|
||||
}
|
||||
}
|
||||
|
||||
final VideoCallRepository _videoCallRepository;
|
||||
@@ -46,7 +54,10 @@ class VideoCallManager {
|
||||
final VideoCallAudioSessionCoordinator _audioSession;
|
||||
|
||||
static const lk.AudioCaptureOptions _micCaptureOptions =
|
||||
lk.AudioCaptureOptions(stopAudioCaptureOnMute: false);
|
||||
lk.AudioCaptureOptions(
|
||||
stopAudioCaptureOnMute: false,
|
||||
voiceIsolation: false,
|
||||
);
|
||||
static const Duration _mutedMicPollInterval = Duration(milliseconds: 320);
|
||||
static const Duration _mutedMicOverlayMinInterval = Duration(
|
||||
milliseconds: 2600,
|
||||
@@ -65,6 +76,7 @@ class VideoCallManager {
|
||||
num? _lastMutedMicTotalEnergy;
|
||||
bool _liveKitDisposed = true;
|
||||
bool _callEndedNotified = true;
|
||||
bool _forceEndRequested = false;
|
||||
final Set<String> _callAcceptNotifiedConversationUuids = {};
|
||||
bool _callStageEntered = false;
|
||||
bool _joinInProgress = false;
|
||||
@@ -86,6 +98,8 @@ class VideoCallManager {
|
||||
String? get counterpartName => _snapshot.counterpartName;
|
||||
String? get localParticipantName => _snapshot.localParticipantName;
|
||||
|
||||
bool get callEndedNotified => _callEndedNotified;
|
||||
|
||||
bool get isActiveCall =>
|
||||
_snapshot.connectPhase == VideoCallConnectPhase.connected &&
|
||||
_callStageEntered &&
|
||||
@@ -207,6 +221,7 @@ class VideoCallManager {
|
||||
|
||||
_resetSessionFlagsForNewRoom();
|
||||
_callEndedNotified = false;
|
||||
_forceEndRequested = false;
|
||||
_liveKitDisposed = false;
|
||||
_callStageEntered = false;
|
||||
_room = lk.Room(
|
||||
@@ -391,10 +406,13 @@ class VideoCallManager {
|
||||
|
||||
Future<void> endCall() async {
|
||||
final uuid = _snapshot.conversationUuid;
|
||||
final shouldNotifyBackend = !_callEndedNotified && uuid.isNotEmpty;
|
||||
if (uuid.isNotEmpty) {
|
||||
_callEndedNotified = true;
|
||||
}
|
||||
_disposeRoomEphemerals();
|
||||
await _disconnectLiveKitOnly();
|
||||
if (!_callEndedNotified && uuid.isNotEmpty) {
|
||||
_callEndedNotified = true;
|
||||
if (shouldNotifyBackend) {
|
||||
try {
|
||||
await _videoCallRepository.notifyCallEnded(uuid);
|
||||
} catch (_) {}
|
||||
@@ -477,6 +495,9 @@ class VideoCallManager {
|
||||
next,
|
||||
audioCaptureOptions: _micCaptureOptions,
|
||||
);
|
||||
if (next) {
|
||||
await _audioSession.restartCapture();
|
||||
}
|
||||
} catch (_) {
|
||||
return;
|
||||
}
|
||||
@@ -674,6 +695,7 @@ class VideoCallManager {
|
||||
true,
|
||||
audioCaptureOptions: _micCaptureOptions,
|
||||
);
|
||||
await _audioSession.restartCapture();
|
||||
} catch (_) {}
|
||||
}
|
||||
await _resumeVideoAfterForeground();
|
||||
@@ -814,6 +836,12 @@ class VideoCallManager {
|
||||
}
|
||||
}
|
||||
|
||||
void _requestForceEnd() {
|
||||
if (_forceEndRequested || _liveKitDisposed) return;
|
||||
_forceEndRequested = true;
|
||||
_emitSnapshot(_snapshot.copyWith(forceEndCall: true));
|
||||
}
|
||||
|
||||
void _attachRoomEventListeners() {
|
||||
final room = _room;
|
||||
if (room == null) return;
|
||||
@@ -825,11 +853,34 @@ class VideoCallManager {
|
||||
if (_snapshot.linkIssue.needsManualReconnect) return;
|
||||
_emitSnapshot(_snapshot.copyWith(linkIssue: VideoCallLinkIssue.none));
|
||||
})
|
||||
..on<lk.RoomDisconnectedEvent>((_) {
|
||||
..on<lk.RoomDisconnectedEvent>((event) {
|
||||
if (_liveKitDisposed) return;
|
||||
if (_snapshot.connectPhase != VideoCallConnectPhase.connected) return;
|
||||
if (shouldForceEndCallAfterRoomDisconnect(
|
||||
callStageEntered: _callStageEntered,
|
||||
sessionConnected: true,
|
||||
reason: event.reason,
|
||||
)) {
|
||||
_requestForceEnd();
|
||||
return;
|
||||
}
|
||||
_emitSnapshot(_snapshot.copyWith(linkIssue: VideoCallLinkIssue.lost));
|
||||
})
|
||||
..on<lk.ParticipantDisconnectedEvent>((_) {
|
||||
final room = _room;
|
||||
if (room == null) return;
|
||||
if (!shouldForceEndCallAfterPeerLeft(
|
||||
callStageEntered: _callStageEntered,
|
||||
sessionConnected:
|
||||
_snapshot.connectPhase == VideoCallConnectPhase.connected,
|
||||
remoteIdentities: room.remoteParticipants.values.map(
|
||||
(participant) => participant.identity,
|
||||
),
|
||||
)) {
|
||||
return;
|
||||
}
|
||||
_requestForceEnd();
|
||||
})
|
||||
..on<lk.ParticipantConnectionQualityUpdatedEvent>((e) {
|
||||
if (e.participant != room.localParticipant) return;
|
||||
_syncPoorConnectionOverlay(e.connectionQuality);
|
||||
@@ -1089,6 +1140,7 @@ class VideoCallManager {
|
||||
true,
|
||||
audioCaptureOptions: _micCaptureOptions,
|
||||
);
|
||||
await _audioSession.restartCapture();
|
||||
} catch (_) {
|
||||
effectiveMic = false;
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ class VideoCallManagerSnapshot {
|
||||
required this.connectPhase,
|
||||
required this.linkIssue,
|
||||
this.counterpartPresentInLobby = false,
|
||||
this.forceEndCall = false,
|
||||
});
|
||||
|
||||
final String conversationUuid;
|
||||
@@ -22,6 +23,7 @@ class VideoCallManagerSnapshot {
|
||||
final VideoCallConnectPhase connectPhase;
|
||||
final VideoCallLinkIssue linkIssue;
|
||||
final bool counterpartPresentInLobby;
|
||||
final bool forceEndCall;
|
||||
|
||||
static const initial = VideoCallManagerSnapshot(
|
||||
conversationUuid: '',
|
||||
@@ -41,6 +43,7 @@ class VideoCallManagerSnapshot {
|
||||
VideoCallConnectPhase? connectPhase,
|
||||
VideoCallLinkIssue? linkIssue,
|
||||
bool? counterpartPresentInLobby,
|
||||
bool? forceEndCall,
|
||||
bool clearConnectError = false,
|
||||
}) {
|
||||
return VideoCallManagerSnapshot(
|
||||
@@ -54,6 +57,7 @@ class VideoCallManagerSnapshot {
|
||||
linkIssue: linkIssue ?? this.linkIssue,
|
||||
counterpartPresentInLobby:
|
||||
counterpartPresentInLobby ?? this.counterpartPresentInLobby,
|
||||
forceEndCall: forceEndCall ?? this.forceEndCall,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import 'package:eatvio_app/core/cubit/base_cubit.dart';
|
||||
import 'package:eatvio_app/features/video_calls/managers/video_call_manager.dart';
|
||||
import 'package:eatvio_app/features/video_calls/managers/video_call_manager_snapshot.dart';
|
||||
import 'package:eatvio_app/features/video_calls/presentation/bloc/video_call_session/video_call_session_state.dart';
|
||||
import 'package:eatvio_app/features/video_calls/services/video_call_notification_navigator.dart';
|
||||
import 'package:eatvio_app/features/video_calls/services/video_call_screen_wake_lock_coordinator.dart';
|
||||
import 'package:eatvio_app/features/video_calls/services/video_call_system_pip_coordinator.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
@@ -30,6 +31,8 @@ class VideoCallSessionCubit extends BaseCubit<VideoCallSessionState> {
|
||||
final VideoCallSystemPipCoordinator _systemPipCoordinator;
|
||||
final VideoCallScreenWakeLockCoordinator _screenWakeLock;
|
||||
bool _callScreenRouteVisible = false;
|
||||
bool _handlingRemoteCallEnd = false;
|
||||
bool _suppressDisposeSideEffects = false;
|
||||
bool _autoEnterInFlight = false;
|
||||
bool _counterpartPresentWhenLobbyReady = false;
|
||||
bool _sawEmptyLobbyWhenConnected = false;
|
||||
@@ -74,6 +77,10 @@ class VideoCallSessionCubit extends BaseCubit<VideoCallSessionState> {
|
||||
|
||||
void _onManagerSnapshot(VideoCallManagerSnapshot snapshot) {
|
||||
if (isClosed) return;
|
||||
if (snapshot.forceEndCall) {
|
||||
unawaited(handleRemoteCallEnded());
|
||||
return;
|
||||
}
|
||||
if (_isCallSessionInactive(snapshot)) {
|
||||
unawaited(_systemPipCoordinator.disarm());
|
||||
if (_callScreenRouteVisible) {
|
||||
@@ -377,8 +384,32 @@ class VideoCallSessionCubit extends BaseCubit<VideoCallSessionState> {
|
||||
}
|
||||
}
|
||||
|
||||
/// The other participant hung up or was removed from the room.
|
||||
Future<void> handleRemoteCallEnded() async {
|
||||
if (_handlingRemoteCallEnd || isClosed) return;
|
||||
if (_videoCallManager.callEndedNotified) return;
|
||||
final inCall =
|
||||
state.callStageEntered ||
|
||||
state.connectPhase == VideoCallConnectPhase.connected;
|
||||
if (!inCall) return;
|
||||
|
||||
_handlingRemoteCallEnd = true;
|
||||
_suppressDisposeSideEffects = true;
|
||||
try {
|
||||
await endCall();
|
||||
if (isClosed) return;
|
||||
emit(const VideoCallSessionState());
|
||||
await _restoreSystemUiAfterCall();
|
||||
VideoCallNotificationNavigator.closeCallRouteIfOpen();
|
||||
} finally {
|
||||
_suppressDisposeSideEffects = false;
|
||||
_handlingRemoteCallEnd = false;
|
||||
}
|
||||
}
|
||||
|
||||
void onCallScreenDisposed() {
|
||||
_callScreenRouteVisible = false;
|
||||
if (_suppressDisposeSideEffects) return;
|
||||
if (state.uiMode != VideoCallUiMode.fullscreen) {
|
||||
if (state.uiMode == VideoCallUiMode.pip && state.callStageEntered) {
|
||||
// [isCallScreenRouteVisible] is not in bloc state; refresh the global
|
||||
|
||||
@@ -31,7 +31,7 @@ class VideoCallAudioSessionCoordinator {
|
||||
avAudioSessionCategoryOptions:
|
||||
AVAudioSessionCategoryOptions.allowBluetooth |
|
||||
AVAudioSessionCategoryOptions.defaultToSpeaker,
|
||||
avAudioSessionMode: AVAudioSessionMode.voiceChat,
|
||||
avAudioSessionMode: AVAudioSessionMode.videoChat,
|
||||
avAudioSessionRouteSharingPolicy:
|
||||
AVAudioSessionRouteSharingPolicy.defaultPolicy,
|
||||
avAudioSessionSetActiveOptions: AVAudioSessionSetActiveOptions.none,
|
||||
@@ -88,6 +88,15 @@ class VideoCallAudioSessionCoordinator {
|
||||
}
|
||||
}
|
||||
|
||||
/// Restarts capture after the microphone track has been created.
|
||||
Future<void> restartCapture() async {
|
||||
if (!_isIos) return;
|
||||
try {
|
||||
await _iosCallAudioSession('restart');
|
||||
_iosActive = true;
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
Future<void> stop() async {
|
||||
if (_isIos) {
|
||||
_iosActive = false;
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:eatvio_app/core/locator_service.dart';
|
||||
import 'package:eatvio_app/features/video_calls/presentation/bloc/video_call_session/video_call_session_cubit.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_callkit_incoming/flutter_callkit_incoming.dart';
|
||||
|
||||
@@ -9,7 +11,10 @@ abstract final class VideoCallEndedPushHandler {
|
||||
'com.eatvioapp/video_call_push',
|
||||
);
|
||||
|
||||
static Future<void> dismissIncomingCallUi() async {
|
||||
static Future<void> dismissIncomingCallUi({
|
||||
String? conversationUuid,
|
||||
}) async {
|
||||
await endActiveCallIfMatches(conversationUuid);
|
||||
if (Platform.isAndroid) {
|
||||
try {
|
||||
await _androidPushChannel.invokeMethod<void>('dismissIncomingCalls');
|
||||
@@ -20,6 +25,18 @@ abstract final class VideoCallEndedPushHandler {
|
||||
await _dismissViaCallKit();
|
||||
}
|
||||
|
||||
static Future<void> endActiveCallIfMatches(String? conversationUuid) async {
|
||||
final uuid = conversationUuid?.trim();
|
||||
if (uuid == null || uuid.isEmpty) return;
|
||||
if (!sl.isRegistered<VideoCallSessionCubit>()) return;
|
||||
|
||||
final cubit = sl<VideoCallSessionCubit>();
|
||||
if (cubit.state.conversationUuid != uuid) return;
|
||||
try {
|
||||
await cubit.handleRemoteCallEnded();
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
static Future<void> _dismissViaCallKit() async {
|
||||
try {
|
||||
final activeCalls = await FlutterCallkitIncoming.activeCalls();
|
||||
|
||||
@@ -1,11 +1,23 @@
|
||||
import 'package:eatvio_app/features/video_calls/services/video_call_push_payload.dart';
|
||||
|
||||
/// Marker for a data-only `video_call_ended` push (no conversation uuid required).
|
||||
/// Marker for a data-only `video_call_ended` push.
|
||||
class VideoCallEndedPushPayload {
|
||||
const VideoCallEndedPushPayload._();
|
||||
const VideoCallEndedPushPayload({this.conversationUuid});
|
||||
|
||||
final String? conversationUuid;
|
||||
|
||||
static VideoCallEndedPushPayload? tryParse(Map<String, dynamic> data) {
|
||||
if (!VideoCallPushPayload.isEndedVideoCall(data)) return null;
|
||||
return const VideoCallEndedPushPayload._();
|
||||
final rawUuid =
|
||||
data['conversationUuid'] ??
|
||||
data['conversation_uuid'] ??
|
||||
data['uuid'];
|
||||
final conversationUuid = rawUuid?.toString().trim();
|
||||
return VideoCallEndedPushPayload(
|
||||
conversationUuid:
|
||||
conversationUuid == null || conversationUuid.isEmpty
|
||||
? null
|
||||
: conversationUuid,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,7 +62,9 @@ abstract final class VideoCallFcmHandler {
|
||||
name: 'VideoCallFcm',
|
||||
);
|
||||
try {
|
||||
await VideoCallEndedPushHandler.dismissIncomingCallUi();
|
||||
await VideoCallEndedPushHandler.dismissIncomingCallUi(
|
||||
conversationUuid: endedPayload.conversationUuid,
|
||||
);
|
||||
} catch (e, st) {
|
||||
log(
|
||||
'VideoCallFcmHandler: failed to dismiss incoming call UI: $e',
|
||||
|
||||
@@ -74,6 +74,17 @@ class VideoCallNotificationNavigator {
|
||||
return isOnCallRoute();
|
||||
}
|
||||
|
||||
static void closeCallRouteIfOpen() {
|
||||
try {
|
||||
if (!isOnCallRoute()) return;
|
||||
final context = AppRouter.navigatorKey.currentContext;
|
||||
if (context == null || !context.mounted) return;
|
||||
if (Navigator.canPop(context)) {
|
||||
Navigator.pop(context);
|
||||
}
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
static bool isOnCallRoute() {
|
||||
final matches =
|
||||
AppRouter.appRouter.routerDelegate.currentConfiguration.matches;
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import 'package:livekit_client/livekit_client.dart' as lk;
|
||||
|
||||
/// Real call participants use the `user-{id}` identity. Egress and other
|
||||
/// bots must not keep the call alive after the other person leaves.
|
||||
bool videoCallRoomStillHasRemoteUser(Iterable<String> remoteIdentities) {
|
||||
for (final identity in remoteIdentities) {
|
||||
if (identity.startsWith('user-')) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool shouldForceEndCallAfterPeerLeft({
|
||||
required bool callStageEntered,
|
||||
required bool sessionConnected,
|
||||
required Iterable<String> remoteIdentities,
|
||||
}) {
|
||||
if (!callStageEntered || !sessionConnected) return false;
|
||||
return !videoCallRoomStillHasRemoteUser(remoteIdentities);
|
||||
}
|
||||
|
||||
bool shouldForceEndCallAfterRoomDisconnect({
|
||||
required bool callStageEntered,
|
||||
required bool sessionConnected,
|
||||
required lk.DisconnectReason? reason,
|
||||
}) {
|
||||
if (!callStageEntered || !sessionConnected) return false;
|
||||
return reason == lk.DisconnectReason.participantRemoved ||
|
||||
reason == lk.DisconnectReason.roomDeleted;
|
||||
}
|
||||
Reference in New Issue
Block a user