Files
Siro/siro_driver/lib/controller/voice_call_controller.dart
2026-06-09 08:40:31 +03:00

750 lines
27 KiB
Dart

import 'dart:async';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_webrtc/flutter_webrtc.dart' as rtc;
import 'package:get/get.dart' hide Response;
import 'package:permission_handler/permission_handler.dart';
import 'package:just_audio/just_audio.dart';
import '../../constant/box_name.dart';
import '../../constant/links.dart';
import '../../main.dart';
import '../../print.dart';
import '../../services/signaling_service.dart';
import '../../views/widgets/voice_call_bottom_sheet.dart';
import 'functions/crud.dart';
// EN: Enum representing the different states of a voice call.
// AR: تعداد يمثل الحالات المختلفة للمكالمة الصوتية.
enum VoiceCallState { idle, dialing, ringing, connecting, active, ended }
class VoiceCallController extends GetxController with WidgetsBindingObserver {
// EN: Instance of the signaling service to manage WebSocket communication.
// AR: مثيل لخدمة الإشارات لإدارة الاتصال عبر الـ WebSocket.
final SignalingService _signaling = SignalingService();
// --- Observable Variables (GetX) / المتغيرات التفاعلية ---
// EN: Current state of the call.
// AR: الحالة الحالية للمكالمة.
var state = VoiceCallState.idle.obs;
// EN: Unique identifier for the WebRTC session.
// AR: المعرف الفريد لجلسة الاتصال.
var sessionId = "".obs;
// EN: ID of the current active ride.
// AR: معرف الرحلة النشطة الحالية.
var rideId = "".obs;
// EN: Name of the other party (Driver/Passenger).
// AR: اسم الطرف الآخر في المكالمة (سائق/راكب).
var remoteName = "User".obs;
// EN: Microphone mute status.
// AR: حالة كتم الميكروفون.
var isMuted = false.obs;
// EN: Speakerphone status.
// AR: حالة مكبر الصوت الخارجي.
var isSpeakerOn = false.obs;
// EN: Timer countdown variable, starts from 60 seconds.
// AR: متغير العد التنازلي للمؤقت، يبدأ من 60 ثانية.
var elapsedSeconds = 60.obs;
// EN: Error message to display in UI when call setup fails.
// AR: رسالة الخطأ لعرضها في الواجهة عندما يفشل إعداد المكالمة.
var errorMessage = "".obs;
// --- Core State Variables / متغيرات الحالة الأساسية ---
// EN: Flag to determine if the current user initiated the call.
// AR: مؤشر لتحديد ما إذا كان المستخدم الحالي هو من بدأ المكالمة.
bool isCaller = false;
// EN: ID of the current user.
// AR: معرف المستخدم الحالي.
String currentUserId = "";
// --- WebRTC Internal Variables / متغيرات WebRTC الداخلية ---
// EN: The main connection object between peers.
// AR: كائن الاتصال الرئيسي بين الطرفين.
rtc.RTCPeerConnection? _peerConnection;
// EN: The local audio stream captured from the microphone.
// AR: دفق الصوت المحلي الملتقط من الميكروفون.
rtc.MediaStream? _localStream;
// EN: Timer to enforce the 60-second call limit.
// AR: مؤقت لفرض حد الـ 60 ثانية للمكالمة.
Timer? _countdownTimer;
// EN: Timer to hang up if the call is not answered within 30 seconds.
// AR: مؤقت لإنهاء المكالمة إذا لم يتم الرد خلال 30 ثانية.
Timer? _ringingTimeoutTimer;
// EN: Flag to indicate if the peer connection is currently attempting ICE reconnection.
// AR: مؤشر يوضح ما إذا كان الاتصال يحاول إعادة بناء مسارات الشبكة حالياً.
bool _isReconnecting = false;
Timer? _reconnectTimer;
List<dynamic> _dynamicIceServers = [];
AudioPlayer? _ringtonePlayer;
void _startRingtone() async {
try {
_ringtonePlayer ??= AudioPlayer();
await _ringtonePlayer!.setAsset('assets/order.mp3');
await _ringtonePlayer!.setLoopMode(LoopMode.one);
_ringtonePlayer!.play();
} catch (e) {
Log.print("Error playing ringtone: $e");
}
}
void _stopRingtone() {
try {
_ringtonePlayer?.stop();
} catch (e) {
Log.print("Error stopping ringtone: $e");
}
}
@override
void onInit() {
super.onInit();
// EN: Add lifecycle observer.
// AR: إضافة مراقب لدورة حياة التطبيق.
WidgetsBinding.instance.addObserver(this);
// EN: Initialize WebSocket signaling listeners.
// AR: تهيئة مستمعي إشارات الـ WebSocket.
_initSignalingCallbacks();
}
// EN: Lifecycle hook: handle app switching background/foreground.
// AR: معالجة انتقال التطبيق إلى الخلفية أو العودة للواجهة.
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
Log.print("VoiceCall: didChangeAppLifecycleState -> $state");
if (state == AppLifecycleState.paused) {
Log.print(
"WARNING: App is in background. Microphone access might be suspended by the OS.");
} else if (state == AppLifecycleState.resumed) {
Log.print("App resumed. Verifying WebRTC connection health.");
if (this.state.value == VoiceCallState.active) {
_ensureMicrophoneActive();
_attemptIceRestart();
}
}
}
// EN: Registers all event listeners for the signaling server.
// AR: تسجيل جميع مستمعي الأحداث لخادم الإشارات.
void _initSignalingCallbacks() {
// EN: Triggered when successfully connected to the signaling server.
// AR: يُستدعى عند الاتصال بنجاح بخادم الإشارات.
_signaling.onConnected = (iceServers) {
Log.print("WebRTC Signaling Connected & Authenticated");
_dynamicIceServers = iceServers;
};
// EN: Triggered when the WebSocket connection drops.
// AR: يُستدعى عند انقطاع اتصال الـ WebSocket.
_signaling.onDisconnected = (reason) {
Log.print("WebRTC Signaling Disconnected: $reason");
if (state.value != VoiceCallState.idle) {
_endCallInternal("signaling_disconnected");
}
};
// EN: Triggered when the remote user joins the room.
// AR: يُستدعى عند انضمام الطرف الآخر إلى غرفة الاتصال.
_signaling.onParticipantJoined = () async {
Log.print("Remote participant joined signaling session");
// EN: If we are the caller, initiate the WebRTC handshake by creating an Offer.
// AR: إذا كنا نحن المتصل، نبدأ مصافحة WebRTC بإنشاء عرض (Offer).
if (isCaller && state.value == VoiceCallState.dialing) {
state.value = VoiceCallState.connecting;
await _createOffer();
}
};
// EN: Triggered when an SDP Offer is received from the remote peer.
// AR: يُستدعى عند استلام عرض اتصال (Offer) من الطرف الآخر.
_signaling.onOffer = (sdpMap) async {
Log.print("Received WebRTC SDP Offer");
if (!isCaller) {
state.value = VoiceCallState.connecting;
await _initializePeerConnection();
// EN: Set the remote peer's settings.
// AR: تعيين إعدادات الطرف الآخر.
final description =
rtc.RTCSessionDescription(sdpMap['sdp'], sdpMap['type']);
await _peerConnection!.setRemoteDescription(description);
// EN: Respond with an Answer.
// AR: الرد بإجابة (Answer).
await _createAnswer();
}
};
// EN: Triggered when an SDP Answer is received.
// AR: يُستدعى عند استلام إجابة (Answer) من الطرف الآخر.
_signaling.onAnswer = (sdpMap) async {
Log.print("Received WebRTC SDP Answer");
if (isCaller && _peerConnection != null) {
final description =
rtc.RTCSessionDescription(sdpMap['sdp'], sdpMap['type']);
await _peerConnection!.setRemoteDescription(description);
}
};
// EN: Triggered when ICE candidates (Network routing info) are exchanged.
// AR: يُستدعى عند تبادل مسارات الشبكة (ICE Candidates) لتأسيس الاتصال.
_signaling.onIceCandidate = (candidateMap) async {
Log.print("Received Remote ICE Candidate");
if (_peerConnection != null) {
final candidate = rtc.RTCIceCandidate(
candidateMap['candidate'],
candidateMap['sdpMid'],
candidateMap['sdpMLineIndex'],
);
await _peerConnection!.addCandidate(candidate);
}
};
// EN: Triggered when a hangup event is received from the server.
// AR: يُستدعى عند استلام حدث إنهاء المكالمة من السيرفر.
_signaling.onCallEnded = (reason) {
Log.print("WebRTC Call Ended: $reason");
_endCallInternal(reason);
};
}
// --- CALL LIFECYCLE / دورة حياة المكالمة ---
// EN: Initiates an outgoing call.
// AR: يبدأ مكالمة صادرة.
Future<void> startCall({
required String rideIdVal,
required String driverId,
required String passengerId,
required String remoteNameVal,
}) async {
if (state.value != VoiceCallState.idle) return;
// EN: Setup call variables.
// AR: إعداد متغيرات المكالمة.
state.value = VoiceCallState.dialing;
isCaller = true;
currentUserId = driverId;
rideId.value = rideIdVal;
remoteName.value = remoteNameVal;
isMuted.value = false;
isSpeakerOn.value = false;
elapsedSeconds.value = 60;
_isReconnecting = false;
errorMessage.value = "";
_showCallBottomSheet();
HapticFeedback.vibrate();
try {
// 1. EN: Request Microphone Permission / AR: طلب صلاحية الميكروفون
if (!GetPlatform.isIOS) {
final permissionStatus = await Permission.microphone.request();
if (!permissionStatus.isGranted) {
errorMessage.value =
"Microphone permission is required for voice calls".tr;
_endCallInternal("permission_denied");
return;
}
}
// 2. EN: Call PHP Backend to create Node.js session & notify Passenger via FCM.
// AR: استدعاء واجهة PHP لإنشاء الجلسة على Node.js وإشعار الراكب عبر FCM.
final response = await CRUD().post(
link: "${AppLink.server}/ride/call/driver/create_call_session.php",
payload: {'ride_id': rideIdVal},
);
if (response == null ||
response == 'failure' ||
response['status'] != 'success') {
errorMessage.value =
"Failed to initiate call session. Please try again.".tr;
_endCallInternal("session_creation_failed");
return;
}
final data = response['data'];
sessionId.value = data['session_id'];
// 3. EN: Connect to WebRTC signaling server / AR: الاتصال بخادم الإشارات
await _signaling.connect(sessionId.value, currentUserId);
// 4. EN: Initialize Local WebRTC Audio Stream / AR: تهيئة دفق الصوت المحلي
await _initializeLocalStream();
// 5. EN: Start Ringing Timeout Timer (30s max wait for passenger to answer).
// AR: بدء مؤقت الرنين (أقصى انتظار 30 ثانية لرد الراكب).
_ringingTimeoutTimer = Timer(const Duration(seconds: 30), () {
if (state.value == VoiceCallState.dialing) {
_signaling.send("hangup", {"reason": "no_answer"});
_endCallInternal("no_answer");
}
});
} catch (e) {
Log.print("Error starting WebRTC call: $e");
final errStr = e.toString().toLowerCase();
if (errStr.contains("permission") || errStr.contains("denied")) {
errorMessage.value =
"Microphone permission is required for voice calls".tr;
} else {
errorMessage.value = "Error starting voice call".tr;
}
_endCallInternal("error");
}
}
// EN: Handles incoming call requests via FCM/Socket.
// AR: معالجة طلبات المكالمات الواردة.
Future<void> receiveCall({
required String sessionIdVal,
required String remoteNameVal,
required String rideIdVal,
}) async {
// EN: If already in a call, send busy signal.
// AR: إذا كان في مكالمة بالفعل، إرسال إشارة مشغول.
if (state.value != VoiceCallState.idle) {
_signaling.send("hangup", {"reason": "busy"});
return;
}
state.value = VoiceCallState.ringing;
isCaller = false;
currentUserId = box.read(BoxName.driverID).toString();
sessionId.value = sessionIdVal;
rideId.value = rideIdVal;
remoteName.value = remoteNameVal;
isMuted.value = false;
isSpeakerOn.value = false;
elapsedSeconds.value = 60;
_isReconnecting = false;
errorMessage.value = "";
_showCallBottomSheet();
_startRingtone();
HapticFeedback.vibrate();
// EN: Max 30s ringing timeout for receiver before auto-decline.
// AR: أقصى مدة للرنين 30 ثانية قبل الرفض التلقائي.
_ringingTimeoutTimer = Timer(const Duration(seconds: 30), () {
if (state.value == VoiceCallState.ringing) {
declineCall();
}
});
}
// EN: Accepts the incoming call.
// AR: قبول المكالمة الواردة.
Future<void> acceptCall() async {
if (state.value != VoiceCallState.ringing) return;
_ringingTimeoutTimer?.cancel();
_stopRingtone();
state.value = VoiceCallState.connecting;
errorMessage.value = "";
try {
// EN: Check Mic permissions / AR: التحقق من صلاحيات الميكروفون
if (!GetPlatform.isIOS) {
final permissionStatus = await Permission.microphone.request();
if (!permissionStatus.isGranted) {
errorMessage.value =
"Microphone permission is required for voice calls".tr;
declineCall();
return;
}
}
await _signaling.connect(sessionId.value, currentUserId);
await _initializeLocalStream();
// EN: Notify caller we accepted / AR: إشعار المتصل بأننا قبلنا المكالمة
_signaling.send("join", {});
} catch (e) {
Log.print("Error accepting call: $e");
final errStr = e.toString().toLowerCase();
if (errStr.contains("permission") || errStr.contains("denied")) {
errorMessage.value =
"Microphone permission is required for voice calls".tr;
} else {
errorMessage.value = "Error connecting call".tr;
}
declineCall();
}
}
// EN: Declines an incoming call.
// AR: رفض المكالمة الواردة.
void declineCall() {
_ringingTimeoutTimer?.cancel();
_stopRingtone();
_signaling.send("hangup", {"reason": "declined"});
_endCallInternal("declined");
}
// EN: Ends an active or dialing call.
// AR: إنهاء المكالمة النشطة أو الجاري الاتصال بها.
void hangup() {
_signaling.send("hangup", {"reason": "normal"});
_endCallInternal("hangup");
}
// --- WEBRTC CORE HELPERS / دوال WebRTC الأساسية ---
// EN: Captures the audio from the microphone with optimization constraints.
// AR: التقاط الصوت من الميكروفون مع قيود تحسين الجودة (إلغاء الصدى والضوضاء).
Future<void> _initializeLocalStream() async {
final Map<String, dynamic> mediaConstraints = {
'audio': {
'echoCancellation': true,
'noiseSuppression': true,
'autoGainControl': true,
},
'video': false, // EN: Audio only / AR: صوت فقط
};
_localStream =
await rtc.navigator.mediaDevices.getUserMedia(mediaConstraints);
rtc.Helper.setSpeakerphoneOn(isSpeakerOn.value);
}
// EN: Verifies local microphone stream health on app resume and recreates/replaces track if suspended.
// AR: التحقق من سلامة مسار الميكروفون المحلي عند استئناف التطبيق وإعادة إنشائه إذا تم تعليقه.
Future<void> _ensureMicrophoneActive() async {
if (_localStream == null || _peerConnection == null) return;
bool needsRecreation = false;
if (_localStream!.active == false) {
needsRecreation = true;
} else {
for (var track in _localStream!.getAudioTracks()) {
if (!track.enabled && !isMuted.value) {
needsRecreation = true;
break;
}
}
}
if (needsRecreation) {
Log.print(
"Local audio track ended or disabled. Recreating local stream...");
try {
_localStream?.getTracks().forEach((track) => track.stop());
_localStream?.dispose();
_localStream = null;
await _initializeLocalStream();
final senders = await _peerConnection!.getSenders();
for (var sender in senders) {
final track = sender.track;
if (track != null && track.kind == 'audio') {
final newTracks = _localStream?.getAudioTracks();
if (newTracks != null && newTracks.isNotEmpty) {
await sender.replaceTrack(newTracks.first);
Log.print(
"Replaced suspended/ended audio track with a new active one.");
}
break;
}
}
} catch (e) {
Log.print("Error recreating local stream on resume: $e");
}
} else {
_localStream!.getAudioTracks().forEach((track) {
track.enabled = !isMuted.value;
});
}
}
// EN: Creates the peer connection object and sets up ICE servers (STUN/TURN).
// AR: إنشاء كائن الاتصال المباشر وإعداد خوادم STUN/TURN لاختراق الجدران النارية.
Future<void> _initializePeerConnection() async {
if (_peerConnection != null) return;
final List<Map<String, dynamic>> iceServers = [];
if (_dynamicIceServers.isNotEmpty) {
for (var server in _dynamicIceServers) {
if (server is Map) {
iceServers.add({
"urls": server["urls"] ?? server["url"],
if (server["username"] != null) "username": server["username"],
if (server["credential"] != null)
"credential": server["credential"],
});
}
}
} else {
// EN: Fallback STUN servers / AR: خوام STUN الاحتياطية
iceServers.addAll([
{"urls": "stun:stun.l.google.com:19302"},
{"urls": "stun:stun1.l.google.com:19302"},
]);
}
final Map<String, dynamic> configuration = {
"iceServers": iceServers,
};
_peerConnection = await rtc.createPeerConnection(configuration);
// EN: Gather local network routing info and send to remote peer.
// AR: جمع بيانات مسارات الشبكة المحلية وإرسالها للطرف الآخر.
_peerConnection!.onIceCandidate = (candidate) {
if (candidate.candidate != null) {
_signaling.send("ice_candidate", {
"candidate": {
"candidate": candidate.candidate,
"sdpMid": candidate.sdpMid,
"sdpMLineIndex": candidate.sdpMLineIndex,
}
});
}
};
// EN: Monitor connection status changes and handle disconnections.
// AR: مراقبة تغيرات حالة الاتصال ومعالجة انقطاع الشبكة.
_peerConnection!.onConnectionState = (connState) {
Log.print("RTCPeerConnectionState: $connState");
if (connState ==
rtc.RTCPeerConnectionState.RTCPeerConnectionStateConnected) {
_onCallConnected();
} else if (connState ==
rtc.RTCPeerConnectionState.RTCPeerConnectionStateFailed ||
connState ==
rtc.RTCPeerConnectionState.RTCPeerConnectionStateDisconnected) {
_handleIceConnectionFailure();
}
};
// EN: Add local audio stream to the connection to send it to the other peer.
// AR: إضافة دفق الصوت المحلي للاتصال لإرساله للطرف الآخر.
if (_localStream != null) {
_localStream!.getTracks().forEach((track) {
_peerConnection!.addTrack(track, _localStream!);
});
}
}
// EN: Attempts an ICE restart to reconnect the WebRTC session when disconnections occur.
// AR: محاولة إعادة تأسيس الاتصال (ICE Restart) في حالة انقطاع الشبكة.
void _handleIceConnectionFailure() {
if (_isReconnecting) return;
_isReconnecting = true;
Log.print(
"ICE connection dropped. Attempting ICE Restart reconnection for 5s...");
if (isCaller) {
_attemptIceRestart();
}
_reconnectTimer?.cancel();
_reconnectTimer = Timer(const Duration(seconds: 5), () {
if (state.value == VoiceCallState.active &&
_peerConnection?.connectionState !=
rtc.RTCPeerConnectionState.RTCPeerConnectionStateConnected) {
Log.print("ICE reconnection timed out. Hanging up.");
_endCallInternal("connection_lost");
} else {
_isReconnecting = false;
Log.print("ICE Reconnection succeeded!");
}
});
}
// EN: Initiates ICE Restart SDP exchange.
// AR: بدء تبادل حزم SDP لإعادة بناء مسارات الاتصال.
Future<void> _attemptIceRestart() async {
if (_peerConnection == null || !isCaller) return;
try {
Log.print("Caller initiating WebRTC ICE Restart...");
final constraints = {
'mandatory': {
'OfferToReceiveAudio': true,
'OfferToReceiveVideo': false,
},
'optional': [
{'IceRestart': true}
],
};
final offer = await _peerConnection!.createOffer(constraints);
await _peerConnection!.setLocalDescription(offer);
_signaling.send("offer", {
"sdp": {
"sdp": offer.sdp,
"type": offer.type,
}
});
} catch (e) {
Log.print("Error initiating WebRTC ICE Restart: $e");
}
}
// EN: Generates an SDP Offer to initialize the connection.
// AR: إنشاء عرض (Offer) لبدء الاتصال وتحديد قدرات الجهاز.
Future<void> _createOffer() async {
await _initializePeerConnection();
final constraints = {
'mandatory': {
'OfferToReceiveAudio': true,
'OfferToReceiveVideo': false,
},
'optional': [],
};
final offer = await _peerConnection!.createOffer(constraints);
await _peerConnection!.setLocalDescription(offer);
_signaling.send("offer", {
"sdp": {
"sdp": offer.sdp,
"type": offer.type,
}
});
}
// EN: Generates an SDP Answer in response to an Offer.
// AR: الرد بإنشاء إجابة (Answer) بناءً على العرض المستلم.
Future<void> _createAnswer() async {
final constraints = {
'mandatory': {
'OfferToReceiveAudio': true,
'OfferToReceiveVideo': false,
},
'optional': [],
};
final answer = await _peerConnection!.createAnswer(constraints);
await _peerConnection!.setLocalDescription(answer);
_signaling.send("answer", {
"sdp": {
"sdp": answer.sdp,
"type": answer.type,
}
});
}
// EN: Triggered when connection is fully established. Starts the 60s timer.
// AR: يُستدعى عند تأسيس الاتصال بنجاح، ويقوم ببدء مؤقت الـ 60 ثانية.
void _onCallConnected() {
_ringingTimeoutTimer?.cancel();
_reconnectTimer?.cancel();
_isReconnecting = false;
if (state.value != VoiceCallState.active) {
state.value = VoiceCallState.active;
HapticFeedback.vibrate();
// EN: Start 120s countdown timer / AR: بدء العد التنازلي لمدة 120 ثانية
_countdownTimer?.cancel();
elapsedSeconds.value = 120;
_countdownTimer = Timer.periodic(const Duration(seconds: 1), (timer) {
if (elapsedSeconds.value > 1) {
elapsedSeconds.value--;
} else {
elapsedSeconds.value = 0;
_countdownTimer?.cancel();
// EN: Force hangup when timer reaches 0 / AR: إغلاق إجباري عند وصول المؤقت لصفر
hangup();
}
});
}
}
// EN: Internal cleanup function. Closes all connections and streams.
// AR: دالة التنظيف الداخلية. تقوم بإغلاق جميع الاتصالات وتفريغ الذاكرة.
void _endCallInternal(String reason) {
_countdownTimer?.cancel();
_ringingTimeoutTimer?.cancel();
_reconnectTimer?.cancel();
_stopRingtone();
state.value = VoiceCallState.ended;
// EN: Close WebRTC connection / AR: إغلاق اتصال WebRTC
_peerConnection?.close();
_peerConnection = null;
// EN: Stop mic capture / AR: إيقاف التقاط الميكروفون
_localStream?.getTracks().forEach((track) => track.stop());
_localStream?.dispose();
_localStream = null;
// EN: Disconnect WebSockets / AR: إغلاق اتصال الـ WebSockets
_signaling.disconnect();
// EN: Close UI BottomSheet after delay / AR: إغلاق واجهة المكالمة بعد فترة زمنية قصيرة
Future.delayed(const Duration(milliseconds: 1500), () {
if (state.value == VoiceCallState.ended) {
state.value = VoiceCallState.idle;
Get.back();
}
});
}
// --- ACTIONS (UI Controls) / إجراءات الواجهة ---
// EN: Toggles microphone mute state.
// AR: تبديل حالة كتم الميكروفون.
void toggleMute() {
isMuted.value = !isMuted.value;
_localStream?.getAudioTracks().forEach((track) {
track.enabled = !isMuted.value;
});
}
// EN: Toggles loudspeaker mode.
// AR: تبديل حالة مكبر الصوت الخارجي.
void toggleSpeaker() {
isSpeakerOn.value = !isSpeakerOn.value;
rtc.Helper.setSpeakerphoneOn(isSpeakerOn.value);
}
// EN: Displays the call UI overlay.
// AR: إظهار نافذة المكالمة السفلية.
void _showCallBottomSheet() {
Get.bottomSheet(
const VoiceCallBottomSheet(),
isScrollControlled: true,
enableDrag: false,
isDismissible: false,
);
}
// EN: Lifecycle hook: clean up resources when controller is destroyed.
// AR: دورة الحياة: تفريغ الذاكرة وإغلاق الموارد عند تدمير المتحكم.
@override
void onClose() {
WidgetsBinding.instance.removeObserver(this);
_countdownTimer?.cancel();
_ringingTimeoutTimer?.cancel();
_reconnectTimer?.cancel();
_stopRingtone();
_ringtonePlayer?.dispose();
_peerConnection?.close();
_localStream?.dispose();
_signaling.disconnect();
super.onClose();
}
}