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.
433 lines
15 KiB
Dart
433 lines
15 KiB
Dart
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...";
|
|
|
|
Future<void> _disposeConnection() async {
|
|
try {
|
|
if (callHubConnection != null) {
|
|
await callHubConnection!.stop();
|
|
}
|
|
} catch (e) {
|
|
} finally {
|
|
callHubConnection = null;
|
|
}
|
|
// isLoading = false;
|
|
// notifyListeners();
|
|
}
|
|
|
|
@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,
|
|
// Keep false: signalr_netcore 1.4.4 crashes when logging + LongPolling.
|
|
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 {
|
|
if (hubC != null) {
|
|
callHubConnection = hubC;
|
|
await callHubConnection!.start();
|
|
return true;
|
|
}
|
|
try {
|
|
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;
|
|
}
|
|
|
|
return false;
|
|
} catch (e) {
|
|
await _disposeConnection();
|
|
return false;
|
|
}
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
//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 {
|
|
// {"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"}
|
|
]
|
|
};
|
|
|
|
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;
|
|
invoke(invokeMethod: "CallUserAsync", currentUserID: senderN!, targetUserID: recipientN!, isVideoCall: isVideo!, data: [this.moduleId, referenceID]);
|
|
}
|
|
|
|
void acceptCall(String senderN, String recipientN, bool isVideo, {required int moduleId}) async {
|
|
invoke(invokeMethod: "AnswerCallAsync", currentUserID: senderN, targetUserID: recipientN, isVideoCall: isVideo, data: [this.moduleId]);
|
|
}
|
|
|
|
Future declineCall(String senderN, String recipientN, {required int moduleId}) async {
|
|
await invoke(invokeMethod: "CallDeclinedAsync", currentUserID: senderN, targetUserID: recipientN, isVideoCall: false, data: [this.moduleId]);
|
|
}
|
|
|
|
void endCall(String senderN, String recipientN, {required int moduleId}) {
|
|
invoke(invokeMethod: "HangUpAsync", currentUserID: senderN, targetUserID: recipientN, data: [moduleId]);
|
|
}
|
|
|
|
void toggleMute() async {
|
|
await invoke(invokeMethod: "AudioToggle", currentUserID: senderN!, targetUserID: recipientN!);
|
|
isMuted = !isMuted;
|
|
notifyListeners();
|
|
}
|
|
|
|
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");
|
|
}
|
|
|
|
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}");
|
|
await callHubConnection?.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');
|
|
}
|
|
|
|
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("");
|
|
}
|
|
}
|
|
}
|
|
}
|