LCOV - code coverage report
Current view: top level - lib/src/voip/backend - livekit_backend.dart (source / functions) Hit Total Coverage
Test: merged.info Lines: 0 192 0.0 %
Date: 2024-07-12 20:20:16 Functions: 0 0 -

          Line data    Source code
       1             : import 'dart:async';
       2             : import 'dart:convert';
       3             : import 'dart:typed_data';
       4             : 
       5             : import 'package:matrix/matrix.dart';
       6             : import 'package:matrix/src/utils/crypto/crypto.dart';
       7             : import 'package:matrix/src/voip/models/call_membership.dart';
       8             : 
       9             : class LiveKitBackend extends CallBackend {
      10             :   final String livekitServiceUrl;
      11             :   final String livekitAlias;
      12             : 
      13             :   /// A delay after a member leaves before we create and publish a new key, because people
      14             :   /// tend to leave calls at the same time
      15             :   final Duration makeKeyDelay;
      16             : 
      17             :   /// The delay between creating and sending a new key and starting to encrypt with it. This gives others
      18             :   /// a chance to receive the new key to minimise the chance they don't get media they can't decrypt.
      19             :   /// The total time between a member leaving and the call switching to new keys is therefore
      20             :   /// makeKeyDelay + useKeyDelay
      21             :   final Duration useKeyDelay;
      22             : 
      23             :   @override
      24             :   final bool e2eeEnabled;
      25             : 
      26           0 :   LiveKitBackend({
      27             :     required this.livekitServiceUrl,
      28             :     required this.livekitAlias,
      29             :     super.type = 'livekit',
      30             :     this.e2eeEnabled = true,
      31             :     this.makeKeyDelay = CallTimeouts.makeKeyDelay,
      32             :     this.useKeyDelay = CallTimeouts.useKeyDelay,
      33             :   });
      34             : 
      35             :   Timer? _memberLeaveEncKeyRotateDebounceTimer;
      36             : 
      37             :   /// participant:keyIndex:keyBin
      38             :   final Map<CallParticipant, Map<int, Uint8List>> _encryptionKeysMap = {};
      39             : 
      40             :   final List<Future<void>> _setNewKeyTimeouts = [];
      41             : 
      42             :   int _indexCounter = 0;
      43             : 
      44             :   /// used to send the key again incase someone `onCallEncryptionKeyRequest` but don't just send
      45             :   /// the last one because you also cycle back in your window which means you
      46             :   /// could potentially end up sharing a past key
      47           0 :   int get latestLocalKeyIndex => _latestLocalKeyIndex;
      48             :   int _latestLocalKeyIndex = 0;
      49             : 
      50             :   /// the key currently being used by the local cryptor, can possibly not be the latest
      51             :   /// key, check `latestLocalKeyIndex` for latest key
      52           0 :   int get currentLocalKeyIndex => _currentLocalKeyIndex;
      53             :   int _currentLocalKeyIndex = 0;
      54             : 
      55           0 :   Map<int, Uint8List>? _getKeysForParticipant(CallParticipant participant) {
      56           0 :     return _encryptionKeysMap[participant];
      57             :   }
      58             : 
      59             :   /// always chooses the next possible index, we cycle after 16 because
      60             :   /// no real adv with infinite list
      61           0 :   int _getNewEncryptionKeyIndex() {
      62           0 :     final newIndex = _indexCounter % 16;
      63           0 :     _indexCounter++;
      64             :     return newIndex;
      65             :   }
      66             : 
      67           0 :   @override
      68             :   Future<void> preShareKey(GroupCallSession groupCall) async {
      69           0 :     await groupCall.onMemberStateChanged();
      70           0 :     await _changeEncryptionKey(groupCall, groupCall.participants, false);
      71             :   }
      72             : 
      73             :   /// makes a new e2ee key for local user and sets it with a delay if specified
      74             :   /// used on first join and when someone leaves
      75             :   ///
      76             :   /// also does the sending for you
      77           0 :   Future<void> _makeNewSenderKey(
      78             :       GroupCallSession groupCall, bool delayBeforeUsingKeyOurself) async {
      79           0 :     final key = secureRandomBytes(32);
      80           0 :     final keyIndex = _getNewEncryptionKeyIndex();
      81           0 :     Logs().i('[VOIP E2EE] Generated new key $key at index $keyIndex');
      82             : 
      83           0 :     await _setEncryptionKey(
      84             :       groupCall,
      85           0 :       groupCall.localParticipant!,
      86             :       keyIndex,
      87             :       key,
      88             :       delayBeforeUsingKeyOurself: delayBeforeUsingKeyOurself,
      89             :       send: true,
      90             :     );
      91             :   }
      92             : 
      93             :   DateTime lastRatchetAt = DateTime(1980);
      94             : 
      95             :   /// also does the sending for you
      96           0 :   Future<void> _ratchetLocalParticipantKey(
      97             :     GroupCallSession groupCall,
      98             :     List<CallParticipant> sendTo,
      99             :   ) async {
     100           0 :     final keyProvider = groupCall.voip.delegate.keyProvider;
     101             : 
     102             :     if (keyProvider == null) {
     103           0 :       throw MatrixSDKVoipException(
     104             :           '_ratchetKey called but KeyProvider was null');
     105             :     }
     106             : 
     107           0 :     final myKeys = _encryptionKeysMap[groupCall.localParticipant];
     108             : 
     109           0 :     if (myKeys == null || myKeys.isEmpty) {
     110           0 :       await _makeNewSenderKey(groupCall, false);
     111             :       return;
     112             :     }
     113             : 
     114           0 :     if (_currentLocalKeyIndex != _latestLocalKeyIndex) {
     115             :       /// Leave causes rotate, new user joins after making new key but
     116             :       /// before using new key, this then causes a ratchet of the latestLocalKey
     117             :       /// returns null until that key is set when useKeyDelay is done.
     118             :       ///
     119             :       /// You will see some onRatchetKey sending empty responses here
     120             :       /// therefore the below while loop.
     121           0 :       Logs().w(
     122           0 :           '[VOIP E2EE] Leave and join / rotate and ratchet scenario detected, expect ${useKeyDelay.inSeconds} seconds disruption. latest: $latestLocalKeyIndex, current: $currentLocalKeyIndex');
     123             :     }
     124             : 
     125           0 :     if (lastRatchetAt.isBefore(DateTime.now().subtract(makeKeyDelay))) {
     126             :       Uint8List? ratchedKey;
     127           0 :       while (ratchedKey == null || ratchedKey.isEmpty) {
     128           0 :         Logs().d(
     129           0 :             '[VOIP E2EE] Ignoring empty ratcheted key, probably waiting for useKeyDelay to finish, expect around ${useKeyDelay.inSeconds} seconds of disruption. latest: $latestLocalKeyIndex, current: $currentLocalKeyIndex');
     130           0 :         ratchedKey = await keyProvider.onRatchetKey(
     131           0 :           groupCall.localParticipant!,
     132           0 :           latestLocalKeyIndex,
     133             :         );
     134             :       }
     135           0 :       lastRatchetAt = DateTime.now();
     136           0 :       Logs().i(
     137           0 :           '[VOIP E2EE] Ratched latest key to $ratchedKey at idx $latestLocalKeyIndex');
     138           0 :       await _setEncryptionKey(
     139             :         groupCall,
     140           0 :         groupCall.localParticipant!,
     141           0 :         latestLocalKeyIndex,
     142             :         ratchedKey,
     143             :         delayBeforeUsingKeyOurself: false,
     144             :         send: true,
     145             :         sendTo: sendTo,
     146             :       );
     147             :     } else {
     148           0 :       Logs().d(
     149           0 :           '[VOIP E2EE] Skipped ratcheting because lastRatchet run was at ${lastRatchetAt.millisecondsSinceEpoch}');
     150             :       // send without setting because it is already set
     151           0 :       await _sendEncryptionKeysEvent(
     152             :         groupCall,
     153           0 :         latestLocalKeyIndex,
     154             :         sendTo: sendTo,
     155             :       );
     156             :     }
     157             :   }
     158             : 
     159           0 :   Future<void> _changeEncryptionKey(
     160             :     GroupCallSession groupCall,
     161             :     List<CallParticipant> anyJoined,
     162             :     bool delayBeforeUsingKeyOurself,
     163             :   ) async {
     164           0 :     if (!e2eeEnabled) return;
     165           0 :     if (groupCall.voip.enableSFUE2EEKeyRatcheting) {
     166           0 :       await _ratchetLocalParticipantKey(groupCall, anyJoined);
     167             :     } else {
     168           0 :       await _makeNewSenderKey(groupCall, delayBeforeUsingKeyOurself);
     169             :     }
     170             :   }
     171             : 
     172             :   /// sets incoming keys and also sends the key if it was for the local user
     173             :   /// if sendTo is null, its sent to all _participants, see `_sendEncryptionKeysEvent`
     174           0 :   Future<void> _setEncryptionKey(
     175             :     GroupCallSession groupCall,
     176             :     CallParticipant participant,
     177             :     int encryptionKeyIndex,
     178             :     Uint8List encryptionKeyBin, {
     179             :     bool delayBeforeUsingKeyOurself = false,
     180             :     bool send = false,
     181             :     List<CallParticipant>? sendTo,
     182             :   }) async {
     183             :     final encryptionKeys =
     184           0 :         _encryptionKeysMap[participant] ?? <int, Uint8List>{};
     185             : 
     186           0 :     encryptionKeys[encryptionKeyIndex] = encryptionKeyBin;
     187           0 :     _encryptionKeysMap[participant] = encryptionKeys;
     188           0 :     if (participant.isLocal) {
     189           0 :       _latestLocalKeyIndex = encryptionKeyIndex;
     190             :     }
     191             : 
     192             :     if (send) {
     193           0 :       await _sendEncryptionKeysEvent(
     194             :         groupCall,
     195             :         encryptionKeyIndex,
     196             :         sendTo: sendTo,
     197             :       );
     198             :     }
     199             : 
     200             :     if (delayBeforeUsingKeyOurself) {
     201             :       // now wait for the key to propogate and then set it, hopefully users can
     202             :       // stil decrypt everything
     203           0 :       final useKeyTimeout = Future<void>.delayed(useKeyDelay, () async {
     204           0 :         Logs().i(
     205           0 :             '[VOIP E2EE] setting key changed event for ${participant.id} idx $encryptionKeyIndex key $encryptionKeyBin');
     206           0 :         await groupCall.voip.delegate.keyProvider?.onSetEncryptionKey(
     207             :             participant, encryptionKeyBin, encryptionKeyIndex);
     208           0 :         if (participant.isLocal) {
     209           0 :           _currentLocalKeyIndex = encryptionKeyIndex;
     210             :         }
     211             :       });
     212           0 :       _setNewKeyTimeouts.add(useKeyTimeout);
     213             :     } else {
     214           0 :       Logs().i(
     215           0 :           '[VOIP E2EE] setting key changed event for ${participant.id} idx $encryptionKeyIndex key $encryptionKeyBin');
     216           0 :       await groupCall.voip.delegate.keyProvider?.onSetEncryptionKey(
     217             :           participant, encryptionKeyBin, encryptionKeyIndex);
     218           0 :       if (participant.isLocal) {
     219           0 :         _currentLocalKeyIndex = encryptionKeyIndex;
     220             :       }
     221             :     }
     222             :   }
     223             : 
     224             :   /// sends the enc key to the devices using todevice, passing a list of
     225             :   /// sendTo only sends events to them
     226             :   /// setting keyIndex to null will send the latestKey
     227           0 :   Future<void> _sendEncryptionKeysEvent(
     228             :     GroupCallSession groupCall,
     229             :     int keyIndex, {
     230             :     List<CallParticipant>? sendTo,
     231             :   }) async {
     232           0 :     final myKeys = _getKeysForParticipant(groupCall.localParticipant!);
     233           0 :     final myLatestKey = myKeys?[keyIndex];
     234             : 
     235             :     final sendKeysTo =
     236           0 :         sendTo ?? groupCall.participants.where((p) => !p.isLocal);
     237             : 
     238             :     if (myKeys == null || myLatestKey == null) {
     239           0 :       Logs().w(
     240             :           '[VOIP E2EE] _sendEncryptionKeysEvent Tried to send encryption keys event but no keys found!');
     241           0 :       await _makeNewSenderKey(groupCall, false);
     242           0 :       await _sendEncryptionKeysEvent(
     243             :         groupCall,
     244             :         keyIndex,
     245             :         sendTo: sendTo,
     246             :       );
     247             :       return;
     248             :     }
     249             : 
     250             :     try {
     251           0 :       final keyContent = EncryptionKeysEventContent(
     252           0 :         [EncryptionKeyEntry(keyIndex, base64Encode(myLatestKey))],
     253           0 :         groupCall.groupCallId,
     254             :       );
     255           0 :       final Map<String, Object> data = {
     256           0 :         ...keyContent.toJson(),
     257             :         // used to find group call in groupCalls when ToDeviceEvent happens,
     258             :         // plays nicely with backwards compatibility for mesh calls
     259           0 :         'conf_id': groupCall.groupCallId,
     260           0 :         'device_id': groupCall.client.deviceID!,
     261           0 :         'room_id': groupCall.room.id,
     262             :       };
     263           0 :       await _sendToDeviceEvent(
     264             :         groupCall,
     265           0 :         sendTo ?? sendKeysTo.toList(),
     266             :         data,
     267             :         EventTypes.GroupCallMemberEncryptionKeys,
     268             :       );
     269             :     } catch (e, s) {
     270           0 :       Logs().e('[VOIP] Failed to send e2ee keys, retrying', e, s);
     271           0 :       await _sendEncryptionKeysEvent(
     272             :         groupCall,
     273             :         keyIndex,
     274             :         sendTo: sendTo,
     275             :       );
     276             :     }
     277             :   }
     278             : 
     279           0 :   Future<void> _sendToDeviceEvent(
     280             :     GroupCallSession groupCall,
     281             :     List<CallParticipant> remoteParticipants,
     282             :     Map<String, Object> data,
     283             :     String eventType,
     284             :   ) async {
     285           0 :     if (remoteParticipants.isEmpty) return;
     286           0 :     Logs().d(
     287           0 :         '[VOIP] _sendToDeviceEvent: sending ${data.toString()} to ${remoteParticipants.map((e) => e.id)} ');
     288             :     final txid =
     289           0 :         VoIP.customTxid ?? groupCall.client.generateUniqueTransactionId();
     290             :     final mustEncrypt =
     291           0 :         groupCall.room.encrypted && groupCall.client.encryptionEnabled;
     292             : 
     293             :     // could just combine the two but do not want to rewrite the enc thingy
     294             :     // wrappers here again.
     295           0 :     final List<DeviceKeys> mustEncryptkeysToSendTo = [];
     296             :     final Map<String, Map<String, Map<String, Object>>> unencryptedDataToSend =
     297           0 :         {};
     298             : 
     299           0 :     for (final participant in remoteParticipants) {
     300           0 :       if (participant.deviceId == null) continue;
     301             :       if (mustEncrypt) {
     302           0 :         await groupCall.client.userDeviceKeysLoading;
     303           0 :         final deviceKey = groupCall.client.userDeviceKeys[participant.userId]
     304           0 :             ?.deviceKeys[participant.deviceId];
     305             :         if (deviceKey != null) {
     306           0 :           mustEncryptkeysToSendTo.add(deviceKey);
     307             :         }
     308             :       } else {
     309           0 :         unencryptedDataToSend.addAll({
     310           0 :           participant.userId: {participant.deviceId!: data}
     311             :         });
     312             :       }
     313             :     }
     314             : 
     315             :     // prepped data, now we send
     316             :     if (mustEncrypt) {
     317           0 :       await groupCall.client.sendToDeviceEncrypted(
     318             :         mustEncryptkeysToSendTo,
     319             :         eventType,
     320             :         data,
     321             :       );
     322             :     } else {
     323           0 :       await groupCall.client.sendToDevice(
     324             :         eventType,
     325             :         txid,
     326             :         unencryptedDataToSend,
     327             :       );
     328             :     }
     329             :   }
     330             : 
     331           0 :   @override
     332             :   Map<String, Object?> toJson() {
     333           0 :     return {
     334           0 :       'type': type,
     335           0 :       'livekit_service_url': livekitServiceUrl,
     336           0 :       'livekit_alias': livekitAlias,
     337             :     };
     338             :   }
     339             : 
     340           0 :   @override
     341             :   Future<void> requestEncrytionKey(
     342             :     GroupCallSession groupCall,
     343             :     List<CallParticipant> remoteParticipants,
     344             :   ) async {
     345           0 :     final Map<String, Object> data = {
     346           0 :       'conf_id': groupCall.groupCallId,
     347           0 :       'device_id': groupCall.client.deviceID!,
     348           0 :       'room_id': groupCall.room.id,
     349             :     };
     350             : 
     351           0 :     await _sendToDeviceEvent(
     352             :       groupCall,
     353             :       remoteParticipants,
     354             :       data,
     355             :       EventTypes.GroupCallMemberEncryptionKeysRequest,
     356             :     );
     357             :   }
     358             : 
     359           0 :   @override
     360             :   Future<void> onCallEncryption(
     361             :     GroupCallSession groupCall,
     362             :     String userId,
     363             :     String deviceId,
     364             :     Map<String, dynamic> content,
     365             :   ) async {
     366           0 :     if (!e2eeEnabled) {
     367           0 :       Logs().w('[VOIP] got sframe key but we do not support e2ee');
     368             :       return;
     369             :     }
     370           0 :     final keyContent = EncryptionKeysEventContent.fromJson(content);
     371             : 
     372           0 :     final callId = keyContent.callId;
     373             :     final p =
     374           0 :         CallParticipant(groupCall.voip, userId: userId, deviceId: deviceId);
     375             : 
     376           0 :     if (keyContent.keys.isEmpty) {
     377           0 :       Logs().w(
     378           0 :           '[VOIP E2EE] Received m.call.encryption_keys where keys is empty: callId=$callId');
     379             :       return;
     380             :     } else {
     381           0 :       Logs().i(
     382           0 :           '[VOIP E2EE]: onCallEncryption, got keys from ${p.id} ${keyContent.toJson()}');
     383             :     }
     384             : 
     385           0 :     for (final key in keyContent.keys) {
     386           0 :       final encryptionKey = key.key;
     387           0 :       final encryptionKeyIndex = key.index;
     388           0 :       await _setEncryptionKey(
     389             :         groupCall,
     390             :         p,
     391             :         encryptionKeyIndex,
     392             :         // base64Decode here because we receive base64Encoded version
     393           0 :         base64Decode(encryptionKey),
     394             :         delayBeforeUsingKeyOurself: false,
     395             :         send: false,
     396             :       );
     397             :     }
     398             :   }
     399             : 
     400           0 :   @override
     401             :   Future<void> onCallEncryptionKeyRequest(
     402             :     GroupCallSession groupCall,
     403             :     String userId,
     404             :     String deviceId,
     405             :     Map<String, dynamic> content,
     406             :   ) async {
     407           0 :     if (!e2eeEnabled) {
     408           0 :       Logs().w('[VOIP] got sframe key request but we do not support e2ee');
     409             :       return;
     410             :     }
     411           0 :     final mems = groupCall.room.getCallMembershipsForUser(userId);
     412             :     if (mems
     413           0 :         .where(
     414           0 :           (mem) =>
     415           0 :               mem.callId == groupCall.groupCallId &&
     416           0 :               mem.userId == userId &&
     417           0 :               mem.deviceId == deviceId &&
     418           0 :               !mem.isExpired &&
     419             :               // sanity checks
     420           0 :               mem.backend.type == groupCall.backend.type &&
     421           0 :               mem.roomId == groupCall.room.id &&
     422           0 :               mem.application == groupCall.application,
     423             :         )
     424           0 :         .isNotEmpty) {
     425           0 :       Logs().d(
     426           0 :           '[VOIP] onCallEncryptionKeyRequest: request checks out, sending key on index: $latestLocalKeyIndex to $userId:$deviceId');
     427           0 :       await _sendEncryptionKeysEvent(
     428             :         groupCall,
     429           0 :         _latestLocalKeyIndex,
     430           0 :         sendTo: [
     431           0 :           CallParticipant(
     432           0 :             groupCall.voip,
     433             :             userId: userId,
     434             :             deviceId: deviceId,
     435             :           )
     436             :         ],
     437             :       );
     438             :     }
     439             :   }
     440             : 
     441           0 :   @override
     442             :   Future<void> onNewParticipant(
     443             :     GroupCallSession groupCall,
     444             :     List<CallParticipant> anyJoined,
     445             :   ) =>
     446           0 :       _changeEncryptionKey(groupCall, anyJoined, true);
     447             : 
     448           0 :   @override
     449             :   Future<void> onLeftParticipant(
     450             :     GroupCallSession groupCall,
     451             :     List<CallParticipant> anyLeft,
     452             :   ) async {
     453           0 :     _encryptionKeysMap.removeWhere((key, value) => anyLeft.contains(key));
     454             : 
     455             :     // debounce it because people leave at the same time
     456           0 :     if (_memberLeaveEncKeyRotateDebounceTimer != null) {
     457           0 :       _memberLeaveEncKeyRotateDebounceTimer!.cancel();
     458             :     }
     459           0 :     _memberLeaveEncKeyRotateDebounceTimer = Timer(makeKeyDelay, () async {
     460           0 :       await _makeNewSenderKey(groupCall, true);
     461             :     });
     462             :   }
     463             : 
     464           0 :   @override
     465             :   Future<void> dispose(GroupCallSession groupCall) async {
     466             :     // only remove our own, to save requesting if we join again, yes the other side
     467             :     // will send it anyway but welp
     468           0 :     _encryptionKeysMap.remove(groupCall.localParticipant!);
     469           0 :     _currentLocalKeyIndex = 0;
     470           0 :     _latestLocalKeyIndex = 0;
     471           0 :     _memberLeaveEncKeyRotateDebounceTimer?.cancel();
     472             :   }
     473             : 
     474           0 :   @override
     475             :   List<Map<String, String>>? getCurrentFeeds() {
     476             :     return null;
     477             :   }
     478             : 
     479           0 :   @override
     480             :   bool operator ==(Object other) =>
     481             :       identical(this, other) ||
     482           0 :       other is LiveKitBackend &&
     483           0 :           type == other.type &&
     484           0 :           livekitServiceUrl == other.livekitServiceUrl &&
     485           0 :           livekitAlias == other.livekitAlias;
     486             : 
     487           0 :   @override
     488             :   int get hashCode =>
     489           0 :       type.hashCode ^ livekitServiceUrl.hashCode ^ livekitAlias.hashCode;
     490             : 
     491             :   /// get everything else from your livekit sdk in your client
     492           0 :   @override
     493             :   Future<WrappedMediaStream?> initLocalStream(GroupCallSession groupCall,
     494             :       {WrappedMediaStream? stream}) async {
     495             :     return null;
     496             :   }
     497             : 
     498           0 :   @override
     499             :   CallParticipant? get activeSpeaker => null;
     500             : 
     501             :   /// these are unimplemented on purpose so that you know you have
     502             :   /// used the wrong method
     503           0 :   @override
     504             :   bool get isLocalVideoMuted =>
     505           0 :       throw UnimplementedError('Use livekit sdk for this');
     506             : 
     507           0 :   @override
     508             :   bool get isMicrophoneMuted =>
     509           0 :       throw UnimplementedError('Use livekit sdk for this');
     510             : 
     511           0 :   @override
     512             :   WrappedMediaStream? get localScreenshareStream =>
     513           0 :       throw UnimplementedError('Use livekit sdk for this');
     514             : 
     515           0 :   @override
     516             :   WrappedMediaStream? get localUserMediaStream =>
     517           0 :       throw UnimplementedError('Use livekit sdk for this');
     518             : 
     519           0 :   @override
     520             :   List<WrappedMediaStream> get screenShareStreams =>
     521           0 :       throw UnimplementedError('Use livekit sdk for this');
     522             : 
     523           0 :   @override
     524             :   List<WrappedMediaStream> get userMediaStreams =>
     525           0 :       throw UnimplementedError('Use livekit sdk for this');
     526             : 
     527           0 :   @override
     528             :   Future<void> setDeviceMuted(
     529             :       GroupCallSession groupCall, bool muted, MediaInputKind kind) async {
     530             :     return;
     531             :   }
     532             : 
     533           0 :   @override
     534             :   Future<void> setScreensharingEnabled(GroupCallSession groupCall, bool enabled,
     535             :       String desktopCapturerSourceId) async {
     536             :     return;
     537             :   }
     538             : 
     539           0 :   @override
     540             :   Future<void> setupP2PCallWithNewMember(GroupCallSession groupCall,
     541             :       CallParticipant rp, CallMembership mem) async {
     542             :     return;
     543             :   }
     544             : 
     545           0 :   @override
     546             :   Future<void> setupP2PCallsWithExistingMembers(
     547             :       GroupCallSession groupCall) async {
     548             :     return;
     549             :   }
     550             : 
     551           0 :   @override
     552             :   Future<void> updateMediaDeviceForCalls() async {
     553             :     return;
     554             :   }
     555             : }

Generated by: LCOV version 1.14