diff --git a/android/app/build.gradle b/android/app/build.gradle index ab88f580..a6a95b0f 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -79,8 +79,8 @@ android { // productionReleaseImplementation // } compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 + sourceCompatibility JavaVersion.VERSION_11 + targetCompatibility JavaVersion.VERSION_11 } } diff --git a/lib/config/config.dart b/lib/config/config.dart index 03c74f52..045759a3 100644 --- a/lib/config/config.dart +++ b/lib/config/config.dart @@ -5,8 +5,8 @@ const ONLY_NUMBERS = "[0-9]"; const ONLY_LETTERS = "[a-zA-Z &'\"]"; const ONLY_DATE = "[0-9/]"; const BASE_URL_LIVE_CARE = 'https://livecare.hmg.com/'; -// const BASE_URL = 'https://hmgwebservices.com/'; -const BASE_URL = 'https://uat.hmgwebservices.com/'; +const BASE_URL = 'https://hmgwebservices.com/'; +// const BASE_URL = 'https://uat.hmgwebservices.com/'; const PHARMACY_ITEMS_URL = "Services/Lists.svc/REST/GetPharmcyItems_Region_enh"; const PHARMACY_LIST_URL = "Services/Patients.svc/REST/GetPharmcyList"; const PATIENT_PROGRESS_NOTE_URL = @@ -436,7 +436,7 @@ const TRANSACTION_NO = 0; const LANGUAGE_ID = 2; const STAMP = '2020-04-27T12:17:17.721Z'; const IP_ADDRESS = '9.9.9.9'; -const VERSION_ID = 6.4; +const VERSION_ID = 6.7; const CHANNEL = 9; const SESSION_ID = 'BlUSkYymTt'; const IS_LOGIN_FOR_DOCTOR_APP = true; diff --git a/lib/core/viewModel/authentication_view_model.dart b/lib/core/viewModel/authentication_view_model.dart index d353f882..49b607ca 100644 --- a/lib/core/viewModel/authentication_view_model.dart +++ b/lib/core/viewModel/authentication_view_model.dart @@ -148,7 +148,7 @@ class AuthenticationViewModel extends BaseViewModel { iMEI: user!.iMEI, facilityId: user!.projectID, memberID: user!.doctorID, - loginDoctorID: int.parse(user!.editedBy.toString()), + loginDoctorID: int.tryParse(user!.editedBy.toString()), zipCode: user!.outSA == true ? '971' : '966', mobileNumber: user!.mobile, oTPSendType: authMethodType.getTypeIdService(), @@ -158,7 +158,7 @@ class AuthenticationViewModel extends BaseViewModel { await _authService .sendActivationCodeVerificationScreen(activationCodeModel); if (_authService.hasError) { - error = _authService.error!; + error = _authService.error; setState(ViewState.ErrorLocal); } else setState(ViewState.Idle); diff --git a/lib/screens/auth/verification_methods_screen.dart b/lib/screens/auth/verification_methods_screen.dart index 08a6ac6c..bd029784 100644 --- a/lib/screens/auth/verification_methods_screen.dart +++ b/lib/screens/auth/verification_methods_screen.dart @@ -227,9 +227,7 @@ class _VerificationMethodsScreenState extends State { .user! .editedOn!), isMonthShort: true) - : authenticationViewModel - .user!.createdOn! != - null + : authenticationViewModel.user!.createdOn != null ? AppDateUtils.getDayMonthYearDateFormatted( AppDateUtils .convertStringToDate( @@ -247,16 +245,12 @@ class _VerificationMethodsScreenState extends State { letterSpacing: -0.48, ), AppText( - authenticationViewModel - .user!.editedOn! != - null + authenticationViewModel.user!.editedOn != null ? AppDateUtils.getHour(AppDateUtils .convertStringToDate( authenticationViewModel .user!.editedOn!)) - : authenticationViewModel - .user!.createdOn! != - null + : authenticationViewModel.user!.createdOn != null ? AppDateUtils.getHour( AppDateUtils .convertStringToDate( diff --git a/lib/screens/home/home_screen.dart b/lib/screens/home/home_screen.dart index 5452777c..6e45d033 100644 --- a/lib/screens/home/home_screen.dart +++ b/lib/screens/home/home_screen.dart @@ -13,6 +13,7 @@ import 'package:doctor_app_flutter/screens/home/dashboard_slider-item-widget.dar import 'package:doctor_app_flutter/screens/home/dashboard_swipe_widget.dart'; import 'package:doctor_app_flutter/screens/home/home_patient_card.dart'; import 'package:doctor_app_flutter/screens/live_care/live_care_patient_screen.dart'; +import 'package:doctor_app_flutter/screens/live_care/web-rtc/web_rtc/call_home_page_.dart'; import 'package:doctor_app_flutter/screens/medicine/medicine_search_screen.dart'; import 'package:doctor_app_flutter/screens/patients/In_patient/in_patient_screen.dart'; import 'package:doctor_app_flutter/screens/patients/out_patient/out_patient_screen.dart'; @@ -35,7 +36,6 @@ import 'package:flutter_svg/flutter_svg.dart'; import 'package:provider/provider.dart'; import 'package:sticky_headers/sticky_headers/widget.dart'; -import '../../routes.dart'; import '../../widgets/shared/app_texts_widget.dart'; import 'label.dart'; @@ -396,10 +396,19 @@ class _HomeScreenState extends State { // "isSearchAndOut": false, // "isFromLiveCare": true, // }); + + // /* */ + // Navigator.push( + // context, + // FadePage( + // page: LiveCarePatientScreen(), + // ), + // ); + Navigator.push( context, FadePage( - page: LiveCarePatientScreen(), + page: CallHomePage(callerId: "ZohaibCaller", receiverId: "12340987"), ), ); }, diff --git a/lib/screens/live_care/web-rtc/clipped_video.dart b/lib/screens/live_care/web-rtc/clipped_video.dart new file mode 100644 index 00000000..14711656 --- /dev/null +++ b/lib/screens/live_care/web-rtc/clipped_video.dart @@ -0,0 +1,37 @@ +import 'package:flutter/material.dart'; + +class ClippedVideo extends StatefulWidget { + final double width; + final double height; + final Widget child; + + const ClippedVideo({ + Key? key, + required this.width, + required this.height, + required this.child, + }) : super(key: key); + + @override + _ClippedVideoState createState() => _ClippedVideoState(); +} + +class _ClippedVideoState extends State { + @override + Widget build(BuildContext context) { + return Container( + width: widget.width, + height: widget.height, + decoration: BoxDecoration( + borderRadius: const BorderRadius.all(Radius.circular(20)), + border: Border.all( + color: Colors.white24, + ), + ), + child: ClipRRect( + child: widget.child, + borderRadius: const BorderRadius.all(Radius.circular(20)), + ), + ); + } +} diff --git a/lib/screens/live_care/web-rtc/conference_button_bar.dart b/lib/screens/live_care/web-rtc/conference_button_bar.dart new file mode 100644 index 00000000..3fad4311 --- /dev/null +++ b/lib/screens/live_care/web-rtc/conference_button_bar.dart @@ -0,0 +1,237 @@ +import 'dart:async'; +import 'package:after_layout/after_layout.dart'; +import 'package:doctor_app_flutter/screens/live_care/web-rtc/widgets/circle_button.dart'; +import 'package:flutter/material.dart'; + +class ConferenceButtonBar extends StatefulWidget { + final VoidCallback onVideoEnabled; + final VoidCallback onAudioEnabled; + final VoidCallback onHangup; + final VoidCallback onSwitchCamera; + final VoidCallback onPersonAdd; + final VoidCallback onPersonRemove; + final void Function(double) onHeight; + final VoidCallback onHide; + final VoidCallback onShow; + final Stream videoEnabled; + final Stream audioEnabled; + + const ConferenceButtonBar({ + Key? key, + required this.onVideoEnabled, + required this.onAudioEnabled, + required this.onHangup, + required this.onSwitchCamera, + required this.onPersonAdd, + required this.onPersonRemove, + required this.videoEnabled, + required this.audioEnabled, + required this.onHeight, + required this.onHide, + required this.onShow, + }) : assert(videoEnabled != null), + assert(audioEnabled != null), + super(key: key); + + @override + _ConferenceButtonBarState createState() => _ConferenceButtonBarState(); +} + +class _ConferenceButtonBarState extends State with AfterLayoutMixin { + var _bottom = -100.0; + late Timer? _timer; + late int _remaining; + var _videoEnabled = true; + var _audioEnabled = true; + late double _hidden; + late double _visible; + final _keyButtonBarHeight = GlobalKey(); + + final Duration timeout = const Duration(seconds: 5); + final Duration ms = const Duration(milliseconds: 1); + final Duration periodicDuration = const Duration(milliseconds: 100); + + Timer startTimeout([int? milliseconds]) { + final duration = milliseconds == null ? timeout : ms * milliseconds; + _remaining = duration.inMilliseconds; + return Timer.periodic(periodicDuration, (Timer timer) { + _remaining -= periodicDuration.inMilliseconds; + if (_remaining <= 0) { + timer.cancel(); + _toggleBar(); + } + }); + } + + void _pauseTimer() { + if (_timer == null) { + return; + } + _timer?.cancel(); + _timer = null; + } + + void _resumeTimer() { + // resume the timer only when there is no timer active or when + // the bar is not already hidden. + if ((_timer != null && _timer!.isActive) || _bottom == _hidden) { + return; + } + _timer = startTimeout(_remaining); + } + + void _toggleBar() { + setState(() { + _bottom = _bottom == _visible ? _hidden : _visible; + if (_bottom == _visible && widget.onShow != null) { + widget.onShow(); + } + if (_bottom == _hidden && widget.onHide != null) { + widget.onHide(); + } + }); + } + + void _toggleBarOnEnd() { + if (_timer != null) { + if (_timer!.isActive) { + _timer!.cancel(); + } + _timer = null; + } + if (_bottom == 0) { + _timer = startTimeout(); + } + } + + @override + void initState() { + super.initState(); + _timer = startTimeout(); + } + + @override + void didChangeDependencies() { + _visible = MediaQuery.of(context).viewPadding.bottom; + super.didChangeDependencies(); + } + + @override + void afterFirstLayout(BuildContext context) { + final RenderBox? renderBoxButtonBar = _keyButtonBarHeight.currentContext?.findRenderObject() as RenderBox?; + final heightButtonBar = renderBoxButtonBar?.size.height ?? 0.0; + // Because the `didChangeDependencies` fires before the `afterFirstLayout`, we can use the `_visible` property here. + _hidden = -(heightButtonBar + _visible); + widget.onHeight(heightButtonBar); + _toggleBar(); + } + + @override + void dispose() { + super.dispose(); + if (_timer != null && _timer!.isActive) { + _timer?.cancel(); + _timer = null; + } + } + + @override + Widget build(BuildContext context) { + return Positioned( + top: 0, + left: 0, + right: 0, + bottom: 0, + child: GestureDetector( + key: Key('show-hide-button-bar-gesture'), + behavior: HitTestBehavior.translucent, + onTapDown: (_) => _pauseTimer(), + onTapUp: (_) => _toggleBar(), + onTapCancel: () => _resumeTimer(), + child: Stack( + children: [ + AnimatedPositioned( + key: Key('button-bar'), + bottom: _bottom, + left: 0, + right: 0, + duration: const Duration(milliseconds: 300), + curve: Curves.linear, + child: _buildRow(context), + onEnd: _toggleBarOnEnd, + ), + ], + ), + ), + ); + } + + void _onPressed(VoidCallback callback) { + if (callback != null) { + callback(); + } + if (_timer != null && _timer!.isActive) { + _timer?.cancel(); + } + _timer = startTimeout(); + } + + Widget _buildRow(BuildContext context) { + return Padding( + key: _keyButtonBarHeight, + padding: const EdgeInsets.only(bottom: 8.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + CircleButton( + child: StreamBuilder( + stream: widget.videoEnabled, + initialData: _videoEnabled, + builder: (context, snapshot) { + _videoEnabled = snapshot.data!; + return Icon( + _videoEnabled ? Icons.videocam : Icons.videocam_off, + color: Colors.white, + ); + }), + key: Key('camera-button'), + onPressed: () => _onPressed(widget.onVideoEnabled), + ), + CircleButton( + child: StreamBuilder( + stream: widget.audioEnabled, + initialData: _audioEnabled, + builder: (context, snapshot) { + _audioEnabled = snapshot.data!; + return Icon( + _audioEnabled ? Icons.mic : Icons.mic_off, + color: Colors.white, + ); + }), + key: Key('microphone-button'), + onPressed: () => _onPressed(widget.onAudioEnabled), + ), + CircleButton( + child: const Icon(Icons.switch_camera, color: Colors.white), + key: Key('switch-camera-button'), + onPressed: () => _onPressed(widget.onSwitchCamera), + ), + CircleButton( + radius: 35, + child: const RotationTransition( + turns: AlwaysStoppedAnimation(135 / 360), + child: Icon( + Icons.phone, + color: Colors.white, + size: 40, + ), + ), + color: Colors.red.withAlpha(200), + key: Key('hangup-button'), + onPressed: () => _onPressed(widget.onHangup), + ), + ], + ), + ); + } +} diff --git a/lib/screens/live_care/web-rtc/conference_page.dart b/lib/screens/live_care/web-rtc/conference_page.dart new file mode 100644 index 00000000..a09df631 --- /dev/null +++ b/lib/screens/live_care/web-rtc/conference_page.dart @@ -0,0 +1,390 @@ +// import 'dart:async'; +// +// import 'package:doctor_app_flutter/models/LiveCare/room_model.dart'; +// import 'package:doctor_app_flutter/pages/conference/conference_button_bar.dart'; +// import 'package:doctor_app_flutter/pages/conference/conference_room.dart'; +// import 'package:doctor_app_flutter/pages/landing/landing_page.dart'; +// import 'package:doctor_app_flutter/pages/conference/draggable_publisher.dart'; +// import 'package:doctor_app_flutter/pages/conference/participant_widget.dart'; +// import 'package:doctor_app_flutter/pages/conference/widgets/noise_box.dart'; +// import 'package:doctor_app_flutter/pages/conference/widgets/platform_alert_dialog.dart'; +// import 'package:flutter/material.dart'; +// import 'package:flutter/services.dart'; +// import 'package:wakelock/wakelock.dart'; +// +// class ConferencePage extends StatefulWidget { +// final RoomModel roomModel; +// +// const ConferencePage({Key key, this.roomModel}) : super(key: key); +// +// @override +// _ConferencePageState createState() => _ConferencePageState(); +// } +// +// class _ConferencePageState extends State { +// final StreamController _onButtonBarVisibleStreamController = StreamController.broadcast(); +// final StreamController _onButtonBarHeightStreamController = StreamController.broadcast(); +// // ConferenceRoom _conferenceRoom; +// StreamSubscription _onConferenceRoomException; +// +// @override +// void initState() { +// super.initState(); +// _lockInPortrait(); +// _connectToRoom(); +// _wakeLock(true); +// } +// +// void _connectToRoom() async { +// try { +// final conferenceRoom = ConferenceRoom( +// name: widget.roomModel.name, +// token: widget.roomModel.token, +// identity: widget.roomModel.identity, +// ); +// await conferenceRoom.connect(); +// setState(() { +// _conferenceRoom = conferenceRoom; +// _onConferenceRoomException = _conferenceRoom.onException.listen((err) async { +// await PlatformAlertDialog( +// title: err is PlatformException ? err.message : 'An error occured', +// content: err is PlatformException ? err.details : err.toString(), +// defaultActionText: 'OK', +// ).show(context); +// }); +// _conferenceRoom.addListener(_conferenceRoomUpdated); +// }); +// } catch (err) { +// print(err); +// await PlatformAlertDialog( +// title: err is PlatformException ? err.message : 'An error occured', +// content: err is PlatformException ? err.details : err.toString(), +// defaultActionText: 'OK', +// ).show(context); +// +// Navigator.of(context).pop(); +// } +// } +// +// Future _lockInPortrait() async { +// await SystemChrome.setPreferredOrientations([ +// DeviceOrientation.portraitUp, +// DeviceOrientation.portraitDown, +// ]); +// } +// +// @override +// void dispose() { +// _freePortraitLock(); +// _wakeLock(false); +// _disposeStreamsAndSubscriptions(); +// if (_conferenceRoom != null) _conferenceRoom.removeListener(_conferenceRoomUpdated); +// super.dispose(); +// } +// +// Future _freePortraitLock() async { +// await SystemChrome.setPreferredOrientations([ +// DeviceOrientation.landscapeRight, +// DeviceOrientation.landscapeLeft, +// DeviceOrientation.portraitUp, +// DeviceOrientation.portraitDown, +// ]); +// } +// +// Future _disposeStreamsAndSubscriptions() async { +// if (_onButtonBarVisibleStreamController != null) await _onButtonBarVisibleStreamController.close(); +// if (_onButtonBarHeightStreamController != null) await _onButtonBarHeightStreamController.close(); +// if (_onConferenceRoomException != null) await _onConferenceRoomException.cancel(); +// } +// +// @override +// Widget build(BuildContext context) { +// return WillPopScope( +// onWillPop: () async => false, +// child: Scaffold( +// backgroundColor: Colors.white, +// body: _conferenceRoom == null ? showProgress() : buildLayout(), +// ), +// ); +// } +// +// LayoutBuilder buildLayout() { +// return LayoutBuilder( +// builder: (BuildContext context, BoxConstraints constraints) { +// return Stack( +// children: [ +// _buildParticipants(context, constraints.biggest, _conferenceRoom), +// ConferenceButtonBar( +// audioEnabled: _conferenceRoom.onAudioEnabled, +// videoEnabled: _conferenceRoom.onVideoEnabled, +// onAudioEnabled: _conferenceRoom.toggleAudioEnabled, +// onVideoEnabled: _conferenceRoom.toggleVideoEnabled, +// onHangup: _onHangup, +// onSwitchCamera: _conferenceRoom.switchCamera, +// onPersonAdd: _onPersonAdd, +// onPersonRemove: _onPersonRemove, +// onHeight: _onHeightBar, +// onShow: _onShowBar, +// onHide: _onHideBar, +// ), +// ], +// ); +// }, +// ); +// } +// +// Widget showProgress() { +// return Column( +// mainAxisAlignment: MainAxisAlignment.center, +// crossAxisAlignment: CrossAxisAlignment.center, +// children: [ +// Center(child: CircularProgressIndicator()), +// SizedBox( +// height: 10, +// ), +// Text( +// 'Connecting to the call...', +// style: TextStyle(color: Colors.white), +// ), +// ], +// ); +// } +// +// Future _onHangup() async { +// print('onHangup'); +// await _conferenceRoom.disconnect(); +// LandingPage.isOpenCallPage = false; +// Navigator.of(context).pop(); +// } +// +// void _onPersonAdd() { +// print('onPersonAdd'); +// try { +// _conferenceRoom.addDummy( +// child: Stack( +// children: [ +// const Placeholder(), +// Center( +// child: Text( +// (_conferenceRoom.participants.length + 1).toString(), +// style: const TextStyle( +// shadows: [ +// Shadow( +// blurRadius: 3.0, +// color: Color.fromARGB(255, 0, 0, 0), +// ), +// Shadow( +// blurRadius: 8.0, +// color: Color.fromARGB(255, 255, 255, 255), +// ), +// ], +// fontSize: 80, +// ), +// ), +// ), +// ], +// ), +// ); +// } on PlatformException catch (err) { +// PlatformAlertDialog( +// title: err.message, +// content: err.details, +// defaultActionText: 'OK', +// ).show(context); +// } +// } +// +// void _onPersonRemove() { +// print('onPersonRemove'); +// _conferenceRoom.removeDummy(); +// } +// +// Widget _buildParticipants(BuildContext context, Size size, ConferenceRoom conferenceRoom) { +// final children = []; +// final length = conferenceRoom.participants.length; +// +// if (length <= 2) { +// _buildOverlayLayout(context, size, children); +// return Stack(children: children); +// } +// +// void buildInCols(bool removeLocalBeforeChunking, bool moveLastOfEachRowToNextRow, int columns) { +// _buildLayoutInGrid( +// context, +// size, +// children, +// removeLocalBeforeChunking: removeLocalBeforeChunking, +// moveLastOfEachRowToNextRow: moveLastOfEachRowToNextRow, +// columns: columns, +// ); +// } +// +// // if (length <= 3) { +// // buildInCols(true, false, 1); +// // } else if (length == 5) { +// // buildInCols(false, true, 2); +// // } else if (length <= 6 || length == 8) { +// // buildInCols(false, false, 2); +// // } else if (length == 7 || length == 9) { +// // buildInCols(true, false, 2); +// // } else if (length == 10) { +// // buildInCols(false, true, 3); +// // } else if (length == 13 || length == 16) { +// // buildInCols(true, false, 3); +// // } else if (length <= 18) { +// // buildInCols(false, false, 3); +// // } +// +// return Column( +// children: children, +// ); +// } +// +// void _buildOverlayLayout(BuildContext context, Size size, List children) { +// final participants = _conferenceRoom.participants; +// if (participants.length == 1) { +// children.add(_buildNoiseBox()); +// } else { +// final remoteParticipant = participants.firstWhere((ParticipantWidget participant) => participant.isRemote, orElse: () => null); +// if (remoteParticipant != null) { +// children.add(remoteParticipant); +// } +// } +// +// final localParticipant = participants.firstWhere((ParticipantWidget participant) => !participant.isRemote, orElse: () => null); +// if (localParticipant != null) { +// children.add(DraggablePublisher( +// key: Key('publisher'), +// child: localParticipant, +// availableScreenSize: size, +// onButtonBarVisible: _onButtonBarVisibleStreamController.stream, +// onButtonBarHeight: _onButtonBarHeightStreamController.stream, +// )); +// } +// } +// +// void _buildLayoutInGrid( +// BuildContext context, +// Size size, +// List children, { +// bool removeLocalBeforeChunking = false, +// bool moveLastOfEachRowToNextRow = false, +// int columns = 2, +// }) { +// final participants = _conferenceRoom.participants; +// ParticipantWidget localParticipant; +// if (removeLocalBeforeChunking) { +// localParticipant = participants.firstWhere((ParticipantWidget participant) => !participant.isRemote, orElse: () => null); +// if (localParticipant != null) { +// participants.remove(localParticipant); +// } +// } +// final chunkedParticipants = chunk(array: participants, size: columns); +// if (localParticipant != null) { +// chunkedParticipants.last.add(localParticipant); +// participants.add(localParticipant); +// } +// +// if (moveLastOfEachRowToNextRow) { +// for (var i = 0; i < chunkedParticipants.length - 1; i++) { +// var participant = chunkedParticipants[i].removeLast(); +// chunkedParticipants[i + 1].insert(0, participant); +// } +// } +// +// for (final participantChunk in chunkedParticipants) { +// final rowChildren = []; +// for (final participant in participantChunk) { +// rowChildren.add( +// Container( +// width: size.width / participantChunk.length, +// height: size.height / chunkedParticipants.length, +// child: participant, +// ), +// ); +// } +// children.add( +// Container( +// height: size.height / chunkedParticipants.length, +// child: Row( +// mainAxisAlignment: MainAxisAlignment.spaceEvenly, +// children: rowChildren, +// ), +// ), +// ); +// } +// } +// +// NoiseBox _buildNoiseBox() { +// return NoiseBox( +// density: NoiseBoxDensity.xLow, +// backgroundColor: Colors.grey.shade900, +// child: Center( +// child: Container( +// color: Colors.black54, +// width: double.infinity, +// height: 40, +// child: Center( +// child: Text( +// 'Waiting for another participant to connect to the call...', +// key: Key('text-wait'), +// textAlign: TextAlign.center, +// style: TextStyle(color: Colors.white), +// ), +// ), +// ), +// ), +// ); +// } +// +// List> chunk({required List array, required int size}) { +// final result = >[]; +// if (array.isEmpty || size <= 0) { +// return result; +// } +// var first = 0; +// var last = size; +// final totalLoop = array.length % size == 0 ? array.length ~/ size : array.length ~/ size + 1; +// for (var i = 0; i < totalLoop; i++) { +// if (last > array.length) { +// result.add(array.sublist(first, array.length)); +// } else { +// result.add(array.sublist(first, last)); +// } +// first = last; +// last = last + size; +// } +// return result; +// } +// +// void _onHeightBar(double height) { +// _onButtonBarHeightStreamController.add(height); +// } +// +// void _onShowBar() { +// setState(() { +// SystemChrome.setEnabledSystemUIOverlays([SystemUiOverlay.bottom, SystemUiOverlay.top]); +// }); +// _onButtonBarVisibleStreamController.add(true); +// } +// +// void _onHideBar() { +// setState(() { +// SystemChrome.setEnabledSystemUIOverlays([SystemUiOverlay.bottom]); +// }); +// _onButtonBarVisibleStreamController.add(false); +// } +// +// Future _wakeLock(bool enable) async { +// try { +// return await (enable ? Wakelock.enable() : Wakelock.disable()); +// } catch (err) { +// print('Unable to change the Wakelock and set it to $enable'); +// print(err); +// } +// } +// +// void _conferenceRoomUpdated() { +// setState(() {}); +// } +// } diff --git a/lib/screens/live_care/web-rtc/conference_room.dart b/lib/screens/live_care/web-rtc/conference_room.dart new file mode 100644 index 00000000..ab3dfe1e --- /dev/null +++ b/lib/screens/live_care/web-rtc/conference_room.dart @@ -0,0 +1,529 @@ +// import 'dart:async'; +// import 'dart:typed_data'; +// +// import 'package:doctor_app_flutter/pages/conference/participant_widget.dart'; +// import 'package:flutter/material.dart'; +// import 'package:flutter/services.dart'; +// +// class ConferenceRoom with ChangeNotifier { +// final String name; +// final String token; +// final String identity; +// +// final StreamController _onAudioEnabledStreamController = StreamController.broadcast(); +// Stream onAudioEnabled; +// final StreamController _onVideoEnabledStreamController = StreamController.broadcast(); +// Stream onVideoEnabled; +// final StreamController _onExceptionStreamController = StreamController.broadcast(); +// Stream onException; +// +// final Completer _completer = Completer(); +// +// final List _participants = []; +// final List _participantBuffer = []; +// final List _streamSubscriptions = []; +// final List _dataTracks = []; +// final List _messages = []; +// +// CameraCapturer _cameraCapturer; +// Room _room; +// Timer _timer; +// +// ConferenceRoom({ +// required this.name, +// required this.token, +// required this.identity, +// }) { +// onAudioEnabled = _onAudioEnabledStreamController.stream; +// onVideoEnabled = _onVideoEnabledStreamController.stream; +// onException = _onExceptionStreamController.stream; +// } +// +// List get participants { +// return [..._participants]; +// } +// +// Future connect() async { +// print('ConferenceRoom.connect()'); +// try { +// await TwilioProgrammableVideo.debug(dart: true, native: true); +// await TwilioProgrammableVideo.setSpeakerphoneOn(true); +// +// _cameraCapturer = CameraCapturer(CameraSource.FRONT_CAMERA); +// var connectOptions = ConnectOptions( +// token, +// roomName: name, +// preferredAudioCodecs: [OpusCodec()], +// audioTracks: [LocalAudioTrack(true)], +// dataTracks: [LocalDataTrack()], +// videoTracks: [LocalVideoTrack(true, _cameraCapturer)], +// enableDominantSpeaker: true, +// ); +// +// _room = await TwilioProgrammableVideo.connect(connectOptions); +// +// _streamSubscriptions.add(_room.onConnected.listen(_onConnected)); +// _streamSubscriptions.add(_room.onConnectFailure.listen(_onConnectFailure)); +// +// return _completer.future; +// } catch (err) { +// print(err); +// rethrow; +// } +// } +// +// Future disconnect() async { +// print('ConferenceRoom.disconnect()'); +// if (_timer != null) { +// _timer.cancel(); +// } +// await _room.disconnect(); +// } +// +// @override +// void dispose() { +// print('ConferenceRoom.dispose()'); +// _disposeStreamsAndSubscriptions(); +// super.dispose(); +// } +// +// Future _disposeStreamsAndSubscriptions() async { +// await _onAudioEnabledStreamController.close(); +// await _onVideoEnabledStreamController.close(); +// await _onExceptionStreamController.close(); +// for (var streamSubscription in _streamSubscriptions) { +// await streamSubscription.cancel(); +// } +// } +// +// Future sendMessage(String message) async { +// final tracks = _room.localParticipant.localDataTracks; +// final localDataTrack = tracks.isEmpty ? null : tracks[0].localDataTrack; +// if (localDataTrack == null || _messages.isNotEmpty) { +// print('ConferenceRoom.sendMessage => Track is not available yet, buffering message.'); +// _messages.add(message); +// return; +// } +// await localDataTrack.send(message); +// } +// +// Future sendBufferMessage(ByteBuffer message) async { +// final tracks = _room.localParticipant.localDataTracks; +// final localDataTrack = tracks.isEmpty ? null : tracks[0].localDataTrack; +// if (localDataTrack == null) { +// return; +// } +// await localDataTrack.sendBuffer(message); +// } +// +// Future toggleVideoEnabled() async { +// final tracks = _room.localParticipant.localVideoTracks; +// final localVideoTrack = tracks.isEmpty ? null : tracks[0].localVideoTrack; +// if (localVideoTrack == null) { +// print('ConferenceRoom.toggleVideoEnabled() => Track is not available yet!'); +// return; +// } +// await localVideoTrack.enable(!localVideoTrack.isEnabled); +// +// var index = _participants.indexWhere((ParticipantWidget participant) => !participant.isRemote); +// if (index < 0) { +// return; +// } +// var participant = _participants[index]; +// _participants.replaceRange( +// index, +// index + 1, +// [ +// participant.copyWith(videoEnabled: localVideoTrack.isEnabled), +// ], +// ); +// print('ConferenceRoom.toggleVideoEnabled() => ${localVideoTrack.isEnabled}'); +// _onVideoEnabledStreamController.add(localVideoTrack.isEnabled); +// notifyListeners(); +// } +// +// Future toggleAudioEnabled() async { +// final tracks = _room.localParticipant.localAudioTracks; +// final localAudioTrack = tracks.isEmpty ? null : tracks[0].localAudioTrack; +// if (localAudioTrack == null) { +// print('ConferenceRoom.toggleAudioEnabled() => Track is not available yet!'); +// return; +// } +// await localAudioTrack.enable(!localAudioTrack.isEnabled); +// +// var index = _participants.indexWhere((ParticipantWidget participant) => !participant.isRemote); +// if (index < 0) { +// return; +// } +// var participant = _participants[index]; +// _participants.replaceRange( +// index, +// index + 1, +// [ +// participant.copyWith(audioEnabled: localAudioTrack.isEnabled), +// ], +// ); +// print('ConferenceRoom.toggleAudioEnabled() => ${localAudioTrack.isEnabled}'); +// _onAudioEnabledStreamController.add(localAudioTrack.isEnabled); +// notifyListeners(); +// } +// +// Future switchCamera() async { +// print('ConferenceRoom.switchCamera()'); +// try { +// await _cameraCapturer.switchCamera(); +// } on FormatException catch (e) { +// print( +// 'ConferenceRoom.switchCamera() failed because of FormatException with message: ${e.message}', +// ); +// } +// } +// +// void addDummy({Widget child}) { +// print('ConferenceRoom.addDummy()'); +// if (_participants.length >= 18) { +// throw PlatformException( +// code: 'ConferenceRoom.maximumReached', +// message: 'Maximum reached', +// details: 'Currently the lay-out can only render a maximum of 18 participants', +// ); +// } +// _participants.insert( +// 0, +// ParticipantWidget( +// id: (_participants.length + 1).toString(), +// child: child, +// isRemote: true, +// audioEnabled: true, +// videoEnabled: true, +// isDummy: true, +// ), +// ); +// notifyListeners(); +// } +// +// void removeDummy() { +// print('ConferenceRoom.removeDummy()'); +// var dummy = _participants.firstWhere((participant) => participant.isDummy, orElse: () => null); +// if (dummy != null) { +// _participants.remove(dummy); +// notifyListeners(); +// } +// } +// +// void _onConnected(Room room) { +// print('ConferenceRoom._onConnected => state: ${room.state}'); +// +// // When connected for the first time, add remote participant listeners +// _streamSubscriptions.add(_room.onParticipantConnected.listen(_onParticipantConnected)); +// _streamSubscriptions.add(_room.onParticipantDisconnected.listen(_onParticipantDisconnected)); +// _streamSubscriptions.add(_room.onDominantSpeakerChange.listen(_onDominantSpeakerChanged)); +// // Only add ourselves when connected for the first time too. +// _participants.add( +// _buildParticipant( +// child: room.localParticipant.localVideoTracks[0].localVideoTrack.widget(), +// id: identity, +// audioEnabled: true, +// videoEnabled: true, +// ), +// ); +// for (final remoteParticipant in room.remoteParticipants) { +// var participant = _participants.firstWhere((participant) => participant.id == remoteParticipant.sid, orElse: () => null); +// if (participant == null) { +// print('Adding participant that was already present in the room ${remoteParticipant.sid}, before I connected'); +// _addRemoteParticipantListeners(remoteParticipant); +// } +// } +// // We have to listen for the [onDataTrackPublished] event on the [LocalParticipant] in +// // order to be able to use the [send] method. +// _streamSubscriptions.add(room.localParticipant.onDataTrackPublished.listen(_onLocalDataTrackPublished)); +// notifyListeners(); +// _completer.complete(room); +// +// _timer = Timer.periodic(const Duration(minutes: 1), (_) { +// // Let's see if we can send some data over the DataTrack API +// sendMessage('And another minute has passed since I connected...'); +// // Also try the ByteBuffer way of sending data +// final list = 'This data has been sent over the ByteBuffer channel of the DataTrack API'.codeUnits; +// var bytes = Uint8List.fromList(list); +// sendBufferMessage(bytes.buffer); +// }); +// } +// +// void _onLocalDataTrackPublished(LocalDataTrackPublishedEvent event) { +// // Send buffered messages, if any... +// while (_messages.isNotEmpty) { +// var message = _messages.removeAt(0); +// print('Sending buffered message: $message'); +// event.localDataTrackPublication.localDataTrack.send(message); +// } +// } +// +// void _onConnectFailure(RoomConnectFailureEvent event) { +// print('ConferenceRoom._onConnectFailure: ${event.exception}'); +// _completer.completeError(event.exception); +// } +// +// void _onDominantSpeakerChanged(DominantSpeakerChangedEvent event) { +// print('ConferenceRoom._onDominantSpeakerChanged: ${event.remoteParticipant.identity}'); +// var oldDominantParticipant = _participants.firstWhere((p) => p.isDominant, orElse: () => null); +// if (oldDominantParticipant != null) { +// var oldDominantParticipantIndex = _participants.indexOf(oldDominantParticipant); +// _participants.replaceRange(oldDominantParticipantIndex, oldDominantParticipantIndex + 1, [oldDominantParticipant.copyWith(isDominant: false)]); +// } +// +// var newDominantParticipant = _participants.firstWhere((p) => p.id == event.remoteParticipant.sid); +// var newDominantParticipantIndex = _participants.indexOf(newDominantParticipant); +// _participants.replaceRange(newDominantParticipantIndex, newDominantParticipantIndex + 1, [newDominantParticipant.copyWith(isDominant: true)]); +// notifyListeners(); +// } +// +// void _onParticipantConnected(RoomParticipantConnectedEvent event) { +// print('ConferenceRoom._onParticipantConnected, ${event.remoteParticipant.sid}'); +// _addRemoteParticipantListeners(event.remoteParticipant); +// } +// +// void _onParticipantDisconnected(RoomParticipantDisconnectedEvent event) { +// print('ConferenceRoom._onParticipantDisconnected: ${event.remoteParticipant.sid}'); +// _participants.removeWhere((ParticipantWidget p) => p.id == event.remoteParticipant.sid); +// notifyListeners(); +// } +// +// ParticipantWidget _buildParticipant({ +// required Widget child, +// required String id, +// required bool audioEnabled, +// required bool videoEnabled, +// RemoteParticipant remoteParticipant, +// }) { +// return ParticipantWidget( +// id: remoteParticipant?.sid, +// isRemote: remoteParticipant != null, +// child: child, +// audioEnabled: audioEnabled, +// videoEnabled: videoEnabled, +// ); +// } +// +// void _addRemoteParticipantListeners(RemoteParticipant remoteParticipant) { +// print('ConferenceRoom._addRemoteParticipantListeners() => Adding listeners to remoteParticipant ${remoteParticipant.sid}'); +// _streamSubscriptions.add(remoteParticipant.onAudioTrackDisabled.listen(_onAudioTrackDisabled)); +// _streamSubscriptions.add(remoteParticipant.onAudioTrackEnabled.listen(_onAudioTrackEnabled)); +// _streamSubscriptions.add(remoteParticipant.onAudioTrackPublished.listen(_onAudioTrackPublished)); +// _streamSubscriptions.add(remoteParticipant.onAudioTrackSubscribed.listen(_onAudioTrackSubscribed)); +// _streamSubscriptions.add(remoteParticipant.onAudioTrackSubscriptionFailed.listen(_onAudioTrackSubscriptionFailed)); +// _streamSubscriptions.add(remoteParticipant.onAudioTrackUnpublished.listen(_onAudioTrackUnpublished)); +// _streamSubscriptions.add(remoteParticipant.onAudioTrackUnsubscribed.listen(_onAudioTrackUnsubscribed)); +// +// _streamSubscriptions.add(remoteParticipant.onDataTrackPublished.listen(_onDataTrackPublished)); +// _streamSubscriptions.add(remoteParticipant.onDataTrackSubscribed.listen(_onDataTrackSubscribed)); +// _streamSubscriptions.add(remoteParticipant.onDataTrackSubscriptionFailed.listen(_onDataTrackSubscriptionFailed)); +// _streamSubscriptions.add(remoteParticipant.onDataTrackUnpublished.listen(_onDataTrackUnpublished)); +// _streamSubscriptions.add(remoteParticipant.onDataTrackUnsubscribed.listen(_onDataTrackUnsubscribed)); +// +// _streamSubscriptions.add(remoteParticipant.onVideoTrackDisabled.listen(_onVideoTrackDisabled)); +// _streamSubscriptions.add(remoteParticipant.onVideoTrackEnabled.listen(_onVideoTrackEnabled)); +// _streamSubscriptions.add(remoteParticipant.onVideoTrackPublished.listen(_onVideoTrackPublished)); +// _streamSubscriptions.add(remoteParticipant.onVideoTrackSubscribed.listen(_onVideoTrackSubscribed)); +// _streamSubscriptions.add(remoteParticipant.onVideoTrackSubscriptionFailed.listen(_onVideoTrackSubscriptionFailed)); +// _streamSubscriptions.add(remoteParticipant.onVideoTrackUnpublished.listen(_onVideoTrackUnpublished)); +// _streamSubscriptions.add(remoteParticipant.onVideoTrackUnsubscribed.listen(_onVideoTrackUnsubscribed)); +// } +// +// void _onAudioTrackDisabled(RemoteAudioTrackEvent event) { +// print('ConferenceRoom._onAudioTrackDisabled(), ${event.remoteParticipant.sid}, ${event.remoteAudioTrackPublication.trackSid}, isEnabled: ${event.remoteAudioTrackPublication.isTrackEnabled}'); +// _setRemoteAudioEnabled(event); +// } +// +// void _onAudioTrackEnabled(RemoteAudioTrackEvent event) { +// print('ConferenceRoom._onAudioTrackEnabled(), ${event.remoteParticipant.sid}, ${event.remoteAudioTrackPublication.trackSid}, isEnabled: ${event.remoteAudioTrackPublication.isTrackEnabled}'); +// _setRemoteAudioEnabled(event); +// } +// +// void _onAudioTrackPublished(RemoteAudioTrackEvent event) { +// print('ConferenceRoom._onAudioTrackPublished(), ${event.remoteParticipant.sid}}'); +// } +// +// void _onAudioTrackSubscribed(RemoteAudioTrackSubscriptionEvent event) { +// print('ConferenceRoom._onAudioTrackSubscribed(), ${event.remoteParticipant.sid}, ${event.remoteAudioTrackPublication.trackSid}'); +// _addOrUpdateParticipant(event); +// } +// +// void _onAudioTrackSubscriptionFailed(RemoteAudioTrackSubscriptionFailedEvent event) { +// print('ConferenceRoom._onAudioTrackSubscriptionFailed(), ${event.remoteParticipant.sid}, ${event.remoteAudioTrackPublication.trackSid}'); +// _onExceptionStreamController.add( +// PlatformException( +// code: 'ConferenceRoom.audioTrackSubscriptionFailed', +// message: 'AudioTrack Subscription Failed', +// details: event.exception.toString(), +// ), +// ); +// } +// +// void _onAudioTrackUnpublished(RemoteAudioTrackEvent event) { +// print('ConferenceRoom._onAudioTrackUnpublished(), ${event.remoteParticipant.sid}, ${event.remoteAudioTrackPublication.trackSid}'); +// } +// +// void _onAudioTrackUnsubscribed(RemoteAudioTrackSubscriptionEvent event) { +// print('ConferenceRoom._onAudioTrackUnsubscribed(), ${event.remoteParticipant.sid}, ${event.remoteAudioTrack.sid}'); +// } +// +// void _onDataTrackPublished(RemoteDataTrackEvent event) { +// print('ConferenceRoom._onDataTrackPublished(), ${event.remoteParticipant.sid}}'); +// } +// +// void _onDataTrackSubscribed(RemoteDataTrackSubscriptionEvent event) { +// print('ConferenceRoom._onDataTrackSubscribed(), ${event.remoteParticipant.sid}, ${event.remoteDataTrackPublication.trackSid}'); +// final dataTrack = event.remoteDataTrackPublication.remoteDataTrack; +// _dataTracks.add(dataTrack); +// _streamSubscriptions.add(dataTrack.onMessage.listen(_onMessage)); +// _streamSubscriptions.add(dataTrack.onBufferMessage.listen(_onBufferMessage)); +// } +// +// void _onDataTrackSubscriptionFailed(RemoteDataTrackSubscriptionFailedEvent event) { +// print('ConferenceRoom._onDataTrackSubscriptionFailed(), ${event.remoteParticipant.sid}, ${event.remoteDataTrackPublication.trackSid}'); +// _onExceptionStreamController.add( +// PlatformException( +// code: 'ConferenceRoom.dataTrackSubscriptionFailed', +// message: 'DataTrack Subscription Failed', +// details: event.exception.toString(), +// ), +// ); +// } +// +// void _onDataTrackUnpublished(RemoteDataTrackEvent event) { +// print('ConferenceRoom._onDataTrackUnpublished(), ${event.remoteParticipant.sid}, ${event.remoteDataTrackPublication.trackSid}'); +// } +// +// void _onDataTrackUnsubscribed(RemoteDataTrackSubscriptionEvent event) { +// print('ConferenceRoom._onDataTrackUnsubscribed(), ${event.remoteParticipant.sid}, ${event.remoteDataTrack.sid}'); +// } +// +// void _onVideoTrackDisabled(RemoteVideoTrackEvent event) { +// print('ConferenceRoom._onVideoTrackDisabled(), ${event.remoteParticipant.sid}, ${event.remoteVideoTrackPublication.trackSid}, isEnabled: ${event.remoteVideoTrackPublication.isTrackEnabled}'); +// _setRemoteVideoEnabled(event); +// } +// +// void _onVideoTrackEnabled(RemoteVideoTrackEvent event) { +// print('ConferenceRoom._onVideoTrackEnabled(), ${event.remoteParticipant.sid}, ${event.remoteVideoTrackPublication.trackSid}, isEnabled: ${event.remoteVideoTrackPublication.isTrackEnabled}'); +// _setRemoteVideoEnabled(event); +// } +// +// void _onVideoTrackPublished(RemoteVideoTrackEvent event) { +// print('ConferenceRoom._onVideoTrackPublished(), ${event.remoteParticipant.sid}, ${event.remoteVideoTrackPublication.trackSid}'); +// } +// +// void _onVideoTrackSubscribed(RemoteVideoTrackSubscriptionEvent event) { +// print('ConferenceRoom._onVideoTrackSubscribed(), ${event.remoteParticipant.sid}, ${event.remoteVideoTrack.sid}'); +// _addOrUpdateParticipant(event); +// } +// +// void _onVideoTrackSubscriptionFailed(RemoteVideoTrackSubscriptionFailedEvent event) { +// print('ConferenceRoom._onVideoTrackSubscriptionFailed(), ${event.remoteParticipant.sid}, ${event.remoteVideoTrackPublication.trackSid}'); +// _onExceptionStreamController.add( +// PlatformException( +// code: 'ConferenceRoom.videoTrackSubscriptionFailed', +// message: 'VideoTrack Subscription Failed', +// details: event.exception.toString(), +// ), +// ); +// } +// +// void _onVideoTrackUnpublished(RemoteVideoTrackEvent event) { +// print('ConferenceRoom._onVideoTrackUnpublished(), ${event.remoteParticipant.sid}, ${event.remoteVideoTrackPublication.trackSid}'); +// } +// +// void _onVideoTrackUnsubscribed(RemoteVideoTrackSubscriptionEvent event) { +// print('ConferenceRoom._onVideoTrackUnsubscribed(), ${event.remoteParticipant.sid}, ${event.remoteVideoTrack.sid}'); +// } +// +// void _onMessage(RemoteDataTrackStringMessageEvent event) { +// print('onMessage => ${event.remoteDataTrack.sid}, ${event.message}'); +// } +// +// void _onBufferMessage(RemoteDataTrackBufferMessageEvent event) { +// print('onBufferMessage => ${event.remoteDataTrack.sid}, ${String.fromCharCodes(event.message.asUint8List())}'); +// } +// +// void _setRemoteAudioEnabled(RemoteAudioTrackEvent event) { +// if (event.remoteAudioTrackPublication == null) { +// return; +// } +// var index = _participants.indexWhere((ParticipantWidget participant) => participant.id == event.remoteParticipant.sid); +// if (index < 0) { +// return; +// } +// var participant = _participants[index]; +// _participants.replaceRange( +// index, +// index + 1, +// [ +// participant.copyWith(audioEnabled: event.remoteAudioTrackPublication.isTrackEnabled), +// ], +// ); +// notifyListeners(); +// } +// +// void _setRemoteVideoEnabled(RemoteVideoTrackEvent event) { +// if (event.remoteVideoTrackPublication == null) { +// return; +// } +// var index = _participants.indexWhere((ParticipantWidget participant) => participant.id == event.remoteParticipant.sid); +// if (index < 0) { +// return; +// } +// var participant = _participants[index]; +// _participants.replaceRange( +// index, +// index + 1, +// [ +// participant.copyWith(videoEnabled: event.remoteVideoTrackPublication.isTrackEnabled), +// ], +// ); +// notifyListeners(); +// } +// +// void _addOrUpdateParticipant(RemoteParticipantEvent event) { +// print('ConferenceRoom._addOrUpdateParticipant(), ${event.remoteParticipant.sid}'); +// final participant = _participants.firstWhere( +// (ParticipantWidget participant) => participant.id == event.remoteParticipant.sid, +// orElse: () => null, +// ); +// if (participant != null) { +// print('Participant found: ${participant.id}, updating A/V enabled values'); +// _setRemoteVideoEnabled(event); +// _setRemoteAudioEnabled(event); +// } else { +// final bufferedParticipant = _participantBuffer.firstWhere( +// (ParticipantBuffer participant) => participant.id == event.remoteParticipant.sid, +// orElse: () => null, +// ); +// if (bufferedParticipant != null) { +// _participantBuffer.remove(bufferedParticipant); +// } else if (event is RemoteAudioTrackEvent) { +// print('Audio subscription came first, waiting for the video subscription...'); +// _participantBuffer.add( +// ParticipantBuffer( +// id: event.remoteParticipant.sid, +// audioEnabled: event.remoteAudioTrackPublication?.remoteAudioTrack?.isEnabled ?? true, +// ), +// ); +// return; +// } +// if (event is RemoteVideoTrackSubscriptionEvent) { +// print('New participant, adding: ${event.remoteParticipant.sid}'); +// _participants.insert( +// 0, +// _buildParticipant( +// child: event.remoteVideoTrack.widget(), +// id: event.remoteParticipant.sid, +// remoteParticipant: event.remoteParticipant, +// audioEnabled: bufferedParticipant?.audioEnabled ?? true, +// videoEnabled: event.remoteVideoTrackPublication?.remoteVideoTrack?.isEnabled ?? true, +// ), +// ); +// } +// notifyListeners(); +// } +// } +// } diff --git a/lib/screens/live_care/web-rtc/draggable_publisher.dart b/lib/screens/live_care/web-rtc/draggable_publisher.dart new file mode 100644 index 00000000..bbec744e --- /dev/null +++ b/lib/screens/live_care/web-rtc/draggable_publisher.dart @@ -0,0 +1,172 @@ +import 'dart:async'; +import 'dart:io'; +import 'package:doctor_app_flutter/screens/live_care/web-rtc/clipped_video.dart'; +import 'package:flutter/material.dart'; + +class DraggablePublisher extends StatefulWidget { + final Size availableScreenSize; + final Widget child; + final double scaleFactor; + final Stream onButtonBarVisible; + final Stream onButtonBarHeight; + + const DraggablePublisher({ + 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() { + final 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/screens/live_care/web-rtc/participant_widget.dart b/lib/screens/live_care/web-rtc/participant_widget.dart new file mode 100644 index 00000000..b9bfafcf --- /dev/null +++ b/lib/screens/live_care/web-rtc/participant_widget.dart @@ -0,0 +1,197 @@ +import 'dart:ui'; + +import 'package:flutter/material.dart'; + +class ParticipantBuffer { + final bool audioEnabled; + final String id; + + ParticipantBuffer({ + required this.audioEnabled, + required this.id, + }) : assert(audioEnabled != null), + assert(id != null); +} + +class ParticipantWidget extends StatelessWidget { + final Widget child; + final String id; + final bool audioEnabled; + final bool videoEnabled; + final bool isRemote; + final bool isDummy; + final bool isDominant; + + const ParticipantWidget({ + Key? key, + required this.child, + required this.audioEnabled, + required this.videoEnabled, + required this.id, + required this.isRemote, + this.isDominant = false, + this.isDummy = false, + }) : assert(child != null), + assert(audioEnabled != null), + assert(videoEnabled != null), + assert(isRemote != null), + assert(isDominant != null), + assert(isDummy != null), + super(key: key); + + ParticipantWidget copyWith({ + Widget? child, + bool? audioEnabled, + bool? videoEnabled, + bool? isDominant, + }) { + return ParticipantWidget( + id: id, + child: child ?? this.child, + audioEnabled: audioEnabled ?? this.audioEnabled, + videoEnabled: videoEnabled ?? this.videoEnabled, + isDominant: isDominant ?? this.isDominant, + isRemote: isRemote, + ); + } + + @override + Widget build(BuildContext context) { + final children = []; + final icons = []; + if (!videoEnabled) { + icons.add(_buildVideoEnabledIcon()); + children.add( + ClipRect( + // Need to clip this BackdropFilter, otherwise it will blur the entire screen + child: BackdropFilter( + filter: ImageFilter.blur(sigmaX: 10, sigmaY: 10), + child: Container( + decoration: BoxDecoration(color: Colors.black.withOpacity(.1)), + child: child, + ), + ), + ), + ); + } else { + children.add(child); + } + children.add(Padding( + padding: const EdgeInsets.all(8.0), + child: AnimatedOpacity( + duration: Duration(milliseconds: 500), + opacity: isDominant ? 1 : 0, + child: Icon( + Icons.volume_up, + color: Colors.white, + ), + ), + )); + if (!audioEnabled) { + icons.add(_buildAudioEnabledIcon()); + } + if (icons.isNotEmpty) { + if (isRemote) { + final rows = []; + rows.add(_buildRow(icons)); + if (!audioEnabled && !videoEnabled) { + rows.add(_buildRow(_fitText('The camera and microphone are off', Colors.white24))); + } else if (!audioEnabled) { + rows.add(_buildRow(_fitText('The microphone is off', Colors.black26))); + } else if (!videoEnabled) { + rows.add(_buildRow(_fitText('The camera is off', Colors.white24))); + } + children.add( + Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: rows, + ), + ); + } else { + children.add(Column( + mainAxisAlignment: MainAxisAlignment.spaceAround, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: icons, + )); + } + } + + return Stack( + children: children, + ); + } + + List _fitText(String text, Color color) { + return [ + Flexible( + child: FittedBox( + fit: BoxFit.scaleDown, + child: Padding( + padding: const EdgeInsets.only(left: 8.0, right: 8.0), + child: Text(text, maxLines: 1, style: _buildTextStyle(color)), + ), + ), + ), + ]; + } + + TextStyle _buildTextStyle(Color color) { + return TextStyle( + color: color, + shadows: [ + Shadow( + blurRadius: 1.0, + color: Color.fromARGB(255, 0, 0, 0), + ), + Shadow( + blurRadius: 1.0, + color: Color.fromARGB(24, 255, 255, 255), + ), + ], + fontSize: 15, + ); + } + + Widget _buildRow(List children) { + return Row( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: children, + ); + } + + Widget _buildAudioEnabledIcon() { + return Padding( + padding: const EdgeInsets.all(8), + child: CircleAvatar( + maxRadius: 15, + child: FittedBox( + child: Icon( + Icons.mic_off, + color: Colors.black, + key: Key('microphone-off-icon'), + ), + ), + backgroundColor: Colors.white24, + ), + ); + } + + Widget _buildVideoEnabledIcon() { + return Padding( + padding: const EdgeInsets.all(8), + child: CircleAvatar( + maxRadius: 15, + child: FittedBox( + child: Icon( + Icons.videocam_off, + color: Colors.black, + key: Key('videocam-off-icon'), + ), + ), + backgroundColor: Colors.white24, + ), + ); + } +} diff --git a/lib/screens/live_care/web-rtc/signaling.dart b/lib/screens/live_care/web-rtc/signaling.dart new file mode 100644 index 00000000..64d1e376 --- /dev/null +++ b/lib/screens/live_care/web-rtc/signaling.dart @@ -0,0 +1,231 @@ +import 'dart:convert'; + +import 'package:doctor_app_flutter/util/SignalRUtil.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter_webrtc/flutter_webrtc.dart'; + +const PATIENT_PUSH_TOKEN = "eUA6FUiOTbSellej2JQ8yg:APA91bFuDledDvwbeZJ8KU8drHQ7kse7h9UjKPaBZsQgKi-0GCSPRB_yIO9O3PXysWLnLMJYwVmEJWd-jPIqbrLz_Z_yzE--3mC-GZJj92BT0nSzAVd-JVr28pUtejgSjTpZryNFlSn5"; + +typedef void StreamStateCallback(MediaStream stream); +typedef void RTCIceGatheringStateCallback(RTCIceGatheringState state); +typedef void RTCPeerConnectionStateCallback(RTCPeerConnectionState state); +typedef void RTCSignalingStateCallback(RTCSignalingState state); + + + + +Map snapsis_ice_config = { + 'iceServers': [ + { "urls": 'stun:15.185.116.59:3478' }, + { "urls": "turn:15.185.116.59:3479", "username": "admin", "credential": "admin" }, + ], + // 'sdpSemantics': 'unified-plan' +}; +Map twilio_ice_config = { + "ice_servers": [ + { + "url": "stun:global.stun.twilio.com:3478?transport=udp", + "urls": "stun:global.stun.twilio.com:3478?transport=udp" + }, + { + "url": "turn:global.turn.twilio.com:3478?transport=udp", + "username": "ce8042842b62c21bd20b176f80d6067fd3db81b1e9766312418ef5421d9ca2a2", + "urls": "turn:global.turn.twilio.com:3478?transport=udp", + "credential": "UzGOsiLwPZJ32cjafAebfDDpVrqeQjgpFHZEdau/8r4=" + }, + { + "url": "turn:global.turn.twilio.com:3478?transport=tcp", + "username": "ce8042842b62c21bd20b176f80d6067fd3db81b1e9766312418ef5421d9ca2a2", + "urls": "turn:global.turn.twilio.com:3478?transport=tcp", + "credential": "UzGOsiLwPZJ32cjafAebfDDpVrqeQjgpFHZEdau/8r4=" + }, + { + "url": "turn:global.turn.twilio.com:443?transport=tcp", + "username": "ce8042842b62c21bd20b176f80d6067fd3db81b1e9766312418ef5421d9ca2a2", + "urls": "turn:global.turn.twilio.com:443?transport=tcp", + "credential": "UzGOsiLwPZJ32cjafAebfDDpVrqeQjgpFHZEdau/8r4=" + } + ], + // 'sdpSemantics': 'unified-plan' +}; +Map google_ice_config = { + 'iceServers': [ + { + 'urls': [ + 'stun:stun.l.google.com:19302', + 'stun:stun1.l.google.com:19302', + 'stun:stun2.l.google.com:19302', + 'stun:stun3.l.google.com:19302' + ] + }, + ], + // 'sdpSemantics': 'unified-plan' +}; +Map aws_ice_config = { + 'iceServers': [ + {'url': "stun:ec2-15-185-116-59.me-south-1.compute.amazonaws.com:3478"}, + {'url': "turn:ec2-15-185-116-59.me-south-1.compute.amazonaws.com:3479", 'credential': "admin", 'username': "admin"} + ], + // 'sdpSemantics': 'unified-plan' +}; + +final Map _peer_config = { + 'mandatory': {}, + 'optional': [ + {'DtlsSrtpKeyAgreement': true}, + ] +}; + +class Signaling { + + dispose(){ + if(peerConnection != null) + peerConnection.dispose(); + signalR?.closeConnection(); + } + + init(){ + // Create Peer Connection + createPeerConnection(google_ice_config,_peer_config).then((value){ + peerConnection = value; + registerPeerConnectionListeners(); + }); + } + + + initializeSignalR(String userName) async{ + if(signalR != null) + await signalR?.closeConnection(); + // https://vcallapi.hmg.com/webRTCHub?source=web&username=zohaib + signalR = SignalRUtil(hubName: "https://vcallapi.hmg.com/webRTCHub?source=mobile&username=$userName"); + final connected = await signalR?.openConnection(); + if(connected != null && !connected) + throw 'Failed to connect SignalR'; + } + + SignalRUtil? signalR; + + late RTCPeerConnection peerConnection; + MediaStream? localStream; + MediaStream? remoteStream; + RTCDataChannel? dataChannel; + + Future acceptCall(String caller, String receiver, {required MediaStream localMediaStream, required Function(MediaStream) onRemoteMediaStream}) async{ + await initializeSignalR(caller); + signalR?.setContributors(caller: caller, receiver: receiver); + await signalR?.acceptCall(receiver, caller).catchError((e) => throw 'Failed to inform signalR that i accepted a call'); + + peerConnection.addStream(localMediaStream); + + peerConnection.onAddStream = (MediaStream stream) { + remoteStream = stream; + onRemoteMediaStream.call(stream); + }; + + return true; + } + + + Future initiateCall(String caller, String receiver, {required MediaStream localMediaStream, required Function(MediaStream) onRemoteMediaStream}) async{ + await initializeSignalR(caller); + signalR?.setContributors(caller: caller, receiver: receiver); + await signalR?.callUserMobile(caller, receiver).catchError((e) => throw 'Failed to inform signalR to call user: $receiver'); + + peerConnection.addStream(localMediaStream); + + peerConnection.onAddStream = (MediaStream stream) { + remoteStream = stream; + onRemoteMediaStream.call(stream); + }; + + return true; + } + + Future hangupCall(String caller, String receiver) async{ + await signalR?.hangupCall(caller, receiver); + dispose(); + } + + answerOffer(Map offerSdp) async{ + final caller = offerSdp['caller']; + final receiver = offerSdp['target']; + peerConnection.setRemoteDescription(rtcSessionDescriptionFrom(offerSdp)) + .then((value) { + return peerConnection.createAnswer(); + }) + .then((anwser) { + return peerConnection.setLocalDescription(anwser); + }) + .then((value) { + return peerConnection.getLocalDescription(); + }) + .then((answer) { + return signalR?.answerOffer(answer, caller, receiver); + }); + + } + + sdpOfferAnswered(Map sdp){ + final answerSdp = rtcSessionDescriptionFrom(sdp); + peerConnection.setRemoteDescription(answerSdp) + .then((value) { + }).catchError((e) => print(e)); + } + + Future hangUp(RTCVideoRenderer localVideo) async { + + } + + Future createSdpAnswer(String toOfferSdp) async { + final offerSdp = rtcSessionDescriptionFrom(jsonDecode(toOfferSdp)); + peerConnection.setRemoteDescription(offerSdp); + + final answer = await peerConnection.createAnswer(); + var answerSdp = json.encode(answer); // Send SDP via Push or any channel + return answerSdp; + } + + Future createSdpOffer(String caller, String receiver) async { + await Future.delayed(Duration(seconds: 1)); + final offer = await peerConnection.createOffer(); + await peerConnection.setLocalDescription(offer); + final map = offer.toMap(); + var offerSdp = json.encode({'sdp':map, 'target':receiver, 'caller':caller}); // Send SDP via Push or any channel + return offerSdp; + } + + addCandidate(Map candidate){ + peerConnection.addCandidate(rtcIceCandidateFrom(candidate)); + } + + void registerPeerConnectionListeners() { + peerConnection.onIceCandidate = (RTCIceCandidate candidate){ + print(json.encode(candidate.toMap())); + signalR?.addIceCandidate(candidate); + }; + + peerConnection.onIceGatheringState = (RTCIceGatheringState state) { + print('ICE gathering state changed: $state'); + }; + + peerConnection.onConnectionState = (RTCPeerConnectionState state) { + print('Connection state change: $state ${state.index}'); + }; + + peerConnection.onSignalingState = (RTCSignalingState state) { + print('Signaling state change: $state'); + }; + } +} + + +rtcSessionDescriptionFrom(Map sdp){ + return RTCSessionDescription( + sdp['sdp'],sdp['type'], + ); +} + +rtcIceCandidateFrom(Map candidate){ + final _candidate = candidate['candidate']; + return RTCIceCandidate(_candidate['candidate'], _candidate['sdpMid'], _candidate['sdpMLineIndex']); +} diff --git a/lib/screens/live_care/web-rtc/web_rtc/call_home_page_.dart b/lib/screens/live_care/web-rtc/web_rtc/call_home_page_.dart new file mode 100644 index 00000000..73686fd2 --- /dev/null +++ b/lib/screens/live_care/web-rtc/web_rtc/call_home_page_.dart @@ -0,0 +1,198 @@ +import 'dart:async'; +import 'package:doctor_app_flutter/screens/live_care/web-rtc/signaling.dart'; +import 'package:doctor_app_flutter/screens/live_care/web-rtc/web_rtc/widgets/cam_view_widget.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_webrtc/flutter_webrtc.dart'; + +import '../conference_button_bar.dart'; + +class CallHomePage extends StatefulWidget { + final String receiverId; + final String callerId; + + const CallHomePage({Key? key, required this.receiverId, required this.callerId}) : super(key: key); + + @override + _CallHomePageState createState() => _CallHomePageState(); +} + +class _CallHomePageState extends State { + + String get caller => widget.callerId; + String get receiver => widget.receiverId; + + bool showNoise = true; + RTCVideoRenderer _localRenderer = RTCVideoRenderer(); + RTCVideoRenderer _remoteRenderer = RTCVideoRenderer(); + + final StreamController _audioButton = StreamController.broadcast(); + final StreamController _videoButton = StreamController.broadcast(); + final StreamController _onButtonBarVisibleStreamController = StreamController.broadcast(); + final StreamController _onButtonBarHeightStreamController = StreamController.broadcast(); + + //Stream to enable video + late MediaStream localMediaStream; + MediaStream? remoteMediaStream; + Signaling signaling = Signaling()..init(); + + @override + void initState() { + // TODO: implement initState + super.initState(); + startCall(); + } + + startCall() async{ + await _localRenderer.initialize(); + await _remoteRenderer.initialize(); + final connected = await callUser(); + } + + + Future callUser() async { + //Stream local media + localMediaStream = await navigator.mediaDevices.getUserMedia({'video': true, 'audio': true}); + _localRenderer.srcObject = localMediaStream; + + final connected = await signaling.initiateCall(widget.callerId, widget.receiverId, localMediaStream: localMediaStream, onRemoteMediaStream: (remoteMediaStream){ + setState(() { + this.remoteMediaStream = remoteMediaStream; + _remoteRenderer.srcObject = remoteMediaStream; + }); + }); + + if(connected){ + signaling.signalR?.listen( + onAcceptCall: (arg0) async{ + final offer = await signaling.createSdpOffer(caller, receiver); + signaling.signalR?.offer(receiver, caller, offer).catchError((e){ + print(e.toString()); + }); + }, + onCandidate: (candidateJson){ + signaling.addCandidate(candidateJson); + }, + onDeclineCall: (arg0,arg1){ + // _onHangup(); + }, + onHangupCall: (arg0){ + // _onHangup(); + }, + onSdpAnswer: (answerSdp){ + signaling.sdpOfferAnswered(answerSdp); + }, + onSdpOffer: (offerSdp, user) async{ + print('${offerSdp.toString()} | ${user.toString()}'); + await signaling.answerOffer(offerSdp); + } + ); + } + return connected; + } + + @override + void dispose() { + // TODO: implement dispose + super.dispose(); + _localRenderer.dispose(); + _remoteRenderer.dispose(); + _audioButton.close(); + _videoButton.close(); + localMediaStream?.dispose(); + remoteMediaStream?.dispose(); + _disposeStreamsAndSubscriptions(); + } + + Future _disposeStreamsAndSubscriptions() async { + if (_onButtonBarVisibleStreamController != null) await _onButtonBarVisibleStreamController.close(); + if (_onButtonBarHeightStreamController != null) await _onButtonBarHeightStreamController.close(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Colors.white, + body: buildLayout(), + ); + } + + LayoutBuilder buildLayout() { + return LayoutBuilder( + builder: (BuildContext context, BoxConstraints constraints) { + return Stack( + children: [ + CamViewWidget( + localRenderer: _localRenderer, + remoteRenderer: _remoteRenderer, + constraints: constraints, + onButtonBarVisibleStreamController: _onButtonBarVisibleStreamController, + onButtonBarHeightStreamController: _onButtonBarHeightStreamController, + ), + ConferenceButtonBar( + audioEnabled: _audioButton.stream, + videoEnabled: _videoButton.stream, + onAudioEnabled: _onAudioEnable, + onVideoEnabled: _onVideoEnabled, + onSwitchCamera: _onSwitchCamera, + onHangup: _onHangup, + onPersonAdd: () {}, + onPersonRemove: () {}, + onHeight: _onHeightBar, + onShow: _onShowBar, + onHide: _onHideBar, + ), + ], + ); + }, + ); + } + + _onAudioEnable() { + final audioTrack = localMediaStream?.getAudioTracks()[0]; + if(audioTrack != null){ + final mute = audioTrack.muted ?? false; + Helper.setMicrophoneMute(mute, audioTrack); + _audioButton.add(mute); + } + } + + _onVideoEnabled() { + final videoTrack = localMediaStream?.getVideoTracks()[0]; + if(videoTrack != null){ + bool videoEnabled = videoTrack.enabled; + localMediaStream?.getVideoTracks()[0].enabled = !videoEnabled; + _videoButton.add(!videoEnabled); + } + } + + _onSwitchCamera() { + Helper.switchCamera(localMediaStream.getVideoTracks()[0]); + } + + void _onShowBar() { + setState(() { + }); + _onButtonBarVisibleStreamController.add(true); + } + + void _onHeightBar(double height) { + _onButtonBarHeightStreamController.add(height); + } + + void _onHideBar() { + setState(() { + SystemChrome.setEnabledSystemUIOverlays([SystemUiOverlay.bottom]); + }); + _onButtonBarVisibleStreamController.add(false); + } + + Future _onHangup() async { + signaling.hangupCall(widget.callerId, widget.receiverId); + signaling.dispose(); + print('onHangup'); + Navigator.of(context).pop(); + } + + +} diff --git a/lib/screens/live_care/web-rtc/web_rtc/widgets/cam_view_widget.dart b/lib/screens/live_care/web-rtc/web_rtc/widgets/cam_view_widget.dart new file mode 100644 index 00000000..a6b2a612 --- /dev/null +++ b/lib/screens/live_care/web-rtc/web_rtc/widgets/cam_view_widget.dart @@ -0,0 +1,82 @@ +import 'dart:async'; +import 'dart:core'; + +import 'package:doctor_app_flutter/screens/live_care/web-rtc/widgets/noise_box.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_webrtc/flutter_webrtc.dart'; + +import 'draggable_cam.dart'; + +class CamViewWidget extends StatefulWidget { + RTCVideoRenderer localRenderer; + RTCVideoRenderer remoteRenderer; + MediaStream? localStream; + BoxConstraints constraints; + StreamController onButtonBarVisibleStreamController; + StreamController onButtonBarHeightStreamController; + + CamViewWidget({required this.localRenderer, required this.remoteRenderer, required this.constraints, required this.onButtonBarVisibleStreamController, required this.onButtonBarHeightStreamController}); + + @override + _CamViewWidgetState createState() => _CamViewWidgetState(); +} + +class _CamViewWidgetState extends State { + @override + void initState() { + super.initState(); + } + + @override + Widget build(BuildContext context) { + return Container( + width: double.infinity, + height: double.infinity, + child: Stack( + children: [ + FractionallySizedBox( + heightFactor: 1, widthFactor: 1, + child: Container( + color: Colors.black87, + child: RTCVideoView(widget.remoteRenderer, mirror: true,filterQuality: FilterQuality.medium,), + ), + ), + + if(widget.remoteRenderer.srcObject == null) + Positioned.fill(child: _buildNoiseBox()), + + Positioned.fill( + child: RTCVideoView(widget.remoteRenderer) + ), + + DraggableCam( + key: Key('publisher'), + onButtonBarHeight: widget.onButtonBarHeightStreamController.stream, + onButtonBarVisible: widget.onButtonBarVisibleStreamController.stream, + availableScreenSize: widget.constraints.biggest, + child: RTCVideoView(widget.localRenderer) + ), + + if(widget.remoteRenderer.srcObject == null) + Container( + margin: EdgeInsets.all(MediaQuery.of(context).size.width/8), + child: Text( + 'Waiting for another participant to connect to the call...', + key: Key('text-wait'), + textAlign: TextAlign.center, + style: TextStyle(color: Colors.white), + ) + ), + ], + ), + ); + } + + + Widget _buildNoiseBox() { + return NoiseBox( + density: NoiseBoxDensity.xHigh, + backgroundColor: Colors.grey.shade900, + ); + } +} diff --git a/lib/screens/live_care/web-rtc/web_rtc/widgets/draggable_cam.dart b/lib/screens/live_care/web-rtc/web_rtc/widgets/draggable_cam.dart new file mode 100644 index 00000000..5d66d0d7 --- /dev/null +++ b/lib/screens/live_care/web-rtc/web_rtc/widgets/draggable_cam.dart @@ -0,0 +1,173 @@ +import 'dart:async'; +import 'dart:io'; + +import 'package:doctor_app_flutter/screens/live_care/web-rtc/clipped_video.dart'; +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() { + final 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/screens/live_care/web-rtc/widgets/button_to_progress.dart b/lib/screens/live_care/web-rtc/widgets/button_to_progress.dart new file mode 100644 index 00000000..0716e8eb --- /dev/null +++ b/lib/screens/live_care/web-rtc/widgets/button_to_progress.dart @@ -0,0 +1,106 @@ +import 'dart:async'; +import 'dart:math' as math; + +import 'package:flutter/material.dart'; + +class ButtonToProgress extends StatefulWidget { + final double height; + final double progressHeight; + final String loadingText; + final Duration duration; + final TextStyle? loadingTextStyle; + final VoidCallback? onPressed; + final Stream onLoading; + final Widget child; + + const ButtonToProgress({ + Key? key, + this.height = 40.0, + this.progressHeight = 5.0, + required this.loadingText, + this.duration = const Duration(milliseconds: 300), + this.loadingTextStyle, + this.onPressed, + required this.onLoading, + required this.child, + }) : assert(child != null), + assert(height != null && height > 0), + assert(progressHeight != null && progressHeight > 0 && progressHeight <= height), + super(key: key); + + @override + _ButtonToProgressState createState() => _ButtonToProgressState(); +} + +class _ButtonToProgressState extends State { + late double _height; + double _opacity = 0; + bool _isLoading = false; + + late StreamSubscription _subscription; + + @override + void initState() { + super.initState(); + _height = widget.height; + if (widget.onLoading != null) { + _subscription = widget.onLoading.listen((bool isLoading) { + setState(() { + _isLoading = isLoading; + _height = isLoading ? widget.progressHeight : widget.height; + _opacity = isLoading ? 1 : 0; + }); + }); + } + } + + @override + void dispose() { + if (_subscription != null) { + _subscription.cancel(); + } + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Container( + height: widget.height, + child: Stack( + children: [ + if (widget.loadingText == null) + Container() + else + Padding( + padding: EdgeInsets.only(bottom: widget.progressHeight), + child: AnimatedOpacity( + child: Center( + child: FittedBox( + child: Text( + widget.loadingText, + style: widget.loadingTextStyle, + ), + ), + ), + opacity: _opacity, + duration: Duration(milliseconds: widget.duration.inMilliseconds + 200), + curve: Curves.easeInCubic, + ), + ), + AnimatedPadding( + duration: widget.duration, + padding: EdgeInsets.only( + top: math.max(widget.height - _height, 0), + ), + child: AnimatedContainer( + duration: widget.duration, + height: _height, + width: double.infinity, + child: _isLoading ? const LinearProgressIndicator() : widget.child, + ), + ), + ], + ), + ); + } +} diff --git a/lib/screens/live_care/web-rtc/widgets/circle_button.dart b/lib/screens/live_care/web-rtc/widgets/circle_button.dart new file mode 100644 index 00000000..97d268b0 --- /dev/null +++ b/lib/screens/live_care/web-rtc/widgets/circle_button.dart @@ -0,0 +1,103 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:native_device_orientation/native_device_orientation.dart'; + +class CircleButton extends StatefulWidget { + final VoidCallback? onLongPress; + final VoidCallback? onPressed; + final GestureTapDownCallback? onTapDown; + final VoidCallback? onTapCancel; + final Widget child; + final Color? color; + final double radius; + + const CircleButton({ + Key? key, + this.onLongPress, + this.onPressed, + required this.child, + this.color, + this.radius = 25.0, + this.onTapCancel, + this.onTapDown, + }) : assert(radius != null), + super(key: key); + + @override + _CircleButtonState createState() => _CircleButtonState(); +} + +class _CircleButtonState extends State { + double _rotationAngle = 0.0; + + final Stream _orientationStream = NativeDeviceOrientationCommunicator().onOrientationChanged(useSensor: true); + late StreamSubscription _orientationSubscription; + + void _handleOrientationChange(NativeDeviceOrientation orientation) { + var targetAngle = 0.0; + switch (orientation) { + case NativeDeviceOrientation.unknown: + case NativeDeviceOrientation.portraitUp: + targetAngle = 0.0; + break; + case NativeDeviceOrientation.portraitDown: + targetAngle = 180.0; + break; + case NativeDeviceOrientation.landscapeLeft: + targetAngle = 90.0; + break; + case NativeDeviceOrientation.landscapeRight: + targetAngle = 270.0; + break; + } + setState(() { + _rotationAngle = targetAngle; + }); + } + + @override + void initState() { + super.initState(); + _orientationSubscription = _orientationStream.listen( + _handleOrientationChange, + onError: (dynamic err) => print(err), + ); + } + + @override + void dispose() { + super.dispose(); + _orientationSubscription.cancel(); + } + + @override + Widget build(BuildContext context) { + final size = 2 * widget.radius; + + return Container( + width: size, + height: size, + decoration: BoxDecoration( + color: (widget.color ?? Colors.blue).withAlpha(200), + borderRadius: BorderRadius.all( + Radius.circular(widget.radius), + ), + ), + child: GestureDetector( + onLongPress: widget.onLongPress, + onTapDown: widget.onTapDown, + onTapCancel: widget.onTapCancel, + child: RawMaterialButton( + onPressed: widget.onPressed, + child: RotationTransition( + child: widget.child, + turns: AlwaysStoppedAnimation(_rotationAngle / 360), + ), + elevation: 0, + shape: const CircleBorder(), + ), + ), + ); + } +} diff --git a/lib/screens/live_care/web-rtc/widgets/noise_box.dart b/lib/screens/live_care/web-rtc/widgets/noise_box.dart new file mode 100644 index 00000000..4513ba0a --- /dev/null +++ b/lib/screens/live_care/web-rtc/widgets/noise_box.dart @@ -0,0 +1,147 @@ +import 'dart:math' as math; +import 'dart:ui'; + +import 'package:flutter/material.dart'; + +enum NoiseBoxDensity { + high, + medium, + low, + xHigh, + xLow, +} + +class NoiseBox extends StatefulWidget { + final NoiseBoxDensity density; + final Color backgroundColor; + final Widget? child; + + const NoiseBox({ + Key? key, + required this.backgroundColor, + this.child, + this.density = NoiseBoxDensity.medium, + }) : assert(density != null), + super(key: key); + + @override + _NoiseBoxState createState() => _NoiseBoxState(); +} + +class _NoiseBoxState extends State with TickerProviderStateMixin { + late AnimationController _animationController; + late int _density; + + @override + void initState() { + super.initState(); + _animationController = AnimationController( + vsync: this, + duration: const Duration(seconds: 60), + ); + _animationController.repeat(); + switch (widget.density) { + case NoiseBoxDensity.high: + _density = 5; + break; + case NoiseBoxDensity.medium: + _density = 7; + break; + case NoiseBoxDensity.low: + _density = 10; + break; + case NoiseBoxDensity.xHigh: + _density = 3; + break; + case NoiseBoxDensity.xLow: + _density = 12; + break; + } + } + + @override + void dispose() { + _animationController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return LayoutBuilder( + builder: (BuildContext context, BoxConstraints constraints) => Container( + color: widget.backgroundColor, + width: constraints.biggest.width, + height: constraints.biggest.height, + child: AnimatedBuilder( + animation: _animationController, + builder: (BuildContext context, Widget? child) { + final children = [ + CustomPaint( + painter: NoisePainter( + width: constraints.biggest.width, + height: constraints.biggest.height, + density: _density, + ), + ), + ]; + if (widget.child != null) { + children.add(widget.child!); + } + return Stack( + children: children, + ); + }, + ), + ), + ); + } +} + +class NoisePainter extends CustomPainter { + final double width; + final double height; + final int density; + + NoisePainter({ + required this.width, + required this.height, + required this.density, + }) : assert(width != null), + assert(height != null), + assert(density != null && + density >= 3 && + density < math.min(width, height)); + + List colors = [ + Colors.black, + Colors.grey, + Colors.blueGrey, + Colors.red, + Colors.green, + Colors.blue, + Colors.white, + ]; + + @override + void paint(Canvas canvas, Size size) { + final random = math.Random(); + for (var w = 0; w < width; w += density) { + for (var h = 0; h < height; h += density) { + final offset = Offset( + random.nextDouble() * width, + random.nextDouble() * height, + ); + final paint = Paint(); + paint.color = colors[random.nextInt(colors.length)]; + paint.strokeWidth = random.nextDouble() * 2; + + canvas.drawPoints(PointMode.points, [offset], paint); + } + } + } + + @override + bool shouldRepaint(CustomPainter oldDelegate) { + return true; + } +} diff --git a/lib/screens/live_care/web-rtc/widgets/platform_alert_dialog.dart b/lib/screens/live_care/web-rtc/widgets/platform_alert_dialog.dart new file mode 100644 index 00000000..a65c7bde --- /dev/null +++ b/lib/screens/live_care/web-rtc/widgets/platform_alert_dialog.dart @@ -0,0 +1,98 @@ +import 'dart:io'; + +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; + +import './platform_widget.dart'; + +class PlatformAlertDialog extends PlatformWidget { + PlatformAlertDialog({required this.title, required this.content, required this.defaultActionText, this.cancelActionText}) + : assert(title != null), + assert(content != null), + assert(defaultActionText != null); + + final String title; + final String content; + final String defaultActionText; + final String? cancelActionText; + + Future show(BuildContext context) async { + return Platform.isIOS + ? await showCupertinoDialog( + context: context, + builder: (BuildContext context) => this, + ) + : await showDialog( + context: context, + barrierDismissible: false, + builder: (BuildContext context) => this, + ); + } + + @override + Widget buildCupertinoWidget(BuildContext context) { + return CupertinoAlertDialog( + title: Text(title), + content: Text(content), + actions: _buildActions(context), + ); + } + + @override + Widget buildMaterialWidget(BuildContext context) { + return AlertDialog( + title: Text(title), + content: Text(content), + actions: _buildActions(context), + ); + } + + List _buildActions(BuildContext context) { + final actions = []; + if (cancelActionText != null) { + actions.add( + PlatformAlertDialogAction( + child: Text(cancelActionText!), + onPressed: () { + Navigator.of(context).pop(false); + }, + ), + ); + } + actions.add( + PlatformAlertDialogAction( + child: Text(defaultActionText), + onPressed: () { + Navigator.of(context).pop(true); + }, + ), + ); + return actions; + } +} + +class PlatformAlertDialogAction extends PlatformWidget { + PlatformAlertDialogAction({ + required this.child, + required this.onPressed, + }); + + final Widget child; + final VoidCallback onPressed; + + @override + Widget buildCupertinoWidget(BuildContext context) { + return CupertinoDialogAction( + child: child, + onPressed: onPressed, + ); + } + + @override + Widget buildMaterialWidget(BuildContext context) { + return FlatButton( + child: child, + onPressed: onPressed, + ); + } +} diff --git a/lib/screens/live_care/web-rtc/widgets/platform_exception_alert_dialog.dart b/lib/screens/live_care/web-rtc/widgets/platform_exception_alert_dialog.dart new file mode 100644 index 00000000..fea4bca9 --- /dev/null +++ b/lib/screens/live_care/web-rtc/widgets/platform_exception_alert_dialog.dart @@ -0,0 +1,23 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +import './platform_alert_dialog.dart'; + +class PlatformExceptionAlertDialog extends PlatformAlertDialog { + PlatformExceptionAlertDialog({ + String title = 'An error occurred', + required Exception exception, + }) : super( + title: title, + content: exception is PlatformException ? _message(exception) : exception.toString(), + defaultActionText: 'OK', + ); + + static String _message(PlatformException exception) { + return _errors[exception.code] ?? (exception.details != null ? (exception.details['message'] ?? exception.message) : exception.message); + } + + static final Map _errors = { + 'ERROR_CODE': 'Error description...', + }; +} diff --git a/lib/screens/live_care/web-rtc/widgets/platform_widget.dart b/lib/screens/live_care/web-rtc/widgets/platform_widget.dart new file mode 100644 index 00000000..d3366d5e --- /dev/null +++ b/lib/screens/live_care/web-rtc/widgets/platform_widget.dart @@ -0,0 +1,17 @@ +import 'dart:io'; + +import 'package:flutter/material.dart'; + +abstract class PlatformWidget extends StatelessWidget { + Widget buildCupertinoWidget(BuildContext context); + + Widget buildMaterialWidget(BuildContext context); + + @override + Widget build(BuildContext context) { + if (Platform.isIOS) { + return buildCupertinoWidget(context); + } + return buildMaterialWidget(context); + } +} diff --git a/lib/screens/live_care/web-rtc/widgets/responsive_save_area.dart b/lib/screens/live_care/web-rtc/widgets/responsive_save_area.dart new file mode 100644 index 00000000..9e38cbce --- /dev/null +++ b/lib/screens/live_care/web-rtc/widgets/responsive_save_area.dart @@ -0,0 +1,31 @@ +import 'package:flutter/material.dart'; + +typedef ResponsiveBuilder = Widget Function( + BuildContext context, + Size size, +); + +class ResponsiveSafeArea extends StatelessWidget { + const ResponsiveSafeArea({ + required ResponsiveBuilder builder, + Key? key, + }) : responsiveBuilder = builder, + assert(builder != null), + super(key: key); + + final ResponsiveBuilder responsiveBuilder; + + @override + Widget build(BuildContext context) { + return SafeArea( + child: LayoutBuilder( + builder: (BuildContext context, BoxConstraints constraints) { + return responsiveBuilder( + context, + constraints.biggest, + ); + }, + ), + ); + } +} diff --git a/lib/util/SignalRUtil.dart b/lib/util/SignalRUtil.dart new file mode 100644 index 00000000..2fa7afdd --- /dev/null +++ b/lib/util/SignalRUtil.dart @@ -0,0 +1,212 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:doctor_app_flutter/screens/live_care/web-rtc/signaling.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter_webrtc/flutter_webrtc.dart'; +import 'package:http/io_client.dart'; +import 'package:signalr_core/signalr_core.dart'; + +class SignalRUtil { + late String sourceUser; + late String destinationUser; + setContributors({required String caller, required String receiver}){ + this.sourceUser = caller; + this.destinationUser = receiver; + } + + Function(bool)? onConnected; + String hubName; + SignalRUtil({required this.hubName}); + + + late HubConnection connectionHub; + + closeConnection() async{ + if(connectionHub != null) { + connectionHub.off('OnIncomingCallAsync'); + connectionHub.off('OnCallDeclinedAsync'); + connectionHub.off('OnCallAcceptedAsync'); + connectionHub.off('nHangUpAsync'); + connectionHub.off('OnIceCandidateAsync'); + connectionHub.off('OnOfferAsync'); + await connectionHub.stop(); + } + } + + Future openConnection() async { + connectionHub = HubConnectionBuilder() + .withUrl( + hubName, + HttpConnectionOptions( + logMessageContent: true, + client: IOClient(HttpClient()..badCertificateCallback = (x, y, z) => true), + logging: (level, message) => print(message), + )).build(); + + await connectionHub.start(); + await Future.delayed(Duration(seconds: 1)); + + connectionHub.on('ReceiveMessage', (message) { + handleIncomingMessage(message); + }); + + return getConnectionState(); + } + + void handleIncomingMessage(List? message) { + print(message.toString()); + } + + void sendMessage(List args) async { + await connectionHub.invoke('SendMessage', args: args); //['Bob', 'Says hi!'] + } + + listen({ required Function(CallUser) onAcceptCall, onHangupCall, required Function(Map, CallUser) onDeclineCall, onSdpOffer, onSdpAnswer, required Function(Map) onCandidate}){ + + connectionHub.on('OnIncomingCallAsync', (arguments) { + print('OnIncomingCallAsync: ${arguments.toString()}'); + }); + + connectionHub.on('OnCallDeclinedAsync', (arguments) { + print('OnCallDeclinedAsync: ${arguments.toString()}'); + final data = json.decode(arguments?.first); + onDeclineCall(data, CallUser.from(arguments?.last)); + }); + + connectionHub.on('OnCallAcceptedAsync', (arguments) { + print('OnCallAcceptedAsync: ${arguments.toString()}'); + onAcceptCall(CallUser.from(arguments?.last)); + }); + + connectionHub.on('OnHangUpAsync', (arguments) { + print('nHangUpAsync: ${arguments.toString()}'); + onHangupCall(CallUser.from(arguments?.first)); + }); + + connectionHub.on('OnIceCandidateAsync', (arguments) { + print('OnIceCandidateAsync: ${arguments.toString()}'); + final data = json.decode(arguments?.first); + onCandidate(data); + }); + + connectionHub.on('OnOfferAsync', (arguments) { + print('OnOfferAsync: ${arguments.toString()}'); + final data = json.decode(arguments?.first); + onSdpOffer(data, CallUser.from(arguments?.last)); + }); + + connectionHub.on('OnAnswerOffer', (arguments) { + print('OnAnswerOffer: ${arguments.toString()}'); + final sdp = json.decode(arguments?.first)['sdp']; + onSdpAnswer(sdp); + }); + + } + + // CallUserAsyncMobile(string currentUserId, string targerUserId, string patientInfoJson) + Future callUserMobile(String from, to) async{ + final p_info = patientInfo(from, to, PATIENT_PUSH_TOKEN); + return await connectionHub.invoke('CallUserAsyncMobile', args: [p_info]); + } + + // CallUserAsyncRemote(string currentUserId, string targerUserId) + Future callUserRemote(String from, to) async{ + return await connectionHub.invoke('CallUserAsyncRemote', args: [from, to]); + } + + // CallUserAsync(string currentUserId, string targerUserId, string patientInfoJson) + Future callUserWeb(String from, to) async{ + final p_info = patientInfo(from, to, PATIENT_PUSH_TOKEN); + return await connectionHub.invoke('CallUserAsync', args: [from, to, p_info]); + } + patientInfo(String from, to, patientToken){ + final json_ = { + "SessionID" : '123456', + "Sess_token" : 'admin', + "_Token" : patientToken, + "AppointmentNo" : '123456', + 'ProjectID' : '12', + "projectName" : 'Olaya', + 'doctorname' : 'Dr.Habib', + 'Docspeciality' : 'Cardiologist', + 'ClincName' : 'Cardiology', + 'PatientID' : to, + 'callerID' : from + }; + return json.encode(json_); + } + + // CallDeclinedAsync(string currentUserId, string targerUserId) + Future declineCall(String from, to) async{ + return await connectionHub.invoke('CallDeclinedAsync', args: [from, to]); + } + + // AnswerCallAsync(string currentUserId, string targetUserId) + Future answerCall(String from, to) async{ + return await connectionHub.invoke('AnswerCallAsync', args: [from, to]); + } + + // IceCandidateAsync(string targetUserId, string candidate) + Future addIceCandidate(RTCIceCandidate candidate) async{ + final target = destinationUser; + final _candidate = json.encode({'candidate' : candidate.toMap()}); + return await connectionHub.invoke('IceCandidateAsync', args: [target, _candidate]); + } + + // OfferAsync(string targetUserId,string currentUserId, string targetOffer) + Future offer(String from, to, offer) async{ + return await connectionHub.invoke('OfferAsync', args: [from, to, offer]); + } + + // AnswerOfferAsync(string targetUserId, string CallerOffer) + Future answerOffer(RTCSessionDescription? answerSdp, caller, receiver) async{ + final payload = { + 'target': receiver, + 'caller': caller, + 'sdp': answerSdp?.toMap() ?? {}, + }; + return await connectionHub.invoke('AnswerOfferAsync', args: [caller, jsonEncode(payload)]); + } + + // HangUpAsync(string currentUserId, string targetUserId) + Future hangupCall(String from, to) async{ + return await connectionHub.invoke('HangUpAsync', args: [from, to]); + } + + // CallAccepted(string currentUserId,string targetUserId) + Future acceptCall(String from, to) async{ + // return await connectionHub.send(methodName: 'CallAccepted', args: [from, to]); + return await connectionHub.invoke("CallAccepted", args: [ from, to]); + } + + + bool getConnectionState() { + if (connectionHub.state == HubConnectionState.connected) return true; + if (connectionHub.state == HubConnectionState.disconnected) return false; + return false; + } +} + + +class CallUser{ + String? Id; + String? UserName; + String? Email; + String? Phone; + String? Title; + dynamic? UserStatus; + String? Image; + int UnreadMessageCount = 0; + + CallUser.from(Map map){ + Id = map['Id']; + UserName = map['UserName']; + Email = map['Email']; + Phone = map['Phone']; + Title = map['Title']; + UserStatus = map['UserStatus']; + Image = map['Image']; + UnreadMessageCount = map['UnreadMessageCount']; + } +} \ No newline at end of file diff --git a/lib/webrtc-offers/Offer2FromApp b/lib/webrtc-offers/Offer2FromApp new file mode 100644 index 00000000..45bbc7cb --- /dev/null +++ b/lib/webrtc-offers/Offer2FromApp @@ -0,0 +1,98 @@ +v=0 +o=- 5927491327118611904 2 IN IP4 127.0.0.1 +s=- +t=0 0 +a=group:BUNDLE audio video +a=extmap-allow-mixed +a=msid-semantic: WMS e53e47c9-d409-4522-bcc9-49c8b1d093b7 +m=audio 9 UDP/TLS/RTP/SAVPF 111 103 104 9 102 0 8 106 105 13 110 112 113 126 +c=IN IP4 0.0.0.0 +a=rtcp:9 IN IP4 0.0.0.0 +a=ice-ufrag:TPqz +a=ice-pwd:Av/F5Hz76zQdybHAG2DomDog +a=ice-options:trickle renomination +a=fingerprint:sha-256 1F:D1:DD:59:42:65:91:E2:41:99:ED:6C:DB:30:76:4A:6B:5A:F9:82:7C:07:29:66:80:DC:AF:E2:34:05:43:18 +a=setup:actpass +a=mid:audio +a=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level +a=extmap:2 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time +a=extmap:3 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01 +a=sendrecv +a=rtcp-mux +a=rtpmap:111 opus/48000/2 +a=rtcp-fb:111 transport-cc +a=fmtp:111 minptime=10;useinbandfec=1 +a=rtpmap:103 ISAC/16000 +a=rtpmap:104 ISAC/32000 +a=rtpmap:9 G722/8000 +a=rtpmap:102 ILBC/8000 +a=rtpmap:0 PCMU/8000 +a=rtpmap:8 PCMA/8000 +a=rtpmap:106 CN/32000 +a=rtpmap:105 CN/16000 +a=rtpmap:13 CN/8000 +a=rtpmap:110 telephone-event/48000 +a=rtpmap:112 telephone-event/32000 +a=rtpmap:113 telephone-event/16000 +a=rtpmap:126 telephone-event/8000 +a=ssrc:3000425778 cname:kibCM8KmL1PJhPlQ +a=ssrc:3000425778 msid:e53e47c9-d409-4522-bcc9-49c8b1d093b7 a82ca475-9a94-4bdc-a91c-1137d80a0753 +a=ssrc:3000425778 mslabel:e53e47c9-d409-4522-bcc9-49c8b1d093b7 +a=ssrc:3000425778 label:a82ca475-9a94-4bdc-a91c-1137d80a0753 +m=video 9 UDP/TLS/RTP/SAVPF 96 97 98 99 35 36 100 101 127 +c=IN IP4 0.0.0.0 +a=rtcp:9 IN IP4 0.0.0.0 +a=ice-ufrag:TPqz +a=ice-pwd:Av/F5Hz76zQdybHAG2DomDog +a=ice-options:trickle renomination +a=fingerprint:sha-256 1F:D1:DD:59:42:65:91:E2:41:99:ED:6C:DB:30:76:4A:6B:5A:F9:82:7C:07:29:66:80:DC:AF:E2:34:05:43:18 +a=setup:actpass +a=mid:video +a=extmap:14 urn:ietf:params:rtp-hdrext:toffset +a=extmap:2 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time +a=extmap:13 urn:3gpp:video-orientation +a=extmap:3 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01 +a=extmap:5 http://www.webrtc.org/experiments/rtp-hdrext/playout-delay +a=extmap:6 http://www.webrtc.org/experiments/rtp-hdrext/video-content-type +a=extmap:7 http://www.webrtc.org/experiments/rtp-hdrext/video-timing +a=extmap:8 http://www.webrtc.org/experiments/rtp-hdrext/color-space +a=sendrecv +a=rtcp-mux +a=rtcp-rsize +a=rtpmap:96 VP8/90000 +a=rtcp-fb:96 goog-remb +a=rtcp-fb:96 transport-cc +a=rtcp-fb:96 ccm fir +a=rtcp-fb:96 nack +a=rtcp-fb:96 nack pli +a=rtpmap:97 rtx/90000 +a=fmtp:97 apt=96 +a=rtpmap:98 VP9/90000 +a=rtcp-fb:98 goog-remb +a=rtcp-fb:98 transport-cc +a=rtcp-fb:98 ccm fir +a=rtcp-fb:98 nack +a=rtcp-fb:98 nack pli +a=rtpmap:99 rtx/90000 +a=fmtp:99 apt=98 +a=rtpmap:35 AV1X/90000 +a=rtcp-fb:35 goog-remb +a=rtcp-fb:35 transport-cc +a=rtcp-fb:35 ccm fir +a=rtcp-fb:35 nack +a=rtcp-fb:35 nack pli +a=rtpmap:36 rtx/90000 +a=fmtp:36 apt=35 +a=rtpmap:100 red/90000 +a=rtpmap:101 rtx/90000 +a=fmtp:101 apt=100 +a=rtpmap:127 ulpfec/90000 +a=ssrc-group:FID 1697527935 3482614104 +a=ssrc:1697527935 cname:kibCM8KmL1PJhPlQ +a=ssrc:1697527935 msid:e53e47c9-d409-4522-bcc9-49c8b1d093b7 49e96524-ed6c-4fa3-9bc3-745d716698c0 +a=ssrc:1697527935 mslabel:e53e47c9-d409-4522-bcc9-49c8b1d093b7 +a=ssrc:1697527935 label:49e96524-ed6c-4fa3-9bc3-745d716698c0 +a=ssrc:3482614104 cname:kibCM8KmL1PJhPlQ +a=ssrc:3482614104 msid:e53e47c9-d409-4522-bcc9-49c8b1d093b7 49e96524-ed6c-4fa3-9bc3-745d716698c0 +a=ssrc:3482614104 mslabel:e53e47c9-d409-4522-bcc9-49c8b1d093b7 +a=ssrc:3482614104 label:49e96524-ed6c-4fa3-9bc3-745d716698c0 \ No newline at end of file diff --git a/lib/webrtc-offers/OfferFromApp b/lib/webrtc-offers/OfferFromApp new file mode 100644 index 00000000..b97f0419 --- /dev/null +++ b/lib/webrtc-offers/OfferFromApp @@ -0,0 +1,98 @@ +v=0 +o=- 1852792980555145345 2 IN IP4 127.0.0.1 +s=- +t=0 0 +a=group:BUNDLE audio video +a=extmap-allow-mixed +a=msid-semantic: WMS c435b864-09cb-4e9b-b467-0215765153b2 +m=audio 9 UDP/TLS/RTP/SAVPF 111 103 104 9 102 0 8 106 105 13 110 112 113 126 +c=IN IP4 0.0.0.0 +a=rtcp:9 IN IP4 0.0.0.0 +a=ice-ufrag:yZwB +a=ice-pwd:xoT7qMaStLtOryw8gBoh9HBE +a=ice-options:trickle +a=fingerprint:sha-256 E6:93:81:34:89:3A:AE:E9:66:B3:B1:BF:81:F4:F9:EF:71:84:41:21:09:98:BC:D2:0C:7F:98:EF:CA:E5:47:3A +a=setup:actpass +a=mid:audio +a=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level +a=extmap:2 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time +a=extmap:3 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01 +a=sendrecv +a=rtcp-mux +a=rtpmap:111 opus/48000/2 +a=rtcp-fb:111 transport-cc +a=fmtp:111 minptime=10;useinbandfec=1 +a=rtpmap:103 ISAC/16000 +a=rtpmap:104 ISAC/32000 +a=rtpmap:9 G722/8000 +a=rtpmap:102 ILBC/8000 +a=rtpmap:0 PCMU/8000 +a=rtpmap:8 PCMA/8000 +a=rtpmap:106 CN/32000 +a=rtpmap:105 CN/16000 +a=rtpmap:13 CN/8000 +a=rtpmap:110 telephone-event/48000 +a=rtpmap:112 telephone-event/32000 +a=rtpmap:113 telephone-event/16000 +a=rtpmap:126 telephone-event/8000 +a=ssrc:467703518 cname:LpqUpo/SZG/oP5nR +a=ssrc:467703518 msid:c435b864-09cb-4e9b-b467-0215765153b2 112bdcb4-6696-4473-921b-54a9b864134e +a=ssrc:467703518 mslabel:c435b864-09cb-4e9b-b467-0215765153b2 +a=ssrc:467703518 label:112bdcb4-6696-4473-921b-54a9b864134e +m=video 9 UDP/TLS/RTP/SAVPF 96 97 98 99 35 36 100 101 127 +c=IN IP4 0.0.0.0 +a=rtcp:9 IN IP4 0.0.0.0 +a=ice-ufrag:mrRm +a=ice-pwd:eQ/xWrHgvgZol4cryWYtf6kM +a=ice-options:trickle renomination +a=fingerprint:sha-256 08:BF:EB:45:0B:49:A0:51:B1:D8:45:4E:BA:03:81:98:95:64:0A:2A:27:0E:B6:2D:2B:E7:9E:C4:0E:A5:53:14 +a=setup:actpass +a=mid:video +a=extmap:14 urn:ietf:params:rtp-hdrext:toffset +a=extmap:2 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time +a=extmap:13 urn:3gpp:video-orientation +a=extmap:3 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01 +a=extmap:5 http://www.webrtc.org/experiments/rtp-hdrext/playout-delay +a=extmap:6 http://www.webrtc.org/experiments/rtp-hdrext/video-content-type +a=extmap:7 http://www.webrtc.org/experiments/rtp-hdrext/video-timing +a=extmap:8 http://www.webrtc.org/experiments/rtp-hdrext/color-space +a=sendrecv +a=rtcp-mux +a=rtcp-rsize +a=rtpmap:96 VP8/90000 +a=rtcp-fb:96 goog-remb +a=rtcp-fb:96 transport-cc +a=rtcp-fb:96 ccm fir +a=rtcp-fb:96 nack +a=rtcp-fb:96 nack pli +a=rtpmap:97 rtx/90000 +a=fmtp:97 apt=96 +a=rtpmap:98 VP9/90000 +a=rtcp-fb:98 goog-remb +a=rtcp-fb:98 transport-cc +a=rtcp-fb:98 ccm fir +a=rtcp-fb:98 nack +a=rtcp-fb:98 nack pli +a=rtpmap:99 rtx/90000 +a=fmtp:99 apt=98 +a=rtpmap:35 AV1X/90000 +a=rtcp-fb:35 goog-remb +a=rtcp-fb:35 transport-cc +a=rtcp-fb:35 ccm fir +a=rtcp-fb:35 nack +a=rtcp-fb:35 nack pli +a=rtpmap:36 rtx/90000 +a=fmtp:36 apt=35 +a=rtpmap:100 red/90000 +a=rtpmap:101 rtx/90000 +a=fmtp:101 apt=100 +a=rtpmap:127 ulpfec/90000 +a=ssrc-group:FID 2760462647 1007249032 +a=ssrc:2760462647 cname:LpqUpo/SZG/oP5nR +a=ssrc:2760462647 msid:c435b864-09cb-4e9b-b467-0215765153b2 8d8b378e-fc04-428d-a4df-50af401e70d2 +a=ssrc:2760462647 mslabel:c435b864-09cb-4e9b-b467-0215765153b2 +a=ssrc:2760462647 label:8d8b378e-fc04-428d-a4df-50af401e70d2 +a=ssrc:1007249032 cname:LpqUpo/SZG/oP5nR +a=ssrc:1007249032 msid:c435b864-09cb-4e9b-b467-0215765153b2 8d8b378e-fc04-428d-a4df-50af401e70d2 +a=ssrc:1007249032 mslabel:c435b864-09cb-4e9b-b467-0215765153b2 +a=ssrc:1007249032 label:8d8b378e-fc04-428d-a4df-50af401e70d2 diff --git a/lib/webrtc-offers/OfferFromMyMobileWeb b/lib/webrtc-offers/OfferFromMyMobileWeb new file mode 100644 index 00000000..8b0f9736 --- /dev/null +++ b/lib/webrtc-offers/OfferFromMyMobileWeb @@ -0,0 +1,109 @@ +v=0 +o=- 3297723553455155496 2 IN IP4 127.0.0.1 +s=- +t=0 0 +a=group:BUNDLE 0 1 +a=extmap-allow-mixed +a=msid-semantic: WMS h3MZrNWyvufOFj9hhh4M1dQjm53cZXMbEKxM +m=audio 9 UDP/TLS/RTP/SAVPF 111 63 103 104 9 0 8 106 105 13 110 112 113 126 +c=IN IP4 0.0.0.0 +a=rtcp:9 IN IP4 0.0.0.0 +a=ice-ufrag:yZwB +a=ice-pwd:xoT7qMaStLtOryw8gBoh9HBE +a=ice-options:trickle +a=fingerprint:sha-256 E6:93:81:34:89:3A:AE:E9:66:B3:B1:BF:81:F4:F9:EF:71:84:41:21:09:98:BC:D2:0C:7F:98:EF:CA:E5:47:3A +a=setup:actpass +a=mid:0 +a=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level +a=extmap:2 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time +a=extmap:3 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01 +a=extmap:4 urn:ietf:params:rtp-hdrext:sdes:mid +a=extmap:5 urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id +a=extmap:6 urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-id +a=sendrecv +a=msid:h3MZrNWyvufOFj9hhh4M1dQjm53cZXMbEKxM ee14b0b6-bef2-4a88-8550-a7120c16eb41 +a=rtcp-mux +a=rtpmap:111 opus/48000/2 +a=rtcp-fb:111 transport-cc +a=fmtp:111 minptime=10;useinbandfec=1 +a=rtpmap:63 red/48000/2 +a=fmtp:63 111/111 +a=rtpmap:103 ISAC/16000 +a=rtpmap:104 ISAC/32000 +a=rtpmap:9 G722/8000 +a=rtpmap:0 PCMU/8000 +a=rtpmap:8 PCMA/8000 +a=rtpmap:106 CN/32000 +a=rtpmap:105 CN/16000 +a=rtpmap:13 CN/8000 +a=rtpmap:110 telephone-event/48000 +a=rtpmap:112 telephone-event/32000 +a=rtpmap:113 telephone-event/16000 +a=rtpmap:126 telephone-event/8000 +a=ssrc:2100092274 cname:dMYQoj76poQISooy +a=ssrc:2100092274 msid:h3MZrNWyvufOFj9hhh4M1dQjm53cZXMbEKxM ee14b0b6-bef2-4a88-8550-a7120c16eb41 +a=ssrc:2100092274 mslabel:h3MZrNWyvufOFj9hhh4M1dQjm53cZXMbEKxM +a=ssrc:2100092274 label:ee14b0b6-bef2-4a88-8550-a7120c16eb41 +m=video 9 UDP/TLS/RTP/SAVPF 96 97 98 99 100 101 122 121 127 +c=IN IP4 0.0.0.0 +a=rtcp:9 IN IP4 0.0.0.0 +a=ice-ufrag:yZwB +a=ice-pwd:xoT7qMaStLtOryw8gBoh9HBE +a=ice-options:trickle +a=fingerprint:sha-256 E6:93:81:34:89:3A:AE:E9:66:B3:B1:BF:81:F4:F9:EF:71:84:41:21:09:98:BC:D2:0C:7F:98:EF:CA:E5:47:3A +a=setup:actpass +a=mid:1 +a=extmap:14 urn:ietf:params:rtp-hdrext:toffset +a=extmap:2 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time +a=extmap:13 urn:3gpp:video-orientation +a=extmap:3 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01 +a=extmap:12 http://www.webrtc.org/experiments/rtp-hdrext/playout-delay +a=extmap:11 http://www.webrtc.org/experiments/rtp-hdrext/video-content-type +a=extmap:7 http://www.webrtc.org/experiments/rtp-hdrext/video-timing +a=extmap:8 http://www.webrtc.org/experiments/rtp-hdrext/color-space +a=extmap:4 urn:ietf:params:rtp-hdrext:sdes:mid +a=extmap:5 urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id +a=extmap:6 urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-id +a=sendrecv +a=msid:h3MZrNWyvufOFj9hhh4M1dQjm53cZXMbEKxM 5c21bd40-52fb-4c67-9bd2-cd5b64dc9de1 +a=rtcp-mux +a=rtcp-rsize +a=rtpmap:96 VP8/90000 +a=rtcp-fb:96 goog-remb +a=rtcp-fb:96 transport-cc +a=rtcp-fb:96 ccm fir +a=rtcp-fb:96 nack +a=rtcp-fb:96 nack pli +a=rtpmap:97 rtx/90000 +a=fmtp:97 apt=96 +a=rtpmap:98 VP9/90000 +a=rtcp-fb:98 goog-remb +a=rtcp-fb:98 transport-cc +a=rtcp-fb:98 ccm fir +a=rtcp-fb:98 nack +a=rtcp-fb:98 nack pli +a=fmtp:98 profile-id=0 +a=rtpmap:99 rtx/90000 +a=fmtp:99 apt=98 +a=rtpmap:100 H264/90000 +a=rtcp-fb:100 goog-remb +a=rtcp-fb:100 transport-cc +a=rtcp-fb:100 ccm fir +a=rtcp-fb:100 nack +a=rtcp-fb:100 nack pli +a=fmtp:100 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f +a=rtpmap:101 rtx/90000 +a=fmtp:101 apt=100 +a=rtpmap:122 red/90000 +a=rtpmap:121 rtx/90000 +a=fmtp:121 apt=122 +a=rtpmap:127 ulpfec/90000 +a=ssrc-group:FID 2717479063 1967595658 +a=ssrc:2717479063 cname:dMYQoj76poQISooy +a=ssrc:2717479063 msid:h3MZrNWyvufOFj9hhh4M1dQjm53cZXMbEKxM 5c21bd40-52fb-4c67-9bd2-cd5b64dc9de1 +a=ssrc:2717479063 mslabel:h3MZrNWyvufOFj9hhh4M1dQjm53cZXMbEKxM +a=ssrc:2717479063 label:5c21bd40-52fb-4c67-9bd2-cd5b64dc9de1 +a=ssrc:1967595658 cname:dMYQoj76poQISooy +a=ssrc:1967595658 msid:h3MZrNWyvufOFj9hhh4M1dQjm53cZXMbEKxM 5c21bd40-52fb-4c67-9bd2-cd5b64dc9de1 +a=ssrc:1967595658 mslabel:h3MZrNWyvufOFj9hhh4M1dQjm53cZXMbEKxM +a=ssrc:1967595658 label:5c21bd40-52fb-4c67-9bd2-cd5b64dc9de1 \ No newline at end of file diff --git a/lib/webrtc-offers/OfferFromPC1 b/lib/webrtc-offers/OfferFromPC1 new file mode 100644 index 00000000..7b461e4c --- /dev/null +++ b/lib/webrtc-offers/OfferFromPC1 @@ -0,0 +1,114 @@ +v=0 +o=- 5927491327118611904 3 IN IP4 127.0.0.1 +s=- +t=0 0 +a=group:BUNDLE audio video +a=extmap-allow-mixed +a=msid-semantic: WMS e53e47c9-d409-4522-bcc9-49c8b1d093b7 +m=audio 49168 UDP/TLS/RTP/SAVPF 111 103 104 9 102 0 8 106 105 13 110 112 113 126 +c=IN IP4 15.185.116.59 +a=rtcp:9 IN IP4 0.0.0.0 +a=candidate:2473295229 1 udp 2122262783 2a02:cb80:4078:40c3:18c0:72ff:fe47:4312 51566 typ host generation 0 network-id 4 network-cost 10 +a=candidate:3370617219 1 udp 2122194687 172.20.10.11 59256 typ host generation 0 network-id 3 network-cost 10 +a=candidate:559267639 1 udp 2122136831 ::1 57659 typ host generation 0 network-id 2 +a=candidate:1510613869 1 udp 2122063615 127.0.0.1 50875 typ host generation 0 network-id 1 +a=candidate:1876313031 1 tcp 1518157055 ::1 39063 typ host tcptype passive generation 0 network-id 2 +a=candidate:344579997 1 tcp 1518083839 127.0.0.1 49823 typ host tcptype passive generation 0 network-id 1 +a=candidate:842163049 1 udp 1685987071 51.36.24.254 1652 typ srflx raddr 172.20.10.11 rport 59256 generation 0 network-id 3 network-cost 10 +a=candidate:3951447094 1 udp 41819903 15.185.116.59 49168 typ relay raddr 51.36.24.254 rport 1652 generation 0 network-id 3 network-cost 10 +a=ice-ufrag:TPqz +a=ice-pwd:Av/F5Hz76zQdybHAG2DomDog +a=ice-options:trickle renomination +a=fingerprint:sha-256 1F:D1:DD:59:42:65:91:E2:41:99:ED:6C:DB:30:76:4A:6B:5A:F9:82:7C:07:29:66:80:DC:AF:E2:34:05:43:18 +a=setup:actpass +a=mid:audio +a=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level +a=extmap:2 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time +a=extmap:3 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01 +a=sendrecv +a=rtcp-mux +a=rtpmap:111 opus/48000/2 +a=rtcp-fb:111 transport-cc +a=fmtp:111 minptime=10;useinbandfec=1 +a=rtpmap:103 ISAC/16000 +a=rtpmap:104 ISAC/32000 +a=rtpmap:9 G722/8000 +a=rtpmap:102 ILBC/8000 +a=rtpmap:0 PCMU/8000 +a=rtpmap:8 PCMA/8000 +a=rtpmap:106 CN/32000 +a=rtpmap:105 CN/16000 +a=rtpmap:13 CN/8000 +a=rtpmap:110 telephone-event/48000 +a=rtpmap:112 telephone-event/32000 +a=rtpmap:113 telephone-event/16000 +a=rtpmap:126 telephone-event/8000 +a=ssrc:3000425778 cname:kibCM8KmL1PJhPlQ +a=ssrc:3000425778 msid:e53e47c9-d409-4522-bcc9-49c8b1d093b7 a82ca475-9a94-4bdc-a91c-1137d80a0753 +a=ssrc:3000425778 mslabel:e53e47c9-d409-4522-bcc9-49c8b1d093b7 +a=ssrc:3000425778 label:a82ca475-9a94-4bdc-a91c-1137d80a0753 +m=video 49057 UDP/TLS/RTP/SAVPF 96 97 98 99 35 36 100 101 127 +c=IN IP4 15.185.116.59 +a=rtcp:9 IN IP4 0.0.0.0 +a=candidate:2473295229 1 udp 2122262783 2a02:cb80:4078:40c3:18c0:72ff:fe47:4312 39324 typ host generation 0 network-id 4 network-cost 10 +a=candidate:3370617219 1 udp 2122194687 172.20.10.11 41527 typ host generation 0 network-id 3 network-cost 10 +a=candidate:559267639 1 udp 2122136831 ::1 37365 typ host generation 0 network-id 2 +a=candidate:1510613869 1 udp 2122063615 127.0.0.1 43321 typ host generation 0 network-id 1 +a=candidate:1876313031 1 tcp 1518157055 ::1 40159 typ host tcptype passive generation 0 network-id 2 +a=candidate:344579997 1 tcp 1518083839 127.0.0.1 36533 typ host tcptype passive generation 0 network-id 1 +a=candidate:842163049 1 udp 1685987071 51.36.24.254 1653 typ srflx raddr 172.20.10.11 rport 41527 generation 0 network-id 3 network-cost 10 +a=candidate:3951447094 1 udp 41819903 15.185.116.59 49057 typ relay raddr 51.36.24.254 rport 1653 generation 0 network-id 3 network-cost 10 +a=ice-ufrag:TPqz +a=ice-pwd:Av/F5Hz76zQdybHAG2DomDog +a=ice-options:trickle renomination +a=fingerprint:sha-256 1F:D1:DD:59:42:65:91:E2:41:99:ED:6C:DB:30:76:4A:6B:5A:F9:82:7C:07:29:66:80:DC:AF:E2:34:05:43:18 +a=setup:actpass +a=mid:video +a=extmap:14 urn:ietf:params:rtp-hdrext:toffset +a=extmap:2 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time +a=extmap:13 urn:3gpp:video-orientation +a=extmap:3 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01 +a=extmap:5 http://www.webrtc.org/experiments/rtp-hdrext/playout-delay +a=extmap:6 http://www.webrtc.org/experiments/rtp-hdrext/video-content-type +a=extmap:7 http://www.webrtc.org/experiments/rtp-hdrext/video-timing +a=extmap:8 http://www.webrtc.org/experiments/rtp-hdrext/color-space +a=sendrecv +a=rtcp-mux +a=rtcp-rsize +a=rtpmap:96 VP8/90000 +a=rtcp-fb:96 goog-remb +a=rtcp-fb:96 transport-cc +a=rtcp-fb:96 ccm fir +a=rtcp-fb:96 nack +a=rtcp-fb:96 nack pli +a=rtpmap:97 rtx/90000 +a=fmtp:97 apt=96 +a=rtpmap:98 VP9/90000 +a=rtcp-fb:98 goog-remb +a=rtcp-fb:98 transport-cc +a=rtcp-fb:98 ccm fir +a=rtcp-fb:98 nack +a=rtcp-fb:98 nack pli +a=rtpmap:99 rtx/90000 +a=fmtp:99 apt=98 +a=rtpmap:35 AV1X/90000 +a=rtcp-fb:35 goog-remb +a=rtcp-fb:35 transport-cc +a=rtcp-fb:35 ccm fir +a=rtcp-fb:35 nack +a=rtcp-fb:35 nack pli +a=rtpmap:36 rtx/90000 +a=fmtp:36 apt=35 +a=rtpmap:100 red/90000 +a=rtpmap:101 rtx/90000 +a=fmtp:101 apt=100 +a=rtpmap:127 ulpfec/90000 +a=ssrc-group:FID 1697527935 3482614104 +a=ssrc:1697527935 cname:kibCM8KmL1PJhPlQ +a=ssrc:1697527935 msid:e53e47c9-d409-4522-bcc9-49c8b1d093b7 49e96524-ed6c-4fa3-9bc3-745d716698c0 +a=ssrc:1697527935 mslabel:e53e47c9-d409-4522-bcc9-49c8b1d093b7 +a=ssrc:1697527935 label:49e96524-ed6c-4fa3-9bc3-745d716698c0 +a=ssrc:3482614104 cname:kibCM8KmL1PJhPlQ +a=ssrc:3482614104 msid:e53e47c9-d409-4522-bcc9-49c8b1d093b7 49e96524-ed6c-4fa3-9bc3-745d716698c0 +a=ssrc:3482614104 mslabel:e53e47c9-d409-4522-bcc9-49c8b1d093b7 +a=ssrc:3482614104 label:49e96524-ed6c-4fa3-9bc3-745d716698c0 \ No newline at end of file diff --git a/lib/webrtc-offers/OfferFromPC2 b/lib/webrtc-offers/OfferFromPC2 new file mode 100644 index 00000000..5a2f7350 --- /dev/null +++ b/lib/webrtc-offers/OfferFromPC2 @@ -0,0 +1,110 @@ +v=0 +o=- 1852792980555145345 3 IN IP4 127.0.0.1 +s=- +t=0 0 +a=group:BUNDLE audio video +a=extmap-allow-mixed +a=msid-semantic: WMS c435b864-09cb-4e9b-b467-0215765153b2 +m=audio 2488 UDP/TLS/RTP/SAVPF 111 103 104 9 102 0 8 106 105 13 110 112 113 126 +c=IN IP4 51.39.198.194 +a=rtcp:9 IN IP4 0.0.0.0 +a=candidate:2080483951 1 udp 2122260223 192.168.8.146 53264 typ host generation 0 network-id 3 network-cost 10 +a=candidate:559267639 1 udp 2122202367 ::1 58708 typ host generation 0 network-id 2 +a=candidate:1510613869 1 udp 2122129151 127.0.0.1 41026 typ host generation 0 network-id 1 +a=candidate:1876313031 1 tcp 1518222591 ::1 40989 typ host tcptype passive generation 0 network-id 2 +a=candidate:344579997 1 tcp 1518149375 127.0.0.1 58889 typ host tcptype passive generation 0 network-id 1 +a=candidate:842163049 1 udp 1686052607 51.39.198.194 2488 typ srflx raddr 192.168.8.146 rport 53264 generation 0 network-id 3 network-cost 10 +a=ice-ufrag:mrRm +a=ice-pwd:eQ/xWrHgvgZol4cryWYtf6kM +a=ice-options:trickle renomination +a=fingerprint:sha-256 08:BF:EB:45:0B:49:A0:51:B1:D8:45:4E:BA:03:81:98:95:64:0A:2A:27:0E:B6:2D:2B:E7:9E:C4:0E:A5:53:14 +a=setup:actpass +a=mid:audio +a=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level +a=extmap:2 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time +a=extmap:3 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01 +a=sendrecv +a=rtcp-mux +a=rtpmap:111 opus/48000/2 +a=rtcp-fb:111 transport-cc +a=fmtp:111 minptime=10;useinbandfec=1 +a=rtpmap:103 ISAC/16000 +a=rtpmap:104 ISAC/32000 +a=rtpmap:9 G722/8000 +a=rtpmap:102 ILBC/8000 +a=rtpmap:0 PCMU/8000 +a=rtpmap:8 PCMA/8000 +a=rtpmap:106 CN/32000 +a=rtpmap:105 CN/16000 +a=rtpmap:13 CN/8000 +a=rtpmap:110 telephone-event/48000 +a=rtpmap:112 telephone-event/32000 +a=rtpmap:113 telephone-event/16000 +a=rtpmap:126 telephone-event/8000 +a=ssrc:467703518 cname:LpqUpo/SZG/oP5nR +a=ssrc:467703518 msid:c435b864-09cb-4e9b-b467-0215765153b2 112bdcb4-6696-4473-921b-54a9b864134e +a=ssrc:467703518 mslabel:c435b864-09cb-4e9b-b467-0215765153b2 +a=ssrc:467703518 label:112bdcb4-6696-4473-921b-54a9b864134e +m=video 2485 UDP/TLS/RTP/SAVPF 96 97 98 99 35 36 100 101 127 +c=IN IP4 51.39.198.194 +a=rtcp:9 IN IP4 0.0.0.0 +a=candidate:2080483951 1 udp 2122260223 192.168.8.146 33465 typ host generation 0 network-id 3 network-cost 10 +a=candidate:559267639 1 udp 2122202367 ::1 33641 typ host generation 0 network-id 2 +a=candidate:1510613869 1 udp 2122129151 127.0.0.1 41957 typ host generation 0 network-id 1 +a=candidate:1876313031 1 tcp 1518222591 ::1 46553 typ host tcptype passive generation 0 network-id 2 +a=candidate:344579997 1 tcp 1518149375 127.0.0.1 41591 typ host tcptype passive generation 0 network-id 1 +a=candidate:842163049 1 udp 1686052607 51.39.198.194 2485 typ srflx raddr 192.168.8.146 rport 33465 generation 0 network-id 3 network-cost 10 +a=ice-ufrag:mrRm +a=ice-pwd:eQ/xWrHgvgZol4cryWYtf6kM +a=ice-options:trickle renomination +a=fingerprint:sha-256 08:BF:EB:45:0B:49:A0:51:B1:D8:45:4E:BA:03:81:98:95:64:0A:2A:27:0E:B6:2D:2B:E7:9E:C4:0E:A5:53:14 +a=setup:actpass +a=mid:video +a=extmap:14 urn:ietf:params:rtp-hdrext:toffset +a=extmap:2 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time +a=extmap:13 urn:3gpp:video-orientation +a=extmap:3 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01 +a=extmap:5 http://www.webrtc.org/experiments/rtp-hdrext/playout-delay +a=extmap:6 http://www.webrtc.org/experiments/rtp-hdrext/video-content-type +a=extmap:7 http://www.webrtc.org/experiments/rtp-hdrext/video-timing +a=extmap:8 http://www.webrtc.org/experiments/rtp-hdrext/color-space +a=sendrecv +a=rtcp-mux +a=rtcp-rsize +a=rtpmap:96 VP8/90000 +a=rtcp-fb:96 goog-remb +a=rtcp-fb:96 transport-cc +a=rtcp-fb:96 ccm fir +a=rtcp-fb:96 nack +a=rtcp-fb:96 nack pli +a=rtpmap:97 rtx/90000 +a=fmtp:97 apt=96 +a=rtpmap:98 VP9/90000 +a=rtcp-fb:98 goog-remb +a=rtcp-fb:98 transport-cc +a=rtcp-fb:98 ccm fir +a=rtcp-fb:98 nack +a=rtcp-fb:98 nack pli +a=rtpmap:99 rtx/90000 +a=fmtp:99 apt=98 +a=rtpmap:35 AV1X/90000 +a=rtcp-fb:35 goog-remb +a=rtcp-fb:35 transport-cc +a=rtcp-fb:35 ccm fir +a=rtcp-fb:35 nack +a=rtcp-fb:35 nack pli +a=rtpmap:36 rtx/90000 +a=fmtp:36 apt=35 +a=rtpmap:100 red/90000 +a=rtpmap:101 rtx/90000 +a=fmtp:101 apt=100 +a=rtpmap:127 ulpfec/90000 +a=ssrc-group:FID 2760462647 1007249032 +a=ssrc:2760462647 cname:LpqUpo/SZG/oP5nR +a=ssrc:2760462647 msid:c435b864-09cb-4e9b-b467-0215765153b2 8d8b378e-fc04-428d-a4df-50af401e70d2 +a=ssrc:2760462647 mslabel:c435b864-09cb-4e9b-b467-0215765153b2 +a=ssrc:2760462647 label:8d8b378e-fc04-428d-a4df-50af401e70d2 +a=ssrc:1007249032 cname:LpqUpo/SZG/oP5nR +a=ssrc:1007249032 msid:c435b864-09cb-4e9b-b467-0215765153b2 8d8b378e-fc04-428d-a4df-50af401e70d2 +a=ssrc:1007249032 mslabel:c435b864-09cb-4e9b-b467-0215765153b2 +a=ssrc:1007249032 label:8d8b378e-fc04-428d-a4df-50af401e70d2 \ No newline at end of file diff --git a/pubspec.yaml b/pubspec.yaml index c5b14f46..c496a084 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -112,6 +112,13 @@ dependencies: # hijri: ^2.0.3 hijri_picker: ^3.0.0 + #Dependencies for video call implementation + signalr_core: ^1.1.1 + flutter_webrtc: ^0.8.0 + native_device_orientation: ^1.0.0 + wakelock: ^0.5.6 + after_layout: ^1.1.0 + # flutter_math_fork: ^0.6.0 # flutter_math_fork: ^0.6.0 @@ -120,8 +127,8 @@ dependencies: dev_dependencies: flutter_test: sdk: flutter - build_runner: any - build_web_compilers: any +## build_runner: any +# build_web_compilers: any # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec diff --git a/speech_to_text/android/.settings/org.eclipse.buildship.core.prefs b/speech_to_text/android/.settings/org.eclipse.buildship.core.prefs index 7a23d112..0c669305 100644 --- a/speech_to_text/android/.settings/org.eclipse.buildship.core.prefs +++ b/speech_to_text/android/.settings/org.eclipse.buildship.core.prefs @@ -1,11 +1,11 @@ arguments= auto.sync=false build.scans.enabled=false -connection.gradle.distribution=GRADLE_DISTRIBUTION(VERSION(5.6.1)) +connection.gradle.distribution=GRADLE_DISTRIBUTION(VERSION(7.4.2)) connection.project.dir= eclipse.preferences.version=1 gradle.user.home= -java.home= +java.home=/Library/Java/JavaVirtualMachines/jdk1.8.0_281.jdk/Contents/Home jvm.arguments= offline.mode=false override.workspace.settings=true