improvements

main_production_audio_video_call
Sikander Saleem 2 weeks ago
parent f82043f85b
commit 0a72638188

@ -71,7 +71,7 @@ Future<void> notifyCallerDeclined(Map<String, dynamic> extra, {String? reason})
await provider.declineCall(callee, caller, moduleId: int.tryParse(extra["moduleId"]?.toString() ?? '') ?? 0, reason: reason);
}
} catch (_) {
// Best effort - the caller side also times out.
print("errorDecline:$_");
} finally {
try {
await provider.endSession();
@ -86,6 +86,16 @@ class FirebaseNotificationManger {
static Timer? _missedCallTimer;
static String? _activeMissedCallId;
static String? _acceptedCallInProgress;
/// Returns false if this call is already being handled - the caller must then do nothing.
static bool beginAcceptedCall(String callId) {
if (_acceptedCallInProgress == callId) return false;
_acceptedCallInProgress = callId;
return true;
}
static void endAcceptedCall() => _acceptedCallInProgress = null;
/// iOS only. The VoIP token is registered natively in AppDelegate (PushKit) and stored
/// by flutter_callkit_incoming. It is sent to the backend as `voIPToken` on chat login.
static Future<void> refreshVoipToken() async {

@ -33,6 +33,7 @@ class _AudioCallPageState extends State<AudioCallPage> {
// Participants? participants;
bool isDialogOpen = false;
bool _isClosing = false;
@override
void initState() {
@ -75,11 +76,14 @@ class _AudioCallPageState extends State<AudioCallPage> {
}
void _closeCall() {
if (_isClosing) return;
_isClosing = true;
callPro.endSession();
if (mounted) Navigator.pop(context);
}
void _showFailure(String message, {required String title}) {
if (_isClosing) return;
callPro.endSession();
if (!mounted || isDialogOpen) return;
isDialogOpen = true;
@ -98,6 +102,8 @@ class _AudioCallPageState extends State<AudioCallPage> {
okTitle: "Go Back",
onTap: () {
Navigator.pop(cxt); // close dialog
if (_isClosing) return;
_isClosing = true;
if (mounted) Navigator.pop(context); // close call page
}),
),
@ -211,6 +217,8 @@ class _AudioCallPageState extends State<AudioCallPage> {
}),
),
callButton("calling/end_call", () async {
if (_isClosing) return;
_isClosing = true;
callPro.endCall(widget.sender.employeeNumber ?? "", widget.recipient.employeeNumber ?? "", moduleId: widget.moduleId);
Navigator.pop(context);
}, isEndCall: true, size: 68),

@ -31,6 +31,8 @@ class _VideoCallPageState extends State<VideoCallPage> {
bool isDialogOpen = false;
bool isLocalVideoFullscreen = false;
bool _isClosing = false;
@override
void initState() {
super.initState();
@ -69,11 +71,14 @@ class _VideoCallPageState extends State<VideoCallPage> {
}
void _closeCall() {
if (_isClosing) return;
_isClosing = true;
callPro.endSession();
if (mounted) Navigator.pop(context);
}
void _showFailure(String message, {required String title}) {
if (_isClosing) return;
callPro.endSession();
if (!mounted || isDialogOpen) return;
isDialogOpen = true;
@ -92,6 +97,8 @@ class _VideoCallPageState extends State<VideoCallPage> {
okTitle: "Go Back",
onTap: () {
Navigator.pop(cxt);
if (_isClosing) return;
_isClosing = true;
if (mounted) Navigator.pop(context);
}),
),
@ -308,6 +315,8 @@ class _VideoCallPageState extends State<VideoCallPage> {
),
callButton("calling/toggle_camera", () => callPro.switchCamera()),
callButton("calling/end_call", () {
if (_isClosing) return;
_isClosing = true;
callPro.endCall(widget.sender.employeeNumber ?? "", widget.recipient.employeeNumber ?? "", moduleId: widget.moduleId);
Navigator.pop(context);
}, isEndCall: true, size: 68),

@ -1,3 +1,4 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
@ -51,6 +52,7 @@ class _LandPageState extends State<LandPage> {
late List<Widget> _pages;
UserProvider? _userProvider;
bool isFCM = true;
StreamSubscription<CallEvent?>? _callEventSubscription;
@override
void initState() {
@ -58,6 +60,12 @@ class _LandPageState extends State<LandPage> {
super.initState();
}
@override
void dispose() {
_callEventSubscription?.cancel();
super.dispose();
}
void checkLocalAuth() async {
if (await Provider.of<SettingProvider>(context, listen: false).auth.isDeviceSupported()) {
SharedPreferences prefs = await SharedPreferences.getInstance();
@ -217,7 +225,8 @@ class _LandPageState extends State<LandPage> {
}
callingEvents(BuildContext context) {
FlutterCallkitIncoming.onEvent.listen((CallEvent? event) {
_callEventSubscription?.cancel();
_callEventSubscription = FlutterCallkitIncoming.onEvent.listen((CallEvent? event) {
if (event == null) return;
print("callingEvents:$event");
switch (event) {
@ -254,12 +263,23 @@ class _LandPageState extends State<LandPage> {
}
void _handleCallAccepted(CallKitParams callKitParams) async {
final callId = callKitParams.extra?["callId"]?.toString() ?? callKitParams.id;
// Ignore a duplicate accept for a call that is already being handled.
if (!FirebaseNotificationManger.beginAcceptedCall(callId)) return;
try {
await _openAcceptedCall(callKitParams);
} finally {
FirebaseNotificationManger.endAcceptedCall();
}
}
Future<void> _openAcceptedCall(CallKitParams callKitParams) async {
FirebaseNotificationManger.cancelMissedCallTimer(callKitParams.extra?["callId"] as String?);
// iOS: keep the CallKit call alive for the whole call (keeps the app running and the mic
// available when answered from the lock screen); it is ended after the call page closes.
if (Platform.isIOS) {
await FlutterCallkitIncoming.setCallConnected(callKitParams.id);
} else {
// iOS: the user's Accept already answered the call in CallKit, which keeps it alive (app keeps
// running, mic stays available on the lock screen); it is ended after the call page closes.
// Do NOT call setCallConnected here: on iOS it issues another CXAnswerCallAction, which makes
// the plugin emit ACTION_CALL_ACCEPT again -> this handler runs again -> call page opens in a loop.
if (!Platform.isIOS) {
FlutterCallkitIncoming.endCall(callKitParams.extra?["callId"]);
FlutterCallkitIncoming.endAllCalls();
}

@ -259,25 +259,33 @@ class _SplashPageState extends State<SplashPage> {
final extra = lastCall.extra ?? {};
final isVideoCall = extra["isVideoCall"] == true || extra["isVideoCall"]?.toString().toLowerCase() == 'true';
// Asks for mic (+camera) on Accept; offers Settings if permanently denied.
if (!await ensureCallPermissionsOnAccept(context, isVideoCall: isVideoCall)) {
await FlutterCallkitIncoming.endAllCalls();
await notifyCallerDeclined(extra, reason: 'PERMISSION_DENIED');
return;
}
// The same accepted call may also arrive via land_page's callkit listener - open it once.
if (!FirebaseNotificationManger.beginAcceptedCall(extra["callId"]?.toString() ?? callUuid ?? '')) return;
try {
// Asks for mic (+camera) on Accept; offers Settings if permanently denied.
if (!await ensureCallPermissionsOnAccept(context, isVideoCall: isVideoCall)) {
await FlutterCallkitIncoming.endAllCalls();
await notifyCallerDeclined(extra, reason: 'PERMISSION_DENIED');
return;
}
await FlutterCallkitIncoming.setCallConnected(callUuid!);
final sender = Participants(employeeNumber: extra["calleeEmployeeNumber"]?.toString());
final recipient = Participants(employeeNumber: extra["callerEmployeeNumber"]?.toString());
final moduleId = int.tryParse(extra["moduleId"]?.toString() ?? '') ?? 0;
final referenceID = int.tryParse(extra["referenceId"]?.toString() ?? '');
if (!mounted) return;
await Navigator.of(context).push(MaterialPageRoute(
builder: (_) => isVideoCall
? VideoCallPage(sender: sender, recipient: recipient, moduleId: moduleId, referenceID: referenceID, isAccept: true)
: AudioCallPage(sender: sender, recipient: recipient, moduleId: moduleId, referenceID: referenceID, isAccept: true)));
FlutterCallkitIncoming.endCall(lastCall.extra?["callId"]);
FlutterCallkitIncoming.endAllCalls();
// iOS: never call setCallConnected on an already-answered call - it issues another
// CXAnswerCallAction, which re-emits ACTION_CALL_ACCEPT and re-opens the call page.
if (!Platform.isIOS) await FlutterCallkitIncoming.setCallConnected(callUuid!);
final sender = Participants(employeeNumber: extra["calleeEmployeeNumber"]?.toString());
final recipient = Participants(employeeNumber: extra["callerEmployeeNumber"]?.toString());
final moduleId = int.tryParse(extra["moduleId"]?.toString() ?? '') ?? 0;
final referenceID = int.tryParse(extra["referenceId"]?.toString() ?? '');
if (!mounted) return;
await Navigator.of(context).push(MaterialPageRoute(
builder: (_) => isVideoCall
? VideoCallPage(sender: sender, recipient: recipient, moduleId: moduleId, referenceID: referenceID, isAccept: true)
: AudioCallPage(sender: sender, recipient: recipient, moduleId: moduleId, referenceID: referenceID, isAccept: true)));
FlutterCallkitIncoming.endCall(lastCall.extra?["callId"]);
FlutterCallkitIncoming.endAllCalls();
} finally {
FirebaseNotificationManger.endAcceptedCall();
}
}
} else {
await FlutterCallkitIncoming.endAllCalls();

Loading…
Cancel
Save