You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
cloudsolutions-atoms/lib/modules/cx_module/chat/calling/call_provider.dart

560 lines
18 KiB
Dart

This file contains invisible Unicode characters!

This file contains invisible Unicode characters that may be processed differently from what appears below. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to reveal hidden characters.

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

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<void> _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<void> 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<HubConnection> 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: <String, String>{
"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: <int>[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<bool> 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<bool> _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<bool> _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<void>.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<RTCPeerConnection> _createPeerConnection() async {
Map<String, dynamic> 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<String, dynamic> 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<String, dynamic> 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<Object?>? params) {
print("onCallAcceptedAsync: $params");
_setStatus("Connected");
}
void onIceCandidateAsync(List<Object?>? params) {
print("onCallAcceptedAsync: $params");
}
void onOfferAsync(List<Object?>? params) {
print("onCallAcceptedAsync: $params");
print("onCallAcceptedAsync: $params");
}
void onAnswerOffer(List<Object?>? params) {
print("onAnswerOffer: $params");
}
void onHangUpAsync(List<Object?>? params) {
print("onHangUpAsync: $params");
handleResponse(params);
}
void onCallDeclinedAsync(List<Object?>? 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<Object?>? params) {
print("onAudioToggle: $params");
isPeerMuted = !isPeerMuted;
notifyListeners();
}
void onCameraToggle(List<Object?>? params) {
print("onCameraToggle: $params");
}
//// Invoke Methods
Future<void> invoke({required String invokeMethod, required String currentUserID, required String targetUserID, bool isVideoCall = false, var data}) async {
List<Object> 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<Object?>? 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("");
}
}
}
}