diff --git a/lib/pages/conference/conference_page.dart b/lib/pages/conference/conference_page.dart index 7848d4f7..dc00bf6e 100644 --- a/lib/pages/conference/conference_page.dart +++ b/lib/pages/conference/conference_page.dart @@ -101,7 +101,7 @@ class _ConferencePageState extends State { return WillPopScope( onWillPop: () async => false, child: Scaffold( - backgroundColor: Colors.black, + backgroundColor: Colors.white, body: _conferenceRoom == null ? showProgress() : buildLayout(), ), ); diff --git a/lib/pages/conference/web_rtc/call_home_page.dart b/lib/pages/conference/web_rtc/call_home_page.dart new file mode 100644 index 00000000..7153bf92 --- /dev/null +++ b/lib/pages/conference/web_rtc/call_home_page.dart @@ -0,0 +1,172 @@ +import 'dart:async'; + +import 'package:diplomaticquarterapp/pages/conference/web_rtc/widgets/cam_view_widget.dart'; +import 'package:diplomaticquarterapp/pages/conference/widgets/noise_box.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 { + @override + _CallHomePageState createState() => _CallHomePageState(); +} + +class _CallHomePageState extends State { + bool showNoise = false; + 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 + MediaStream stream; + + @override + void initState() { + // TODO: implement initState + super.initState(); + _localRenderer.initialize(); + _remoteRenderer.initialize(); + + enableVideo(); + } + + enableVideo() async { + //Stream to enable video + stream = await navigator.mediaDevices.getUserMedia({'video': true, 'audio': true}); + // _audioButton.add(false); + } + + @override + void dispose() { + // TODO: implement dispose + super.dispose(); + _localRenderer.dispose(); + _remoteRenderer.dispose(); + _audioButton.close(); + _videoButton.close(); + stream.dispose(); + _disposeStreamsAndSubscriptions(); + } + + Future _disposeStreamsAndSubscriptions() async { + if (_onButtonBarVisibleStreamController != null) await _onButtonBarVisibleStreamController.close(); + if (_onButtonBarHeightStreamController != null) await _onButtonBarHeightStreamController.close(); + } + + @override + Widget build(BuildContext context) { + // return WillPopScope( + // onWillPop: () async => false, + // child: Scaffold( + // backgroundColor: Colors.black, + // // body: , + // ), + // ); + + return Scaffold( + backgroundColor: Colors.white, + body: showNoise ? _buildNoiseBox() : buildLayout(), + ); + } + + LayoutBuilder buildLayout() { + return LayoutBuilder( + builder: (BuildContext context, BoxConstraints constraints) { + return Stack( + children: [ + CamViewWidget( + localRenderer: _localRenderer, + remoteRenderer: _remoteRenderer, + stream: stream, + 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, + ), + ], + ); + }, + ); + } + + 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), + ), + ), + ), + ), + ); + } + + Function _onAudioEnable() { + bool enabled = stream.getAudioTracks()[0].enabled; + stream.getAudioTracks()[0].enabled = !enabled; + _audioButton.add(!enabled); + } + + Function _onVideoEnabled() { + bool enabled = stream.getVideoTracks()[0].enabled; + stream.getVideoTracks()[0].enabled = !enabled; + _videoButton.add(!enabled); + } + + Function _onSwitchCamera() { + // stream.getAudioTracks()[0].enabled = false; + // stream.getVideoTracks()[0].enabled = false; + Helper.switchCamera(stream.getVideoTracks()[0]); + } + + void _onShowBar() { + setState(() { + SystemChrome.setEnabledSystemUIOverlays([SystemUiOverlay.bottom, SystemUiOverlay.top]); + }); + _onButtonBarVisibleStreamController.add(true); + } + + void _onHeightBar(double height) { + _onButtonBarHeightStreamController.add(height); + } + + void _onHideBar() { + setState(() { + SystemChrome.setEnabledSystemUIOverlays([SystemUiOverlay.bottom]); + }); + _onButtonBarVisibleStreamController.add(false); + } + + Future _onHangup() async { + print('onHangup'); + Navigator.of(context).pop(); + } +} diff --git a/lib/pages/conference/web_rtc/widgets/cam_view_widget.dart b/lib/pages/conference/web_rtc/widgets/cam_view_widget.dart new file mode 100644 index 00000000..3db0c74b --- /dev/null +++ b/lib/pages/conference/web_rtc/widgets/cam_view_widget.dart @@ -0,0 +1,61 @@ +import 'dart:async'; +import 'dart:core'; + +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 stream; + BoxConstraints constraints; + StreamController onButtonBarVisibleStreamController; + StreamController onButtonBarHeightStreamController; + + CamViewWidget({this.localRenderer, this.remoteRenderer, this.stream, this.constraints, this.onButtonBarVisibleStreamController, this.onButtonBarHeightStreamController}); + + @override + _CamViewWidgetState createState() => _CamViewWidgetState(); +} + +class _CamViewWidgetState extends State { + @override + void initState() { + super.initState(); + Future.delayed(const Duration(milliseconds: 300), () { + showCamera(); + }); + } + + showCamera() async { + setState(() async { + widget.localRenderer.srcObject = widget.stream; + widget.remoteRenderer.srcObject = widget.stream; + }); + } + + @override + Widget build(BuildContext context) { + return Container( + width: double.infinity, + height: double.infinity, + child: Stack( + children: [ + Container( + child: RTCVideoView(widget.localRenderer, mirror: true), + ), + DraggableCam( + key: Key('publisher'), + onButtonBarHeight: widget.onButtonBarHeightStreamController.stream, + onButtonBarVisible: widget.onButtonBarVisibleStreamController.stream, + availableScreenSize: widget.constraints.biggest, + child: RTCVideoView(widget.remoteRenderer), + ), + // Expanded(child: RTCVideoView(widget.remoteRenderer)), + ], + ), + ); + } +} diff --git a/lib/pages/conference/web_rtc/widgets/draggable_cam.dart b/lib/pages/conference/web_rtc/widgets/draggable_cam.dart new file mode 100644 index 00000000..efc59301 --- /dev/null +++ b/lib/pages/conference/web_rtc/widgets/draggable_cam.dart @@ -0,0 +1,173 @@ +import 'dart:async'; +import 'dart:io'; + +import 'package:diplomaticquarterapp/pages/conference/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, + 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; + double _width; + double _height; + double _top; + double _left; + double _viewPaddingTop; + double _viewPaddingBottom; + final double _padding = 8.0; + final Duration _duration300ms = const Duration(milliseconds: 300); + final Duration _duration0ms = const Duration(milliseconds: 0); + Duration _duration; + StreamSubscription _streamSubscription; + 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/pages/landing/fragments/home_page_fragment2.dart b/lib/pages/landing/fragments/home_page_fragment2.dart index 08509085..d9440990 100644 --- a/lib/pages/landing/fragments/home_page_fragment2.dart +++ b/lib/pages/landing/fragments/home_page_fragment2.dart @@ -22,6 +22,7 @@ import 'package:diplomaticquarterapp/widgets/transitions/fade_page.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:provider/provider.dart'; +import 'package:diplomaticquarterapp/pages/conference/web_rtc/call_home_page.dart'; import 'dart:math' as math; class HomePageFragment2 extends StatefulWidget { @@ -95,7 +96,8 @@ class _HomePageFragment2State extends State { margin: EdgeInsets.only(left: 20, right: 20, top: 8, bottom: 6), child: SliderView( onLoginClick: () { - widget.onLoginClick(); + // widget.onLoginClick(); + navigateTo(context, CallHomePage()); }, ), // height: MediaQuery.of(context).size.width / 2.6, diff --git a/lib/pages/livecare/incoming_call.dart b/lib/pages/livecare/incoming_call.dart index de80b4ae..9e164c09 100644 --- a/lib/pages/livecare/incoming_call.dart +++ b/lib/pages/livecare/incoming_call.dart @@ -1,6 +1,7 @@ import 'package:diplomaticquarterapp/models/LiveCare/IncomingCallData.dart'; import 'package:diplomaticquarterapp/models/LiveCare/room_model.dart'; import 'package:diplomaticquarterapp/pages/conference/conference_page.dart'; +import 'package:diplomaticquarterapp/pages/conference/web_rtc/call_home_page.dart'; import 'package:diplomaticquarterapp/pages/conference/widgets/platform_exception_alert_dialog.dart'; import 'package:diplomaticquarterapp/widgets/others/app_scaffold_widget.dart'; import 'package:flutter/material.dart'; @@ -15,16 +16,14 @@ class IncomingCall extends StatefulWidget { _IncomingCallState createState() => _IncomingCallState(); } -class _IncomingCallState extends State - with SingleTickerProviderStateMixin { +class _IncomingCallState extends State with SingleTickerProviderStateMixin { AnimationController _animationController; final player = AudioPlayer(); @override void initState() { - _animationController = - AnimationController(vsync: this, duration: Duration(milliseconds: 500)); + _animationController = AnimationController(vsync: this, duration: Duration(milliseconds: 500)); WidgetsBinding.instance.addPostFrameCallback((_) => _runAnimation()); @@ -57,18 +56,12 @@ class _IncomingCallState extends State Container( margin: EdgeInsets.only(top: 30.0), alignment: Alignment.center, - child: Text("Incoming Video Call", - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 26.0, - color: Colors.white, - letterSpacing: 1.0)), + child: Text("Incoming Video Call", textAlign: TextAlign.center, style: TextStyle(fontSize: 26.0, color: Colors.white, letterSpacing: 1.0)), ), Container( alignment: Alignment.center, margin: EdgeInsets.fromLTRB(50.0, 30.0, 50.0, 20.0), - child: Image.asset( - 'assets/images/new-design/hmg_full_logo_hd_white.png'), + child: Image.asset('assets/images/new-design/hmg_full_logo_hd_white.png'), ), Container( margin: EdgeInsets.fromLTRB(30.0, 10.0, 30.0, 0.0), @@ -80,33 +73,17 @@ class _IncomingCallState extends State Container( margin: EdgeInsets.only(top: 20.0), alignment: Alignment.center, - child: Text("Dr Eyad Ismail Abu Jayab", - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 22.0, - fontWeight: FontWeight.bold, - letterSpacing: 0.8, - color: Colors.white)), + child: Text("Dr Eyad Ismail Abu Jayab", textAlign: TextAlign.center, style: TextStyle(fontSize: 22.0, fontWeight: FontWeight.bold, letterSpacing: 0.8, color: Colors.white)), ), Container( margin: EdgeInsets.only(top: 10.0), alignment: Alignment.center, - child: Text("ENT Clinic", - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 22.0, - letterSpacing: 0.8, - color: Colors.white)), + child: Text("ENT Clinic", textAlign: TextAlign.center, style: TextStyle(fontSize: 22.0, letterSpacing: 0.8, color: Colors.white)), ), Container( margin: EdgeInsets.only(top: 10.0), alignment: Alignment.center, - child: Text("Speciality", - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 22.0, - letterSpacing: 0.8, - color: Colors.white)), + child: Text("Speciality", textAlign: TextAlign.center, style: TextStyle(fontSize: 22.0, letterSpacing: 0.8, color: Colors.white)), ), Container( decoration: BoxDecoration( @@ -117,30 +94,14 @@ class _IncomingCallState extends State margin: EdgeInsets.only(top: 20.0), child: Column( children: [ - Text("Appointment Information", - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 20.0, - fontWeight: FontWeight.bold, - letterSpacing: 1.0, - color: Colors.white)), + Text("Appointment Information", textAlign: TextAlign.center, style: TextStyle(fontSize: 20.0, fontWeight: FontWeight.bold, letterSpacing: 1.0, color: Colors.white)), Container( margin: EdgeInsets.only(top: 20.0), - child: Text("Sun, 15th Dec, 2019, 09:00", - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 20.0, - letterSpacing: 1.0, - color: Colors.white)), + child: Text("Sun, 15th Dec, 2019, 09:00", textAlign: TextAlign.center, style: TextStyle(fontSize: 20.0, letterSpacing: 1.0, color: Colors.white)), ), Container( margin: EdgeInsets.only(top: 20.0), - child: Text("ENT Clinic", - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 20.0, - letterSpacing: 1.0, - color: Colors.white)), + child: Text("ENT Clinic", textAlign: TextAlign.center, style: TextStyle(fontSize: 20.0, letterSpacing: 1.0, color: Colors.white)), ), ], ), @@ -153,9 +114,7 @@ class _IncomingCallState extends State mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ RotationTransition( - turns: Tween(begin: 0.0, end: -.1) - .chain(CurveTween(curve: Curves.elasticIn)) - .animate(_animationController), + turns: Tween(begin: 0.0, end: -.1).chain(CurveTween(curve: Curves.elasticIn)).animate(_animationController), child: Container( child: RawMaterialButton( onPressed: () { @@ -208,16 +167,19 @@ class _IncomingCallState extends State Future _submit() async { backToHome(); try { - final roomModel = RoomModel( - name: widget.incomingCallData.name, - token: widget.incomingCallData.sessionId, - identity: widget.incomingCallData.identity); - + final roomModel = RoomModel(name: widget.incomingCallData.name, token: widget.incomingCallData.sessionId, identity: widget.incomingCallData.identity); + + // await Navigator.of(context).push( + // MaterialPageRoute( + // fullscreenDialog: true, + // builder: (BuildContext context) => + // ConferencePage(roomModel: roomModel), + // ), + // ); await Navigator.of(context).push( MaterialPageRoute( fullscreenDialog: true, - builder: (BuildContext context) => - ConferencePage(roomModel: roomModel), + builder: (BuildContext context) => CallHomePage(), ), ); } catch (err) { diff --git a/pubspec.yaml b/pubspec.yaml index 0d6b4756..bf0e4465 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -71,6 +71,7 @@ dependencies: #GIF image flutter_gifimage: ^1.0.1 + flutter_webrtc: any # UI Reqs dotted_border: 1.0.5