From 5670408c5ed89b90b8a50baa7d6d715d4347776e Mon Sep 17 00:00:00 2001
From: WaseemAbbasi22 <50428976+WaseemAbbasi22@users.noreply.github.com>
Date: Sun, 4 Oct 2026 12:56:03 +0300
Subject: [PATCH] webrtc implemented
---
android/app/src/main/AndroidManifest.xml | 2 +
assets/images/calling/speaker_disable.svg | 17 +
.../firebase_notification_manger.dart | 40 +-
.../chat/calling/audio_call_page.dart | 46 +-
.../cx_module/chat/calling/call_provider.dart | 591 +++++++++-------
.../chat/calling/video_call_page.dart | 337 ++++++++++
.../chat/calling/webrtc_service.dart | 635 ++++++++++++++++++
lib/modules/cx_module/chat/chat_page.dart | 21 +-
lib/modules/cx_module/chat/chat_provider.dart | 25 +-
lib/new_views/pages/land_page/land_page.dart | 56 +-
10 files changed, 1507 insertions(+), 263 deletions(-)
create mode 100644 assets/images/calling/speaker_disable.svg
create mode 100644 lib/modules/cx_module/chat/calling/video_call_page.dart
create mode 100644 lib/modules/cx_module/chat/calling/webrtc_service.dart
diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index 9efe0945..495496a9 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -7,6 +7,8 @@
+
+
diff --git a/assets/images/calling/speaker_disable.svg b/assets/images/calling/speaker_disable.svg
new file mode 100644
index 00000000..06acf25b
--- /dev/null
+++ b/assets/images/calling/speaker_disable.svg
@@ -0,0 +1,17 @@
+
+
+
\ No newline at end of file
diff --git a/lib/controllers/notification/firebase_notification_manger.dart b/lib/controllers/notification/firebase_notification_manger.dart
index 69b062be..bf84f13e 100644
--- a/lib/controllers/notification/firebase_notification_manger.dart
+++ b/lib/controllers/notification/firebase_notification_manger.dart
@@ -18,6 +18,7 @@ import 'package:test_sa/extensions/string_extensions.dart';
import 'package:test_sa/models/device/device_transfer.dart';
import 'package:test_sa/models/new_models/gas_refill_model.dart';
import 'package:test_sa/modules/cm_module/cm_detail_page.dart';
+import 'package:test_sa/modules/cx_module/chat/calling/call_provider.dart';
import 'package:test_sa/modules/cx_module/survey/survey_page.dart';
import 'package:test_sa/modules/pm_module/ppm_wo/ppm_details_page.dart';
import 'package:test_sa/modules/pm_module/recurrent_wo/recurrent_work_order_view.dart';
@@ -72,6 +73,15 @@ class FirebaseNotificationManger {
static FirebaseMessaging messaging = FirebaseMessaging.instance;
static String? token;
static String? voipToken;
+ static Timer? _missedCallTimer;
+ static String? _activeMissedCallId;
+ static void cancelMissedCallTimer([String? callId]) {
+ if (_activeMissedCallId == null) return;
+ if (callId != null && _activeMissedCallId != callId) return;
+ _missedCallTimer?.cancel();
+ _missedCallTimer = null;
+ _activeMissedCallId = null;
+ }
static Future getToken() async {
NotificationSettings settings = await messaging.requestPermission(alert: true, announcement: false, badge: true, carPlay: false, criticalAlert: false, provisional: false, sound: true);
@@ -357,7 +367,8 @@ Future handleDataMessage(Map messageData, bool isFromBack
print("handleDataMessage:moduleId:$moduleId");
WidgetsFlutterBinding.ensureInitialized();
- Timer? _callTimeoutTimer;
+ FirebaseNotificationManger.cancelMissedCallTimer();
+ FirebaseNotificationManger._activeMissedCallId = callId;
try {
final callKitParams = CallKitParams(
id: callId,
@@ -429,18 +440,33 @@ Future handleDataMessage(Map messageData, bool isFromBack
await FlutterCallkitIncoming.requestFullIntentPermission();
FlutterCallkitIncoming.unsilenceEvents();
FlutterCallkitIncoming.showCallkitIncoming(callKitParams);
- _callTimeoutTimer?.cancel();
- _callTimeoutTimer = Timer(const Duration(seconds: 30), () async {
+
+ FirebaseNotificationManger._missedCallTimer = Timer(const Duration(seconds: 30), () async {
+ if (FirebaseNotificationManger._activeMissedCallId != callId) return;
+
await FlutterCallkitIncoming.endCall(callId);
await FlutterCallkitIncoming.endAllCalls();
await FlutterCallkitIncoming.showMissCallNotification(callKitParams);
+ try {
+ final notifyProvider = CallProvider();
+ final connected = await notifyProvider.buildHubConnection(calleeEmployeeNumber);
+ if (connected) {
+ await notifyProvider.declineCall(
+ calleeEmployeeNumber,
+ callerEmployeeNumber,
+ moduleId: int.tryParse(moduleId ?? '') ?? 0,
+ reason: 'NO_ANSWER',
+ );
+ }
+ await notifyProvider.endSession();
+ } catch (_) {
+ // Ignore - this is a best-effort notification only.
+ }
- _callTimeoutTimer?.cancel();
- _callTimeoutTimer = null;
+ FirebaseNotificationManger.cancelMissedCallTimer(callId);
});
} catch (e, stackTrace) {
- _callTimeoutTimer?.cancel();
- _callTimeoutTimer = null;
+ FirebaseNotificationManger.cancelMissedCallTimer(callId);
}
}
}
diff --git a/lib/modules/cx_module/chat/calling/audio_call_page.dart b/lib/modules/cx_module/chat/calling/audio_call_page.dart
index e970a1c6..0d32ff55 100644
--- a/lib/modules/cx_module/chat/calling/audio_call_page.dart
+++ b/lib/modules/cx_module/chat/calling/audio_call_page.dart
@@ -60,7 +60,7 @@ class _AudioCallPageState extends State {
callPro.initAudioCallListeners();
if (widget.isAccept) {
- callPro.acceptCall(widget.sender.employeeNumber ?? "", widget.recipient.employeeNumber ?? "", false, moduleId: widget.moduleId,onSuccess: (String msg) {
+ callPro.acceptCall(widget.sender.employeeNumber ?? "", widget.recipient.employeeNumber ?? "", false, moduleId: widget.moduleId, onSuccess: (String msg) {
_closeCall();
}, onFailed: (String msg) {
_showFailure(msg, title: "Failed");
@@ -163,18 +163,40 @@ class _AudioCallPageState extends State {
Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
- callButton("calling/call_user_avatar", () {}, size: 85),
+ callButton(
+ "calling/call_user_avatar",
+ () {},
+ size: 85,
+ ),
24.height,
- InfoTextLabelWidget(label: chatProvider.recipient?.userName ?? "", textStyle: AppTextStyles.heading4.copyWith(fontWeight: FontWeight.w600), color: Colors.white),
+ InfoTextLabelWidget(
+ label: chatProvider.recipient?.userName ?? '',
+ textStyle: AppTextStyles.heading4.copyWith(
+ fontWeight: FontWeight.w600,
+ ),
+ color: Colors.white,
+ ),
4.height,
Selector(
- selector: (_, myModel) => myModel.currentStatus, // Selects only the userName
- builder: (_, currentStatus, __) => InfoTextLabelWidget(label: currentStatus, textStyle: AppTextStyles.heading6, color: Colors.white),
+ selector: (_, provider) {
+ return provider.currentStatus == "Connected" ? provider.formattedCallDuration : provider.currentStatus;
+ },
+ builder: (_, status, __) => InfoTextLabelWidget(
+ label: status,
+ textStyle: AppTextStyles.heading6,
+ color: Colors.white,
+ ),
),
4.height,
Selector(
- selector: (_, myModel) => myModel.isPeerMuted,
- builder: (_, isPeerMuted, __) => isPeerMuted ? InfoTextLabelWidget(label: "Mic Mute", textStyle: AppTextStyles.tinyFont, color: AppColor.red30) : const SizedBox(),
+ selector: (_, provider) => provider.isPeerMuted,
+ builder: (_, isPeerMuted, __) => isPeerMuted
+ ? InfoTextLabelWidget(
+ label: "Mic Mute",
+ textStyle: AppTextStyles.tinyFont,
+ color: AppColor.red30,
+ )
+ : const SizedBox(),
),
],
),
@@ -192,7 +214,15 @@ class _AudioCallPageState extends State {
callPro.endCall(widget.sender.employeeNumber ?? "", widget.recipient.employeeNumber ?? "", moduleId: widget.moduleId);
Navigator.pop(context);
}, isEndCall: true, size: 68),
- callButton("calling/speaker_enable", () {}),
+ Selector(
+ selector: (_, provider) => provider.isSpeakerOn,
+ builder: (_, isSpeakerOn, __) => callButton(
+ isSpeakerOn ? "calling/speaker_enable" : "calling/speaker_disable", //need to replace with speaker_disable icon
+ () {
+ callPro.toggleSpeaker();
+ },
+ ),
+ ),
],
)
],
diff --git a/lib/modules/cx_module/chat/calling/call_provider.dart b/lib/modules/cx_module/chat/calling/call_provider.dart
index d3397a8a..5bab2d28 100644
--- a/lib/modules/cx_module/chat/calling/call_provider.dart
+++ b/lib/modules/cx_module/chat/calling/call_provider.dart
@@ -1,8 +1,5 @@
+import 'dart:async';
import 'dart:convert';
-import 'dart:math';
-import 'dart:ui';
-
-import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_webrtc/flutter_webrtc.dart';
import 'package:signalr_netcore/http_connection_options.dart';
@@ -12,40 +9,61 @@ import 'package:signalr_netcore/itransport.dart';
import 'package:test_sa/controllers/api_routes/urls.dart';
import 'package:test_sa/modules/cx_module/chat/api_client.dart';
import 'package:http/http.dart' as http;
-
+import 'package:test_sa/modules/cx_module/chat/calling/webrtc_service.dart';
+import 'package:uuid/uuid.dart';
import '../chat_provider.dart';
import '../model/chat_login_response_model.dart';
HubConnection? callHubConnection;
+void _logError(String label, Object e) {
+ if (!kDebugMode) return;
+ try {
+ print('$label: $e');
+ } catch (_) {
+ print('$label: ${e.runtimeType}');
+ }
+}
+
class CallProvider with ChangeNotifier, DiagnosticableTreeMixin {
- ///////////////////// Web RTC Video Calling //////////////////////
- // Video Call
- late RTCPeerConnection _peerConnection;
- RTCVideoRenderer _localVideoRenderer = RTCVideoRenderer();
- final RTCVideoRenderer _remoteRenderer = RTCVideoRenderer();
+ final WebRTCService _webRTCService = WebRTCService();
- MediaStream? _localStream;
- MediaStream? _remoteStream;
+ WebRTCService get webRTCService => _webRTCService;
bool isMuted = false;
bool isSpeakerOn = false;
bool isCameraOn = true;
bool isPeerMuted = false;
bool isPeerCameraOn = true;
-
+ Timer? _callDurationTimer;
+ Timer? _noAnswerTimer;
+ Duration callDuration = Duration.zero;
bool isLoading = false;
String currentStatus = "Connecting...";
bool _ownsConnection = false;
+ String? _currentCallId;
void _setStatus(String status) {
if (currentStatus == status) return;
currentStatus = status;
notifyListeners();
}
-
+ void _startCallDurationTimer() {
+ _callDurationTimer?.cancel();
+ callDuration = Duration.zero;
+
+ _callDurationTimer = Timer.periodic(
+ const Duration(seconds: 1),
+ (_) {
+ callDuration = Duration(
+ seconds: callDuration.inSeconds + 1,
+ );
+ notifyListeners();
+ },
+ );
+ }
Future _disposeConnection() async {
final HubConnection? connection = callHubConnection;
final bool owns = _ownsConnection;
@@ -55,24 +73,36 @@ class CallProvider with ChangeNotifier, DiagnosticableTreeMixin {
try {
await connection.stop();
} catch (e) {
- if (kDebugMode) {
- print('⚠️ Error closing call SignalR connection: $e');
- }
+ _logError('Error closing call SignalR connection', e);
}
}
Future endSession() async {
+ _cancelNoAnswerTimer();
stopListeners();
onFailed = null;
onSuccess = null;
isMuted = false;
isPeerMuted = false;
+ // `CallProvider` is a long-lived singleton (registered once in main.dart),
+ // so without resetting these here, toggling the camera off near the end
+ // of one video call would leak into the *next* call, making it look like
+ // the camera defaults to "off" even though permission is granted.
+ isCameraOn = true;
+ isPeerCameraOn = true;
currentStatus = "Connecting...";
+ _callDurationTimer?.cancel();
+ _callDurationTimer = null;
+ callDuration = Duration.zero;
+ await _webRTCService.dispose();
await _disposeConnection();
}
@override
void dispose() {
+ _cancelNoAnswerTimer();
+ _callDurationTimer?.cancel();
+ _webRTCService.dispose();
_disposeConnection().then((_) {}).catchError((error) {});
super.dispose();
}
@@ -116,46 +146,95 @@ class CallProvider with ChangeNotifier, DiagnosticableTreeMixin {
final String query = uri.query.isEmpty ? '' : '?${uri.query}';
return '${uri.scheme}://${uri.host}:$port${uri.path}$query';
}
+ String get formattedCallDuration {
+ final hours = callDuration.inHours;
+ final minutes = callDuration.inMinutes.remainder(60);
+ final seconds = callDuration.inSeconds.remainder(60);
+
+ if (hours > 0) {
+ return '${hours.toString().padLeft(2, '0')}:'
+ '${minutes.toString().padLeft(2, '0')}:'
+ '${seconds.toString().padLeft(2, '0')}';
+ }
+
+ return '${minutes.toString().padLeft(2, '0')}:'
+ '${seconds.toString().padLeft(2, '0')}';
+ }
+
+ Future _initializeWebRTC() async {
+ _webRTCService.onIceCandidate = _handleLocalIceCandidate;
+
+ _webRTCService.onRemoteStream = (_) => notifyListeners();
+
+ _webRTCService.onIceConnectionStateChange = (RTCIceConnectionState state) {
+ switch (state) {
+ case RTCIceConnectionState.RTCIceConnectionStateConnected:
+ case RTCIceConnectionState.RTCIceConnectionStateCompleted:
+ if (currentStatus != "Connected") {
+ _setStatus("Connected");
+ _startCallDurationTimer();
+ }
+ break;
+ case RTCIceConnectionState.RTCIceConnectionStateFailed:
+ _setStatus("Call failed");
+ break;
+ default:
+ break;
+ }
+ };
+
+ _webRTCService.onCallEnded = () {
+ if (currentStatus != "Call Ended" && currentStatus != "Call failed") {
+ _setStatus("Call failed");
+ onFailed?.call("The call connection was lost.");
+ }
+ };
+
+ if (isVideo == true) {
+ // Always start a fresh video call with both cameras assumed "on",
+ // regardless of whatever state was left over from a previous session
+ // on this singleton CallProvider (see endSession()).
+ isCameraOn = true;
+ isPeerCameraOn = true;
+ await _webRTCService.initializeForVideoCall();
+ _webRTCService.setCameraEnabled(true);
+ notifyListeners();
+ } else {
+ await _webRTCService.initializeForAudioCall();
+ }
+ }
Future buildHubConnection(String employeeNumber, {HubConnection? hubC}) async {
_setStatus("Connecting...");
try {
- if (hubC != null) {
- final bool reused = await _adoptExistingConnection(hubC);
- if (reused) return true;
- if (kDebugMode) {
- print('ℹ️ Shared chat hub not usable (state: ${hubC.state}), opening a dedicated call connection.');
+ if (hubC != null) {
+ final bool reused = await _adoptExistingConnection(hubC);
+ if (reused) return true;
+ if (kDebugMode) print('Shared chat hub not usable (state: ${hubC.state}), opening a dedicated call connection.');
}
- }
- await _disposeConnection();
+ await _disposeConnection();
- final http.Response response = await ApiClient().postJsonForResponse(
- URLs.chatSdkToken,
- {"apiKey": URLs.chatApiKey, "employeeNumber": employeeNumber, "voIPToken": null},
- );
- if (response.statusCode != 200) {
- if (kDebugMode) {
- print('⚠️ Call token request failed: HTTP ${response.statusCode}');
+ final http.Response response = await ApiClient().postJsonForResponse(
+ URLs.chatSdkToken,
+ {"apiKey": URLs.chatApiKey, "employeeNumber": employeeNumber, "voIPToken": null},
+ );
+ if (response.statusCode != 200) {
+ if (kDebugMode) print('Call token request failed: HTTP ${response.statusCode}');
+ return false;
}
- return false;
- }
- final ChatLoginResponse chatResponse = ChatLoginResponse.fromJson(jsonDecode(response.body));
- if (chatResponse.token == null || chatResponse.token!.isEmpty) {
- if (kDebugMode) {
- print('⚠️ Call token request returned an empty token.');
+ final ChatLoginResponse chatResponse = ChatLoginResponse.fromJson(jsonDecode(response.body));
+ if (chatResponse.token == null || chatResponse.token!.isEmpty) {
+ if (kDebugMode) print('Call token request returned an empty token.');
+ return false;
}
- return false;
- }
- callHubConnection = await getHubConnection(chatResponse.token!);
- _ownsConnection = true;
- await callHubConnection!.start();
- return callHubConnection?.state == HubConnectionState.Connected;
+ callHubConnection = await getHubConnection(chatResponse.token!);
+ _ownsConnection = true;
+ await callHubConnection!.start();
+ return callHubConnection?.state == HubConnectionState.Connected;
} catch (e) {
- if (kDebugMode) {
- print('⚠️ Error building call SignalR connection: $e');
- }
+ _logError('Error building call SignalR connection', e);
await _disposeConnection();
return false;
}
@@ -186,9 +265,7 @@ class CallProvider with ChangeNotifier, DiagnosticableTreeMixin {
return true;
}
} catch (e) {
- if (kDebugMode) {
- print('⚠️ Could not restart shared chat hub for call: $e');
- }
+ _logError('Could not restart shared chat hub for call', e);
}
return false;
default:
@@ -216,7 +293,7 @@ class CallProvider with ChangeNotifier, DiagnosticableTreeMixin {
hub.on("OnCallAcceptedAsync", onCallAcceptedAsync);
hub.on("OnIceCandidateAsync", onIceCandidateAsync);
hub.on("OnOfferAsync", onOfferAsync);
- hub.on("OnAnswerOffer", onAnswerOffer);
+ hub.on("OnAnswerOfferAsync", onAnswerOffer);
hub.on("OnHangUpAsync", onHangUpAsync);
hub.on("OnCallDeclinedAsync", onCallDeclinedAsync);
hub.on('OnAudioToggle', onAudioToggle);
@@ -229,104 +306,6 @@ class CallProvider with ChangeNotifier, DiagnosticableTreeMixin {
hub.on('OnCameraToggle', onCameraToggle);
}
- //Video Constraints
- var videoConstraints = {
- "video": {
- "mandatory": {
- "width": {"min": 320},
- "height": {"min": 180}
- },
- "optional": [
- {
- "width": {"max": 1280}
- },
- {"frameRate": 25},
- {"facingMode": "user"}
- ]
- },
- "frameRate": 25,
- "width": 420, //420,//640,//1280,
- "height": 240 //240//480//720
- };
-
- // Audio Constraints
- var audioConstraints = {
- "sampleRate": 8000,
- "sampleSize": 16,
- "channelCount": 2,
- "echoCancellation": true,
- "audio": true,
- };
-
- Future _createPeerConnection() async {
- Map configuration = {
- "iceServers": [
- {"urls": 'stun:15.185.116.59:3478'},
- {
- "urls": "turn:15.185.116.59:3479",
- "username": "admin",
- "credential": "admin",
- },
- {"urls": "stun:stun.l.google.com:19302"},
- {"urls": "stun:stun1.l.google.com:19302"},
- ]
- };
-
- Map offerSdpConstraints = {
- "mandatory": {
- "OfferToReceiveAudio": true,
- "OfferToReceiveVideo": true,
- },
- "optional": [],
- };
-
- RTCPeerConnection pc = await createPeerConnection(configuration, offerSdpConstraints);
- // if (pc != null) print(pc);
- //pc.addStream(widget.localStream);
-
- pc.onIceCandidate = (e) {
- if (e.candidate != null) {
- print(json.encode({
- 'candidate': e.candidate.toString(),
- 'sdpMid': e.sdpMid.toString(),
- 'sdpMlineIndex': e.sdpMLineIndex,
- }));
- }
- };
- pc.onIceConnectionState = (e) {
- print(e);
- };
- pc.onAddStream = (stream) {
- print('addStream: ' + stream.id);
- _remoteRenderer.srcObject = stream;
- };
- return pc;
- }
-
- void init() {
- initRenderers();
- _createPeerConnection().then((pc) {
- _peerConnection = pc;
- // _setRemoteDescription(widget.info);
- });
- }
-
- void initRenderers() {
- _localVideoRenderer.initialize();
- _remoteRenderer.initialize();
- initLocalCamera();
- }
-
- void initLocalCamera() async {
- _localStream = await navigator.mediaDevices.getUserMedia({'video': true, 'audio': true});
- _localVideoRenderer.srcObject = _localStream;
- // _localVideoRenderer.srcObject = await navigator.mediaDevices
- // .getUserMedia({'video': true, 'audio': true});
- print('this source Object');
- print('this suarce ${_localVideoRenderer.srcObject != null}');
- notifyListeners();
- }
-
String? senderN;
String? recipientN;
bool? isVideo;
@@ -337,10 +316,11 @@ class CallProvider with ChangeNotifier, DiagnosticableTreeMixin {
Function(String message)? onSuccess;
void startCall(String? senderN, String? recipientN, bool? isVideo, int? moduleId, int? referenceID, {Function(String message)? onFailed, Function(String message)? onSuccess}) async {
+ _currentCallId = const Uuid().v4();
this.senderN = senderN;
this.recipientN = recipientN;
this.isVideo = isVideo;
- // this.moduleId = moduleId;
+ if (moduleId != null) this.moduleId = moduleId;
this.referenceID = referenceID;
this.onFailed = onFailed;
this.onSuccess = onSuccess;
@@ -349,41 +329,75 @@ class CallProvider with ChangeNotifier, DiagnosticableTreeMixin {
return;
}
try {
+ await _initializeWebRTC();
await invoke(invokeMethod: "CallUserAsync", currentUserID: senderN, targetUserID: recipientN, isVideoCall: isVideo ?? false, data: [this.moduleId, referenceID]);
_setStatus("Ringing...");
+ _startNoAnswerTimer();
} catch (e) {
- if (kDebugMode) {
- print('⚠️ CallUserAsync failed: $e');
- }
+ _logError('CallUserAsync failed', e);
_setStatus("Call failed");
onFailed?.call("Could not reach the call service. Please check your connection and try again.");
}
}
+ /// Starts (or restarts) the outgoing "no answer" timeout. If the callee
+ /// hasn't accepted/declined within the window, we fail the call locally
+ /// and best-effort notify the backend so the callee's ringing UI clears.
+ void _startNoAnswerTimer() {
+ _noAnswerTimer?.cancel();
+ _noAnswerTimer = Timer(const Duration(seconds: 45), _handleNoAnswerTimeout);
+ }
+
+ void _cancelNoAnswerTimer() {
+ _noAnswerTimer?.cancel();
+ _noAnswerTimer = null;
+ }
+
+ Future _handleNoAnswerTimeout() async {
+ _noAnswerTimer = null;
+
+ // The call may have already connected/ended through another path.
+ if (currentStatus != "Ringing...") return;
+
+ _setStatus("Call failed");
+
+ try {
+ if (senderN != null && recipientN != null) {
+ await declineCall(senderN!, recipientN!, moduleId: moduleId, reason: 'NO_ANSWER');
+ }
+ } catch (e) {
+ _logError('Failed to notify backend of no-answer timeout', e);
+ }
+
+ onFailed?.call("No Answer, User did not answer your call.");
+ }
+
void acceptCall(String senderN, String recipientN, bool isVideo, {required int moduleId, Function(String message)? onFailed, Function(String message)? onSuccess}) async {
this.senderN = senderN;
this.recipientN = recipientN;
+ this.isVideo = isVideo;
this.onFailed = onFailed;
this.onSuccess = onSuccess;
try {
+ _setStatus("Connecting...");
await invoke(invokeMethod: "AnswerCallAsync", currentUserID: senderN, targetUserID: recipientN, isVideoCall: isVideo, data: [this.moduleId]);
- _setStatus("Connected");
} catch (e) {
- if (kDebugMode) {
- print('⚠️ AnswerCallAsync failed: $e');
- }
+ _logError('AnswerCallAsync failed', e);
_setStatus("Call failed");
onFailed?.call("Could not join the call. Please try again.");
}
}
- Future declineCall(String senderN, String recipientN, {required int moduleId}) async {
+ Future declineCall(String senderN, String recipientN, {required int moduleId, String? reason}) async {
try {
- await invoke(invokeMethod: "CallDeclinedAsync", currentUserID: senderN, targetUserID: recipientN, isVideoCall: false, data: [this.moduleId]);
+ await invoke(
+ invokeMethod: "CallDeclinedAsync",
+ currentUserID: senderN,
+ targetUserID: recipientN,
+ data: reason != null ? [moduleId, reason] : [moduleId],
+ );
} catch (e) {
- if (kDebugMode) {
- print('⚠️ CallDeclinedAsync failed: $e');
- }
+ _logError('CallDeclinedAsync failed', e);
}
}
@@ -391,106 +405,206 @@ class CallProvider with ChangeNotifier, DiagnosticableTreeMixin {
try {
await invoke(invokeMethod: "HangUpAsync", currentUserID: senderN, targetUserID: recipientN, data: [moduleId]);
} catch (e) {
- if (kDebugMode) {
- print('⚠️ HangUpAsync failed: $e');
- }
+ _logError('HangUpAsync failed', e);
}
}
- void toggleMute() async {
+ Future toggleMute() async {
if (senderN == null || recipientN == null) return;
+
+ final newMutedState = !isMuted;
+
+ _webRTCService.setMicrophoneMuted(newMutedState);
+
+ isMuted = newMutedState;
+ notifyListeners();
+
try {
- await invoke(invokeMethod: "AudioToggle", currentUserID: senderN!, targetUserID: recipientN!);
- isMuted = !isMuted;
- notifyListeners();
+ await invoke(
+ invokeMethod: "AudioToggle",
+ currentUserID: senderN!,
+ targetUserID: recipientN!,
+ );
} catch (e) {
- if (kDebugMode) {
- print('⚠️ AudioToggle failed: $e');
- }
+ _logError('AudioToggle failed', e);
}
}
- void checkCall(Map message) {
- switch (message["callStatus"]) {
- case 'connected':
- {}
- break;
- case 'offer':
- {}
- break;
- case 'accept':
- {}
- break;
- case 'candidate':
- {}
- break;
- case 'bye':
- {}
- break;
- case 'leave':
- {}
- break;
+ Future toggleCamera() async {
+ if (senderN == null || recipientN == null) return;
+
+ final newCameraState = !isCameraOn;
+
+ _webRTCService.setCameraEnabled(newCameraState);
+
+ isCameraOn = newCameraState;
+ notifyListeners();
+
+ try {
+ await invoke(
+ invokeMethod: "CameraToggle",
+ currentUserID: senderN!,
+ targetUserID: recipientN!,
+ );
+ } catch (e) {
+ _logError('CameraToggle failed', e);
}
}
+ Future switchCamera() async {
+ await _webRTCService.switchCamera();
+ }
+
//// Listeners Methods ////
+ Future onCallAcceptedAsync(List