2020-04-20 11:24:40 +00:00
|
|
|
import 'package:comunic/helpers/calls_helper.dart';
|
2020-04-20 11:19:49 +00:00
|
|
|
import 'package:comunic/helpers/conversations_helper.dart';
|
2020-04-20 12:24:35 +00:00
|
|
|
import 'package:comunic/helpers/events_helper.dart';
|
2020-04-20 12:13:03 +00:00
|
|
|
import 'package:comunic/helpers/users_helper.dart';
|
2020-04-20 12:02:32 +00:00
|
|
|
import 'package:comunic/lists/call_members_list.dart';
|
2020-04-20 12:13:03 +00:00
|
|
|
import 'package:comunic/lists/users_list.dart';
|
2020-04-20 11:43:17 +00:00
|
|
|
import 'package:comunic/models/call_config.dart';
|
2020-04-20 12:13:03 +00:00
|
|
|
import 'package:comunic/models/call_member.dart';
|
2020-04-20 11:19:49 +00:00
|
|
|
import 'package:comunic/models/conversation.dart';
|
2020-04-20 12:32:57 +00:00
|
|
|
import 'package:comunic/ui/routes/main_route.dart';
|
2020-04-20 11:19:49 +00:00
|
|
|
import 'package:comunic/ui/widgets/safe_state.dart';
|
2020-04-20 12:13:03 +00:00
|
|
|
import 'package:comunic/utils/account_utils.dart';
|
2020-04-20 11:19:49 +00:00
|
|
|
import 'package:comunic/utils/intl_utils.dart';
|
|
|
|
import 'package:comunic/utils/ui_utils.dart';
|
2020-04-20 08:53:25 +00:00
|
|
|
import 'package:flutter/material.dart';
|
2020-04-20 15:24:42 +00:00
|
|
|
import 'package:flutter_webrtc/rtc_peerconnection.dart';
|
|
|
|
import 'package:flutter_webrtc/webrtc.dart';
|
2020-04-20 08:53:25 +00:00
|
|
|
|
|
|
|
/// 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();
|
|
|
|
}
|
|
|
|
|
2020-04-20 11:19:49 +00:00
|
|
|
class _CallScreenState extends SafeState<CallScreen> {
|
|
|
|
// Widget properties
|
|
|
|
int get convID => widget.convID;
|
|
|
|
|
|
|
|
// State properties
|
|
|
|
Conversation _conversation;
|
|
|
|
String _convName;
|
2020-04-20 11:43:17 +00:00
|
|
|
CallConfig _conf;
|
2020-04-20 11:19:49 +00:00
|
|
|
var _error = false;
|
2020-04-20 12:02:32 +00:00
|
|
|
CallMembersList _membersList;
|
2020-04-20 12:13:03 +00:00
|
|
|
UsersList _usersList;
|
2020-04-20 15:24:42 +00:00
|
|
|
final _peersConnections = Map<int, RTCPeerConnection>();
|
2020-04-20 11:19:49 +00:00
|
|
|
|
|
|
|
@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(() {});
|
2020-04-20 11:24:40 +00:00
|
|
|
|
|
|
|
// Join the call
|
|
|
|
await CallsHelper.join(convID);
|
2020-04-20 11:43:17 +00:00
|
|
|
|
|
|
|
// Get call configuration
|
|
|
|
_conf = await CallsHelper.getConfig();
|
2020-04-20 12:02:32 +00:00
|
|
|
|
|
|
|
// Get current members of the call
|
2020-04-20 12:13:03 +00:00
|
|
|
final membersList = await CallsHelper.getMembers(convID);
|
|
|
|
membersList.removeUser(userID());
|
|
|
|
_usersList = await UsersHelper().getListWithThrow(membersList.usersID);
|
|
|
|
_membersList = membersList;
|
|
|
|
|
|
|
|
setState(() {});
|
2020-04-20 12:24:35 +00:00
|
|
|
|
|
|
|
// Register to events
|
|
|
|
this.listenChangeState<UserJoinedCallEvent>((e) {
|
2020-04-20 12:58:23 +00:00
|
|
|
// TODO : get user information if required
|
2020-04-20 13:02:49 +00:00
|
|
|
if (e.callID == convID) _membersList.add(CallMember(userID: e.userID));
|
2020-04-20 12:24:35 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
this.listen<UserLeftCallEvent>((e) {
|
|
|
|
if (e.callID == convID) _removeMember(e.userID);
|
|
|
|
});
|
2020-04-20 12:32:57 +00:00
|
|
|
|
2020-04-20 14:23:33 +00:00
|
|
|
this.listen<NewCallSignalEvent>((e) {
|
|
|
|
if (e.callID == convID) _newSignal(e);
|
|
|
|
});
|
|
|
|
|
2020-04-20 12:58:23 +00:00
|
|
|
this.listen<CallPeerReadyEvent>((e) {
|
|
|
|
if (e.callID == convID) _memberReady(e.peerID);
|
|
|
|
});
|
|
|
|
|
|
|
|
this.listen<CallPeerInterruptedStreamingEvent>((e) {
|
|
|
|
if (e.callID == convID) _removeRemotePeerConnection(e.peerID);
|
|
|
|
});
|
|
|
|
|
2020-04-20 12:32:57 +00:00
|
|
|
this.listen<CallClosedEvent>((e) {
|
2020-04-20 12:41:09 +00:00
|
|
|
if (e.callID == convID) _leaveCall(needConfirm: false);
|
2020-04-20 12:32:57 +00:00
|
|
|
});
|
2020-04-20 13:50:01 +00:00
|
|
|
|
|
|
|
// Connect to ready peers
|
|
|
|
for (final peer in _membersList.readyPeers)
|
|
|
|
await this._memberReady(peer.userID);
|
2020-04-20 11:19:49 +00:00
|
|
|
} catch (e, stack) {
|
|
|
|
print("Could not initialize call! $e\n$stack");
|
|
|
|
setState(() => _error = true);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-04-20 12:58:23 +00:00
|
|
|
/// Do clean up operations when call screen is destroyed
|
2020-04-20 11:24:40 +00:00
|
|
|
void _endCall() async {
|
|
|
|
try {
|
|
|
|
// Leave the call
|
|
|
|
await CallsHelper.leave(convID);
|
|
|
|
} catch (e, stack) {
|
|
|
|
print("Could not end call properly! $e\n$stack");
|
|
|
|
}
|
|
|
|
}
|
2020-04-20 11:19:49 +00:00
|
|
|
|
2020-04-20 12:58:23 +00:00
|
|
|
/// Make us leave the call
|
2020-04-20 12:41:09 +00:00
|
|
|
void _leaveCall({bool needConfirm = true}) async {
|
|
|
|
if (needConfirm &&
|
|
|
|
!await showConfirmDialog(
|
|
|
|
context: context,
|
|
|
|
message: tr("Do you really want to leave this call ?"))) return;
|
|
|
|
|
2020-04-20 12:32:57 +00:00
|
|
|
MainController.of(context).popPage();
|
|
|
|
}
|
|
|
|
|
2020-04-20 12:58:23 +00:00
|
|
|
/// Call this when a user started to stream media
|
2020-04-20 13:50:01 +00:00
|
|
|
Future<void> _memberReady(int memberID) async {
|
|
|
|
try {
|
|
|
|
_membersList.getUser(memberID).status = MemberStatus.READY;
|
|
|
|
setState(() {});
|
2020-04-20 12:58:23 +00:00
|
|
|
|
2020-04-20 15:24:42 +00:00
|
|
|
// Create peer connection
|
|
|
|
final peerConnection = await createPeerConnection(_conf.pluginConfig, {
|
|
|
|
"mandatory": {
|
|
|
|
"OfferToReceiveAudio": true,
|
|
|
|
"OfferToReceiveVideo":
|
|
|
|
_conversation.callCapabilities == CallCapabilities.VIDEO,
|
|
|
|
},
|
|
|
|
"optional": [],
|
|
|
|
});
|
|
|
|
|
|
|
|
_peersConnections[memberID] = peerConnection;
|
|
|
|
|
2020-04-20 13:50:01 +00:00
|
|
|
// 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!"));
|
|
|
|
}
|
2020-04-20 12:58:23 +00:00
|
|
|
}
|
|
|
|
|
2020-04-20 14:23:33 +00:00
|
|
|
/// Call this method each time we get a new signal
|
2020-04-20 15:24:42 +00:00
|
|
|
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({});
|
|
|
|
|
2020-04-20 15:29:36 +00:00
|
|
|
await CallsHelper.sendSessionDescription(convID, ev.peerID, answer);
|
2020-04-20 15:24:42 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// 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!"));
|
|
|
|
}
|
2020-04-20 14:23:33 +00:00
|
|
|
}
|
|
|
|
|
2020-04-20 12:58:23 +00:00
|
|
|
/// Call this when a user has interrupted streaming
|
|
|
|
void _removeRemotePeerConnection(int memberID) {
|
|
|
|
_membersList.getUser(memberID).status = MemberStatus.JOINED;
|
2020-04-20 13:20:01 +00:00
|
|
|
setState(() {});
|
2020-04-20 12:58:23 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/// Call this when a members has completely left the call
|
2020-04-20 12:24:35 +00:00
|
|
|
void _removeMember(int memberID) {
|
2020-04-20 12:58:23 +00:00
|
|
|
_removeRemotePeerConnection(memberID);
|
|
|
|
|
2020-04-20 12:24:35 +00:00
|
|
|
_membersList.removeUser(memberID);
|
|
|
|
setState(() {});
|
|
|
|
}
|
|
|
|
|
2020-04-20 08:53:25 +00:00
|
|
|
@override
|
|
|
|
Widget build(BuildContext context) {
|
2020-04-20 11:19:49 +00:00
|
|
|
return Scaffold(
|
|
|
|
appBar: AppBar(
|
2020-04-20 12:41:09 +00:00
|
|
|
leading: IconButton(
|
|
|
|
icon: Icon(Icons.arrow_back),
|
|
|
|
onPressed: () => _leaveCall(),
|
|
|
|
),
|
2020-04-20 11:19:49 +00:00
|
|
|
title:
|
|
|
|
_convName == null ? CircularProgressIndicator() : Text(_convName),
|
|
|
|
),
|
|
|
|
body: _buildBody(),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Build widget body
|
|
|
|
Widget _buildBody() {
|
2020-04-20 12:02:32 +00:00
|
|
|
// Handle errors
|
2020-04-20 11:19:49 +00:00
|
|
|
if (_error)
|
|
|
|
return buildErrorCard(tr("Could not initialize call!"), actions: [
|
|
|
|
MaterialButton(
|
|
|
|
onPressed: () => _initCall(),
|
|
|
|
child: Text(tr("Try again").toUpperCase()),
|
|
|
|
)
|
|
|
|
]);
|
2020-04-20 12:13:03 +00:00
|
|
|
|
|
|
|
// Check if are not ready to show call UI
|
|
|
|
if (_membersList == null) return buildCenteredProgressBar();
|
|
|
|
|
|
|
|
return Column(
|
2020-04-20 13:20:01 +00:00
|
|
|
children: <Widget>[_buildMembersArea(), Spacer(), _buildFooterArea()],
|
2020-04-20 12:13:03 +00:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Build members area
|
|
|
|
Widget _buildMembersArea() {
|
|
|
|
return Center(
|
|
|
|
child: Padding(
|
|
|
|
padding: const EdgeInsets.all(8.0),
|
|
|
|
child: RichText(
|
|
|
|
text: TextSpan(
|
|
|
|
children: _membersList
|
|
|
|
.map((f) => TextSpan(
|
2020-04-20 13:02:49 +00:00
|
|
|
text: _usersList.getUser(f.userID).displayName + " ",
|
2020-04-20 12:13:03 +00:00
|
|
|
style: TextStyle(
|
|
|
|
color: f.status == MemberStatus.JOINED
|
|
|
|
? null
|
|
|
|
: Colors.green)))
|
|
|
|
.toList())),
|
|
|
|
),
|
|
|
|
);
|
2020-04-20 08:53:25 +00:00
|
|
|
}
|
2020-04-20 13:20:01 +00:00
|
|
|
|
|
|
|
/// Footer area
|
|
|
|
Widget _buildFooterArea() {
|
|
|
|
return Material(
|
|
|
|
color: Colors.black,
|
|
|
|
child: Row(
|
|
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
|
|
children: <Widget>[
|
|
|
|
// Hang up call
|
|
|
|
IconButton(
|
|
|
|
icon: Icon(Icons.phone, color: Colors.red),
|
|
|
|
onPressed: () => _leaveCall(),
|
|
|
|
),
|
|
|
|
],
|
|
|
|
),
|
|
|
|
);
|
|
|
|
}
|
2020-04-20 08:53:25 +00:00
|
|
|
}
|