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 _callSessions = []; /// participant:volume final Map _audioLevelsMap = {}; /// The stream is used to prepare for incoming peer calls like registering listeners StreamSubscription? _callSetupSubscription; /// The stream is used to signal the start of an incoming peer call StreamSubscription? _callStartSubscription; Timer? _activeSpeakerLoopTimeout; final CachedStreamController onStreamAdd = CachedStreamController(); final CachedStreamController onStreamRemoved = CachedStreamController(); final CachedStreamController onGroupCallFeedsChanged = CachedStreamController(); @override Map toJson() { return { 'type': type, }; } CallParticipant? _activeSpeaker; WrappedMediaStream? _localUserMediaStream; WrappedMediaStream? _localScreenshareStream; final List _userMediaStreams = []; final List _screenshareStreams = []; List _getLocalStreams() { final feeds = []; if (localUserMediaStream != null) { feeds.add(localUserMediaStream!); } if (localScreenshareStream != null) { feeds.add(localScreenshareStream!); } return feeds; } Future _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 _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 _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 _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 _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 _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 _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.from(_userMediaStreams); for (final stream in userMediaStreamsCopyList) { if (stream.participant.isLocal && stream.pc == null) { continue; } final List 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 _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 _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 _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 _onCallHangup( GroupCallSession groupCall, CallSession call, ) async { if (call.hangupReason == CallErrorCode.replaced) { return; } await _onStreamsChanged(groupCall, call); await _removeCall(groupCall, call, call.hangupReason!); } Future _addUserMediaStream( GroupCallSession groupCall, WrappedMediaStream stream, ) async { _userMediaStreams.add(stream); onStreamAdd.add(stream); groupCall.onGroupCallEvent .add(GroupCallStateChange.userMediaStreamsChanged); } Future _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 _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 get userMediaStreams => List.unmodifiable(_userMediaStreams); @override List get screenShareStreams => List.unmodifiable(_screenshareStreams); @override Future 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 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 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 _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 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 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 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 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>? 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 onCallEncryption( GroupCallSession groupCall, String userId, String deviceId, Map content, ) async { return; } @override Future onCallEncryptionKeyRequest( GroupCallSession groupCall, String userId, String deviceId, Map content, ) async { return; } @override Future onLeftParticipant( GroupCallSession groupCall, List anyLeft, ) async { return; } @override Future onNewParticipant( GroupCallSession groupCall, List anyJoined, ) async { return; } @override Future requestEncrytionKey( GroupCallSession groupCall, List remoteParticipants, ) async { return; } @override Future preShareKey(GroupCallSession groupCall) async { return; } }