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

3 months ago
import 'dart:convert';
2 months ago
import 'dart:math';
3 months ago
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';
3 months ago
import 'package:test_sa/controllers/api_routes/urls.dart';
3 months ago
import 'package:test_sa/modules/cx_module/chat/api_client.dart';
import 'package:http/http.dart' as http;
3 months ago
import '../chat_provider.dart';
3 months ago
import '../model/chat_login_response_model.dart';
HubConnection? callHubConnection;
3 months ago
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;
3 months ago
bool isLoading = false;
2 months ago
String currentStatus = "Connecting...";
1 month ago
bool _ownsConnection = false;
void _setStatus(String status) {
if (currentStatus == status) return;
currentStatus = status;
notifyListeners();
}
3 months ago
Future<void> _disposeConnection() async {
1 month ago
final HubConnection? connection = callHubConnection;
final bool owns = _ownsConnection;
callHubConnection = null;
_ownsConnection = false;
if (connection == null || !owns) return;
3 months ago
try {
1 month ago
await connection.stop();
3 months ago
} catch (e) {
1 month ago
if (kDebugMode) {
print('⚠️ Error closing call SignalR connection: $e');
}
3 months ago
}
1 month ago
}
Future<void> endSession() async {
stopListeners();
onFailed = null;
onSuccess = null;
isMuted = false;
isPeerMuted = false;
currentStatus = "Connecting...";
await _disposeConnection();
3 months ago
}
@override
void dispose() {
3 months ago
_disposeConnection().then((_) {}).catchError((error) {});
3 months ago
super.dispose();
}
1 month ago
/// 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.
3 months ago
Future<HubConnection> getHubConnection(String token) async {
1 month ago
// 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,
}),
);
3 months ago
1 month ago
final HttpConnectionOptions httpOp = HttpConnectionOptions(
transport: HttpTransportType.LongPolling,
1 month ago
skipNegotiation: false,
logMessageContent: false,
1 month ago
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';
3 months ago
}
2 months ago
Future<bool> buildHubConnection(String employeeNumber, {HubConnection? hubC}) async {
1 month ago
_setStatus("Connecting...");
3 months ago
try {
1 month ago
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.');
}
}
3 months ago
await _disposeConnection();
1 month ago
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;
3 months ago
}
2 months ago
1 month ago
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;
3 months ago
} catch (e) {
1 month ago
if (kDebugMode) {
print('⚠️ Error building call SignalR connection: $e');
}
3 months ago
await _disposeConnection();
return false;
}
}
1 month ago
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;
3 months ago
void initAudioCallListeners() {
1 month ago
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);
3 months ago
}
void initCallListeners() {
1 month ago
final HubConnection? hub = callHubConnection;
if (hub == null) return;
initAudioCallListeners();
hub.on('OnCameraToggle', onCameraToggle);
3 months ago
}
//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 {
1 month ago
3 months ago
Map<String, dynamic> configuration = {
"iceServers": [
{"urls": 'stun:15.185.116.59:3478'},
1 month ago
{
"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"},
3 months ago
]
};
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();
}
2 months ago
String? senderN;
String? recipientN;
bool? isVideo;
2 months ago
int moduleId = 1004;
3 months ago
int? referenceID;
2 months ago
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 {
3 months ago
this.senderN = senderN;
this.recipientN = recipientN;
this.isVideo = isVideo;
2 months ago
// this.moduleId = moduleId;
3 months ago
this.referenceID = referenceID;
2 months ago
this.onFailed = onFailed;
this.onSuccess = onSuccess;
1 month ago
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.");
}
3 months ago
}
2 months ago
void acceptCall(String senderN, String recipientN, bool isVideo, {required int moduleId}) async {
1 month ago
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.");
}
3 months ago
}
2 months ago
Future declineCall(String senderN, String recipientN, {required int moduleId}) async {
1 month ago
try {
await invoke(invokeMethod: "CallDeclinedAsync", currentUserID: senderN, targetUserID: recipientN, isVideoCall: false, data: [this.moduleId]);
} catch (e) {
if (kDebugMode) {
print('⚠️ CallDeclinedAsync failed: $e');
}
}
3 months ago
}
1 month ago
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');
}
}
3 months ago
}
2 months ago
void toggleMute() async {
1 month ago
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');
}
}
3 months ago
}
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");
1 month ago
_setStatus("Connected");
3 months ago
}
void onIceCandidateAsync(List<Object?>? params) {
print("onCallAcceptedAsync: $params");
}
void onOfferAsync(List<Object?>? params) {
print("onCallAcceptedAsync: $params");
2 months ago
print("onCallAcceptedAsync: $params");
3 months ago
}
void onAnswerOffer(List<Object?>? params) {
print("onAnswerOffer: $params");
}
void onHangUpAsync(List<Object?>? params) {
print("onHangUpAsync: $params");
2 months ago
handleResponse(params);
3 months ago
}
void onCallDeclinedAsync(List<Object?>? params) {
print("onCallDeclinedAsync: $params");
2 months ago
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.");
// }
3 months ago
}
void onAudioToggle(List<Object?>? params) {
print("onAudioToggle: $params");
2 months ago
isPeerMuted = !isPeerMuted;
3 months ago
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 = [];
2 months ago
if (data != null) {
data = data.toString().replaceFirst("[", "").replaceFirst("]", "");
}
2 months ago
if (invokeMethod == "AnswerCallAsync") {
2 months ago
args = [currentUserID, targetUserID, data];
2 months ago
} else if (invokeMethod == 'CallDeclinedAsync') {
2 months ago
args = [currentUserID, targetUserID, data];
3 months ago
} else if (invokeMethod == "CallUserAsync") {
args = [currentUserID, targetUserID, isVideoCall];
2 months ago
} else if (invokeMethod == "HangUpAsync") {
2 months ago
args = [currentUserID, targetUserID, data];
3 months ago
} else if (invokeMethod == "AudioToggle") {
2 months ago
args = [currentUserID, targetUserID];
3 months ago
} else if (invokeMethod == "IceCandidateAsync") {
args = [targetUserID, data];
} else if (invokeMethod == "OfferAsync") {
args = [targetUserID, data];
} else if (invokeMethod == "AnswerOfferAsync") {
args = [targetUserID, data];
}
1 month ago
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);
3 months ago
}
void stopListeners() async {
3 months ago
callHubConnection?.off('OnCallDeclinedAsync');
callHubConnection?.off('OnCallAcceptedAsync');
callHubConnection?.off('OnIceCandidateAsync');
callHubConnection?.off('OnAnswerOffer');
callHubConnection?.off('OnOfferAsync');
callHubConnection?.off('OnHangUpAsync');
callHubConnection?.off('OnAudioToggle');
1 month ago
callHubConnection?.off('OnCameraToggle');
3 months ago
}
2 months ago
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");
2 months ago
} else if (params[0] == 'USER_DECLINE') {
onFailed?.call("Call Declined, User has declined your call.");
2 months ago
} else if (params[0] == 'USER_OFFLINE') {
onFailed?.call("The user is currently offline");
} else {
onSuccess?.call("");
}
}
}
3 months ago
}