Skip to content

Commit

Permalink
feat: add group call initializing (#101)
Browse files Browse the repository at this point in the history
  • Loading branch information
Mo2Hefny authored Dec 20, 2024
1 parent 623bf61 commit ad3d2b0
Show file tree
Hide file tree
Showing 10 changed files with 119 additions and 49 deletions.
1 change: 1 addition & 0 deletions lib/core/constants/keys.dart
Original file line number Diff line number Diff line change
Expand Up @@ -187,5 +187,6 @@ class Keys {

// General
static const popupMenuButton = ValueKey('popup_menu_button');
static const popupMenuItemPrefix = ValueKey('popup_menu_item_');
Keys._();
}
6 changes: 5 additions & 1 deletion lib/core/routes/routes.dart
Original file line number Diff line number Diff line change
Expand Up @@ -310,7 +310,11 @@ class Routes {
path: Routes.callScreen,
builder: (context, state) {
final Map<String, dynamic>? extra = state.extra as Map<String, dynamic>?;
return CallScreen(callee: extra?['user'] as UserModel?, voiceCallId: extra?['voiceCallId'] as String?);
return CallScreen(
chatId: extra?['chatId'] as String?,
callee: extra?['user'] as UserModel?,
voiceCallId: extra?['voiceCallId'] as String?
);
},
),
GoRoute(
Expand Down
6 changes: 5 additions & 1 deletion lib/core/view/widget/popup_menu_widget.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
import 'package:telware_cross_platform/core/constants/keys.dart';
import 'package:telware_cross_platform/core/view/widget/popup_menu_item_widget.dart';
import 'package:telware_cross_platform/core/theme/palette.dart';
import 'package:telware_cross_platform/features/chat/view/screens/pinned_messages_screen.dart';

class PopupMenuWidget extends StatelessWidget {
final List<PopupMenuEntry> items;
Expand All @@ -26,10 +29,11 @@ class PopupMenuWidget extends StatelessWidget {
color: Palette.secondary,
position: RelativeRect.fromLTRB(position.dx, position.dy - renderBox.size.height, position.dx + 100, position.dy),
items: <PopupMenuEntry<dynamic>>[
...items.map((item) {
...items.mapIndexed((index, item) {
return PopupMenuItem<dynamic>(
value: item['value'],
child: PopupMenuItemWidget(
key: ValueKey(Keys.popupMenuItemPrefix.value + index.toString()),
icon: item['icon'],
text: item['text'],
trailing: item['trailing'],
Expand Down
1 change: 1 addition & 0 deletions lib/features/chat/enum/chatting_enums.dart
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ enum EventType {
receiveLeftCall(event: 'CLIENT-LEFT'),
receiveCallSignal(event: 'SIGNAL-CLIENT'),
receiveCallStarted(event: 'CALL-STARTED'),
receiveCallEnded(event: 'CALL-ENDED'),
;

final String event;
Expand Down
6 changes: 5 additions & 1 deletion lib/features/chat/enum/message_enums.dart
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,9 @@ enum MessageContentType {
@HiveField(7)
file(content: 'file'),
@HiveField(8)
link(content: 'link');
link(content: 'link'),
@HiveField(9)
call(content: 'call');

static MessageContentType getType(String type) {
switch (type) {
Expand All @@ -91,6 +93,8 @@ enum MessageContentType {
return MessageContentType.file;
case 'link':
return MessageContentType.link;
case 'call':
return MessageContentType.call;
default:
return MessageContentType.text;
}
Expand Down
92 changes: 53 additions & 39 deletions lib/features/chat/services/signaling_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import 'package:telware_cross_platform/features/chat/view_model/event_handler.da

typedef StreamStateCallback = void Function(MediaStream stream, String senderId);
typedef DescriptionCallback = void Function(RTCSessionDescription description, String senderId);
typedef CandidateCallback = void Function(RTCIceCandidate candidate);
typedef CandidateCallback = void Function(RTCIceCandidate candidate, String senderId);
typedef ResponseCallback = void Function(dynamic response);

final TURN_SERVER_USERNAME = dotenv.env['TURN_SERVER_USERNAME'] ?? '';
Expand Down Expand Up @@ -46,7 +46,7 @@ class Signaling {
]
};

RTCPeerConnection? peerConnection;
Map<String, RTCPeerConnection> peerConnections = {};
MediaStream? localStream;
Map<String, MediaStream> remoteStreams = {};
StreamStateCallback? onAddRemoteStream;
Expand All @@ -56,6 +56,7 @@ class Signaling {
ResponseCallback? onCallStarted;
ResponseCallback? onReceiveJoinedCall;
ResponseCallback? onReceiveLeftCall;
ResponseCallback? onReceiveEndCall;

String? roomId;

Expand All @@ -75,59 +76,59 @@ class Signaling {
data['data']['sdpMid'],
data['data']['sdpMLineIndex'],
);
onICECandidate?.call(candidate);
onICECandidate?.call(candidate, data['senderId']);
}
}

Future<RTCSessionDescription> createOffer(calleeId) async {
await registerPeerConnection(calleeId);
Future<RTCSessionDescription> createOffer(clientId) async {
await registerPeerConnection(clientId);

localStream?.getTracks().forEach((track) {
peerConnection!.addTrack(track, localStream!);
peerConnections[clientId]!.addTrack(track, localStream!);
});

var offer = await peerConnection!.createOffer();
await setLocalDescription(offer);
var offer = await peerConnections[clientId]!.createOffer();
await setLocalDescription(offer, clientId);

return offer;
}

Future<RTCSessionDescription> createAnswer(String callerId) async {
Future<RTCSessionDescription> createAnswer(String clientId) async {
localStream?.getTracks().forEach((track) {
peerConnection!.addTrack(track, localStream!);
peerConnections[clientId]!.addTrack(track, localStream!);
});

peerConnection!.onTrack = (RTCTrackEvent event) async {
peerConnections[clientId]!.onTrack = (RTCTrackEvent event) async {
debugPrint('Remote stream added');
event.streams[0].getTracks().forEach((track) {
remoteStreams[callerId]!.addTrack(track);
remoteStreams[clientId]!.addTrack(track);
});
};

var answer = await peerConnection!.createAnswer();
await setLocalDescription(answer);
var answer = await peerConnections[clientId]!.createAnswer();
await setLocalDescription(answer, clientId);

return answer;
}

Future<void> addCandidate(RTCIceCandidate candidate) async {
await peerConnection!.addCandidate(candidate);
Future<void> addCandidate(RTCIceCandidate candidate, String clientId) async {
await peerConnections[clientId]!.addCandidate(candidate);
}

Future<void> registerPeerConnection(String callerId) async {
peerConnection ??= await createPeerConnection(configuration);
registerPeerConnectionListeners(callerId);
Future<void> registerPeerConnection(String clientId) async {
peerConnections[clientId] ??= await createPeerConnection(configuration);
registerPeerConnectionListeners(clientId);
}

Future<void> setRemoteDescription(RTCSessionDescription description) async {
Future<void> setRemoteDescription(RTCSessionDescription description, String clientId) async {
debugPrint('Setting remote description to: ${description.sdp}');
debugPrint('Remote SDP: ${description.sdp}');
await peerConnection!.setRemoteDescription(description);
await peerConnections[clientId]!.setRemoteDescription(description);
}

Future<void> setLocalDescription(RTCSessionDescription description) async {
Future<void> setLocalDescription(RTCSessionDescription description, String clientId) async {
debugPrint('Local SDP: ${description.sdp}');
await peerConnection!.setLocalDescription(description);
await peerConnections[clientId]!.setLocalDescription(description);
}

Future<void> hangUp(RTCVideoRenderer localRenderer) async {
Expand All @@ -138,9 +139,13 @@ class Signaling {
track.stop();
});
});
remoteStreams.clear();

peerConnections.forEach((key, value) {
value.close();
});
peerConnections.clear();

if (peerConnection != null) peerConnection!.close();
peerConnection = null;
_eventHandler.addEvent(
LeaveCallEvent(
{
Expand All @@ -150,11 +155,20 @@ class Signaling {
);

localStream!.dispose();
remoteStreams.clear();
}

void registerPeerConnectionListeners(String calleeId) {
peerConnection!.onIceCandidate = (RTCIceCandidate? candidate) {
Future<void> removeRemoteClient(String clientId) async {
peerConnections[clientId]?.close();
peerConnections.remove(clientId);

remoteStreams[clientId]?.getTracks().forEach((track) {
track.stop();
});
remoteStreams.remove(clientId);
}

void registerPeerConnectionListeners(String clientId) {
peerConnections[clientId]!.onIceCandidate = (RTCIceCandidate? candidate) {
if (candidate != null) {
debugPrint('Generated ICE Candidate');
_eventHandler.addEvent(
Expand All @@ -166,43 +180,43 @@ class Signaling {
'sdpMid': candidate.sdpMid,
'sdpMLineIndex': candidate.sdpMLineIndex,
},
'targetId': calleeId,
'targetId': clientId,
'voiceCallId': roomId,
},
)
);
}
};

peerConnection!.onConnectionState = (RTCPeerConnectionState state) {
peerConnections[clientId]!.onConnectionState = (RTCPeerConnectionState state) {
debugPrint('Connection state changed: $state');
};

peerConnection!.onSignalingState = (RTCSignalingState state) {
peerConnections[clientId]!.onSignalingState = (RTCSignalingState state) {
debugPrint('Signaling state changed: $state');
};

peerConnection!.onIceConnectionState = (RTCIceConnectionState state) {
peerConnections[clientId]!.onIceConnectionState = (RTCIceConnectionState state) {
debugPrint('ICE connection state changed: $state');
};

peerConnection!.onTrack = (RTCTrackEvent event) async {
peerConnections[clientId]!.onTrack = (RTCTrackEvent event) async {
if (event.streams.isNotEmpty) {
for (var track in event.streams[0].getTracks()) {
remoteStreams[calleeId]?.addTrack(track);
remoteStreams[clientId]?.addTrack(track);
}
onAddRemoteStream?.call(event.streams[0], calleeId);
onAddRemoteStream?.call(event.streams[0], clientId);
}
};

peerConnection!.onIceGatheringState = (RTCIceGatheringState state) {
peerConnections[clientId]!.onIceGatheringState = (RTCIceGatheringState state) {
debugPrint('ICE gathering state changed: $state');
};

peerConnection!.onAddStream = (MediaStream stream) {
peerConnections[clientId]!.onAddStream = (MediaStream stream) {
debugPrint('Remote stream added');
onAddRemoteStream?.call(stream, calleeId);
remoteStreams[calleeId] = stream;
onAddRemoteStream?.call(stream, clientId);
remoteStreams[clientId] = stream;
};
}

Expand Down
26 changes: 19 additions & 7 deletions lib/features/chat/view/screens/call_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,13 @@ import 'package:telware_cross_platform/features/chat/view_model/chats_view_model

class CallScreen extends ConsumerStatefulWidget {
static const String route = '/call';
final String? chatId;
final UserModel? callee;
final String? voiceCallId;

const CallScreen({
super.key,
this.chatId,
this.callee,
this.voiceCallId,
});
Expand All @@ -47,6 +49,7 @@ class _CallScreenState extends ConsumerState<CallScreen> {
isCaller = ref.read(callStateProvider).isCaller;
callee = widget.callee ?? ref.read(callStateProvider).callee;
voiceCallId = widget.voiceCallId ?? ref.read(callStateProvider).voiceCallId;
String? chatId = widget.chatId;

isReceivingCall = !isCaller && voiceCallId != null;

Expand All @@ -71,9 +74,12 @@ class _CallScreenState extends ConsumerState<CallScreen> {
_initializeSignaling();
if (voiceCallId == null && isCaller) {
debugPrint("Call: Creating voice call");
ChatModel chat = ref.read(chatsViewModelProvider.notifier)
.getChat(ref.read(userProvider)!, callee!, ChatType.private);
_signaling.createVoiceCall(chat.id, callee!.id);
if (chatId == null) {
ChatModel chat = ref.read(chatsViewModelProvider.notifier)
.getChat(ref.read(userProvider)!, callee!, ChatType.private);
chatId = chat.id;
}
_signaling.createVoiceCall(chatId, callee?.id);
}
});
}
Expand All @@ -88,19 +94,19 @@ class _CallScreenState extends ConsumerState<CallScreen> {
_signaling.onOffer = (description, senderId) async {
// If receiving a call, set remote description and create an answer
await _signaling.registerPeerConnection(senderId);
await _signaling.setRemoteDescription(description);
await _signaling.setRemoteDescription(description, senderId);
final answer = await _signaling.createAnswer(senderId);
_signaling.sendAnswer(answer, senderId);
startCall();
};

_signaling.onAnswer = (description, senderId) async {
await _signaling.setRemoteDescription(description);
await _signaling.setRemoteDescription(description, senderId);
startCall();
};

_signaling.onICECandidate = (candidate) {
_signaling.addCandidate(candidate);
_signaling.onICECandidate = (candidate, senderId) {
_signaling.addCandidate(candidate, senderId);
};

_signaling.onCallStarted = (response) {
Expand All @@ -126,6 +132,12 @@ class _CallScreenState extends ConsumerState<CallScreen> {
};

_signaling.onReceiveLeftCall = (response) {
// Terminate connection with the specific user
final clientId = response['clientId'];
_signaling.removeRemoteClient(clientId);
};

_signaling.onReceiveEndCall = (response) {
// Terminate connection with user
endCall();
context.pop();
Expand Down
15 changes: 15 additions & 0 deletions lib/features/chat/view/screens/chat_info_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import 'package:telware_cross_platform/core/view/widget/tab_bar_widget.dart';
import 'package:telware_cross_platform/core/view/widget/lottie_viewer.dart';
import 'package:telware_cross_platform/core/view/widget/popup_menu_widget.dart';
import 'package:telware_cross_platform/features/auth/view/widget/title_element.dart';
import 'package:telware_cross_platform/features/chat/providers/call_provider.dart';
import 'package:telware_cross_platform/features/chat/view/widget/member_tile_widget.dart';
import 'package:telware_cross_platform/features/chat/view_model/chats_view_model.dart';
import 'package:telware_cross_platform/features/chat/view_model/chatting_controller.dart';
Expand Down Expand Up @@ -337,11 +338,25 @@ class _ChatInfoScreen extends ConsumerState<ChatInfoScreen>
case 'delete-group':
_confirmDelete(context);
break;
case 'video-call':
_createGroupCall();
break;
default:
showToastMessage('Coming soon');
}
}

void _createGroupCall() {
if (ref.read(callStateProvider).voiceCallId == null) {
ref.read(callStateProvider.notifier).setCaller(true);
context.push(Routes.callScreen, extra: {
'chatId': chat.id,
});
} else {
showToastMessage('You are already in a call');
}
}

void _addMembers() {
Navigator.push(
context,
Expand Down
9 changes: 9 additions & 0 deletions lib/features/chat/view_model/event_handler.dart
Original file line number Diff line number Diff line change
Expand Up @@ -314,6 +314,15 @@ class EventHandler {
debugPrint('!!! Error in receiving a call started:\n${e.toString()}');
}
});
// get a call ended
_socket.on(EventType.receiveCallEnded.event, (response) async {
try {
debugPrint('### got a call ended: $response');
signaling.onReceiveEndCall?.call(response);
} on Exception catch (e) {
debugPrint('!!! Error in receiving a call ended:\n${e.toString()}');
}
});
}

// Private constructor
Expand Down
Loading

0 comments on commit ad3d2b0

Please sign in to comment.