import 'package:comunic/helpers/calls_helper.dart'; import 'package:comunic/helpers/conversations_helper.dart'; import 'package:comunic/helpers/events_helper.dart'; import 'package:comunic/helpers/users_helper.dart'; import 'package:comunic/lists/call_members_list.dart'; import 'package:comunic/lists/users_list.dart'; import 'package:comunic/models/call_config.dart'; import 'package:comunic/models/call_member.dart'; import 'package:comunic/models/conversation.dart'; import 'package:comunic/ui/routes/main_route.dart'; import 'package:comunic/ui/widgets/safe_state.dart'; import 'package:comunic/utils/account_utils.dart'; import 'package:comunic/utils/intl_utils.dart'; import 'package:comunic/utils/ui_utils.dart'; import 'package:flutter/material.dart'; import 'package:flutter_webrtc/rtc_peerconnection.dart'; import 'package:flutter_webrtc/webrtc.dart'; /// Call screen /// /// @author Pierre Hubert class CallScreen extends StatefulWidget { final int convID; const CallScreen({Key key, @required this.convID}) : assert(convID != null), assert(convID > 0), super(key: key); @override _CallScreenState createState() => _CallScreenState(); } class _CallScreenState extends SafeState { // Widget properties int get convID => widget.convID; // State properties Conversation _conversation; String _convName; CallConfig _conf; var _error = false; CallMembersList _membersList; UsersList _usersList; final _peersConnections = Map(); @override void initState() { super.initState(); _initCall(); } @override void dispose() { super.dispose(); _endCall(); } void _initCall() async { try { setState(() => _error = false); // First, load information about the conversation _conversation = await ConversationsHelper().getSingleOrThrow(convID); _convName = await ConversationsHelper.getConversationNameAsync(_conversation); assert(_convName != null); setState(() {}); // Join the call await CallsHelper.join(convID); // Get call configuration _conf = await CallsHelper.getConfig(); // Get current members of the call final membersList = await CallsHelper.getMembers(convID); membersList.removeUser(userID()); _usersList = await UsersHelper().getListWithThrow(membersList.usersID); _membersList = membersList; setState(() {}); // Register to events this.listenChangeState((e) { // TODO : get user information if required if (e.callID == convID) _membersList.add(CallMember(userID: e.userID)); }); this.listen((e) { if (e.callID == convID) _removeMember(e.userID); }); this.listen((e) { if (e.callID == convID) _newSignal(e); }); this.listen((e) { if (e.callID == convID) _memberReady(e.peerID); }); this.listen((e) { if (e.callID == convID) _removeRemotePeerConnection(e.peerID); }); this.listen((e) { if (e.callID == convID) _leaveCall(needConfirm: false); }); // Connect to ready peers for (final peer in _membersList.readyPeers) await this._memberReady(peer.userID); } catch (e, stack) { print("Could not initialize call! $e\n$stack"); setState(() => _error = true); } } /// Do clean up operations when call screen is destroyed void _endCall() async { try { // Leave the call await CallsHelper.leave(convID); } catch (e, stack) { print("Could not end call properly! $e\n$stack"); } } /// Make us leave the call void _leaveCall({bool needConfirm = true}) async { if (needConfirm && !await showConfirmDialog( context: context, message: tr("Do you really want to leave this call ?"))) return; MainController.of(context).popPage(); } /// Call this when a user started to stream media Future _memberReady(int memberID) async { try { _membersList.getUser(memberID).status = MemberStatus.READY; setState(() {}); // Create peer connection final peerConnection = await createPeerConnection(_conf.pluginConfig, { "mandatory": { "OfferToReceiveAudio": true, "OfferToReceiveVideo": _conversation.callCapabilities == CallCapabilities.VIDEO, }, "optional": [], }); _peersConnections[memberID] = peerConnection; // Request an offer to establish a peer connection await CallsHelper.requestOffer(convID, memberID); } catch (e, stack) { print("Could not connect to remote peer $e\n$stack!"); showSimpleSnack(context, tr("Could not connect to a remote peer!")); } } /// Call this method each time we get a new signal void _newSignal(NewCallSignalEvent ev) async { try { // Check if we can process this message if (!_peersConnections.containsKey(ev.peerID)) { print( "Could not process a signal for the connection with peer ${ev.peerID}!"); return; } // Check the kind of signal // SessionDescription if (ev.sessionDescription != null) { await _peersConnections[ev.peerID] .setRemoteDescription(ev.sessionDescription); // Send answer if required if (ev.sessionDescription.type == "offer") { final answer = await _peersConnections[ev.peerID].createAnswer({}); //TODO : Send answer back to server print("ANSWER TO SEND ${answer.toMap()}"); } } // Ice Candidate else { await _peersConnections[ev.peerID].addCandidate(ev.candidate); } } catch (e, stack) { print("Error while handling new signal ! $e\n$stack"); showSimpleSnack(context, tr("Error while processing new signal!")); } } /// Call this when a user has interrupted streaming void _removeRemotePeerConnection(int memberID) { _membersList.getUser(memberID).status = MemberStatus.JOINED; setState(() {}); } /// Call this when a members has completely left the call void _removeMember(int memberID) { _removeRemotePeerConnection(memberID); _membersList.removeUser(memberID); setState(() {}); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( leading: IconButton( icon: Icon(Icons.arrow_back), onPressed: () => _leaveCall(), ), title: _convName == null ? CircularProgressIndicator() : Text(_convName), ), body: _buildBody(), ); } /// Build widget body Widget _buildBody() { // Handle errors if (_error) return buildErrorCard(tr("Could not initialize call!"), actions: [ MaterialButton( onPressed: () => _initCall(), child: Text(tr("Try again").toUpperCase()), ) ]); // Check if are not ready to show call UI if (_membersList == null) return buildCenteredProgressBar(); return Column( children: [_buildMembersArea(), Spacer(), _buildFooterArea()], ); } /// Build members area Widget _buildMembersArea() { return Center( child: Padding( padding: const EdgeInsets.all(8.0), child: RichText( text: TextSpan( children: _membersList .map((f) => TextSpan( text: _usersList.getUser(f.userID).displayName + " ", style: TextStyle( color: f.status == MemberStatus.JOINED ? null : Colors.green))) .toList())), ), ); } /// Footer area Widget _buildFooterArea() { return Material( color: Colors.black, child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ // Hang up call IconButton( icon: Icon(Icons.phone, color: Colors.red), onPressed: () => _leaveCall(), ), ], ), ); } }