matrix-dart-sdk/lib/src/voip/backend/mesh_backend.dart

1028 lines
30 KiB
Dart

import 'dart:async';
import 'package:collection/collection.dart';
import 'package:webrtc_interface/webrtc_interface.dart';
import 'package:matrix/matrix.dart';
import 'package:matrix/src/utils/cached_stream_controller.dart';
import 'package:matrix/src/voip/models/call_membership.dart';
import 'package:matrix/src/voip/models/call_options.dart';
import 'package:matrix/src/voip/utils/stream_helper.dart';
import 'package:matrix/src/voip/utils/user_media_constraints.dart';
class MeshBackend extends CallBackend {
MeshBackend({
super.type = 'mesh',
});
final List<CallSession> _callSessions = [];
/// participant:volume
final Map<CallParticipant, double> _audioLevelsMap = {};
/// The stream is used to prepare for incoming peer calls like registering listeners
StreamSubscription<CallSession>? _callSetupSubscription;
/// The stream is used to signal the start of an incoming peer call
StreamSubscription<CallSession>? _callStartSubscription;
Timer? _activeSpeakerLoopTimeout;
final CachedStreamController<WrappedMediaStream> onStreamAdd =
CachedStreamController();
final CachedStreamController<WrappedMediaStream> onStreamRemoved =
CachedStreamController();
final CachedStreamController<GroupCallSession> onGroupCallFeedsChanged =
CachedStreamController();
@override
Map<String, Object?> toJson() {
return {
'type': type,
};
}
CallParticipant? _activeSpeaker;
WrappedMediaStream? _localUserMediaStream;
WrappedMediaStream? _localScreenshareStream;
final List<WrappedMediaStream> _userMediaStreams = [];
final List<WrappedMediaStream> _screenshareStreams = [];
List<WrappedMediaStream> _getLocalStreams() {
final feeds = <WrappedMediaStream>[];
if (localUserMediaStream != null) {
feeds.add(localUserMediaStream!);
}
if (localScreenshareStream != null) {
feeds.add(localScreenshareStream!);
}
return feeds;
}
Future<MediaStream> _getUserMedia(
GroupCallSession groupCall,
CallType type,
) async {
final mediaConstraints = {
'audio': UserMediaConstraints.micMediaConstraints,
'video': type == CallType.kVideo
? UserMediaConstraints.camMediaConstraints
: false,
};
try {
return await groupCall.voip.delegate.mediaDevices
.getUserMedia(mediaConstraints);
} catch (e) {
groupCall.setState(GroupCallState.localCallFeedUninitialized);
rethrow;
}
}
Future<MediaStream> _getDisplayMedia(GroupCallSession groupCall) async {
final mediaConstraints = {
'audio': false,
'video': true,
};
try {
return await groupCall.voip.delegate.mediaDevices
.getDisplayMedia(mediaConstraints);
} catch (e, s) {
throw MatrixSDKVoipException('_getDisplayMedia failed', stackTrace: s);
}
}
CallSession? _getCallForParticipant(
GroupCallSession groupCall,
CallParticipant participant,
) {
return _callSessions.singleWhereOrNull(
(call) =>
call.groupCallId == groupCall.groupCallId &&
CallParticipant(
groupCall.voip,
userId: call.remoteUserId!,
deviceId: call.remoteDeviceId,
) ==
participant,
);
}
/// Register listeners for a peer call to use for the group calls, that is
/// needed before even call is added to `_callSessions`.
/// We do this here for onStreamAdd and onStreamRemoved to make sure we don't
/// miss any events that happen before the call is completely started.
void _registerListenersBeforeCallAdd(CallSession call) {
call.onStreamAdd.stream.listen((stream) {
if (!stream.isLocal()) {
onStreamAdd.add(stream);
}
});
call.onStreamRemoved.stream.listen((stream) {
if (!stream.isLocal()) {
onStreamRemoved.add(stream);
}
});
}
Future<void> _addCall(GroupCallSession groupCall, CallSession call) async {
_callSessions.add(call);
_initCall(groupCall, call);
groupCall.onGroupCallEvent.add(GroupCallStateChange.callsChanged);
}
/// init a peer call from group calls.
void _initCall(GroupCallSession groupCall, CallSession call) {
if (call.remoteUserId == null) {
throw MatrixSDKVoipException(
'Cannot init call without proper invitee user and device Id',
);
}
call.onCallStateChanged.stream.listen(
((event) async {
await _onCallStateChanged(call, event);
}),
);
call.onCallReplaced.stream.listen((CallSession newCall) async {
await _replaceCall(groupCall, call, newCall);
});
call.onCallStreamsChanged.stream.listen((call) async {
await call.tryRemoveStopedStreams();
await _onStreamsChanged(groupCall, call);
});
call.onCallHangupNotifierForGroupCalls.stream.listen((event) async {
await _onCallHangup(groupCall, call);
});
}
Future<void> _replaceCall(
GroupCallSession groupCall,
CallSession existingCall,
CallSession replacementCall,
) async {
final existingCallIndex = _callSessions
.indexWhere((element) => element.callId == existingCall.callId);
if (existingCallIndex == -1) {
throw MatrixSDKVoipException('Couldn\'t find call to replace');
}
_callSessions.removeAt(existingCallIndex);
_callSessions.add(replacementCall);
await _disposeCall(groupCall, existingCall, CallErrorCode.replaced);
_registerListenersBeforeCallAdd(replacementCall);
_initCall(groupCall, replacementCall);
groupCall.onGroupCallEvent.add(GroupCallStateChange.callsChanged);
}
/// Removes a peer call from group calls.
Future<void> _removeCall(
GroupCallSession groupCall,
CallSession call,
CallErrorCode hangupReason,
) async {
await _disposeCall(groupCall, call, hangupReason);
_callSessions.removeWhere((element) => call.callId == element.callId);
groupCall.onGroupCallEvent.add(GroupCallStateChange.callsChanged);
}
Future<void> _disposeCall(
GroupCallSession groupCall,
CallSession call,
CallErrorCode hangupReason,
) async {
if (call.remoteUserId == null) {
throw MatrixSDKVoipException(
'Cannot init call without proper invitee user and device Id',
);
}
if (call.hangupReason == CallErrorCode.replaced) {
return;
}
if (call.state != CallState.kEnded) {
// no need to emit individual handleCallEnded on group calls
// also prevents a loop of hangup and onCallHangupNotifierForGroupCalls
await call.hangup(reason: hangupReason, shouldEmit: false);
}
final usermediaStream = _getUserMediaStreamByParticipantId(
CallParticipant(
groupCall.voip,
userId: call.remoteUserId!,
deviceId: call.remoteDeviceId,
).id,
);
if (usermediaStream != null) {
await _removeUserMediaStream(groupCall, usermediaStream);
}
final screenshareStream = _getScreenshareStreamByParticipantId(
CallParticipant(
groupCall.voip,
userId: call.remoteUserId!,
deviceId: call.remoteDeviceId,
).id,
);
if (screenshareStream != null) {
await _removeScreenshareStream(groupCall, screenshareStream);
}
}
Future<void> _onStreamsChanged(
GroupCallSession groupCall,
CallSession call,
) async {
if (call.remoteUserId == null) {
throw MatrixSDKVoipException(
'Cannot init call without proper invitee user and device Id',
);
}
final currentUserMediaStream = _getUserMediaStreamByParticipantId(
CallParticipant(
groupCall.voip,
userId: call.remoteUserId!,
deviceId: call.remoteDeviceId,
).id,
);
final remoteUsermediaStream = call.remoteUserMediaStream;
final remoteStreamChanged = remoteUsermediaStream != currentUserMediaStream;
if (remoteStreamChanged) {
if (currentUserMediaStream == null && remoteUsermediaStream != null) {
await _addUserMediaStream(groupCall, remoteUsermediaStream);
} else if (currentUserMediaStream != null &&
remoteUsermediaStream != null) {
await _replaceUserMediaStream(
groupCall,
currentUserMediaStream,
remoteUsermediaStream,
);
} else if (currentUserMediaStream != null &&
remoteUsermediaStream == null) {
await _removeUserMediaStream(groupCall, currentUserMediaStream);
}
}
final currentScreenshareStream = _getScreenshareStreamByParticipantId(
CallParticipant(
groupCall.voip,
userId: call.remoteUserId!,
deviceId: call.remoteDeviceId,
).id,
);
final remoteScreensharingStream = call.remoteScreenSharingStream;
final remoteScreenshareStreamChanged =
remoteScreensharingStream != currentScreenshareStream;
if (remoteScreenshareStreamChanged) {
if (currentScreenshareStream == null &&
remoteScreensharingStream != null) {
_addScreenshareStream(groupCall, remoteScreensharingStream);
} else if (currentScreenshareStream != null &&
remoteScreensharingStream != null) {
await _replaceScreenshareStream(
groupCall,
currentScreenshareStream,
remoteScreensharingStream,
);
} else if (currentScreenshareStream != null &&
remoteScreensharingStream == null) {
await _removeScreenshareStream(groupCall, currentScreenshareStream);
}
}
onGroupCallFeedsChanged.add(groupCall);
}
WrappedMediaStream? _getUserMediaStreamByParticipantId(String participantId) {
final stream = _userMediaStreams
.where((stream) => stream.participant.id == participantId);
if (stream.isNotEmpty) {
return stream.first;
}
return null;
}
void _onActiveSpeakerLoop(GroupCallSession groupCall) async {
CallParticipant? nextActiveSpeaker;
// idc about screen sharing atm.
final userMediaStreamsCopyList =
List<WrappedMediaStream>.from(_userMediaStreams);
for (final stream in userMediaStreamsCopyList) {
if (stream.participant.isLocal && stream.pc == null) {
continue;
}
final List<StatsReport> statsReport = await stream.pc!.getStats();
statsReport
.removeWhere((element) => !element.values.containsKey('audioLevel'));
// https://www.w3.org/TR/webrtc-stats/#summary
final otherPartyAudioLevel = statsReport
.singleWhereOrNull(
(element) =>
element.type == 'inbound-rtp' &&
element.values['kind'] == 'audio',
)
?.values['audioLevel'];
if (otherPartyAudioLevel != null) {
_audioLevelsMap[stream.participant] = otherPartyAudioLevel;
}
// https://www.w3.org/TR/webrtc-stats/#dom-rtcstatstype-media-source
// firefox does not seem to have this though. Works on chrome and android
final ownAudioLevel = statsReport
.singleWhereOrNull(
(element) =>
element.type == 'media-source' &&
element.values['kind'] == 'audio',
)
?.values['audioLevel'];
if (groupCall.localParticipant != null &&
ownAudioLevel != null &&
_audioLevelsMap[groupCall.localParticipant] != ownAudioLevel) {
_audioLevelsMap[groupCall.localParticipant!] = ownAudioLevel;
}
}
double maxAudioLevel = double.negativeInfinity;
// TODO: we probably want a threshold here?
_audioLevelsMap.forEach((key, value) {
if (value > maxAudioLevel) {
nextActiveSpeaker = key;
maxAudioLevel = value;
}
});
if (nextActiveSpeaker != null && _activeSpeaker != nextActiveSpeaker) {
_activeSpeaker = nextActiveSpeaker;
groupCall.onGroupCallEvent.add(GroupCallStateChange.activeSpeakerChanged);
}
_activeSpeakerLoopTimeout?.cancel();
_activeSpeakerLoopTimeout = Timer(
CallConstants.activeSpeakerInterval,
() => _onActiveSpeakerLoop(groupCall),
);
}
WrappedMediaStream? _getScreenshareStreamByParticipantId(
String participantId,
) {
final stream = _screenshareStreams
.where((stream) => stream.participant.id == participantId);
if (stream.isNotEmpty) {
return stream.first;
}
return null;
}
void _addScreenshareStream(
GroupCallSession groupCall,
WrappedMediaStream stream,
) {
_screenshareStreams.add(stream);
onStreamAdd.add(stream);
groupCall.onGroupCallEvent
.add(GroupCallStateChange.screenshareStreamsChanged);
}
Future<void> _replaceScreenshareStream(
GroupCallSession groupCall,
WrappedMediaStream existingStream,
WrappedMediaStream replacementStream,
) async {
final streamIndex = _screenshareStreams.indexWhere(
(stream) => stream.participant.id == existingStream.participant.id,
);
if (streamIndex == -1) {
throw MatrixSDKVoipException(
'Couldn\'t find screenshare stream to replace',
);
}
_screenshareStreams.replaceRange(streamIndex, 1, [replacementStream]);
await existingStream.dispose();
groupCall.onGroupCallEvent
.add(GroupCallStateChange.screenshareStreamsChanged);
}
Future<void> _removeScreenshareStream(
GroupCallSession groupCall,
WrappedMediaStream stream,
) async {
final streamIndex = _screenshareStreams
.indexWhere((stream) => stream.participant.id == stream.participant.id);
if (streamIndex == -1) {
throw MatrixSDKVoipException(
'Couldn\'t find screenshare stream to remove',
);
}
_screenshareStreams.removeWhere(
(element) => element.participant.id == stream.participant.id,
);
onStreamRemoved.add(stream);
if (stream.isLocal()) {
await stopMediaStream(stream.stream);
}
groupCall.onGroupCallEvent
.add(GroupCallStateChange.screenshareStreamsChanged);
}
Future<void> _onCallStateChanged(CallSession call, CallState state) async {
final audioMuted = localUserMediaStream?.isAudioMuted() ?? true;
if (call.localUserMediaStream != null &&
call.isMicrophoneMuted != audioMuted) {
await call.setMicrophoneMuted(audioMuted);
}
final videoMuted = localUserMediaStream?.isVideoMuted() ?? true;
if (call.localUserMediaStream != null &&
call.isLocalVideoMuted != videoMuted) {
await call.setLocalVideoMuted(videoMuted);
}
}
Future<void> _onCallHangup(
GroupCallSession groupCall,
CallSession call,
) async {
if (call.hangupReason == CallErrorCode.replaced) {
return;
}
await _onStreamsChanged(groupCall, call);
await _removeCall(groupCall, call, call.hangupReason!);
}
Future<void> _addUserMediaStream(
GroupCallSession groupCall,
WrappedMediaStream stream,
) async {
_userMediaStreams.add(stream);
onStreamAdd.add(stream);
groupCall.onGroupCallEvent
.add(GroupCallStateChange.userMediaStreamsChanged);
}
Future<void> _replaceUserMediaStream(
GroupCallSession groupCall,
WrappedMediaStream existingStream,
WrappedMediaStream replacementStream,
) async {
final streamIndex = _userMediaStreams.indexWhere(
(stream) => stream.participant.id == existingStream.participant.id,
);
if (streamIndex == -1) {
throw MatrixSDKVoipException(
'Couldn\'t find user media stream to replace',
);
}
_userMediaStreams.replaceRange(streamIndex, 1, [replacementStream]);
await existingStream.dispose();
groupCall.onGroupCallEvent
.add(GroupCallStateChange.userMediaStreamsChanged);
}
Future<void> _removeUserMediaStream(
GroupCallSession groupCall,
WrappedMediaStream stream,
) async {
final streamIndex = _userMediaStreams.indexWhere(
(element) => element.participant.id == stream.participant.id,
);
if (streamIndex == -1) {
throw MatrixSDKVoipException(
'Couldn\'t find user media stream to remove',
);
}
_userMediaStreams.removeWhere(
(element) => element.participant.id == stream.participant.id,
);
_audioLevelsMap.remove(stream.participant);
onStreamRemoved.add(stream);
if (stream.isLocal()) {
await stopMediaStream(stream.stream);
}
groupCall.onGroupCallEvent
.add(GroupCallStateChange.userMediaStreamsChanged);
if (_activeSpeaker == stream.participant && _userMediaStreams.isNotEmpty) {
_activeSpeaker = _userMediaStreams[0].participant;
groupCall.onGroupCallEvent.add(GroupCallStateChange.activeSpeakerChanged);
}
}
@override
bool get e2eeEnabled => false;
@override
CallParticipant? get activeSpeaker => _activeSpeaker;
@override
WrappedMediaStream? get localUserMediaStream => _localUserMediaStream;
@override
WrappedMediaStream? get localScreenshareStream => _localScreenshareStream;
@override
List<WrappedMediaStream> get userMediaStreams =>
List.unmodifiable(_userMediaStreams);
@override
List<WrappedMediaStream> get screenShareStreams =>
List.unmodifiable(_screenshareStreams);
@override
Future<void> updateMediaDeviceForCalls() async {
for (final call in _callSessions) {
await call.updateMediaDeviceForCall();
}
}
/// Initializes the local user media stream.
/// The media stream must be prepared before the group call enters.
/// if you allow the user to configure their camera and such ahead of time,
/// you can pass that `stream` on to this function.
/// This allows you to configure the camera before joining the call without
/// having to reopen the stream and possibly losing settings.
@override
Future<WrappedMediaStream?> initLocalStream(
GroupCallSession groupCall, {
WrappedMediaStream? stream,
}) async {
if (groupCall.state != GroupCallState.localCallFeedUninitialized) {
throw MatrixSDKVoipException(
'Cannot initialize local call feed in the ${groupCall.state} state.',
);
}
groupCall.setState(GroupCallState.initializingLocalCallFeed);
WrappedMediaStream localWrappedMediaStream;
if (stream == null) {
MediaStream stream;
try {
stream = await _getUserMedia(groupCall, CallType.kVideo);
} catch (error) {
groupCall.setState(GroupCallState.localCallFeedUninitialized);
rethrow;
}
localWrappedMediaStream = WrappedMediaStream(
stream: stream,
participant: groupCall.localParticipant!,
room: groupCall.room,
client: groupCall.client,
purpose: SDPStreamMetadataPurpose.Usermedia,
audioMuted: stream.getAudioTracks().isEmpty,
videoMuted: stream.getVideoTracks().isEmpty,
isGroupCall: true,
voip: groupCall.voip,
);
} else {
localWrappedMediaStream = stream;
}
_localUserMediaStream = localWrappedMediaStream;
await _addUserMediaStream(groupCall, localWrappedMediaStream);
groupCall.setState(GroupCallState.localCallFeedInitialized);
_activeSpeaker = null;
return localWrappedMediaStream;
}
@override
Future<void> setDeviceMuted(
GroupCallSession groupCall,
bool muted,
MediaInputKind kind,
) async {
if (!await hasMediaDevice(groupCall.voip.delegate, kind)) {
return;
}
if (localUserMediaStream != null) {
switch (kind) {
case MediaInputKind.audioinput:
localUserMediaStream!.setAudioMuted(muted);
setTracksEnabled(
localUserMediaStream!.stream!.getAudioTracks(),
!muted,
);
for (final call in _callSessions) {
await call.setMicrophoneMuted(muted);
}
break;
case MediaInputKind.videoinput:
localUserMediaStream!.setVideoMuted(muted);
setTracksEnabled(
localUserMediaStream!.stream!.getVideoTracks(),
!muted,
);
for (final call in _callSessions) {
await call.setLocalVideoMuted(muted);
}
break;
}
}
groupCall.onGroupCallEvent.add(GroupCallStateChange.localMuteStateChanged);
return;
}
void _onIncomingCallInMeshSetup(
GroupCallSession groupCall,
CallSession newCall,
) {
// The incoming calls may be for another room, which we will ignore.
if (newCall.room.id != groupCall.room.id) return;
if (newCall.state != CallState.kRinging) {
Logs().v(
'[_onIncomingCallInMeshSetup] Incoming call no longer in ringing state. Ignoring.',
);
return;
}
if (newCall.groupCallId == null ||
newCall.groupCallId != groupCall.groupCallId) {
Logs().v(
'[_onIncomingCallInMeshSetup] Incoming call with groupCallId ${newCall.groupCallId} ignored because it doesn\'t match the current group call',
);
return;
}
final existingCall = _getCallForParticipant(
groupCall,
CallParticipant(
groupCall.voip,
userId: newCall.remoteUserId!,
deviceId: newCall.remoteDeviceId,
),
);
// if it's an existing call, `_registerListenersForCall` will be called in
// `_replaceCall` that is used in `_onIncomingCallStart`.
if (existingCall != null) return;
Logs().v(
'[_onIncomingCallInMeshSetup] GroupCallSession: incoming call from: ${newCall.remoteUserId}${newCall.remoteDeviceId}${newCall.remotePartyId}',
);
_registerListenersBeforeCallAdd(newCall);
}
Future<void> _onIncomingCallInMeshStart(
GroupCallSession groupCall,
CallSession newCall,
) async {
// The incoming calls may be for another room, which we will ignore.
if (newCall.room.id != groupCall.room.id) {
return;
}
if (newCall.state != CallState.kRinging) {
Logs().v(
'[_onIncomingCallInMeshStart] Incoming call no longer in ringing state. Ignoring.',
);
return;
}
if (newCall.groupCallId == null ||
newCall.groupCallId != groupCall.groupCallId) {
Logs().v(
'[_onIncomingCallInMeshStart] Incoming call with groupCallId ${newCall.groupCallId} ignored because it doesn\'t match the current group call',
);
await newCall.reject();
return;
}
final existingCall = _getCallForParticipant(
groupCall,
CallParticipant(
groupCall.voip,
userId: newCall.remoteUserId!,
deviceId: newCall.remoteDeviceId,
),
);
if (existingCall != null && existingCall.callId == newCall.callId) {
return;
}
Logs().v(
'[_onIncomingCallInMeshStart] GroupCallSession: incoming call from: ${newCall.remoteUserId}${newCall.remoteDeviceId}${newCall.remotePartyId}',
);
// Check if the user calling has an existing call and use this call instead.
if (existingCall != null) {
await _replaceCall(groupCall, existingCall, newCall);
} else {
await _addCall(groupCall, newCall);
}
await newCall.answerWithStreams(_getLocalStreams());
}
@override
Future<void> setScreensharingEnabled(
GroupCallSession groupCall,
bool enabled,
String desktopCapturerSourceId,
) async {
if (enabled == (localScreenshareStream != null)) {
return;
}
if (enabled) {
try {
Logs().v('Asking for screensharing permissions...');
final stream = await _getDisplayMedia(groupCall);
for (final track in stream.getTracks()) {
// screen sharing should only have 1 video track anyway, so this only
// fires once
track.onEnded = () async {
await setScreensharingEnabled(groupCall, false, '');
};
}
Logs().v(
'Screensharing permissions granted. Setting screensharing enabled on all calls',
);
_localScreenshareStream = WrappedMediaStream(
stream: stream,
participant: groupCall.localParticipant!,
room: groupCall.room,
client: groupCall.client,
purpose: SDPStreamMetadataPurpose.Screenshare,
audioMuted: stream.getAudioTracks().isEmpty,
videoMuted: stream.getVideoTracks().isEmpty,
isGroupCall: true,
voip: groupCall.voip,
);
_addScreenshareStream(groupCall, localScreenshareStream!);
groupCall.onGroupCallEvent
.add(GroupCallStateChange.localScreenshareStateChanged);
for (final call in _callSessions) {
await call.addLocalStream(
await localScreenshareStream!.stream!.clone(),
localScreenshareStream!.purpose,
);
}
await groupCall.sendMemberStateEvent();
return;
} catch (e, s) {
Logs().e('[VOIP] Enabling screensharing error', e, s);
groupCall.onGroupCallEvent.add(GroupCallStateChange.error);
return;
}
} else {
for (final call in _callSessions) {
await call.removeLocalStream(call.localScreenSharingStream!);
}
await stopMediaStream(localScreenshareStream?.stream);
await _removeScreenshareStream(groupCall, localScreenshareStream!);
_localScreenshareStream = null;
await groupCall.sendMemberStateEvent();
groupCall.onGroupCallEvent
.add(GroupCallStateChange.localMuteStateChanged);
return;
}
}
@override
Future<void> dispose(GroupCallSession groupCall) async {
if (localUserMediaStream != null) {
await _removeUserMediaStream(groupCall, localUserMediaStream!);
_localUserMediaStream = null;
}
if (localScreenshareStream != null) {
await stopMediaStream(localScreenshareStream!.stream);
await _removeScreenshareStream(groupCall, localScreenshareStream!);
_localScreenshareStream = null;
}
// removeCall removes it from `_callSessions` later.
final callsCopy = _callSessions.toList();
for (final call in callsCopy) {
await _removeCall(groupCall, call, CallErrorCode.userHangup);
}
_activeSpeaker = null;
_activeSpeakerLoopTimeout?.cancel();
await _callSetupSubscription?.cancel();
await _callStartSubscription?.cancel();
}
@override
bool get isLocalVideoMuted {
if (localUserMediaStream != null) {
return localUserMediaStream!.isVideoMuted();
}
return true;
}
@override
bool get isMicrophoneMuted {
if (localUserMediaStream != null) {
return localUserMediaStream!.isAudioMuted();
}
return true;
}
@override
Future<void> setupP2PCallsWithExistingMembers(
GroupCallSession groupCall,
) async {
for (final call in _callSessions) {
_onIncomingCallInMeshSetup(groupCall, call);
await _onIncomingCallInMeshStart(groupCall, call);
}
_callSetupSubscription = groupCall.voip.onIncomingCallSetup.stream.listen(
(newCall) => _onIncomingCallInMeshSetup(groupCall, newCall),
);
_callStartSubscription = groupCall.voip.onIncomingCallStart.stream.listen(
(newCall) => _onIncomingCallInMeshStart(groupCall, newCall),
);
_onActiveSpeakerLoop(groupCall);
}
@override
Future<void> setupP2PCallWithNewMember(
GroupCallSession groupCall,
CallParticipant rp,
CallMembership mem,
) async {
final existingCall = _getCallForParticipant(groupCall, rp);
if (existingCall != null) {
if (existingCall.remoteSessionId != mem.membershipId) {
await existingCall.hangup(reason: CallErrorCode.unknownError);
} else {
Logs().e(
'[VOIP] onMemberStateChanged Not updating _participants list, already have a ongoing call with ${rp.id}',
);
return;
}
}
// Only initiate a call with a participant who has a id that is lexicographically
// less than your own. Otherwise, that user will call you.
if (groupCall.localParticipant!.id.compareTo(rp.id) > 0) {
Logs().i('[VOIP] Waiting for ${rp.id} to send call invite.');
return;
}
final opts = CallOptions(
callId: genCallID(),
room: groupCall.room,
voip: groupCall.voip,
dir: CallDirection.kOutgoing,
localPartyId: groupCall.voip.currentSessionId,
groupCallId: groupCall.groupCallId,
type: CallType.kVideo,
iceServers: await groupCall.voip.getIceServers(),
);
final newCall = groupCall.voip.createNewCall(opts);
/// both invitee userId and deviceId are set here because there can be
/// multiple devices from same user in a call, so we specifiy who the
/// invite is for
///
/// MOVE TO CREATENEWCALL?
newCall.remoteUserId = mem.userId;
newCall.remoteDeviceId = mem.deviceId;
// party id set to when answered
newCall.remoteSessionId = mem.membershipId;
_registerListenersBeforeCallAdd(newCall);
await newCall.placeCallWithStreams(
_getLocalStreams(),
requestScreenSharing: mem.feeds?.any(
(element) =>
element['purpose'] == SDPStreamMetadataPurpose.Screenshare,
) ??
false,
);
await _addCall(groupCall, newCall);
}
@override
List<Map<String, String>>? getCurrentFeeds() {
return _getLocalStreams()
.map(
(feed) => ({
'purpose': feed.purpose,
}),
)
.toList();
}
@override
bool operator ==(Object other) =>
identical(this, other) || (other is MeshBackend && type == other.type);
@override
int get hashCode => type.hashCode;
/// get everything is livekit specific mesh calls shouldn't be affected by these
@override
Future<void> onCallEncryption(
GroupCallSession groupCall,
String userId,
String deviceId,
Map<String, dynamic> content,
) async {
return;
}
@override
Future<void> onCallEncryptionKeyRequest(
GroupCallSession groupCall,
String userId,
String deviceId,
Map<String, dynamic> content,
) async {
return;
}
@override
Future<void> onLeftParticipant(
GroupCallSession groupCall,
List<CallParticipant> anyLeft,
) async {
return;
}
@override
Future<void> onNewParticipant(
GroupCallSession groupCall,
List<CallParticipant> anyJoined,
) async {
return;
}
@override
Future<void> requestEncrytionKey(
GroupCallSession groupCall,
List<CallParticipant> remoteParticipants,
) async {
return;
}
@override
Future<void> preShareKey(GroupCallSession groupCall) async {
return;
}
}