diff --git a/lib/ui/screens/call_screen.dart b/lib/ui/screens/call_screen.dart index 285f6a2..edfc303 100644 --- a/lib/ui/screens/call_screen.dart +++ b/lib/ui/screens/call_screen.dart @@ -45,6 +45,10 @@ class _CallScreenState extends SafeState { UsersList _usersList; final _peersConnections = Map(); final _renderers = Map(); + MediaStream _localStream; + + bool get isStreamingVideo => + _localStream != null && _localStream.getVideoTracks().length > 0; @override void initState() { @@ -130,12 +134,15 @@ class _CallScreenState extends SafeState { /// Do clean up operations when call screen is destroyed void _endCall() async { try { - // Leave the call - await CallsHelper.leave(convID); - // Close all ready members for (final member in _membersList.readyPeers) await _removeMember(member.userID); + + // Close local stream + await _stopStreaming(); + + // Leave the call + await CallsHelper.leave(convID); } catch (e, stack) { print("Could not end call properly! $e\n$stack"); } @@ -151,6 +158,53 @@ class _CallScreenState extends SafeState { MainController.of(context).popPage(); } + /// Start streaming on our end + Future _startStreaming(bool includeVideo) async { + try { + await _stopStreaming(); + + // Request user media + _localStream = await navigator.getUserMedia({ + "audio": true, + "video": !includeVideo + ? false + : { + "mandatory": { + "maxWidth": '320', + "maxHeight": '240', + "minFrameRate": '24', + }, + "facingMode": "user", + "optional": [], + } + }); + + // Start renderer + _renderers[userID()] = RTCVideoRenderer(); + await _renderers[userID()].initialize(); + _renderers[userID()].srcObject = _localStream; + setState(() {}); + } catch (e, stack) { + print("Could not start streaming! $e\n$stack"); + showSimpleSnack(context, tr("Could not start streaming!")); + } + } + + /// Stop local streaming + Future _stopStreaming() async { + // Stop local stream + if (_localStream != null) { + await _localStream.dispose(); + _localStream = null; + } + + // Close renderer + if (_renderers.containsKey(userID())) { + await _renderers[userID()].dispose(); + _renderers.remove(userID()); + } + } + /// Call this when a user started to stream media Future _memberReady(int memberID) async { try { @@ -170,7 +224,8 @@ class _CallScreenState extends SafeState { _peersConnections[memberID] = peerConnection; // Create a renderer - _renderers[memberID] = RTCVideoRenderer()..initialize(); + _renderers[memberID] = RTCVideoRenderer() + ..initialize(); // TODO Use await instead for initialize // Register callbacks peerConnection.onIceCandidate = @@ -305,10 +360,19 @@ class _CallScreenState extends SafeState { /// Videos area Widget _buildVideosArea() { return Expanded( - child: Column( - children: _membersList.readyPeers - .map((f) => _buildMemberVideo(f.userID)) - .toList(), + child: Stack( + fit: StackFit.expand, + children: [ + // Remove peers videos + Column( + children: _membersList.readyPeers + .map((f) => _buildMemberVideo(f.userID)) + .toList(), + ), + + // Local peer video + isStreamingVideo ? _buildLocalVideo() : Container(), + ], )); } @@ -316,6 +380,16 @@ class _CallScreenState extends SafeState { return Expanded(child: RTCVideoView(_renderers[peerID])); } + Widget _buildLocalVideo() { + return Positioned( + child: RTCVideoView(_renderers[userID()]), + height: 50, + width: 50, + right: 10, + bottom: 10, + ); + } + /// Footer area Widget _buildFooterArea() { return Material( @@ -323,10 +397,28 @@ class _CallScreenState extends SafeState { child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ + // Toggle audio button + Expanded( + child: IconButton( + icon: Icon(Icons.mic_off), + onPressed: () => _startStreaming(false), + ), + ), + // Hang up call - IconButton( - icon: Icon(Icons.phone, color: Colors.red), - onPressed: () => _leaveCall(), + Expanded( + child: IconButton( + icon: Icon(Icons.phone, color: Colors.red), + onPressed: () => _leaveCall(), + ), + ), + + // Toggle video button + Expanded( + child: IconButton( + icon: Icon(Icons.videocam_off), + onPressed: () => _startStreaming(true), + ), ), ], ),