diff --git a/assets/audio/ring_30Sec.caf b/assets/audio/ring_30Sec.caf new file mode 100644 index 0000000..b5bb4b8 Binary files /dev/null and b/assets/audio/ring_30Sec.caf differ diff --git a/assets/audio/ring_60Sec.mp3 b/assets/audio/ring_60Sec.mp3 new file mode 100644 index 0000000..ac32394 Binary files /dev/null and b/assets/audio/ring_60Sec.mp3 differ diff --git a/lib/api/chat/chat_api_client.dart b/lib/api/chat/chat_api_client.dart index b4c0a2c..3ed7366 100644 --- a/lib/api/chat/chat_api_client.dart +++ b/lib/api/chat/chat_api_client.dart @@ -70,9 +70,7 @@ class ChatApiClient { if (!kReleaseMode) { logger.i("res: " + response.body); } - return ChatUserModel.fromJson( - json.decode(response.body), - ); + return ChatUserModel.fromJson(json.decode(response.body)); } catch (e) { throw e; } diff --git a/lib/generated_plugin_registrant.dart b/lib/generated_plugin_registrant.dart index 642c8ec..bcf8ba5 100644 --- a/lib/generated_plugin_registrant.dart +++ b/lib/generated_plugin_registrant.dart @@ -7,7 +7,6 @@ // ignore_for_file: depend_on_referenced_packages import 'package:audio_session/audio_session_web.dart'; -import 'package:camera_web/camera_web.dart'; import 'package:file_picker/_internal/file_picker_web.dart'; import 'package:firebase_core_web/firebase_core_web.dart'; import 'package:firebase_messaging_web/firebase_messaging_web.dart'; @@ -16,7 +15,6 @@ import 'package:geolocator_web/geolocator_web.dart'; import 'package:google_maps_flutter_web/google_maps_flutter_web.dart'; import 'package:image_picker_for_web/image_picker_for_web.dart'; import 'package:just_audio_web/just_audio_web.dart'; -import 'package:record_web/record_web.dart'; import 'package:shared_preferences_web/shared_preferences_web.dart'; import 'package:url_launcher_web/url_launcher_web.dart'; import 'package:video_player_web/video_player_web.dart'; @@ -26,7 +24,6 @@ import 'package:flutter_web_plugins/flutter_web_plugins.dart'; // ignore: public_member_api_docs void registerPlugins(Registrar registrar) { AudioSessionWeb.registerWith(registrar); - CameraPlugin.registerWith(registrar); FilePickerWeb.registerWith(registrar); FirebaseCoreWeb.registerWith(registrar); FirebaseMessagingWeb.registerWith(registrar); @@ -35,7 +32,6 @@ void registerPlugins(Registrar registrar) { GoogleMapsPlugin.registerWith(registrar); ImagePickerPlugin.registerWith(registrar); JustAudioPlugin.registerWith(registrar); - RecordPluginWeb.registerWith(registrar); SharedPreferencesPlugin.registerWith(registrar); UrlLauncherPlugin.registerWith(registrar); VideoPlayerPlugin.registerWith(registrar); diff --git a/lib/main.dart b/lib/main.dart index 07590f3..1856da1 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -70,9 +70,9 @@ Future main() async { ChangeNotifierProvider( create: (_) => MarathonProvider(), ), - // ChangeNotifierProvider( - // create: (_) => ChatCallProvider(), - // ), + ChangeNotifierProvider( + create: (_) => ChatCallProvider(), + ), ], child: const MyApp(), ), diff --git a/lib/models/chat/call.dart b/lib/models/chat/call.dart index ce58ae3..0f0e5a6 100644 --- a/lib/models/chat/call.dart +++ b/lib/models/chat/call.dart @@ -7,19 +7,31 @@ import 'dart:convert'; class CallDataModel { CallDataModel({ this.callerId, - this.callerDetails, + this.callerName, + this.callerEmail, + this.callerTitle, + this.callerPhone, this.receiverId, - this.receiverDetails, + this.receiverName, + this.receiverEmail, + this.receiverTitle, + this.receiverPhone, this.title, - this.calltype, + this.callType, }); - String? callerId; - CallerDetails? callerDetails; - String? receiverId; - ReceiverDetails? receiverDetails; - dynamic title; - String? calltype; + int? callerId; + String? callerName; + String? callerEmail; + String? callerTitle; + dynamic callerPhone; + int? receiverId; + String? receiverName; + String? receiverEmail; + dynamic receiverTitle; + dynamic receiverPhone; + String? title; + String? callType; factory CallDataModel.fromRawJson(String str) => CallDataModel.fromJson(json.decode(str)); @@ -27,171 +39,92 @@ class CallDataModel { factory CallDataModel.fromJson(Map json) => CallDataModel( callerId: json["callerID"], - callerDetails: json["callerDetails"] == null ? null : CallerDetails.fromJson(json["callerDetails"]), + callerName: json["callerName"], + callerEmail: json["callerEmail"], + callerTitle: json["callerTitle"], + callerPhone: json["callerPhone"], receiverId: json["receiverID"], - receiverDetails: json["receiverDetails"] == null ? null : ReceiverDetails.fromJson(json["receiverDetails"]), + receiverName: json["receiverName"], + receiverEmail: json["receiverEmail"], + receiverTitle: json["receiverTitle"], + receiverPhone: json["receiverPhone"], title: json["title"], - calltype: json["calltype"], + callType: json["callType"], ); Map toJson() => { "callerID": callerId, - "callerDetails": callerDetails?.toJson(), + "callerName": callerName, + "callerEmail": callerEmail, + "callerTitle": callerTitle, + "callerPhone": callerPhone, "receiverID": receiverId, - "receiverDetails": receiverDetails?.toJson(), + "receiverName": receiverName, + "receiverEmail": receiverEmail, + "receiverTitle": receiverTitle, + "receiverPhone": receiverPhone, "title": title, - "calltype": calltype, + "callType": callType, }; } -class CallerDetails { - CallerDetails({ - this.response, - this.errorResponses, - }); - Response? response; - dynamic errorResponses; - factory CallerDetails.fromRawJson(String str) => CallerDetails.fromJson(json.decode(str)); - String toRawJson() => json.encode(toJson()); - factory CallerDetails.fromJson(Map json) => CallerDetails( - response: json["response"] == null ? null : Response.fromJson(json["response"]), - errorResponses: json["errorResponses"], - ); +// To parse this JSON data, do +// +// final callSessionPayLoad = callSessionPayLoadFromJson(jsonString); - Map toJson() => { - "response": response?.toJson(), - "errorResponses": errorResponses, - }; -} -class Response { - Response({ - this.id, - this.userName, - this.email, - this.phone, - this.title, - this.token, - this.isDomainUser, - this.isActiveCode, - this.encryptedUserId, - this.encryptedUserName, +class CallSessionPayLoad { + CallSessionPayLoad({ + this.target, + this.caller, + this.sdp, }); - int? id; - String? userName; - String? email; - dynamic phone; - String? title; - String? token; - bool? isDomainUser; - bool? isActiveCode; - String? encryptedUserId; - String? encryptedUserName; + int? target; + int? caller; + Sdp? sdp; - factory Response.fromRawJson(String str) => Response.fromJson(json.decode(str)); + factory CallSessionPayLoad.fromRawJson(String str) => CallSessionPayLoad.fromJson(json.decode(str)); String toRawJson() => json.encode(toJson()); - factory Response.fromJson(Map json) => Response( - id: json["id"], - userName: json["userName"], - email: json["email"], - phone: json["phone"], - title: json["title"], - token: json["token"], - isDomainUser: json["isDomainUser"], - isActiveCode: json["isActiveCode"], - encryptedUserId: json["encryptedUserId"], - encryptedUserName: json["encryptedUserName"], + factory CallSessionPayLoad.fromJson(Map json) => CallSessionPayLoad( + target: json["target"], + caller: json["caller"], + sdp: json["sdp"] == null ? null : Sdp.fromJson(json["sdp"]), ); Map toJson() => { - "id": id, - "userName": userName, - "email": email, - "phone": phone, - "title": title, - "token": token, - "isDomainUser": isDomainUser, - "isActiveCode": isActiveCode, - "encryptedUserId": encryptedUserId, - "encryptedUserName": encryptedUserName, + "target": target, + "caller": caller, + "sdp": sdp?.toJson(), }; } -class ReceiverDetails { - ReceiverDetails({ - this.id, - this.userName, - this.email, - this.phone, - this.title, - this.userStatus, - this.image, - this.unreadMessageCount, - this.userAction, - this.isPin, - this.isFav, - this.isAdmin, - this.rKey, - this.totalCount, +class Sdp { + Sdp({ + this.type, + this.sdp, }); - int? id; - String? userName; - String? email; - dynamic phone; - dynamic title; - int? userStatus; - String? image; - int? unreadMessageCount; - dynamic userAction; - bool? isPin; - bool? isFav; - bool? isAdmin; - String? rKey; - int? totalCount; - - factory ReceiverDetails.fromRawJson(String str) => ReceiverDetails.fromJson(json.decode(str)); + String? type; + String? sdp; + + factory Sdp.fromRawJson(String str) => Sdp.fromJson(json.decode(str)); String toRawJson() => json.encode(toJson()); - factory ReceiverDetails.fromJson(Map json) => ReceiverDetails( - id: json["id"], - userName: json["userName"], - email: json["email"], - phone: json["phone"], - title: json["title"], - userStatus: json["userStatus"], - image: json["image"], - unreadMessageCount: json["unreadMessageCount"], - userAction: json["userAction"], - isPin: json["isPin"], - isFav: json["isFav"], - isAdmin: json["isAdmin"], - rKey: json["rKey"], - totalCount: json["totalCount"], + factory Sdp.fromJson(Map json) => Sdp( + type: json["type"], + sdp: json["sdp"], ); Map toJson() => { - "id": id, - "userName": userName, - "email": email, - "phone": phone, - "title": title, - "userStatus": userStatus, - "image": image, - "unreadMessageCount": unreadMessageCount, - "userAction": userAction, - "isPin": isPin, - "isFav": isFav, - "isAdmin": isAdmin, - "rKey": rKey, - "totalCount": totalCount, + "type": type, + "sdp": sdp, }; } diff --git a/lib/models/chat/webrtc_payloads.dart b/lib/models/chat/webrtc_payloads.dart new file mode 100644 index 0000000..1a3463f --- /dev/null +++ b/lib/models/chat/webrtc_payloads.dart @@ -0,0 +1,61 @@ +// To parse this JSON data, do +// +// final remoteIceCandidatePayLoad = remoteIceCandidatePayLoadFromJson(jsonString); + +import 'dart:convert'; + +class RemoteIceCandidatePayLoad { + RemoteIceCandidatePayLoad({ + this.target, + this.candidate, + }); + + int? target; + Candidate? candidate; + + factory RemoteIceCandidatePayLoad.fromRawJson(String str) => RemoteIceCandidatePayLoad.fromJson(json.decode(str)); + + String toRawJson() => json.encode(toJson()); + + factory RemoteIceCandidatePayLoad.fromJson(Map json) => RemoteIceCandidatePayLoad( + target: json["target"], + candidate: json["candidate"] == null ? null : Candidate.fromJson(json["candidate"]), + ); + + Map toJson() => { + "target": target, + "candidate": candidate?.toJson(), + }; +} + +class Candidate { + Candidate({ + this.candidate, + this.sdpMid, + this.sdpMLineIndex, + this.usernameFragment, + }); + + String? candidate; + String? sdpMid; + int? sdpMLineIndex; + String? usernameFragment; + + factory Candidate.fromRawJson(String str) => Candidate.fromJson(json.decode(str)); + + String toRawJson() => json.encode(toJson()); + + factory Candidate.fromJson(Map json) => Candidate( + candidate: json["candidate"], + sdpMid: json["sdpMid"], + sdpMLineIndex: json["sdpMLineIndex"], + usernameFragment: json["usernameFragment"], + ); + + Map toJson() => { + "candidate": candidate, + "sdpMid": sdpMid, + "sdpMLineIndex": sdpMLineIndex, + "usernameFragment": usernameFragment, + }; +} diff --git a/lib/provider/chat_call_provider.dart b/lib/provider/chat_call_provider.dart index 45205df..06c5201 100644 --- a/lib/provider/chat_call_provider.dart +++ b/lib/provider/chat_call_provider.dart @@ -1,36 +1,62 @@ import 'dart:convert'; -import 'dart:ui'; - +import 'dart:io'; import 'package:flutter/cupertino.dart'; import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_webrtc/flutter_webrtc.dart'; +import 'package:just_audio/just_audio.dart'; +import 'package:mohem_flutter_app/app_state/app_state.dart'; +import 'package:mohem_flutter_app/main.dart'; +import 'package:mohem_flutter_app/models/chat/call.dart'; +import 'package:mohem_flutter_app/models/chat/webrtc_payloads.dart'; +import 'package:mohem_flutter_app/provider/chat_provider_model.dart'; +import 'package:mohem_flutter_app/ui/chat/call/chat_incoming_call_screen.dart'; +import 'package:mohem_flutter_app/ui/chat/call/start_call_screen.dart'; import 'package:mohem_flutter_app/ui/landing/dashboard_screen.dart'; class ChatCallProvider with ChangeNotifier, DiagnosticableTreeMixin { ///////////////////// Web RTC Video Calling ////////////////////// // Video Call - late RTCPeerConnection _peerConnection; - RTCVideoRenderer _localVideoRenderer = RTCVideoRenderer(); - final RTCVideoRenderer _remoteRenderer = RTCVideoRenderer(); + late RTCPeerConnection _pc; + late ChatProviderModel chatProvModel; + RTCVideoRenderer localVideoRenderer = RTCVideoRenderer(); + RTCVideoRenderer remoteRenderer = RTCVideoRenderer(); + final AudioPlayer player = AudioPlayer(); + late MediaStream localStream; + late CallDataModel outGoingCallData; + bool isMicOff = false; + bool isLoudSpeaker = false; + bool isCamOff = false; + bool isCallEnded = false; + bool isVideoCall = false; + bool isCallStarted = false; + bool isFrontCamera = true; + + /// WebRTC Connection Variables + bool _offer = false; - MediaStream? _localStream; - MediaStream? _remoteStream; + late BuildContext providerContext; - void initCallListeners() { + void initCallListeners({required BuildContext context}) { + providerContext = context; + if (kDebugMode) { + print("=================== Call Listeners Registered ======================="); + } chatHubConnection.on("OnCallAcceptedAsync", onCallAcceptedAsync); chatHubConnection.on("OnIceCandidateAsync", onIceCandidateAsync); chatHubConnection.on("OnOfferAsync", onOfferAsync); chatHubConnection.on("OnAnswerOffer", onAnswerOffer); chatHubConnection.on("OnHangUpAsync", onHangUpAsync); chatHubConnection.on("OnCallDeclinedAsync", onCallDeclinedAsync); + chatHubConnection.on("OnIncomingCallAsync", OnIncomingCallAsync); } //Video Constraints - var videoConstraints = { + Map videoConstraints = { "video": { "mandatory": { - "width": {"min": 320}, - "height": {"min": 180} + "width": {"min": 1280}, + "height": {"min": 720} }, "optional": [ { @@ -41,12 +67,13 @@ class ChatCallProvider with ChangeNotifier, DiagnosticableTreeMixin { ] }, "frameRate": 25, - "width": 420, //420,//640,//1280, - "height": 240 //240//480//720 + "width": 1280, //420,//640,//1280, + "height": 720, //240//480//720 + "audio": true, }; // Audio Constraints - var audioConstraints = { + Map audioConstraints = { "sampleRate": 8000, "sampleSize": 16, "channelCount": 2, @@ -54,128 +81,200 @@ class ChatCallProvider with ChangeNotifier, DiagnosticableTreeMixin { "audio": true, }; - Future _createPeerConnection() async { - // {"url": "stun:stun.l.google.com:19302"}, - Map configuration = { - "iceServers": [ - {"urls": 'stun:15.185.116.59:3478'}, - {"urls": "turn:15.185.116.59:3479", "username": "admin", "credential": "admin"} - ] - }; + Future init() async { + creatOfferWithCon(); + } - Map offerSdpConstraints = { - "mandatory": { - "OfferToReceiveAudio": true, - "OfferToReceiveVideo": true, - }, - "optional": [], - }; + Future initLocalCamera({required ChatProviderModel chatProvmodel, required callData, required BuildContext context, bool isIncomingCall = false}) async { + isCallEnded = false; + chatProvModel = chatProvmodel; + outGoingCallData = callData; - RTCPeerConnection pc = await createPeerConnection(configuration, offerSdpConstraints); - // if (pc != null) print(pc); - //pc.addStream(widget.localStream); + await localVideoRenderer.initialize(); + localStream = await navigator.mediaDevices.getUserMedia(isVideoCall ? videoConstraints : audioConstraints); + localVideoRenderer.srcObject = localStream; + await remoteRenderer.initialize(); + // playRingtone(); + await startCall(callType: isVideoCall ? "Video" : "Audio", context: context); + _pc = await creatOfferWithCon(); + notifyListeners(); + } - 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; + Future startCall({required String callType, required BuildContext context}) async { + chatProvModel.isTextMsg = true; + chatProvModel.isAttachmentMsg = false; + chatProvModel.isVoiceMsg = false; + chatProvModel.isReplyMsg = false; + chatProvModel.isCall = true; + chatProvModel.message.text = "Start $callType call ${outGoingCallData.receiverName.toString().replaceAll(".", " ")}"; + chatProvModel.sendChatMessage( + context, + targetUserId: outGoingCallData.receiverId!, + userStatus: 1, + userEmail: outGoingCallData.receiverEmail!, + targetUserName: outGoingCallData.receiverName!, + ); + await invoke( + invokeMethod: "CallUserAsync", + currentUserID: outGoingCallData.callerId!, + targetUserID: outGoingCallData.receiverId!, + ); + await invoke(invokeMethod: "UpdateUserStatusAsync", currentUserID: outGoingCallData.callerId!, targetUserID: outGoingCallData.receiverId!, userStatus: 4); } - void init() { - initRenderers(); - _createPeerConnection().then((pc) { - _peerConnection = pc; - // _setRemoteDescription(widget.info); - }); + Future endCall() async { + await invoke(invokeMethod: "UpdateUserStatusAsync", currentUserID: outGoingCallData.callerId!, targetUserID: outGoingCallData.receiverId!, userStatus: 1); + await invoke(invokeMethod: "HangUpAsync", currentUserID: outGoingCallData.callerId!, targetUserID: outGoingCallData.receiverId!, userStatus: 1); + _pc.dispose(); + isCallStarted = false; + isVideoCall = false; + isCamOff = false; + isMicOff = false; + isLoudSpeaker = false; + localVideoRenderer.srcObject = null; + remoteRenderer.srcObject = null; + //player.stop(); + _offer = false; + return true; } - void initRenderers() { - _localVideoRenderer.initialize(); - _remoteRenderer.initialize(); - initLocalCamera(); + Future startIncomingCall() async { + await localVideoRenderer.initialize(); + localStream = await navigator.mediaDevices.getUserMedia(isVideoCall ? videoConstraints : audioConstraints); + localVideoRenderer.srcObject = localStream; + await remoteRenderer.initialize(); } - 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(); + // OutGoing Listeners + void onCallAcceptedAsync(List? params) async { + print("--------------------- On Call Accept ---------------------------------------"); + dynamic items = params!.toList(); + RTCSessionDescription description = await _createOffer(); + await _pc.setLocalDescription(description); + var payload = {"target": items[0]["id"], "caller": outGoingCallData.callerId, "sdp": description.toMap()}; + invoke(invokeMethod: "OfferAsync", currentUserID: outGoingCallData.callerId!, targetUserID: items[0]["id"], data: jsonEncode(payload)); } - void startCall({required String callType}) {} - - void endCall() {} - - 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; + Future onIceCandidateAsync(List? params) async { + print("--------------------- onIceCandidateAsync ---------------------------------------"); + var items = params!.toList(); + if (kDebugMode) { + logger.i("res: " + items.toString()); + } + RemoteIceCandidatePayLoad data = RemoteIceCandidatePayLoad.fromJson(jsonDecode(items.first.toString())); + if (_pc != null) { + await _pc.addCandidate(RTCIceCandidate(data.candidate!.candidate, data.candidate!.sdpMid, data.candidate!.sdpMLineIndex)); + if (!isCallStarted) { + isCallStarted = true; + if (isCallStarted) { + Navigator.push( + providerContext, + MaterialPageRoute( + builder: (BuildContext context) => StartCallPage(localRenderer: localVideoRenderer, remoteRenderer: remoteRenderer), + allowSnapshotting: false, + )).then((value) { + Navigator.of(providerContext).pop(); + }); + } + } } + notifyListeners(); } - //// Listeners Methods //// + void onOfferAsync(List? params) { + print("--------------------- onOfferAsync ---------------------------------------"); + } - void onCallAcceptedAsync(List? params) {} + // Incoming Listeners - void onIceCandidateAsync(List? params) {} + void onAnswerOffer(List? payload) async { + print("--------------------- On Answer Offer Async ---------------------------------------"); + var items = payload!.toList(); + if (kDebugMode) { + logger.i("res: " + items.toString()); + } + CallSessionPayLoad data = CallSessionPayLoad.fromJson(jsonDecode(items.first.toString())); + RTCSessionDescription description = RTCSessionDescription(data.sdp!.sdp, 'answer'); + _pc.setRemoteDescription(description); + } - void onOfferAsync(List? params) {} + void onHangUpAsync(List? params) { + print("--------------------- onHangUp ---------------------------------------"); + endCall().then((bool value) { + isCallEnded = true; + notifyListeners(); + }); + } - void onAnswerOffer(List? params) {} + Future OnIncomingCallAsync(List? params) async { + print("--------------------- On Incoming Call ---------------------------------------"); + dynamic items = params!.toList(); + logger.d(items); + Map json = { + "callerID": items[0]["id"], + "callerName": items[0]["userName"], + "callerEmail": items[0]["email"], + "callerTitle": items[0]["title"], + "callerPhone": null, + "receiverID": AppState().chatDetails!.response!.id, + "receiverName": AppState().chatDetails!.response!.userName, + "receiverEmail": AppState().chatDetails!.response!.email, + "receiverTitle": AppState().chatDetails!.response!.title, + "receiverPhone": AppState().chatDetails!.response!.phone, + "title": AppState().chatDetails!.response!.userName!.replaceAll(".", " "), + "callType": items[1] ? "Video" : "Audio", + }; - void onHangUpAsync(List? params) {} + CallDataModel callData = CallDataModel.fromJson(json); + await Navigator.push( + providerContext, + MaterialPageRoute( + builder: (BuildContext context) => + IncomingCall( + isVideoCall: items[1] ? true : false, + outGoingCallData: callData, + ), + ), + ); + } - void onCallDeclinedAsync(List? params) {} + void onCallDeclinedAsync(List? params) { + print("--------------------- on Call Declined ---------------------------------------"); + endCall().then((bool value) { + if (value) { + isCallEnded = true; + notifyListeners(); + } + }); + } //// Invoke Methods - Future invoke({required String invokeMethod, required String currentUserID, required String targetUserID, bool isVideoCall = false, var data}) async { + Future invoke({required String invokeMethod, required int currentUserID, required int targetUserID, var data, int userStatus = 1}) async { List args = []; - if (invokeMethod == "answerCallAsync") { - args = [currentUserID, targetUserID]; - } else if (invokeMethod == "CallUserAsync") { + // logger.w(currentUserID.toString() + " -- " + targetUserID.toString() + " -- " + isVideoCall.toString()); + if (invokeMethod == "CallUserAsync") { args = [currentUserID, targetUserID, isVideoCall]; + } else if (invokeMethod == "answerCallAsync") { + args = [currentUserID, targetUserID]; } else if (invokeMethod == "IceCandidateAsync") { args = [targetUserID, data]; } else if (invokeMethod == "OfferAsync") { args = [targetUserID, data]; } else if (invokeMethod == "AnswerOfferAsync") { args = [targetUserID, data]; - //json In Data + // json In Data + } else if (invokeMethod == "UpdateUserStatusAsync") { + args = [currentUserID, userStatus]; + } else if (invokeMethod == "HangUpAsync") { + args = [currentUserID, targetUserID]; + } + logger.d(args); + try { + await chatHubConnection.invoke("$invokeMethod", args: args); + } catch (e) { + logger.w(e); } - await chatHubConnection.invoke(invokeMethod, args: args); } void stopListeners() async { @@ -184,4 +283,139 @@ class ChatCallProvider with ChangeNotifier, DiagnosticableTreeMixin { chatHubConnection.off('OnIceCandidateAsync'); chatHubConnection.off('OnAnswerOffer'); } + + void disposeRenders() async { + await localVideoRenderer.dispose(); + localStream.dispose(); + notifyListeners(); + } + + Future creatOfferWithCon() async { + Map configuration = { + "sdpSemantics": "plan-b", + 'iceServers': [ + { + 'urls': 'stun:15.185.116.59:3478', + }, + { + 'urls': 'turn:15.185.116.59:3479', + 'username': 'admin', + 'credential': 'admin', + }, + ] + }; + Map offerSdpConstraints = { + 'mandatory': { + 'OfferToReceiveAudio': true, + 'OfferToReceiveVideo': true, + }, + 'optional': [] + }; + RTCPeerConnection pc = await createPeerConnection(configuration, offerSdpConstraints); + await pc!.addStream(localStream!); + pc?.onConnectionState = (RTCPeerConnectionState state) {}; + pc?.onAddStream = (MediaStream stream) { + remoteRenderer.srcObject = stream; + notifyListeners(); + }; + pc!.onIceCandidate = (RTCIceCandidate e) async { + if (e.candidate != null) { + var payload = {"target": outGoingCallData.callerId, "candidate": e.toMap()}; + logger.i("Candidate:" + e.toMap().toString()); + await invoke(invokeMethod: "IceCandidateAsync", currentUserID: outGoingCallData.callerId!, targetUserID: outGoingCallData.receiverId!, data: jsonEncode(payload)); + } + }; + // pc!.onTrack = (RTCTrackEvent event) async { + // + // String streamId = const Uuid().toString(); + // MediaStream remoteStream = await createLocalMediaStream(streamId); + // event.streams[0].getTracks().forEach((MediaStreamTrack element) { + // logger.i("Stream Track: " + element.id.toString()); + // // remoteRenderer.srcObject = element; + // remoteStream.addTrack(element); + // }); + // }; + pc!.onSignalingState = (RTCSignalingState state) { + logger.i("signaling state: " + state.name); + }; + pc!.onIceGatheringState = (RTCIceGatheringState state) { + logger.i("rtc ice gathering state: " + state.name); + }; + pc!.onIceConnectionState = (RTCIceConnectionState state) { + logger.i("rtc ice connection state: " + state.name); + }; + pc!.onRenegotiationNeeded = () {}; + return pc; + } + + void playRingtone() async { + player.stop(); + await player.setVolume(1.0); + String audioAsset = ""; + if (Platform.isAndroid) { + audioAsset = "assets/audio/ring_60Sec.mp3"; + } else { + audioAsset = "assets/audio/ring_30Sec.caf"; + } + try { + await player.setAsset(audioAsset); + await player.load(); + player.play(); + } catch (e) { + print("Error: $e"); + } + } + + Future _createOffer() async { + RTCSessionDescription description = await _pc!.createOffer(); + _offer = true; + return description; + } + +// Future _createAnswer() async { +// RTCSessionDescription description = await _pc!.createAnswer(); +// var session = description.sdp.toString(); +// return description; +// _pc!.setLocalDescription(description); +// } + + void micOff() { + isMicOff = !isMicOff; + localStream.getAudioTracks().forEach((track) { + track.enabled = !track.enabled; + }); + notifyListeners(); + } + + void camOff() { + isCamOff = !isCamOff; + localStream.getVideoTracks().forEach((track) { + track.enabled = !track.enabled; + }); + if (isCamOff) { + isVideoCall = false; + } else { + isVideoCall = true; + } + notifyListeners(); + } + + void loudOn() { + isLoudSpeaker = !isLoudSpeaker; + remoteRenderer.srcObject?.getAudioTracks().forEach((track) { + if (isLoudSpeaker) { + track.enableSpeakerphone(true); + } else { + track.enableSpeakerphone(false); + } + }); + notifyListeners(); + } + + void switchCamera() { + isFrontCamera = !isFrontCamera; + print("================= Camera Switch Triggered ==================="); + Helper.switchCamera(localStream.getVideoTracks()[0]); + notifyListeners(); + } } diff --git a/lib/provider/chat_provider_model.dart b/lib/provider/chat_provider_model.dart index d374114..b04bcc6 100644 --- a/lib/provider/chat_provider_model.dart +++ b/lib/provider/chat_provider_model.dart @@ -24,12 +24,14 @@ import 'package:mohem_flutter_app/models/chat/get_single_user_chat_list_model.da import 'package:mohem_flutter_app/models/chat/get_user_login_token_model.dart' as userLoginToken; import 'package:mohem_flutter_app/models/chat/make_user_favotire_unfavorite_chat_model.dart' as fav; import 'package:mohem_flutter_app/models/my_team/get_employee_subordinates_list.dart'; +import 'package:mohem_flutter_app/provider/chat_call_provider.dart'; import 'package:mohem_flutter_app/ui/chat/chat_detailed_screen.dart'; import 'package:mohem_flutter_app/ui/landing/dashboard_screen.dart'; import 'package:mohem_flutter_app/widgets/image_picker.dart'; import 'package:open_file/open_file.dart'; import 'package:path_provider/path_provider.dart'; import 'package:permission_handler/permission_handler.dart'; +import 'package:provider/provider.dart'; import 'package:signalr_netcore/hub_connection.dart'; import 'package:signalr_netcore/signalr_client.dart'; import 'package:uuid/uuid.dart'; @@ -80,6 +82,7 @@ class ChatProviderModel with ChangeNotifier, DiagnosticableTreeMixin { int pageNo = 1; bool disbaleChatForThisUser = false; + bool isCall = false; Future getUserAutoLoginToken() async { userLoginToken.UserAutoLoginModel userLoginResponse = await ChatApiClient().getUserLoginToken(); @@ -95,7 +98,7 @@ class ChatProviderModel with ChangeNotifier, DiagnosticableTreeMixin { } } - Future buildHubConnection() async { + Future buildHubConnection({required BuildContext context, required ChatCallProvider ccProvider}) async { chatHubConnection = await getHubConnection(); await chatHubConnection.start(); if (kDebugMode) { @@ -103,6 +106,7 @@ class ChatProviderModel with ChangeNotifier, DiagnosticableTreeMixin { } chatHubConnection.on("OnDeliveredChatUserAsync", onMsgReceived); chatHubConnection.on("OnGetChatConversationCount", onNewChatConversion); + ccProvider.initCallListeners(context: context); } Future getHubConnection() async { @@ -124,6 +128,15 @@ class ChatProviderModel with ChangeNotifier, DiagnosticableTreeMixin { chatHubConnection.on("OnGetUserChatHistoryNotDeliveredAsync", chatNotDelivered); chatHubConnection.on("OnUpdateUserChatHistoryStatusAsync", updateUserChatStatus); + // Calling + + // chatHubConnection.on("OnCallAcceptedAsync", callP.onCallAcceptedAsync); + // chatHubConnection.on("OnIceCandidateAsync", callP.onIceCandidateAsync); + // chatHubConnection.on("OnOfferAsync", callP.onOfferAsync); + // chatHubConnection.on("OnAnswerOffer", callP.onAnswerOffer); + // chatHubConnection.on("OnHangUpAsync", callP.onHangUpAsync); + // chatHubConnection.on("OnCallDeclinedAsync", callP.onCallDeclinedAsync); + if (kDebugMode) { logger.i("All listeners registered"); } @@ -354,6 +367,7 @@ class ChatProviderModel with ChangeNotifier, DiagnosticableTreeMixin { Future onMsgReceived(List? parameters) async { List data = [], temp = []; + for (dynamic msg in parameters!) { data = getSingleUserChatModel(jsonEncode(msg)); temp = getSingleUserChatModel(jsonEncode(msg)); @@ -363,7 +377,6 @@ class ChatProviderModel with ChangeNotifier, DiagnosticableTreeMixin { data.first.currentUserId = temp.first.targetUserId; data.first.currentUserName = temp.first.targetUserName; data.first.currentUserEmail = temp.first.targetUserEmail; - if (data.first.fileTypeId == 12 || data.first.fileTypeId == 4 || data.first.fileTypeId == 3) { data.first.image = await ChatApiClient().downloadURL(fileName: data.first.contant!, fileTypeDescription: data.first.fileTypeResponse!.fileTypeDescription ?? "image/jpg"); } @@ -429,10 +442,6 @@ class ChatProviderModel with ChangeNotifier, DiagnosticableTreeMixin { } void OnSubmitChatAsync(List? parameters) { - print(isChatScreenActive); - print(receiverID); - print(isChatScreenActive); - logger.i(parameters); List data = [], temp = []; for (dynamic msg in parameters!) { data = getSingleUserChatModel(jsonEncode(msg)); @@ -632,7 +641,7 @@ class ChatProviderModel with ChangeNotifier, DiagnosticableTreeMixin { return; } sendChatToServer( - chatEventId: 1, + chatEventId: isCall ? 3 : 1, fileTypeId: null, targetUserId: targetUserId, targetUserName: targetUserName, @@ -1030,6 +1039,7 @@ class ChatProviderModel with ChangeNotifier, DiagnosticableTreeMixin { favUsersList.clear(); searchedChats?.clear(); pChatHistory?.clear(); + // callP.stopListeners(); chatHubConnection.stop(); AppState().chatDetails = null; } diff --git a/lib/ui/chat/call/chat_incoming_call_screen.dart b/lib/ui/chat/call/chat_incoming_call_screen.dart index 1b6a5aa..df7293e 100644 --- a/lib/ui/chat/call/chat_incoming_call_screen.dart +++ b/lib/ui/chat/call/chat_incoming_call_screen.dart @@ -1,381 +1,233 @@ +import 'dart:convert'; import 'dart:ui'; -import 'package:camera/camera.dart'; +import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:flutter_webrtc/flutter_webrtc.dart'; import 'package:mohem_flutter_app/classes/colors.dart'; import 'package:mohem_flutter_app/classes/utils.dart'; +import 'package:mohem_flutter_app/extensions/int_extensions.dart'; +import 'package:mohem_flutter_app/main.dart'; import 'package:mohem_flutter_app/models/chat/call.dart'; +import 'package:mohem_flutter_app/provider/chat_call_provider.dart'; +import 'package:mohem_flutter_app/provider/chat_provider_model.dart'; +import 'package:provider/provider.dart'; class IncomingCall extends StatefulWidget { - CallDataModel incomingCallData; - bool? isVideoCall; + CallDataModel outGoingCallData; + bool isVideoCall; - IncomingCall({Key? key, required this.incomingCallData, this.isVideoCall}) : super(key: key); + IncomingCall({Key? key, required this.outGoingCallData, required this.isVideoCall}) : super(key: key); @override _IncomingCallState createState() => _IncomingCallState(); } class _IncomingCallState extends State with SingleTickerProviderStateMixin { - AnimationController? _animationController; - CameraController? _controller; - Future? _initializeControllerFuture; - bool isCameraReady = false; + late ChatCallProvider callProvider; + late ChatProviderModel chatProvider; @override void initState() { - _animationController = AnimationController( - vsync: this, - duration: const Duration( - milliseconds: 500, - ), - ); - //_runAnimation(); - // connectSignaling(); - WidgetsBinding.instance.addPostFrameCallback( - (_) => _runAnimation(), - ); - + chatProvider = Provider.of(context, listen: false); + callProvider = Provider.of(context, listen: false); + init(); super.initState(); } + void init() { + widget.isVideoCall ? callProvider.isVideoCall = true : callProvider.isVideoCall = false; + callProvider.initLocalCamera(chatProvmodel: chatProvider, callData: widget.outGoingCallData, context: context, isIncomingCall: true); + callProvider.init(); + } + @override Widget build(BuildContext context) { return Scaffold( - body: FutureBuilder( - future: _initializeControllerFuture, - builder: (BuildContext context, AsyncSnapshot snapshot) { - if (snapshot.connectionState == ConnectionState.done) { - return Stack( - alignment: FractionalOffset.center, - children: [ - if (widget.isVideoCall!) - Positioned.fill( - child: AspectRatio( - aspectRatio: _controller!.value.aspectRatio, - child: CameraPreview( - _controller!, + body: Consumer(builder: (BuildContext context, ChatCallProvider chatcp, Widget? child) { + if (chatcp.isCallEnded) Navigator.pop(context); + return Stack( + alignment: FractionalOffset.center, + children: [ + if (widget.isVideoCall) + Positioned.fill( + child: RTCVideoView( + callProvider.localVideoRenderer, + objectFit: RTCVideoViewObjectFit.RTCVideoViewObjectFitCover, + ), + ), + Positioned.fill( + child: ClipRect( + child: BackdropFilter( + filter: ImageFilter.blur(sigmaX: 5.0, sigmaY: 5.0), + child: Container( + decoration: BoxDecoration( + color: MyColors.grey57Color.withOpacity( + 0.3, ), ), - ), - Positioned.fill( - child: ClipRect( - child: BackdropFilter( - filter: ImageFilter.blur(sigmaX: 10.0, sigmaY: 10.0), - child: Container( - decoration: BoxDecoration( - color: MyColors.grey57Color.withOpacity( - 0.7, - ), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.max, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.max, + children: [ + 40.height, + Row( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.center, children: [ Container( margin: const EdgeInsets.all(21.0), - child: Row( - children: [ - Image.asset( - "assets/images/logos/main_mohemm_logo.png", - height: 70, - width: 70, - ), - Container( - margin: const EdgeInsets.only( - left: 10.0, - right: 10.0, - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.spaceAround, - children: const [ - - // todo @aamir, need to use extension mehtods - Text( - "Aamir Saleem Ahmad", - style: TextStyle( - fontSize: 21, - fontWeight: FontWeight.bold, - color: MyColors.white, - letterSpacing: -1.26, - height: 23 / 12, - ), - ), - Text( - "Calling...", - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.w600, - color: Color( - 0xffC6C6C6, - ), - letterSpacing: -0.48, - height: 23 / 24, - ), - ), - SizedBox( - height: 2, - ), - ], + child: Container( + margin: const EdgeInsets.only( + left: 10.0, + right: 10.0, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + SvgPicture.asset( + "assets/images/user.svg", + height: 70, + width: 70, + fit: BoxFit.cover, ), - ), - ], - ), - ), - // Container( - // margin: const EdgeInsets.all(21.0), - // width: MediaQuery.of(context).size.width, - // decoration: cardRadius(15.0, color: MyColors.black, elevation: null), - // child: Column( - // crossAxisAlignment: CrossAxisAlignment.start, - // mainAxisSize: MainAxisSize.min, - // children: [ - // Container( - // padding: const EdgeInsets.fromLTRB(16.0, 16.0, 16.0, 6.0), - // child: Text( - // "TranslationBase.of(context).appoInfo", - // style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: MyColors.white, letterSpacing: -0.64, height: 23 / 12), - // ), - // ), - // Container( - // padding: const EdgeInsets.only(left: 16.0, right: 16.0), - // child: Text( - // "widget.incomingCallData.appointmentdate + widget.incomingCallData.appointmenttime", - // style: TextStyle(fontSize: 12.0, letterSpacing: -0.48, color: Color(0xff8E8E8E), fontWeight: FontWeight.w600), - // ), - // ), - // Container( - // padding: const EdgeInsets.only(left: 16.0, right: 16.0, bottom: 21.0), - // child: Text( - // "widget.incomingCallData.clinicname", - // style: TextStyle(fontSize: 12.0, letterSpacing: -0.48, color: Color(0xff8E8E8E), fontWeight: FontWeight.w600), - // ), - // ), - // ], - // ), - // ), - const Spacer(), - Container( - margin: const EdgeInsets.only( - bottom: 70.0, - left: 49, - right: 49, - ), - child: Row( - mainAxisSize: MainAxisSize.max, - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - RotationTransition( - turns: Tween( - begin: 0.0, - end: -.1, - ) - .chain( - CurveTween( - curve: Curves.elasticIn, - ), - ) - .animate( - _animationController!, - ), - child: RawMaterialButton( - onPressed: () { - _submit(); - }, - elevation: 2.0, - fillColor: MyColors.green2DColor, - padding: const EdgeInsets.all( - 15.0, - ), - shape: const CircleBorder(), - child: const Icon( - Icons.call, + 10.height, + Text( + widget.outGoingCallData.receiverName.toString().replaceAll(".", " "), + style: const TextStyle( + fontSize: 21, + fontWeight: FontWeight.bold, color: MyColors.white, - size: 35.0, + letterSpacing: -1.26, + height: 23 / 12, ), ), - ), - RawMaterialButton( - onPressed: () { - backToHome(); - }, - elevation: 2.0, - fillColor: MyColors.redA3Color, - padding: const EdgeInsets.all( - 15.0, + const Text( + "Ringing...", + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + color: Color( + 0xffC6C6C6, + ), + letterSpacing: -0.48, + height: 23 / 24, + ), ), - shape: const CircleBorder(), - child: const Icon( - Icons.call_end, - color: MyColors.white, - size: 35.0, + const SizedBox( + height: 2, ), - ), - ], + ], + ), ), ), ], ), - ), + const Spacer(), + Container( + margin: const EdgeInsets.only( + bottom: 70.0, + left: 49, + right: 49, + ), + child: Row( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + if (widget.isVideoCall) + RawMaterialButton( + onPressed: () { + callProvider.camOff(); + }, + elevation: 2.0, + fillColor: callProvider.isCamOff ? MyColors.green2DColor : Colors.grey, + padding: const EdgeInsets.all( + 15.0, + ), + shape: const CircleBorder(), + child: Icon( + callProvider.isCamOff ? Icons.videocam_off : Icons.videocam, + color: MyColors.white, + size: 35.0, + ), + ) + else + RawMaterialButton( + onPressed: () { + callProvider.loudOn(); + }, + elevation: 2.0, + fillColor: callProvider.isLoudSpeaker ? MyColors.green2DColor : Colors.grey, + padding: const EdgeInsets.all( + 15.0, + ), + shape: const CircleBorder(), + child: const Icon( + Icons.volume_up, + color: MyColors.white, + size: 35.0, + ), + ), + RawMaterialButton( + onPressed: () { + callProvider.micOff(); + }, + elevation: 2.0, + fillColor: callProvider.isMicOff ? MyColors.green2DColor : Colors.grey, + padding: const EdgeInsets.all( + 15.0, + ), + shape: const CircleBorder(), + child: Icon( + callProvider.isMicOff ? Icons.mic_off : Icons.mic, + color: MyColors.white, + size: 35.0, + ), + ), + RawMaterialButton( + onPressed: () { + callProvider.endCall().then((value) { + if (value) { + Navigator.of(context).pop(); + } + }); + }, + elevation: 2.0, + fillColor: MyColors.redA3Color, + padding: const EdgeInsets.all( + 15.0, + ), + shape: const CircleBorder(), + child: const Icon( + Icons.call_end, + color: MyColors.white, + size: 35.0, + ), + ), + ], + ), + ), + ], ), ), ), - ], - ); - } else { - return const Center( - child: CircularProgressIndicator(), - ); - } - }, - ), + ), + ), + ], + ); + }), ); } - void _runAnimation() async { - List cameras = await availableCameras(); - CameraDescription firstCamera = cameras[1]; - _controller = CameraController( - firstCamera, - ResolutionPreset.medium, - ); - _initializeControllerFuture = _controller!.initialize(); - setState(() {}); - // setAudioFile(); - for (int i = 0; i < 100; i++) { - await _animationController!.forward(); - await _animationController!.reverse(); - } - } - - Future _submit() async { - try { - // backToHome(); - // final roomModel = RoomModel(name: widget.incomingCallData.name, token: widget.incomingCallData.sessionId, identity: widget.incomingCallData.identity); - await _controller?.dispose(); - - // changeCallStatusAPI(4); - - // if (_session != null && _signaling != null) { - // await Navigator.of(context).pushReplacement( - // MaterialPageRoute( - // // fullscreenDialog: true, - // builder: (BuildContext context) { - // // if (widget.incomingCallData.isWebRTC == "true") { - // return StartVideoCall(signaling: _signaling, session: _session); - // - // // else { - // // return OpenTokConnectCallPage(apiKey: OPENTOK_API_KEY, sessionId: widget.incomingCallData.sessionId, token: widget.incomingCallData.token); - // // } - // - // // return VideoCallWebPage(receiverId: widget.incomingCallData.receiverID, callerId: widget.incomingCallData.callerID); // Web WebRTC VideoCall - // - // // return CallHomePage(receiverId: widget.incomingCallData.receiverID, callerId: widget.incomingCallData.callerID); // App WebRTC VideoCall - // }, - // ), - // ); - // } else { - // // Invalid Params/Data - // Utils.showToast("Failed to establish connection with server"); - // } - } catch (err) { - print(err); - // await PlatformExceptionAlertDialog( - // exception: err, - // ).show(context); - - Utils.showToast(err.toString()); - } - } - - // void changeCallStatusAPI(int sessionStatus) { - // LiveCareService service = new LiveCareService(); - // service.endCallAPI(widget.incomingCallData.sessionId, sessionStatus, context).then((res) {}).catchError((err) { - // print(err); - // }); - // } - - void backToHome() async { - // final connected = await signaling.declineCall(widget.incomingCallData.callerID, widget.incomingCallData.receiverID); - // LandingPage.isOpenCallPage = false; - // _signaling - _animationController!.dispose(); - // player.stop(); - // changeCallStatusAPI(3); - // _signaling.bye(_session, callRejected: true); - // _signaling.callDisconnected(_session, callRejected: true); - Navigator.of(context).pop(); - } - - // - // void disposeAudioResources() async { - // await player.dispose(); - // } - // - // void setAudioFile() async { - // player.stop(); - // await player.setVolume(1.0); // full volume - // try { - // await player.setAsset('assets/sounds/ring_60Sec.mp3').then((value) { - // player.setLoopMode(LoopMode.one); // loop ring sound - // player.play(); - // }).catchError((err) { - // print("Error: $err"); - // }); - // } catch (e) { - // print("Error: $e"); - // } - // } - // - // void connectSignaling({@required bool iAmCaller = false}) async { - // print("----------------- + Signaling Connection Started ---------------------------"); - // var caller = widget.incomingCallData.callerID; - // var receiver = widget.incomingCallData.receiverID; - // var host = widget.incomingCallData.server; - // - // var selfRole = iAmCaller ? "Caller" : "Receiver"; - // var selfId = iAmCaller ? caller : receiver; - // var selfUser = SocketUser(id: selfId, name: "$selfRole-$selfId", userAgent: DeviceInfo.userAgent, moreInfo: {}); - // - // var remoteRole = !iAmCaller ? "Caller" : "Receiver"; - // var remoteId = !iAmCaller ? caller : receiver; - // var remoteUser = SocketUser(id: remoteId, name: "$remoteRole-$remoteId", userAgent: DeviceInfo.userAgent, moreInfo: {}); - // - // var sessionId = "$caller-$receiver"; - // _session = SessionOneToOne(id: sessionId, local_user: selfUser, remote_user: remoteUser); - // - // _signaling = Signaling(host, session: _session); - // await _signaling.connect(); - // - // if (_signaling.state == SignalingState.Open) { - // return; - // } - // } - BoxDecoration cardRadius(double radius, {required Color color, double? elevation}) { return BoxDecoration( shape: BoxShape.rectangle, color: color ?? Colors.white, - borderRadius: BorderRadius.all( - Radius.circular(radius), - ), - boxShadow: [ - BoxShadow( - color: const Color( - 0xff000000, - ).withOpacity( - .05, - ), - //spreadRadius: 5, - blurRadius: elevation ?? 27, - offset: const Offset( - -2, - 3, - ), - ), - ], + borderRadius: BorderRadius.all(Radius.circular(radius)), + boxShadow: [BoxShadow(color: const Color(0xff000000).withOpacity(.05), blurRadius: elevation ?? 27, offset: const Offset(-2, 3))], ); } } diff --git a/lib/ui/chat/call/chat_outgoing_call_screen.dart b/lib/ui/chat/call/chat_outgoing_call_screen.dart index 2356e10..f38c67c 100644 --- a/lib/ui/chat/call/chat_outgoing_call_screen.dart +++ b/lib/ui/chat/call/chat_outgoing_call_screen.dart @@ -1,16 +1,18 @@ import 'dart:convert'; import 'dart:ui'; -import 'package:camera/camera.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; +import 'package:flutter_webrtc/flutter_webrtc.dart'; import 'package:mohem_flutter_app/classes/colors.dart'; import 'package:mohem_flutter_app/classes/utils.dart'; import 'package:mohem_flutter_app/extensions/int_extensions.dart'; import 'package:mohem_flutter_app/main.dart'; import 'package:mohem_flutter_app/models/chat/call.dart'; import 'package:mohem_flutter_app/provider/chat_call_provider.dart'; +import 'package:mohem_flutter_app/provider/chat_provider_model.dart'; +import 'package:mohem_flutter_app/ui/chat/call/start_call_screen.dart'; import 'package:provider/provider.dart'; class OutGoingCall extends StatefulWidget { @@ -23,407 +25,242 @@ class OutGoingCall extends StatefulWidget { _OutGoingCallState createState() => _OutGoingCallState(); } -class _OutGoingCallState extends State with SingleTickerProviderStateMixin { - AnimationController? _animationController; - late CameraController controller; - late List _cameras; - Future? _initializeControllerFuture; - bool isCameraReady = false; - bool isMicOff = false; - bool isLoudSpeaker = false; - bool isCamOff = false; - late ChatCallProvider callProviderd; +class _OutGoingCallState extends State{ + late ChatCallProvider callProvider; + late ChatProviderModel chatProvider; @override void initState() { - callProviderd = Provider.of(context, listen: false); - _animationController = AnimationController( - vsync: this, - duration: const Duration( - milliseconds: 500, - ), - ); - // _runAnimation(); - // connectSignaling(); - WidgetsBinding.instance.addPostFrameCallback( - (_) => _runAnimation(), - ); - + chatProvider = Provider.of(context, listen: false); + callProvider = Provider.of(context, listen: false); + init(); super.initState(); } + void init() { + widget.isVideoCall ? callProvider.isVideoCall = true : callProvider.isVideoCall = false; + callProvider.initLocalCamera(chatProvmodel: chatProvider, callData: widget.outGoingCallData, context: context); + //callProvider.init(); + } + @override Widget build(BuildContext context) { return Scaffold( - body: FutureBuilder( - future: _initializeControllerFuture, - builder: (BuildContext context, AsyncSnapshot snapshot) { - if (snapshot.connectionState == ConnectionState.done) { - return Stack( - alignment: FractionalOffset.center, - children: [ - if (widget.isVideoCall) - Positioned.fill( - child: CameraPreview( - controller, + body: Consumer(builder: (BuildContext context, ChatCallProvider chatcp, Widget? child) { + if (chatcp.isCallEnded) Navigator.pop(context); + return Stack( + alignment: FractionalOffset.center, + children: [ + if (widget.isVideoCall) + Positioned.fill( + child: RTCVideoView( + callProvider.localVideoRenderer, + objectFit: RTCVideoViewObjectFit.RTCVideoViewObjectFitCover, + ), + ), + Positioned.fill( + child: ClipRect( + child: BackdropFilter( + filter: ImageFilter.blur(sigmaX: 5.0, sigmaY: 5.0), + child: Container( + decoration: BoxDecoration( + color: MyColors.grey57Color.withOpacity( + 0.3, + ), ), - ), - Positioned.fill( - child: ClipRect( - child: BackdropFilter( - filter: ImageFilter.blur(sigmaX: 10.0, sigmaY: 10.0), - child: Container( - decoration: BoxDecoration( - color: MyColors.grey57Color.withOpacity( - 0.3, - ), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.max, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.max, + children: [ + 40.height, + Row( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.center, children: [ - 40.height, - Row( - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Container( - margin: const EdgeInsets.all(21.0), - child: Container( - margin: const EdgeInsets.only( - left: 10.0, - right: 10.0, - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.spaceAround, - children: [ - SvgPicture.asset( - "assets/images/user.svg", - height: 70, - width: 70, - fit: BoxFit.cover, - ), - 10.height, - Text( - widget.outGoingCallData.title, - style: const TextStyle( - fontSize: 21, - fontWeight: FontWeight.bold, - color: MyColors.white, - letterSpacing: -1.26, - height: 23 / 12, - ), - ), - const Text( - "Ringing...", - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.w600, - color: Color( - 0xffC6C6C6, - ), - letterSpacing: -0.48, - height: 23 / 24, - ), - ), - const SizedBox( - height: 2, - ), - ], - ), - ), - ), - ], - ), - // Container( - // margin: const EdgeInsets.all(21.0), - // width: MediaQuery.of(context).size.width, - // decoration: cardRadius(15.0, color: MyColors.black, elevation: null), - // child: Column( - // crossAxisAlignment: CrossAxisAlignment.start, - // mainAxisSize: MainAxisSize.min, - // children: [ - // Container( - // padding: const EdgeInsets.fromLTRB(16.0, 16.0, 16.0, 6.0), - // child: Text( - // "TranslationBase.of(context).appoInfo", - // style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: MyColors.white, letterSpacing: -0.64, height: 23 / 12), - // ), - // ), - // Container( - // padding: const EdgeInsets.only(left: 16.0, right: 16.0), - // child: Text( - // "widget.OutGoingCallData.appointmentdate + widget.OutGoingCallData.appointmenttime", - // style: TextStyle(fontSize: 12.0, letterSpacing: -0.48, color: Color(0xff8E8E8E), fontWeight: FontWeight.w600), - // ), - // ), - // Container( - // padding: const EdgeInsets.only(left: 16.0, right: 16.0, bottom: 21.0), - // child: Text( - // "widget.OutGoingCallData.clinicname", - // style: TextStyle(fontSize: 12.0, letterSpacing: -0.48, color: Color(0xff8E8E8E), fontWeight: FontWeight.w600), - // ), - // ), - // ], - // ), - // ), - const Spacer(), Container( - margin: const EdgeInsets.only( - bottom: 70.0, - left: 49, - right: 49, - ), - child: Row( - mainAxisSize: MainAxisSize.max, - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - if (widget.isVideoCall) - RawMaterialButton( - onPressed: () { - _camOff(); - }, - elevation: 2.0, - fillColor: isCamOff ? MyColors.green2DColor : Colors.grey, - padding: const EdgeInsets.all( - 15.0, - ), - shape: const CircleBorder(), - child: Icon( - isCamOff ? Icons.videocam_off : Icons.videocam, - color: MyColors.white, - size: 35.0, - ), - ) - else - RawMaterialButton( - onPressed: () { - _loudOn(); - }, - elevation: 2.0, - fillColor: isLoudSpeaker ? MyColors.green2DColor : Colors.grey, - padding: const EdgeInsets.all( - 15.0, - ), - shape: const CircleBorder(), - child: const Icon( - Icons.volume_up, + margin: const EdgeInsets.all(21.0), + child: Container( + margin: const EdgeInsets.only( + left: 10.0, + right: 10.0, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + SvgPicture.asset( + "assets/images/user.svg", + height: 70, + width: 70, + fit: BoxFit.cover, + ), + 10.height, + Text( + widget.outGoingCallData.receiverName.toString().replaceAll(".", " "), + style: const TextStyle( + fontSize: 21, + fontWeight: FontWeight.bold, color: MyColors.white, - size: 35.0, + letterSpacing: -1.26, + height: 23 / 12, ), ), - RawMaterialButton( - onPressed: () { - _micOff(); - }, - elevation: 2.0, - fillColor: isMicOff ? MyColors.green2DColor : Colors.grey, - padding: const EdgeInsets.all( - 15.0, - ), - shape: const CircleBorder(), - child: Icon( - isMicOff ? Icons.mic_off : Icons.mic, - color: MyColors.white, - size: 35.0, - ), - ), - RawMaterialButton( - onPressed: () { - backToHome(); - }, - elevation: 2.0, - fillColor: MyColors.redA3Color, - padding: const EdgeInsets.all( - 15.0, + const Text( + "Ringing...", + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + color: Color( + 0xffC6C6C6, + ), + letterSpacing: -0.48, + height: 23 / 24, + ), ), - shape: const CircleBorder(), - child: const Icon( - Icons.call_end, - color: MyColors.white, - size: 35.0, + const SizedBox( + height: 2, ), - ), - ], + ], + ), ), ), ], ), - ), + // Container( + // margin: const EdgeInsets.all(21.0), + // width: MediaQuery.of(context).size.width, + // decoration: cardRadius(15.0, color: MyColors.black, elevation: null), + // child: Column( + // crossAxisAlignment: CrossAxisAlignment.start, + // mainAxisSize: MainAxisSize.min, + // children: [ + // Container( + // padding: const EdgeInsets.fromLTRB(16.0, 16.0, 16.0, 6.0), + // child: Text( + // "TranslationBase.of(context).appoInfo", + // style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: MyColors.white, letterSpacing: -0.64, height: 23 / 12), + // ), + // ), + // Container( + // padding: const EdgeInsets.only(left: 16.0, right: 16.0), + // child: Text( + // "widget.OutGoingCallData.appointmentdate + widget.OutGoingCallData.appointmenttime", + // style: TextStyle(fontSize: 12.0, letterSpacing: -0.48, color: Color(0xff8E8E8E), fontWeight: FontWeight.w600), + // ), + // ), + // Container( + // padding: const EdgeInsets.only(left: 16.0, right: 16.0, bottom: 21.0), + // child: Text( + // "widget.OutGoingCallData.clinicname", + // style: TextStyle(fontSize: 12.0, letterSpacing: -0.48, color: Color(0xff8E8E8E), fontWeight: FontWeight.w600), + // ), + // ), + // ], + // ), + // ), + const Spacer(), + Container( + margin: const EdgeInsets.only( + bottom: 70.0, + left: 49, + right: 49, + ), + child: Row( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + // if (widget.isVideoCall) + // RawMaterialButton( + // onPressed: () { + // callProvider.camOff(); + // }, + // elevation: 2.0, + // fillColor: callProvider.isCamOff ? MyColors.green2DColor : Colors.grey, + // padding: const EdgeInsets.all( + // 15.0, + // ), + // shape: const CircleBorder(), + // child: Icon( + // callProvider.isCamOff ? Icons.videocam_off : Icons.videocam, + // color: MyColors.white, + // size: 35.0, + // ), + // ) + // else + // RawMaterialButton( + // onPressed: () { + // callProvider.loudOn(); + // }, + // elevation: 2.0, + // fillColor: callProvider.isLoudSpeaker ? MyColors.green2DColor : Colors.grey, + // padding: const EdgeInsets.all( + // 15.0, + // ), + // shape: const CircleBorder(), + // child: const Icon( + // Icons.volume_up, + // color: MyColors.white, + // size: 35.0, + // ), + // ), + // RawMaterialButton( + // onPressed: () { + // callProvider.micOff(); + // }, + // elevation: 2.0, + // fillColor: callProvider.isMicOff ? MyColors.green2DColor : Colors.grey, + // padding: const EdgeInsets.all( + // 15.0, + // ), + // shape: const CircleBorder(), + // child: Icon( + // callProvider.isMicOff ? Icons.mic_off : Icons.mic, + // color: MyColors.white, + // size: 35.0, + // ), + // ), + RawMaterialButton( + onPressed: () { + callProvider.endCall().then((bool value) { + if (value) { + Navigator.of(context).pop(); + } + }); + }, + elevation: 2.0, + fillColor: MyColors.redA3Color, + padding: const EdgeInsets.all( + 15.0, + ), + shape: const CircleBorder(), + child: const Icon( + Icons.call_end, + color: MyColors.white, + size: 35.0, + ), + ), + ], + ), + ), + ], ), ), ), - ], - ); - } else { - return const Center( - child: CircularProgressIndicator(), - ); - } - }, - ), + ), + ), + ], + ); + }), ); } - void _runAnimation() async { - _cameras = await availableCameras(); - CameraDescription firstCamera = _cameras[1]; - controller = CameraController(firstCamera, ResolutionPreset.medium); - _initializeControllerFuture = controller.initialize(); - setState(() {}); - // setAudioFile(); - for (int i = 0; i < 100; i++) { - await _animationController!.forward(); - await _animationController!.reverse(); - } - } - - void _micOff() { - setState(() { - isMicOff = !isMicOff; - }); - } - - void _camOff() { - setState(() { - isCamOff = !isCamOff; - }); - } - - void _loudOn() { - setState(() { - isLoudSpeaker = !isLoudSpeaker; - }); - } - - Future _submit() async { - try { - // backToHome(); - // final roomModel = RoomModel(name: widget.OutGoingCallData.name, token: widget.OutGoingCallData.sessionId, identity: widget.OutGoingCallData.identity); - await controller?.dispose(); - - // changeCallStatusAPI(4); - - // if (_session != null && _signaling != null) { - // await Navigator.of(context).pushReplacement( - // MaterialPageRoute( - // // fullscreenDialog: true, - // builder: (BuildContext context) { - // // if (widget.OutGoingCallData.isWebRTC == "true") { - // return StartVideoCall(signaling: _signaling, session: _session); - // - // // else { - // // return OpenTokConnectCallPage(apiKey: OPENTOK_API_KEY, sessionId: widget.OutGoingCallData.sessionId, token: widget.OutGoingCallData.token); - // // } - // - // // return VideoCallWebPage(receiverId: widget.OutGoingCallData.receiverID, callerId: widget.OutGoingCallData.callerID); // Web WebRTC VideoCall - // - // // return CallHomePage(receiverId: widget.OutGoingCallData.receiverID, callerId: widget.OutGoingCallData.callerID); // App WebRTC VideoCall - // }, - // ), - // ); - // } else { - // // Invalid Params/Data - // Utils.showToast("Failed to establish connection with server"); - // } - } catch (err) { - print(err); - // await PlatformExceptionAlertDialog( - // exception: err, - // ).show(context); - - Utils.showToast(err.toString()); - } - } - - // void changeCallStatusAPI(int sessionStatus) { - // LiveCareService service = new LiveCareService(); - // service.endCallAPI(widget.OutGoingCallData.sessionId, sessionStatus, context).then((res) {}).catchError((err) { - // print(err); - // }); - // } - - void backToHome() async { - // final connected = await signaling.declineCall(widget.OutGoingCallData.callerID, widget.OutGoingCallData.receiverID); - // LandingPage.isOpenCallPage = false; - // _signaling - _animationController!.dispose(); - // player.stop(); - // changeCallStatusAPI(3); - // _signaling.bye(_session, callRejected: true); - // _signaling.callDisconnected(_session, callRejected: true); - Navigator.of(context).pop(); - } - - // - // void disposeAudioResources() async { - // await player.dispose(); - // } - // - // void setAudioFile() async { - // player.stop(); - // await player.setVolume(1.0); // full volume - // try { - // await player.setAsset('assets/sounds/ring_60Sec.mp3').then((value) { - // player.setLoopMode(LoopMode.one); // loop ring sound - // player.play(); - // }).catchError((err) { - // print("Error: $err"); - // }); - // } catch (e) { - // print("Error: $e"); - // } - // } - // - // void connectSignaling({@required bool iAmCaller = false}) async { - // print("----------------- + Signaling Connection Started ---------------------------"); - // var caller = widget.OutGoingCallData.callerID; - // var receiver = widget.OutGoingCallData.receiverID; - // var host = widget.OutGoingCallData.server; - // - // var selfRole = iAmCaller ? "Caller" : "Receiver"; - // var selfId = iAmCaller ? caller : receiver; - // var selfUser = SocketUser(id: selfId, name: "$selfRole-$selfId", userAgent: DeviceInfo.userAgent, moreInfo: {}); - // - // var remoteRole = !iAmCaller ? "Caller" : "Receiver"; - // var remoteId = !iAmCaller ? caller : receiver; - // var remoteUser = SocketUser(id: remoteId, name: "$remoteRole-$remoteId", userAgent: DeviceInfo.userAgent, moreInfo: {}); - // - // var sessionId = "$caller-$receiver"; - // _session = SessionOneToOne(id: sessionId, local_user: selfUser, remote_user: remoteUser); - // - // _signaling = Signaling(host, session: _session); - // await _signaling.connect(); - // - // if (_signaling.state == SignalingState.Open) { - // return; - // } - // } - BoxDecoration cardRadius(double radius, {required Color color, double? elevation}) { return BoxDecoration( shape: BoxShape.rectangle, color: color ?? Colors.white, - borderRadius: BorderRadius.all( - Radius.circular(radius), - ), - boxShadow: [ - BoxShadow( - color: const Color( - 0xff000000, - ).withOpacity( - .05, - ), - //spreadRadius: 5, - blurRadius: elevation ?? 27, - offset: const Offset( - -2, - 3, - ), - ), - ], + borderRadius: BorderRadius.all(Radius.circular(radius)), + boxShadow: [BoxShadow(color: const Color(0xff000000).withOpacity(.05), blurRadius: elevation ?? 27, offset: const Offset(-2, 3))], ); } } diff --git a/lib/ui/chat/call/draggable_cam_screen.dart b/lib/ui/chat/call/draggable_cam_screen.dart new file mode 100644 index 0000000..14f0fc8 --- /dev/null +++ b/lib/ui/chat/call/draggable_cam_screen.dart @@ -0,0 +1,171 @@ +// import 'dart:async'; +// import 'dart:io'; +// import 'package:flutter/material.dart'; +// +// class DraggableCam extends StatefulWidget { +// //final Size availableScreenSize; +// final Widget child; +// final double scaleFactor; +// // final Stream onButtonBarVisible; +// // final Stream onButtonBarHeight; +// +// const DraggableCam({ +// Key? key, +// //@required this.availableScreenSize, +// required this.child, +// // @required this.onButtonBarVisible, +// // @required this.onButtonBarHeight, +// +// /// The portion of the screen the DraggableWidget should use. +// this.scaleFactor = .25, +// }) : assert(scaleFactor != null && scaleFactor > 0 && scaleFactor <= .4), +// // assert(availableScreenSize != null), +// // assert(onButtonBarVisible != null), +// // assert(onButtonBarHeight != null), +// super(key: key); +// +// @override +// _DraggablePublisherState createState() => _DraggablePublisherState(); +// } +// +// class _DraggablePublisherState extends State { +// bool _isButtonBarVisible = true; +// double _buttonBarHeight = 0; +// late double _width; +// late double _height; +// late double _top; +// late double _left; +// late double _viewPaddingTop; +// late double _viewPaddingBottom; +// final double _padding = 8.0; +// final Duration _duration300ms = const Duration(milliseconds: 300); +// final Duration _duration0ms = const Duration(milliseconds: 0); +// late Duration _duration; +// late StreamSubscription _streamSubscription; +// late StreamSubscription _streamHeightSubscription; +// +// @override +// void initState() { +// super.initState(); +// _duration = _duration300ms; +// _width = widget.availableScreenSize.width * widget.scaleFactor; +// _height = _width * (widget.availableScreenSize.height / widget.availableScreenSize.width); +// _top = widget.availableScreenSize.height - (_buttonBarHeight + _padding) - _height; +// _left = widget.availableScreenSize.width - _padding - _width; +// +// _streamSubscription = widget.onButtonBarVisible.listen(_buttonBarVisible); +// _streamHeightSubscription = widget.onButtonBarHeight.listen(_getButtonBarHeight); +// } +// +// @override +// void didChangeDependencies() { +// var mediaQuery = MediaQuery.of(context); +// _viewPaddingTop = mediaQuery.viewPadding.top; +// _viewPaddingBottom = mediaQuery.viewPadding.bottom; +// super.didChangeDependencies(); +// } +// +// @override +// void dispose() { +// _streamSubscription.cancel(); +// _streamHeightSubscription.cancel(); +// super.dispose(); +// } +// +// void _getButtonBarHeight(double height) { +// setState(() { +// _buttonBarHeight = height; +// _positionWidget(); +// }); +// } +// +// void _buttonBarVisible(bool visible) { +// if (!mounted) { +// return; +// } +// setState(() { +// _isButtonBarVisible = visible; +// if (_duration == _duration300ms) { +// // only position the widget when we are not currently dragging it around +// _positionWidget(); +// } +// }); +// } +// +// @override +// Widget build(BuildContext context) { +// return AnimatedPositioned( +// top: _top, +// left: _left, +// width: _width, +// height: _height, +// duration: _duration, +// child: Listener( +// onPointerDown: (_) => _duration = _duration0ms, +// onPointerMove: (PointerMoveEvent event) { +// setState(() { +// _left = (_left + event.delta.dx).roundToDouble(); +// _top = (_top + event.delta.dy).roundToDouble(); +// }); +// }, +// onPointerUp: (_) => _positionWidget(), +// onPointerCancel: (_) => _positionWidget(), +// child: ClippedVideo( +// height: _height, +// width: _width, +// child: widget.child, +// ), +// ), +// ); +// } +// +// double _getCurrentStatusBarHeight() { +// if (_isButtonBarVisible) { +// return _viewPaddingTop; +// } +// final _defaultViewPaddingTop = Platform.isIOS ? 20.0 : Platform.isAndroid ? 24.0 : 0.0; +// if (_viewPaddingTop > _defaultViewPaddingTop) { +// // There must be a hardware notch in the display. +// return _viewPaddingTop; +// } +// return 0.0; +// } +// +// double _getCurrentButtonBarHeight() { +// if (_isButtonBarVisible) { +// return _buttonBarHeight + _viewPaddingBottom; +// } +// return _viewPaddingBottom; +// } +// +// void _positionWidget() { +// // Determine the center of the object being dragged so we can decide +// // in which corner the object should be placed. +// var dx = (_width / 2) + _left; +// dx = dx < 0 ? 0 : dx >= widget.availableScreenSize.width ? widget.availableScreenSize.width - 1 : dx; +// var dy = (_height / 2) + _top; +// dy = dy < 0 ? 0 : dy >= widget.availableScreenSize.height ? widget.availableScreenSize.height - 1 : dy; +// final draggableCenter = Offset(dx, dy); +// +// setState(() { +// _duration = _duration300ms; +// if (Rect.fromLTRB(0, 0, widget.availableScreenSize.width / 2, widget.availableScreenSize.height / 2).contains(draggableCenter)) { +// // Top-left +// _top = _getCurrentStatusBarHeight() + _padding; +// _left = _padding; +// } else if (Rect.fromLTRB(widget.availableScreenSize.width / 2, 0, widget.availableScreenSize.width, widget.availableScreenSize.height / 2).contains(draggableCenter)) { +// // Top-right +// _top = _getCurrentStatusBarHeight() + _padding; +// _left = widget.availableScreenSize.width - _padding - _width; +// } else if (Rect.fromLTRB(0, widget.availableScreenSize.height / 2, widget.availableScreenSize.width / 2, widget.availableScreenSize.height).contains(draggableCenter)) { +// // Bottom-left +// _top = widget.availableScreenSize.height - (_getCurrentButtonBarHeight() + _padding) - _height; +// _left = _padding; +// } else if (Rect.fromLTRB(widget.availableScreenSize.width / 2, widget.availableScreenSize.height / 2, widget.availableScreenSize.width, widget.availableScreenSize.height).contains(draggableCenter)) { +// // Bottom-right +// _top = widget.availableScreenSize.height - (_getCurrentButtonBarHeight() + _padding) - _height; +// _left = widget.availableScreenSize.width - _padding - _width; +// } +// }); +// } +// } diff --git a/lib/ui/chat/call/start_call_screen.dart b/lib/ui/chat/call/start_call_screen.dart new file mode 100644 index 0000000..ceb66d0 --- /dev/null +++ b/lib/ui/chat/call/start_call_screen.dart @@ -0,0 +1,267 @@ +import 'dart:async'; +import 'dart:core'; +import 'dart:ui'; +import 'package:draggable_widget/draggable_widget.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:flutter_webrtc/flutter_webrtc.dart'; +import 'package:mohem_flutter_app/classes/colors.dart'; +import 'package:mohem_flutter_app/extensions/int_extensions.dart'; +import 'package:mohem_flutter_app/provider/chat_call_provider.dart'; +import 'package:provider/provider.dart'; + +class StartCallPage extends StatefulWidget { + RTCVideoRenderer localRenderer; + RTCVideoRenderer remoteRenderer; + + StartCallPage({required this.localRenderer, required this.remoteRenderer}); + + @override + _StartCallPageState createState() => _StartCallPageState(); +} + +class _StartCallPageState extends State { + final dragController = DragController(); + late ChatCallProvider callProvider; + + @override + void initState() { + callProvider = Provider.of(context, listen: false); + super.initState(); + refresh(); + } + + void refresh() { + Future.delayed(const Duration(seconds: 1), () { + callProvider.notifyListeners(); + }); + } + + @override + Widget build(BuildContext context) { + return Consumer(builder: (BuildContext context, ChatCallProvider provider, Widget? child) { + return SizedBox( + width: double.infinity, + height: double.infinity, + child: Stack( + alignment: FractionalOffset.center, + children: [ + if (provider.isVideoCall) + Positioned.fill( + child: RTCVideoView( + widget.remoteRenderer, + objectFit: RTCVideoViewObjectFit.RTCVideoViewObjectFitCover, + key: const Key('remote'), + ), + ), + if (provider.isVideoCall) + DraggableWidget( + bottomMargin: 20, + topMargin: 40, + intialVisibility: true, + horizontalSpace: 20, + shadowBorderRadius: 50, + initialPosition: AnchoringPosition.topLeft, + dragController: dragController, + normalShadow: const BoxShadow(spreadRadius: 0.0, blurRadius: 0.0), + draggingShadow: const BoxShadow(spreadRadius: 0.0, blurRadius: 0.0), + child: SizedBox( + height: 200, + width: 140, + child: RTCVideoView( + widget.localRenderer, + mirror: true, + objectFit: RTCVideoViewObjectFit.RTCVideoViewObjectFitCover, + ), + ), + ), + if (!provider.isVideoCall) + Positioned.fill( + child: ClipRect( + child: BackdropFilter( + filter: ImageFilter.blur(sigmaX: 5.0, sigmaY: 5.0), + child: Container( + decoration: BoxDecoration( + color: MyColors.grey57Color.withOpacity( + 0.3, + ), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.max, + children: [ + 40.height, + Row( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Container( + margin: const EdgeInsets.all(21.0), + child: Container( + margin: const EdgeInsets.only( + left: 10.0, + right: 10.0, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + SvgPicture.asset( + "assets/images/user.svg", + height: 70, + width: 70, + fit: BoxFit.cover, + ), + 10.height, + Text( + callProvider.outGoingCallData.receiverName!, + style: const TextStyle( + fontSize: 21, + decoration: TextDecoration.none, + fontWeight: FontWeight.bold, + color: MyColors.white, + letterSpacing: -1.26, + height: 23 / 12, + ), + ), + const Text( + "On Call", + style: TextStyle( + fontSize: 16, + decoration: TextDecoration.none, + fontWeight: FontWeight.w600, + color: Color( + 0xffC6C6C6, + ), + letterSpacing: -0.48, + height: 23 / 24, + ), + ), + const SizedBox( + height: 2, + ), + ], + ), + ), + ), + ], + ), + ], + ), + ), + ), + ), + ), + Align( + alignment: Alignment.bottomCenter, + child: Container( + padding: const EdgeInsets.only( + bottom: 20, + left: 40, + right: 40, + ), + child: Row( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + // if (provider.isVideoCall) + RawMaterialButton( + constraints: BoxConstraints(), + onPressed: () { + callProvider.loudOn(); + }, + elevation: 2.0, + fillColor: callProvider.isLoudSpeaker ? MyColors.green2DColor : Colors.grey, + padding: const EdgeInsets.all( + 10.0, + ), + shape: const CircleBorder(), + child: const Icon( + Icons.volume_up, + color: MyColors.white, + size: 30.0, + ), + ), + RawMaterialButton( + constraints: BoxConstraints(), + onPressed: () { + provider.camOff(); + }, + elevation: 2.0, + fillColor: provider.isCamOff ? MyColors.green2DColor : Colors.grey, + padding: const EdgeInsets.all( + 10.0, + ), + shape: const CircleBorder(), + child: Icon( + provider.isCamOff ? Icons.videocam_off : Icons.videocam, + color: MyColors.white, + size: 30.0, + ), + ), + RawMaterialButton( + constraints: BoxConstraints(), + onPressed: () { + provider.switchCamera(); + }, + elevation: 2.0, + fillColor: provider.isFrontCamera ? Colors.grey : MyColors.green2DColor, + padding: const EdgeInsets.all( + 10.0, + ), + shape: const CircleBorder(), + child: Icon( + provider.isFrontCamera ? Icons.switch_camera_outlined : Icons.switch_camera, + color: MyColors.white, + size: 30.0, + ), + ), + RawMaterialButton( + constraints: BoxConstraints(), + onPressed: () { + provider.micOff(); + }, + elevation: 2.0, + fillColor: provider.isMicOff ? MyColors.green2DColor : Colors.grey, + padding: const EdgeInsets.all( + 10.0, + ), + shape: const CircleBorder(), + child: Icon( + provider.isMicOff ? Icons.mic_off : Icons.mic, + color: MyColors.white, + size: 30.0, + ), + ), + RawMaterialButton( + constraints: BoxConstraints(), + onPressed: () { + provider.endCall().then((value) { + if (value) { + Navigator.of(context).pop(); + } + }); + }, + elevation: 2.0, + fillColor: MyColors.redA3Color, + padding: const EdgeInsets.all( + 10.0, + ), + shape: const CircleBorder(), + child: const Icon( + Icons.call_end, + color: MyColors.white, + size: 30.0, + ), + ), + ], + ), + ), + ), + ], + ), + ); + }); + } +} diff --git a/lib/ui/chat/chat_detailed_screen.dart b/lib/ui/chat/chat_detailed_screen.dart index 421eb91..328ddd4 100644 --- a/lib/ui/chat/chat_detailed_screen.dart +++ b/lib/ui/chat/chat_detailed_screen.dart @@ -10,11 +10,9 @@ import 'package:mohem_flutter_app/extensions/int_extensions.dart'; import 'package:mohem_flutter_app/extensions/string_extensions.dart'; import 'package:mohem_flutter_app/extensions/widget_extensions.dart'; import 'package:mohem_flutter_app/generated/locale_keys.g.dart'; -import 'package:mohem_flutter_app/main.dart'; import 'package:mohem_flutter_app/models/chat/call.dart'; import 'package:mohem_flutter_app/models/chat/get_search_user_chat_model.dart'; import 'package:mohem_flutter_app/models/chat/get_single_user_chat_list_model.dart'; -import 'package:mohem_flutter_app/models/chat/get_user_login_token_model.dart'; import 'package:mohem_flutter_app/provider/chat_call_provider.dart'; import 'package:mohem_flutter_app/provider/chat_provider_model.dart'; import 'package:mohem_flutter_app/ui/chat/custom_auto_direction.dart'; @@ -23,9 +21,9 @@ import 'package:mohem_flutter_app/ui/chat/chat_bubble.dart'; import 'package:mohem_flutter_app/ui/chat/common.dart'; import 'package:mohem_flutter_app/widgets/chat_app_bar_widge.dart'; import 'package:mohem_flutter_app/widgets/shimmer/dashboard_shimmer_widget.dart'; +import 'package:permission_handler/permission_handler.dart'; import 'package:provider/provider.dart'; import 'package:pull_to_refresh/pull_to_refresh.dart'; -import 'package:signalr_netcore/signalr_client.dart'; import 'package:swipe_to/swipe_to.dart'; class ChatDetailedScreenParams { @@ -78,7 +76,7 @@ class _ChatDetailScreenState extends State { Widget build(BuildContext context) { params = ModalRoute.of(context)!.settings.arguments as ChatDetailedScreenParams; data = Provider.of(context, listen: false); - // callPro = Provider.of(context, listen: false); + // callPro = Provider.of(context, listen: false); if (params != null) { data.getSingleUserChatHistory( senderUID: AppState().chatDetails!.response!.id!.toInt(), @@ -98,14 +96,28 @@ class _ChatDetailScreenState extends State { showTyping: true, chatUser: params!.chatUser, actions: [ - // SvgPicture.asset("assets/icons/chat/call.svg", width: 21, height: 23).onPress(() { - // makeCall(callType: "AUDIO"); - // }), - // 24.width, - // SvgPicture.asset("assets/icons/chat/video_call.svg", width: 21, height: 18).onPress(() { - // makeCall(callType: "VIDEO"); - // }), - // 21.width, + SvgPicture.asset("assets/icons/chat/call.svg", width: 21, height: 23).onPress(() async { + Future micPer = Permission.microphone.request(); + if (await micPer.isGranted) { + makeCall(callType: "AUDIO"); + } else { + Permission.microphone.request().isGranted.then((value) { + makeCall(callType: "AUDIO"); + }); + } + }), + 24.width, + SvgPicture.asset("assets/icons/chat/video_call.svg", width: 21, height: 18).onPress(() async { + Future camPer = Permission.camera.request(); + if (await camPer.isGranted) { + makeCall(callType: "VIDEO"); + } else { + Permission.camera.request().isGranted.then((value) { + makeCall(callType: "VIDEO"); + }); + } + }), + 21.width, ], ), body: SafeArea( @@ -149,7 +161,7 @@ class _ChatDetailScreenState extends State { ); }, ).onPress(() async { - logger.w(m.userChatHistory[i].toJson()); + // logger.w(m.userChatHistory[i].toJson()); if (m.userChatHistory[i].fileTypeResponse != null && m.userChatHistory[i].fileTypeId != null) { if (m.userChatHistory[i].fileTypeId! == 1 || m.userChatHistory[i].fileTypeId! == 5 || @@ -352,29 +364,21 @@ class _ChatDetailScreenState extends State { } void makeCall({required String callType}) async { - callPro.initCallListeners(); - print("================== Make call Triggered ============================"); Map json = { - "callerID": AppState().chatDetails!.response!.id!.toString(), - "callerDetails": AppState().chatDetails!.toJson(), - "receiverID": params!.chatUser!.id.toString(), - "receiverDetails": params!.chatUser!.toJson(), + "callerID": AppState().chatDetails!.response!.id, + "callerName": AppState().chatDetails!.response!.userName, + "callerEmail": AppState().chatDetails!.response!.email, + "callerTitle": AppState().chatDetails!.response!.title, + "callerPhone": AppState().chatDetails!.response!.phone, + "receiverID": params!.chatUser!.id, + "receiverName": params!.chatUser!.userName, + "receiverEmail": params!.chatUser!.email, + "receiverTitle": params!.chatUser!.title, + "receiverPhone": params!.chatUser!.phone, "title": params!.chatUser!.userName!.replaceAll(".", " "), - "calltype": callType == "VIDEO" ? "Video" : "Audio", + "callType": callType == "VIDEO" ? "Video" : "Audio", }; - logger.w(json); CallDataModel callData = CallDataModel.fromJson(json); - await Navigator.push( - context, - MaterialPageRoute( - builder: (BuildContext context) => OutGoingCall( - isVideoCall: callType == "VIDEO" ? true : false, - outGoingCallData: callData, - ), - ), - ).then((value) { - print("then"); - callPro.stopListeners(); - }); + await Navigator.push(context, MaterialPageRoute(builder: (BuildContext context) => OutGoingCall(isVideoCall: callType == "VIDEO" ? true : false, outGoingCallData: callData))); } } diff --git a/lib/ui/chat/chat_home.dart b/lib/ui/chat/chat_home.dart index 9c5f216..50fe6ed 100644 --- a/lib/ui/chat/chat_home.dart +++ b/lib/ui/chat/chat_home.dart @@ -7,6 +7,7 @@ import 'package:mohem_flutter_app/extensions/int_extensions.dart'; import 'package:mohem_flutter_app/extensions/string_extensions.dart'; import 'package:mohem_flutter_app/extensions/widget_extensions.dart'; import 'package:mohem_flutter_app/generated/locale_keys.g.dart'; +import 'package:mohem_flutter_app/provider/chat_call_provider.dart'; import 'package:mohem_flutter_app/provider/chat_provider_model.dart'; import 'package:mohem_flutter_app/ui/chat/chat_home_screen.dart'; import 'package:mohem_flutter_app/ui/chat/favorite_users_screen.dart'; @@ -27,12 +28,15 @@ class _ChatHomeState extends State { int tabIndex = 0; PageController controller = PageController(); late ChatProviderModel data; + late ChatCallProvider callProvider; @override void initState() { super.initState(); data = Provider.of(context, listen: false); + callProvider = Provider.of(context, listen: false); data.registerEvents(); + // callProvider.initCallListeners(); } @override @@ -44,7 +48,7 @@ class _ChatHomeState extends State { void fetchAgain() { if (chatHubConnection.state != HubConnectionState.Connected) { data.getUserAutoLoginToken().whenComplete(() async { - await data.buildHubConnection(); + await data.buildHubConnection(context: context, ccProvider: callProvider); data.getUserRecentChats(); }); return; @@ -55,7 +59,7 @@ class _ChatHomeState extends State { // String isAppOpendByChat = await Utils.getStringFromPrefs("isAppOpendByChat"); // String notificationData = await Utils.getStringFromPrefs("notificationData"); // if (isAppOpendByChat != "null" || isAppOpendByChat == "true" && notificationData != "null") { - // data.openChatByNoti(context); + // data.openChatByNoti(context); // } }); } diff --git a/lib/ui/chat/common.dart b/lib/ui/chat/common.dart index e0cb4d0..17e61da 100644 --- a/lib/ui/chat/common.dart +++ b/lib/ui/chat/common.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'dart:math'; import 'package:audio_waveforms/audio_waveforms.dart'; import 'package:flutter/material.dart'; @@ -187,3 +188,40 @@ class WaveBubble extends StatelessWidget { ); } } + +class CallTimer extends StatefulWidget { + const CallTimer({Key? key}) : super(key: key); + + @override + State createState() => _CallTimerState(); +} + +class _CallTimerState extends State { + late Timer _timer; + int _timeExpandedBySeconds = 0; + + @override + void initState() { + // TODO: implement initState + super.initState(); + _timer = Timer.periodic(const Duration(seconds: 1), (Timer timer) { + setState(() { + _timeExpandedBySeconds += 1; + }); + }); + } + + @override + Widget build(BuildContext context) { + return Text( + _timeExpandedBySeconds.toString(), + style: const TextStyle( + fontSize: 17, + fontWeight: FontWeight.bold, + color: MyColors.white, + letterSpacing: -1.26, + height: 23 / 12, + ), + ); + } +} diff --git a/lib/ui/landing/dashboard_screen.dart b/lib/ui/landing/dashboard_screen.dart index 7a8f857..e2d3da5 100644 --- a/lib/ui/landing/dashboard_screen.dart +++ b/lib/ui/landing/dashboard_screen.dart @@ -18,6 +18,7 @@ import 'package:mohem_flutter_app/extensions/widget_extensions.dart'; import 'package:mohem_flutter_app/generated/locale_keys.g.dart'; import 'package:mohem_flutter_app/main.dart'; import 'package:mohem_flutter_app/models/offers_and_discounts/get_offers_list.dart'; +import 'package:mohem_flutter_app/provider/chat_call_provider.dart'; import 'package:mohem_flutter_app/provider/chat_provider_model.dart'; import 'package:mohem_flutter_app/provider/dashboard_provider_model.dart'; import 'package:mohem_flutter_app/ui/landing/widget/app_drawer.dart'; @@ -48,6 +49,7 @@ class _DashboardScreenState extends State with WidgetsBindingOb late DashboardProviderModel data; late MarathonProvider marathonProvider; late ChatProviderModel cProvider; + late ChatCallProvider chatCallProvider; final GlobalKey _scaffoldState = GlobalKey(); final RefreshController _refreshController = RefreshController(initialRefresh: false); @@ -62,6 +64,7 @@ class _DashboardScreenState extends State with WidgetsBindingOb data = Provider.of(context, listen: false); marathonProvider = Provider.of(context, listen: false); cProvider = Provider.of(context, listen: false); + chatCallProvider = Provider.of(context, listen: false); _bHubCon(); _onRefresh(true); }); @@ -97,13 +100,13 @@ class _DashboardScreenState extends State with WidgetsBindingOb String isAppOpendByChat = await Utils.getStringFromPrefs("isAppOpendByChat"); if (isAppOpendByChat != null && isAppOpendByChat == "true") { Utils.showLoading(context); - cProvider.buildHubConnection(); + cProvider.buildHubConnection(context: context,ccProvider: chatCallProvider); Future.delayed(const Duration(seconds: 2), () async { cProvider.invokeChatCounter(userId: AppState().chatDetails!.response!.id!); gotoChat(context); }); } else { - cProvider.buildHubConnection(); + cProvider.buildHubConnection(context: context, ccProvider: chatCallProvider); Future.delayed(const Duration(seconds: 2), () { cProvider.invokeChatCounter(userId: AppState().chatDetails!.response!.id!); }); diff --git a/pubspec.yaml b/pubspec.yaml index 20328fe..fa12e67 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -90,8 +90,8 @@ dependencies: signalr_netcore: ^1.3.3 logging: ^1.0.1 swipe_to: ^1.0.2 - flutter_webrtc: ^0.9.16 - camera: ^0.10.3 + flutter_webrtc: ^0.9.20 + draggable_widget: ^2.0.0 flutter_local_notifications: any #firebase_analytics: any