forked from eatvio/eatvio_app
feat(video_calls): ios voip push, mix and crushes fix
This commit is contained in:
@@ -27,7 +27,7 @@ class ChatsCubit extends Cubit<ChatsState> {
|
||||
|
||||
/// Test seam. Production uses CallKit.
|
||||
Future<void> Function(VideoCallPushPayload payload)? presentIncomingVideoCall;
|
||||
Future<void> Function()? dismissIncomingVideoCall;
|
||||
Future<void> Function(String? conversationUuid)? dismissIncomingVideoCall;
|
||||
|
||||
ChatsCubit({
|
||||
required this.chatRepository,
|
||||
@@ -230,7 +230,7 @@ class ChatsCubit extends Cubit<ChatsState> {
|
||||
}) {
|
||||
final kind = VideoCallChatSignal.kindOf(message);
|
||||
if (kind == VideoCallChatSignalKind.ended) {
|
||||
unawaited(_dismissIncomingVideoCallUi());
|
||||
unawaited(_dismissIncomingVideoCallUi(message.roomId));
|
||||
unawaited(
|
||||
VideoCallEndedPushHandler.endActiveCallIfMatches(message.roomId),
|
||||
);
|
||||
@@ -260,10 +260,16 @@ class ChatsCubit extends Cubit<ChatsState> {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _dismissIncomingVideoCallUi() async {
|
||||
Future<void> _dismissIncomingVideoCallUi(String? conversationUuid) async {
|
||||
try {
|
||||
await (dismissIncomingVideoCall ??
|
||||
VideoCallEndedPushHandler.dismissIncomingCallUi)();
|
||||
final dismiss = dismissIncomingVideoCall;
|
||||
if (dismiss != null) {
|
||||
await dismiss(conversationUuid);
|
||||
return;
|
||||
}
|
||||
await VideoCallEndedPushHandler.dismissIncomingCallUi(
|
||||
conversationUuid: conversationUuid,
|
||||
);
|
||||
} catch (e, st) {
|
||||
log(
|
||||
'ChatsCubit: failed to dismiss incoming video call: $e',
|
||||
|
||||
@@ -3,6 +3,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/entities/call_kit_params.dart';
|
||||
import 'package:flutter_callkit_incoming/flutter_callkit_incoming.dart';
|
||||
|
||||
/// Dismisses incoming call UI after a remote `video_call_ended` push.
|
||||
@@ -11,9 +12,7 @@ abstract final class VideoCallEndedPushHandler {
|
||||
'com.eatvioapp/video_call_push',
|
||||
);
|
||||
|
||||
static Future<void> dismissIncomingCallUi({
|
||||
String? conversationUuid,
|
||||
}) async {
|
||||
static Future<void> dismissIncomingCallUi({String? conversationUuid}) async {
|
||||
await endActiveCallIfMatches(conversationUuid);
|
||||
if (Platform.isAndroid) {
|
||||
try {
|
||||
@@ -22,7 +21,7 @@ abstract final class VideoCallEndedPushHandler {
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
await _dismissViaCallKit();
|
||||
await _dismissViaCallKit(conversationUuid);
|
||||
}
|
||||
|
||||
static Future<void> endActiveCallIfMatches(String? conversationUuid) async {
|
||||
@@ -37,18 +36,37 @@ abstract final class VideoCallEndedPushHandler {
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
static Future<void> _dismissViaCallKit() async {
|
||||
static bool callMatchesConversation(
|
||||
CallKitParams call,
|
||||
String conversationUuid,
|
||||
) {
|
||||
final uuid = conversationUuid.trim().toLowerCase();
|
||||
if (uuid.isEmpty) return false;
|
||||
final handle = call.handle?.trim().toLowerCase();
|
||||
if (handle != null && handle == uuid) return true;
|
||||
final extra = call.extra;
|
||||
if (extra == null) return false;
|
||||
for (final key in const ['conversationUuid', 'conversation_uuid']) {
|
||||
final value = extra[key];
|
||||
if (value is String && value.trim().toLowerCase() == uuid) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static Future<void> _dismissViaCallKit(String? conversationUuid) async {
|
||||
final uuid = conversationUuid?.trim();
|
||||
try {
|
||||
final activeCalls = await FlutterCallkitIncoming.activeCalls();
|
||||
if (uuid == null || uuid.isEmpty) {
|
||||
await FlutterCallkitIncoming.endAllCalls();
|
||||
return;
|
||||
}
|
||||
for (final call in activeCalls) {
|
||||
if (!callMatchesConversation(call, uuid)) continue;
|
||||
try {
|
||||
await FlutterCallkitIncoming.endCall(call.id);
|
||||
} catch (_) {}
|
||||
}
|
||||
} catch (_) {}
|
||||
|
||||
try {
|
||||
await FlutterCallkitIncoming.endAllCalls();
|
||||
} catch (_) {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,18 +2,48 @@ import 'dart:io';
|
||||
|
||||
import 'package:eatvio_app/features/video_calls/services/video_call_kit_id.dart';
|
||||
import 'package:eatvio_app/features/video_calls/services/video_call_push_payload.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_callkit_incoming/entities/entities.dart';
|
||||
import 'package:flutter_callkit_incoming/flutter_callkit_incoming.dart';
|
||||
|
||||
typedef VideoCallIncomingClaim = Future<bool> Function(String callId);
|
||||
typedef VideoCallIncomingShow = Future<void> Function(CallKitParams params);
|
||||
typedef VideoCallIncomingDelay = Future<void> Function(Duration delay);
|
||||
|
||||
/// Shows the system incoming-call UI.
|
||||
///
|
||||
/// On iOS the VoIP push reports the call even when the app is in the
|
||||
/// foreground. A second report for the same id dismisses that UI, so the
|
||||
/// socket path waits for the push and presents CallKit only if the push
|
||||
/// never arrived.
|
||||
abstract final class VideoCallIncomingPushPresenter {
|
||||
static const String _androidAccentColor = '#2787F5';
|
||||
static const MethodChannel _voipChannel = MethodChannel(
|
||||
'com.eatvioapp/voip_diagnostics',
|
||||
);
|
||||
|
||||
static Future<void> showIncomingCall(VideoCallPushPayload payload) async {
|
||||
/// Absolute delays from the start of an iOS present attempt.
|
||||
static const List<Duration> iosClaimPollDelays = <Duration>[
|
||||
Duration.zero,
|
||||
Duration(milliseconds: 300),
|
||||
Duration(milliseconds: 800),
|
||||
Duration(milliseconds: 1500),
|
||||
];
|
||||
|
||||
static Future<void> showIncomingCall(
|
||||
VideoCallPushPayload payload, {
|
||||
bool? isIos,
|
||||
VideoCallIncomingClaim? isIncomingCallClaimed,
|
||||
VideoCallIncomingClaim? reserveIncomingCall,
|
||||
VideoCallIncomingShow? showCallkitIncoming,
|
||||
VideoCallIncomingDelay? delay,
|
||||
List<Duration>? claimPollDelays,
|
||||
}) async {
|
||||
final callerName = payload.counterpartName?.trim().isNotEmpty == true
|
||||
? payload.counterpartName!.trim()
|
||||
: 'Итвио';
|
||||
|
||||
final callId = Platform.isIOS
|
||||
final ios = isIos ?? Platform.isIOS;
|
||||
final callId = ios
|
||||
? VideoCallKitId.iosUuid(
|
||||
conversationUuid: payload.conversationUuid,
|
||||
messageId: payload.messageId,
|
||||
@@ -21,6 +51,22 @@ abstract final class VideoCallIncomingPushPresenter {
|
||||
payload.conversationUuid
|
||||
: payload.conversationUuid;
|
||||
|
||||
if (ios) {
|
||||
final claimed = isIncomingCallClaimed ?? _isIncomingCallClaimed;
|
||||
final reserve = reserveIncomingCall ?? _reserveIncomingCall;
|
||||
final wait = delay ?? Future<void>.delayed;
|
||||
final polls = claimPollDelays ?? iosClaimPollDelays;
|
||||
final started = DateTime.now();
|
||||
for (final checkpoint in polls) {
|
||||
final remaining = checkpoint - DateTime.now().difference(started);
|
||||
if (remaining > Duration.zero) {
|
||||
await wait(remaining);
|
||||
}
|
||||
if (await _claimOrFalse(claimed, callId)) return;
|
||||
}
|
||||
if (!await _claimOrFalse(reserve, callId)) return;
|
||||
}
|
||||
|
||||
final params = CallKitParams(
|
||||
id: callId,
|
||||
nameCaller: callerName,
|
||||
@@ -57,10 +103,42 @@ abstract final class VideoCallIncomingPushPresenter {
|
||||
ios: const IOSParams(
|
||||
handleType: 'generic',
|
||||
supportsVideo: true,
|
||||
configureAudioSession: false,
|
||||
audioSessionActive: false,
|
||||
audioSessionMode: 'videoChat',
|
||||
ringtonePath: 'system_ringtone_default',
|
||||
),
|
||||
);
|
||||
|
||||
await FlutterCallkitIncoming.showCallkitIncoming(params);
|
||||
await (showCallkitIncoming ?? FlutterCallkitIncoming.showCallkitIncoming)(
|
||||
params,
|
||||
);
|
||||
}
|
||||
|
||||
static Future<bool> _claimOrFalse(
|
||||
VideoCallIncomingClaim claim,
|
||||
String callId,
|
||||
) async {
|
||||
try {
|
||||
return await claim(callId);
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static Future<bool> _isIncomingCallClaimed(String callId) async {
|
||||
final claimed = await _voipChannel.invokeMethod<bool>(
|
||||
'isIncomingCallClaimed',
|
||||
<String, String>{'id': callId},
|
||||
);
|
||||
return claimed ?? false;
|
||||
}
|
||||
|
||||
static Future<bool> _reserveIncomingCall(String callId) async {
|
||||
final reserved = await _voipChannel.invokeMethod<bool>(
|
||||
'reserveIncomingCall',
|
||||
<String, String>{'id': callId},
|
||||
);
|
||||
return reserved ?? false;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user