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'; import 'package:signalr_netcore/hub_connection.dart'; import 'package:signalr_netcore/hub_connection_builder.dart'; 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 '../chat_provider.dart'; import '../model/chat_login_response_model.dart'; HubConnection? callHubConnection; class CallProvider with ChangeNotifier, DiagnosticableTreeMixin { ///////////////////// Web RTC Video Calling ////////////////////// // Video Call late RTCPeerConnection _peerConnection; RTCVideoRenderer _localVideoRenderer = RTCVideoRenderer(); final RTCVideoRenderer _remoteRenderer = RTCVideoRenderer(); MediaStream? _localStream; MediaStream? _remoteStream; bool isMuted = false; bool isSpeakerOn = false; bool isCameraOn = true; bool isPeerMuted = false; bool isPeerCameraOn = true; bool isLoading = false; String currentStatus = "Connecting..."; bool _ownsConnection = false; void _setStatus(String status) { if (currentStatus == status) return; currentStatus = status; notifyListeners(); } Future _disposeConnection() async { final HubConnection? connection = callHubConnection; final bool owns = _ownsConnection; callHubConnection = null; _ownsConnection = false; if (connection == null || !owns) return; try { await connection.stop(); } catch (e) { if (kDebugMode) { print('⚠️ Error closing call SignalR connection: $e'); } } } Future endSession() async { stopListeners(); onFailed = null; onSuccess = null; isMuted = false; isPeerMuted = false; currentStatus = "Connecting..."; await _disposeConnection(); } @override void dispose() { _disposeConnection().then((_) {}).catchError((error) {}); super.dispose(); } /// See [ChatProvider.buildHubConnection] for why a `:0` port can show up in /// SignalR websocket errors - it is a `dart:io` artifact (Uri.port == 0 for /// the `wss` scheme), not a real port. Read the trailing HTTP status instead. Future getHubConnection(String token) async { // Encode the JWT properly and also pass it as an Authorization header: // on mobile signalr_netcore ignores the query token for the websocket // handshake and relies on `accessTokenFactory`. final String hubUrl = _urlWithExplicitPort( Uri.parse(URLs.chatHubUrlChat).replace(queryParameters: { "access_token": token, }), ); final HttpConnectionOptions httpOp = HttpConnectionOptions( transport: HttpTransportType.LongPolling, skipNegotiation: false, logMessageContent: false, accessTokenFactory: () async => token, requestTimeout: 30000, ); return HubConnectionBuilder() .withUrl(hubUrl, options: httpOp) .withAutomaticReconnect(retryDelays: [2000, 5000, 10000, 20000]) .build(); } /// Writes the port into the url *textually*. /// /// Why this is not just `uri.replace(port: 443)`: `Uri` normalises a port away /// when it equals the scheme default, so `.replace(port: 443)` on an https uri /// serialises back to `https://host/...` with no port at all. signalr_netcore /// only does string surgery on this url (`url.replaceFirst('http', 'ws')`), so /// the port has to be present in the string to survive into the `wss://` form. /// Without it `Uri.port` is `0` for `ws`/`wss` (dart:io only knows defaults for /// http/https), which is exactly what gets echoed back as `https://host:0/...` /// in WebSocketException messages. static String _urlWithExplicitPort(Uri uri) { final int port = uri.hasPort ? uri.port : (uri.scheme == 'https' || uri.scheme == 'wss' ? 443 : 80); final String query = uri.query.isEmpty ? '' : '?${uri.query}'; return '${uri.scheme}://${uri.host}:$port${uri.path}$query'; } 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.'); } } 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}'); } 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.'); } return false; } 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'); } await _disposeConnection(); return false; } } Future _adoptExistingConnection(HubConnection hubC) async { switch (hubC.state) { case HubConnectionState.Connected: callHubConnection = hubC; _ownsConnection = false; return true; case HubConnectionState.Connecting: case HubConnectionState.Reconnecting: final bool connected = await _waitUntilConnected(hubC); if (connected) { callHubConnection = hubC; _ownsConnection = false; } return connected; case HubConnectionState.Disconnected: // Chat side dropped it - we can legally restart it, but we do not own it // so we must not stop it when the call ends. try { await hubC.start(); if (hubC.state == HubConnectionState.Connected) { callHubConnection = hubC; _ownsConnection = false; return true; } } catch (e) { if (kDebugMode) { print('⚠️ Could not restart shared chat hub for call: $e'); } } return false; default: // Disconnecting (or unknown) - do not touch it, build our own instead. return false; } } Future _waitUntilConnected(HubConnection hub, {Duration timeout = const Duration(seconds: 15)}) async { final DateTime deadline = DateTime.now().add(timeout); while (DateTime.now().isBefore(deadline)) { final HubConnectionState? state = hub.state; if (state == HubConnectionState.Connected) return true; if (state == HubConnectionState.Disconnected || state == HubConnectionState.Disconnecting) return false; await Future.delayed(const Duration(milliseconds: 200)); } return hub.state == HubConnectionState.Connected; } bool get isHubConnected => callHubConnection?.state == HubConnectionState.Connected; void initAudioCallListeners() { final HubConnection? hub = callHubConnection; if (hub == null) return; hub.on("OnCallAcceptedAsync", onCallAcceptedAsync); hub.on("OnIceCandidateAsync", onIceCandidateAsync); hub.on("OnOfferAsync", onOfferAsync); hub.on("OnAnswerOffer", onAnswerOffer); hub.on("OnHangUpAsync", onHangUpAsync); hub.on("OnCallDeclinedAsync", onCallDeclinedAsync); hub.on('OnAudioToggle', onAudioToggle); } void initCallListeners() { final HubConnection? hub = callHubConnection; if (hub == null) return; initAudioCallListeners(); 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; int moduleId = 1004; int? referenceID; Function(String message)? onFailed; 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 { this.senderN = senderN; this.recipientN = recipientN; this.isVideo = isVideo; // this.moduleId = moduleId; this.referenceID = referenceID; this.onFailed = onFailed; this.onSuccess = onSuccess; if (senderN == null || senderN.isEmpty || recipientN == null || recipientN.isEmpty) { onFailed?.call("Call participants could not be resolved. Please reopen the chat and try again."); return; } try { await invoke(invokeMethod: "CallUserAsync", currentUserID: senderN, targetUserID: recipientN, isVideoCall: isVideo ?? false, data: [this.moduleId, referenceID]); _setStatus("Ringing..."); } catch (e) { if (kDebugMode) { print('⚠️ CallUserAsync failed: $e'); } _setStatus("Call failed"); onFailed?.call("Could not reach the call service. Please check your connection and try again."); } } void acceptCall(String senderN, String recipientN, bool isVideo, {required int moduleId}) async { this.senderN = senderN; this.recipientN = recipientN; try { await invoke(invokeMethod: "AnswerCallAsync", currentUserID: senderN, targetUserID: recipientN, isVideoCall: isVideo, data: [this.moduleId]); } catch (e) { if (kDebugMode) { print('⚠️ 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 { try { await invoke(invokeMethod: "CallDeclinedAsync", currentUserID: senderN, targetUserID: recipientN, isVideoCall: false, data: [this.moduleId]); } catch (e) { if (kDebugMode) { print('⚠️ CallDeclinedAsync failed: $e'); } } } void endCall(String senderN, String recipientN, {required int moduleId}) async { try { await invoke(invokeMethod: "HangUpAsync", currentUserID: senderN, targetUserID: recipientN, data: [moduleId]); } catch (e) { if (kDebugMode) { print('⚠️ HangUpAsync failed: $e'); } } } void toggleMute() async { if (senderN == null || recipientN == null) return; try { await invoke(invokeMethod: "AudioToggle", currentUserID: senderN!, targetUserID: recipientN!); isMuted = !isMuted; notifyListeners(); } catch (e) { if (kDebugMode) { print('⚠️ 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; } } //// Listeners Methods //// void onCallAcceptedAsync(List? params) { print("onCallAcceptedAsync: $params"); _setStatus("Connected"); } void onIceCandidateAsync(List? params) { print("onCallAcceptedAsync: $params"); } void onOfferAsync(List? params) { print("onCallAcceptedAsync: $params"); print("onCallAcceptedAsync: $params"); } void onAnswerOffer(List? params) { print("onAnswerOffer: $params"); } void onHangUpAsync(List? params) { print("onHangUpAsync: $params"); handleResponse(params); } void onCallDeclinedAsync(List? params) { print("onCallDeclinedAsync: $params"); handleResponse(params); // try { // if (params?.isNotEmpty ?? false) { // print("onCallDeclinedAsync:${params?.length}:${params}"); // if (params!.length > 2) { // print("runTimeData:${params[1].runtimeType}"); // var data = jsonDecode(jsonEncode(params[1].toString())); // print("runTimeData:${data.runtimeType}"); // onFailed?.call("Call Declined, ${data["targetUserId"]} has declined your call."); // return; // } else { print("runTimeData:${params[0].runtimeType}"); // var data = jsonDecode(params[0].toString()); // print("runTimeData:${data.runtimeType}"); // onFailed?.call("Call Declined, ${data["targetUserId"]} has declined your call."); // return; // } // } // } catch (ex) { // print("Ex:$ex"); // onFailed?.call("Call Declined, User has declined your call."); // } } void onAudioToggle(List? params) { print("onAudioToggle: $params"); isPeerMuted = !isPeerMuted; notifyListeners(); } void onCameraToggle(List? params) { print("onCameraToggle: $params"); } //// Invoke Methods Future invoke({required String invokeMethod, required String currentUserID, required String targetUserID, bool isVideoCall = false, var data}) async { List args = []; if (data != null) { data = data.toString().replaceFirst("[", "").replaceFirst("]", ""); } if (invokeMethod == "AnswerCallAsync") { args = [currentUserID, targetUserID, data]; } else if (invokeMethod == 'CallDeclinedAsync') { args = [currentUserID, targetUserID, data]; } else if (invokeMethod == "CallUserAsync") { args = [currentUserID, targetUserID, isVideoCall]; } else if (invokeMethod == "HangUpAsync") { args = [currentUserID, targetUserID, data]; } else if (invokeMethod == "AudioToggle") { args = [currentUserID, targetUserID]; } else if (invokeMethod == "IceCandidateAsync") { args = [targetUserID, data]; } else if (invokeMethod == "OfferAsync") { args = [targetUserID, data]; } else if (invokeMethod == "AnswerOfferAsync") { args = [targetUserID, data]; } print("Invoking:$invokeMethod:$args"); final HubConnection? hub = callHubConnection; if (hub == null || hub.state != HubConnectionState.Connected) { throw StateError("Call hub is not connected (state: ${hub?.state}). Cannot invoke $invokeMethod."); } await hub.invoke(invokeMethod, args: args); } void stopListeners() async { callHubConnection?.off('OnCallDeclinedAsync'); callHubConnection?.off('OnCallAcceptedAsync'); callHubConnection?.off('OnIceCandidateAsync'); callHubConnection?.off('OnAnswerOffer'); callHubConnection?.off('OnOfferAsync'); callHubConnection?.off('OnHangUpAsync'); callHubConnection?.off('OnAudioToggle'); callHubConnection?.off('OnCameraToggle'); } handleResponse(List? params) { if (params?.isNotEmpty ?? false) { if (params![0] == "NO_ANSWER") { onFailed?.call("No Answer, User did not answer your call."); return; } else if (params[0] == "NO_ANSWER") { onFailed?.call("Call Declined, User has declined your call."); return; } else if (params[0] == 'USER_BUSY') { onFailed?.call("The user is currently busy on another call"); } else if (params[0] == 'USER_DECLINE') { onFailed?.call("Call Declined, User has declined your call."); } else if (params[0] == 'USER_OFFLINE') { onFailed?.call("The user is currently offline"); } else { onSuccess?.call(""); } } } }