|
|
|
|
@ -38,17 +38,37 @@ class CallProvider with ChangeNotifier, DiagnosticableTreeMixin {
|
|
|
|
|
|
|
|
|
|
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 {
|
|
|
|
|
if (callHubConnection != null) {
|
|
|
|
|
await callHubConnection!.stop();
|
|
|
|
|
}
|
|
|
|
|
await connection.stop();
|
|
|
|
|
} catch (e) {
|
|
|
|
|
} finally {
|
|
|
|
|
callHubConnection = null;
|
|
|
|
|
if (kDebugMode) {
|
|
|
|
|
print('⚠️ Error closing call SignalR connection: $e');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
// isLoading = false;
|
|
|
|
|
// notifyListeners();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Future<void> endSession() async {
|
|
|
|
|
stopListeners();
|
|
|
|
|
onFailed = null;
|
|
|
|
|
onSuccess = null;
|
|
|
|
|
isMuted = false;
|
|
|
|
|
isPeerMuted = false;
|
|
|
|
|
currentStatus = "Connecting...";
|
|
|
|
|
await _disposeConnection();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
@ -73,7 +93,6 @@ class CallProvider with ChangeNotifier, DiagnosticableTreeMixin {
|
|
|
|
|
final HttpConnectionOptions httpOp = HttpConnectionOptions(
|
|
|
|
|
transport: HttpTransportType.LongPolling,
|
|
|
|
|
skipNegotiation: false,
|
|
|
|
|
// Keep false: signalr_netcore 1.4.4 crashes when logging + LongPolling.
|
|
|
|
|
logMessageContent: false,
|
|
|
|
|
accessTokenFactory: () async => token,
|
|
|
|
|
requestTimeout: 30000,
|
|
|
|
|
@ -102,61 +121,115 @@ class CallProvider with ChangeNotifier, DiagnosticableTreeMixin {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Future<bool> buildHubConnection(String employeeNumber, {HubConnection? hubC}) async {
|
|
|
|
|
if (hubC != null) {
|
|
|
|
|
callHubConnection = hubC;
|
|
|
|
|
await callHubConnection!.start();
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
_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();
|
|
|
|
|
http.Response response = await ApiClient().postJsonForResponse(URLs.chatSdkToken, {"apiKey": URLs.chatApiKey, "employeeNumber": employeeNumber, "voIPToken": null});
|
|
|
|
|
if (response.statusCode == 200) {
|
|
|
|
|
ChatLoginResponse chatResponse = ChatLoginResponse.fromJson(jsonDecode(response.body));
|
|
|
|
|
callHubConnection = await getHubConnection(chatResponse.token!);
|
|
|
|
|
await callHubConnection!.start();
|
|
|
|
|
return true;
|
|
|
|
|
|
|
|
|
|
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.');
|
|
|
|
|
}
|
|
|
|
|
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() {
|
|
|
|
|
callHubConnection!.on("OnCallAcceptedAsync", onCallAcceptedAsync);
|
|
|
|
|
callHubConnection!.on("OnIceCandidateAsync", onIceCandidateAsync);
|
|
|
|
|
callHubConnection!.on("OnOfferAsync", onOfferAsync);
|
|
|
|
|
callHubConnection!.on("OnAnswerOffer", onAnswerOffer);
|
|
|
|
|
callHubConnection!.on("OnHangUpAsync", onHangUpAsync);
|
|
|
|
|
callHubConnection!.on("OnCallDeclinedAsync", onCallDeclinedAsync);
|
|
|
|
|
callHubConnection!.on('OnAudioToggle', onAudioToggle);
|
|
|
|
|
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() {
|
|
|
|
|
callHubConnection!.on("OnCallAcceptedAsync", onCallAcceptedAsync);
|
|
|
|
|
callHubConnection!.on("OnIceCandidateAsync", onIceCandidateAsync);
|
|
|
|
|
callHubConnection!.on("OnOfferAsync", onOfferAsync);
|
|
|
|
|
callHubConnection!.on("OnAnswerOffer", onAnswerOffer);
|
|
|
|
|
callHubConnection!.on("OnHangUpAsync", onHangUpAsync);
|
|
|
|
|
callHubConnection!.on("OnCallDeclinedAsync", onCallDeclinedAsync);
|
|
|
|
|
callHubConnection!.on('OnAudioToggle', onAudioToggle);
|
|
|
|
|
callHubConnection!.on('OnCameraToggle', onCameraToggle);
|
|
|
|
|
|
|
|
|
|
// callHubConnection!.on('OnIncomingCallAsync', onHandleIncomingCall);
|
|
|
|
|
// callHubConnection!.on('OnCallAcceptedAsync', _handleCallAccepted);
|
|
|
|
|
// callHubConnection!.on('OnCallDeclinedAsync', _handleCallDeclined);
|
|
|
|
|
// callHubConnection!.on('OnHangUpAsync', onHangUpAsync);
|
|
|
|
|
// callHubConnection!.on('OnOfferAsync', _handleOffer);
|
|
|
|
|
// callHubConnection!.on('OnAnswerOfferAsync', onAnswerOffer);
|
|
|
|
|
// callHubConnection!.on('OnIceCandidateAsync', _handleIceCandidate);
|
|
|
|
|
// callHubConnection!.on('OnAudioToggle', _handleAudioToggle);
|
|
|
|
|
// callHubConnection!.on('OnCameraToggle', _handleCameraToggle);
|
|
|
|
|
// callHubConnection!.on('OnUserOnlineAsync', _handleUserOnline);
|
|
|
|
|
// callHubConnection!.on('OnUserOfflineAsync', _handleUserOffline);
|
|
|
|
|
// callHubConnection!.on('OnCallHistoryUpdated', _handleCallHistoryUpdated);
|
|
|
|
|
// callHubConnection!.on('OnError', _handleError);
|
|
|
|
|
final HubConnection? hub = callHubConnection;
|
|
|
|
|
if (hub == null) return;
|
|
|
|
|
initAudioCallListeners();
|
|
|
|
|
hub.on('OnCameraToggle', onCameraToggle);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
//Video Constraints
|
|
|
|
|
@ -189,11 +262,17 @@ class CallProvider with ChangeNotifier, DiagnosticableTreeMixin {
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
Future<RTCPeerConnection> _createPeerConnection() async {
|
|
|
|
|
// {"url": "stun:stun.l.google.com:19302"},
|
|
|
|
|
|
|
|
|
|
Map<String, dynamic> configuration = {
|
|
|
|
|
"iceServers": [
|
|
|
|
|
{"urls": 'stun:15.185.116.59:3478'},
|
|
|
|
|
{"urls": "turn:15.185.116.59:3479", "username": "admin", "credential": "admin"}
|
|
|
|
|
{
|
|
|
|
|
"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"},
|
|
|
|
|
]
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
@ -269,25 +348,67 @@ class CallProvider with ChangeNotifier, DiagnosticableTreeMixin {
|
|
|
|
|
this.referenceID = referenceID;
|
|
|
|
|
this.onFailed = onFailed;
|
|
|
|
|
this.onSuccess = onSuccess;
|
|
|
|
|
invoke(invokeMethod: "CallUserAsync", currentUserID: senderN!, targetUserID: recipientN!, isVideoCall: isVideo!, data: [this.moduleId, referenceID]);
|
|
|
|
|
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 {
|
|
|
|
|
invoke(invokeMethod: "AnswerCallAsync", currentUserID: senderN, targetUserID: recipientN, isVideoCall: isVideo, data: [this.moduleId]);
|
|
|
|
|
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 {
|
|
|
|
|
await invoke(invokeMethod: "CallDeclinedAsync", currentUserID: senderN, targetUserID: recipientN, isVideoCall: false, data: [this.moduleId]);
|
|
|
|
|
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}) {
|
|
|
|
|
invoke(invokeMethod: "HangUpAsync", currentUserID: senderN, targetUserID: recipientN, data: [moduleId]);
|
|
|
|
|
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 {
|
|
|
|
|
await invoke(invokeMethod: "AudioToggle", currentUserID: senderN!, targetUserID: recipientN!);
|
|
|
|
|
isMuted = !isMuted;
|
|
|
|
|
notifyListeners();
|
|
|
|
|
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) {
|
|
|
|
|
@ -317,6 +438,7 @@ class CallProvider with ChangeNotifier, DiagnosticableTreeMixin {
|
|
|
|
|
|
|
|
|
|
void onCallAcceptedAsync(List<Object?>? params) {
|
|
|
|
|
print("onCallAcceptedAsync: $params");
|
|
|
|
|
_setStatus("Connected");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void onIceCandidateAsync(List<Object?>? params) {
|
|
|
|
|
@ -396,8 +518,12 @@ class CallProvider with ChangeNotifier, DiagnosticableTreeMixin {
|
|
|
|
|
} else if (invokeMethod == "AnswerOfferAsync") {
|
|
|
|
|
args = [targetUserID, data];
|
|
|
|
|
}
|
|
|
|
|
print("Invoking:$invokeMethod:${args}");
|
|
|
|
|
await callHubConnection?.invoke(invokeMethod, args: args);
|
|
|
|
|
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 {
|
|
|
|
|
@ -408,6 +534,7 @@ class CallProvider with ChangeNotifier, DiagnosticableTreeMixin {
|
|
|
|
|
callHubConnection?.off('OnOfferAsync');
|
|
|
|
|
callHubConnection?.off('OnHangUpAsync');
|
|
|
|
|
callHubConnection?.off('OnAudioToggle');
|
|
|
|
|
callHubConnection?.off('OnCameraToggle');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
handleResponse(List<Object?>? params) {
|
|
|
|
|
|