diff --git a/.changes/fix-session-start-reentrance b/.changes/fix-session-start-reentrance new file mode 100644 index 000000000..286024ba9 --- /dev/null +++ b/.changes/fix-session-start-reentrance @@ -0,0 +1 @@ +patch type="fixed" "Guard Session.start() against concurrent calls" diff --git a/.changes/simplify-session-e2ee b/.changes/simplify-session-e2ee new file mode 100644 index 000000000..d63c8d54c --- /dev/null +++ b/.changes/simplify-session-e2ee @@ -0,0 +1 @@ +minor type="added" "Simplify enabling E2EE with Session API" diff --git a/lib/src/agent/session.dart b/lib/src/agent/session.dart index deae32893..43d44922c 100644 --- a/lib/src/agent/session.dart +++ b/lib/src/agent/session.dart @@ -150,6 +150,7 @@ class Session extends DisposableChangeNotifier { final _TokenSourceConfiguration _tokenSourceConfiguration; final Agent _agent = Agent(); + bool _isStarting = false; Agent get agent => _agent; SessionError? get error => _error; @@ -175,12 +176,20 @@ class Session extends DisposableChangeNotifier { EventsListener? _roomListener; Timer? _agentTimeoutTimer; + /// Enables or disables end-to-end encryption for the session. + /// + /// Requires that encryption was configured via [SessionOptions] (by passing + /// `encryption:`) or that the [Room] was created with [E2EEOptions]. + /// Throws [LiveKitE2EEException] if encryption was not configured. + Future setEncryptionEnabled(bool enabled) => room.setE2EEEnabled(enabled); + /// Starts the session by fetching credentials and connecting to the room. Future start() async { - if (room.connectionState != ConnectionState.disconnected) { + if (_isStarting || room.connectionState != ConnectionState.disconnected) { logger.info('Session.start() ignored: room already connecting or connected.'); return; } + _isStarting = true; _setError(null); _agentTimeoutTimer?.cancel(); @@ -229,6 +238,8 @@ class Session extends DisposableChangeNotifier { _setError(SessionError.connection(error)); _setConnectionState(ConnectionState.disconnected); _agent.disconnected(); + } finally { + _isStarting = false; } } diff --git a/lib/src/agent/session_options.dart b/lib/src/agent/session_options.dart index eb6d262c5..f912687c9 100644 --- a/lib/src/agent/session_options.dart +++ b/lib/src/agent/session_options.dart @@ -13,10 +13,16 @@ // limitations under the License. import '../core/room.dart'; +import '../e2ee/options.dart'; +import '../options.dart'; /// Options for creating a [Session]. class SessionOptions { /// The underlying [Room] used by the session. + /// + /// If neither [room] nor [encryption] is provided, a default [Room] is + /// created. Passing both throws [ArgumentError] — configure E2EE on the + /// [Room] directly if you need a custom [Room] with encryption. final Room room; /// Whether to enable audio pre-connect with [PreConnectAudioBuffer]. @@ -30,11 +36,33 @@ class SessionOptions { /// to a failed state. final Duration agentConnectTimeout; + /// Creates [SessionOptions]. + /// + /// Pass [encryption] to configure end-to-end encryption on the internally + /// created [Room]. Use [E2EEOptions.sharedKey] for the common shared-key + /// case. For advanced setups (custom [RoomOptions], per-participant keys), + /// build a [Room] yourself and pass it via [room] instead. + /// + /// Passing both [room] and [encryption] throws [ArgumentError]. SessionOptions({ Room? room, + E2EEOptions? encryption, this.preConnectAudio = true, this.agentConnectTimeout = const Duration(seconds: 20), - }) : room = room ?? Room(); + }) : room = _buildRoom(room, encryption); + + static Room _buildRoom(Room? room, E2EEOptions? encryption) { + if (room != null && encryption != null) { + throw ArgumentError( + 'SessionOptions: pass either `room` or `encryption`, not both. ' + 'To use encryption with a custom Room, configure E2EE on the Room directly.', + ); + } + if (encryption != null) { + return Room(roomOptions: RoomOptions(encryption: encryption)); + } + return room ?? Room(); + } SessionOptions copyWith({ Room? room, diff --git a/lib/src/e2ee/options.dart b/lib/src/e2ee/options.dart index 4b26fecee..bfb1f01cb 100644 --- a/lib/src/e2ee/options.dart +++ b/lib/src/e2ee/options.dart @@ -24,4 +24,12 @@ class E2EEOptions { final BaseKeyProvider keyProvider; final EncryptionType encryptionType = EncryptionType.kGcm; const E2EEOptions({required this.keyProvider}); + + /// Creates [E2EEOptions] configured with a shared-key [BaseKeyProvider] + /// derived from the given passphrase. + static Future sharedKey(String key) async { + final keyProvider = await BaseKeyProvider.create(); + await keyProvider.setSharedKey(key); + return E2EEOptions(keyProvider: keyProvider); + } }