diff --git a/.changes/connect-room-options-ignored b/.changes/connect-room-options-ignored new file mode 100644 index 000000000..7827fc1da --- /dev/null +++ b/.changes/connect-room-options-ignored @@ -0,0 +1 @@ +patch type="fixed" "Room.connect no longer ignores the roomOptions argument passed to it" diff --git a/lib/src/core/room.dart b/lib/src/core/room.dart index 60916913a..34efd6cd6 100644 --- a/lib/src/core/room.dart +++ b/lib/src/core/room.dart @@ -274,21 +274,26 @@ class Room extends DisposableChangeNotifier with EventsEmittable { @Deprecated('deprecated, please use roomOptions in Room constructor') RoomOptions? roomOptions, FastConnectOptions? fastConnectOptions, }) async { - var roomOptions = this.roomOptions; - if (lkPlatformIs(PlatformType.web) && (roomOptions.networkOptions.certificatePinning?.isEnabled ?? false)) { + var effectiveRoomOptions = roomOptions ?? this.roomOptions; + if (lkPlatformIs(PlatformType.web) && + (effectiveRoomOptions.networkOptions.certificatePinning?.isEnabled ?? false)) { throw UnsupportedError('Certificate pinning is not supported on Flutter web, ' 'remove certificatePinning from NetworkOptions when targeting web'); } connectOptions ??= ConnectOptions(); _pendingTrackQueue.updateTtl(connectOptions.timeouts.subscribe); // ignore: deprecated_member_use_from_same_package - if ((roomOptions.encryption != null || roomOptions.e2eeOptions != null) && engine.e2eeManager == null) { + if ((effectiveRoomOptions.encryption != null || effectiveRoomOptions.e2eeOptions != null) && + engine.e2eeManager == null) { if (!lkPlatformSupportsE2EE()) { throw LiveKitE2EEException('E2EE is not supported on this platform'); } // ignore: deprecated_member_use_from_same_package - final e2eeOptions = roomOptions.encryption ?? roomOptions.e2eeOptions; - _e2eeManager = E2EEManager(e2eeOptions!.keyProvider, dcEncryptionEnabled: roomOptions.encryption != null); + final e2eeOptions = effectiveRoomOptions.encryption ?? effectiveRoomOptions.e2eeOptions; + _e2eeManager = E2EEManager( + e2eeOptions!.keyProvider, + dcEncryptionEnabled: effectiveRoomOptions.encryption != null, + ); await _e2eeManager!.setup(this); engine.setE2eeManager(_e2eeManager); } else { @@ -297,8 +302,8 @@ class Room extends DisposableChangeNotifier with EventsEmittable { if (_e2eeManager != null) { // Disable backup codec when e2ee is enabled - roomOptions = roomOptions.copyWith( - defaultVideoPublishOptions: roomOptions.defaultVideoPublishOptions.copyWith( + effectiveRoomOptions = effectiveRoomOptions.copyWith( + defaultVideoPublishOptions: effectiveRoomOptions.defaultVideoPublishOptions.copyWith( backupVideoCodec: const BackupVideoCodec(enabled: false), ), ); @@ -310,7 +315,11 @@ class Room extends DisposableChangeNotifier with EventsEmittable { } if (isCloudUrl(Uri.parse(url))) { if (_regionUrlProvider == null) { - _regionUrlProvider = RegionUrlProvider(url: url, token: token, networkOptions: roomOptions.networkOptions); + _regionUrlProvider = RegionUrlProvider( + url: url, + token: token, + networkOptions: effectiveRoomOptions.networkOptions, + ); } else { _regionUrlProvider?.updateToken(token); } @@ -328,7 +337,7 @@ class Room extends DisposableChangeNotifier with EventsEmittable { // AudioManager once, on the first connect. Skipping it on a later manual // connect of the same Room keeps a runtime speaker change from being // reverted. New code should call setSpeakerOutputPreferred directly. - final legacySpeakerOn = roomOptions.defaultAudioOutputOptions.speakerOn; + final legacySpeakerOn = effectiveRoomOptions.defaultAudioOutputOptions.speakerOn; if (legacySpeakerOn != null && !_legacySpeakerBridged && lkPlatformIsMobile()) { _legacySpeakerBridged = true; await AudioManager.instance.setSpeakerOutputPreferred(legacySpeakerOn); @@ -343,7 +352,7 @@ class Room extends DisposableChangeNotifier with EventsEmittable { _regionUrl ?? url, token, connectOptions: connectOptions, - roomOptions: roomOptions, + roomOptions: effectiveRoomOptions, fastConnectOptions: fastConnectOptions, regionUrlProvider: _regionUrlProvider, ); @@ -366,7 +375,7 @@ class Room extends DisposableChangeNotifier with EventsEmittable { nextUrl, token, connectOptions: connectOptions, - roomOptions: roomOptions, + roomOptions: effectiveRoomOptions, fastConnectOptions: fastConnectOptions, regionUrlProvider: _regionUrlProvider, ); diff --git a/test/core/connect_options_test.dart b/test/core/connect_options_test.dart new file mode 100644 index 000000000..1587e0e48 --- /dev/null +++ b/test/core/connect_options_test.dart @@ -0,0 +1,58 @@ +// Copyright 2026 LiveKit, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +@Timeout(Duration(seconds: 10)) +library; + +import 'package:flutter_test/flutter_test.dart'; + +import 'package:livekit_client/livekit_client.dart'; +import '../mock/e2e_container.dart'; +import '../mock/peerconnection_mock.dart'; + +void main() { + setUp(resetMockDataChannels); + + group('Room.connect options', () { + // Regression: the deprecated `roomOptions` parameter was shadowed by a local of the same name + // in the first line of `connect`, which Dart permits silently. Everything passed here was + // discarded, so callers saw the Room's own options with no indication anything was wrong. + test('honors the roomOptions passed to connect', () async { + final container = E2EContainer( + roomOptions: const RoomOptions(dynacast: false, adaptiveStream: false), + ); + addTearDown(container.dispose); + + await container.connectRoom( + // ignore: deprecated_member_use_from_same_package + roomOptions: const RoomOptions(dynacast: true, adaptiveStream: true), + ); + + expect(container.room.roomOptions.dynacast, isTrue); + expect(container.room.roomOptions.adaptiveStream, isTrue); + }); + + test('falls back to the Room\'s options when connect is given none', () async { + final container = E2EContainer( + roomOptions: const RoomOptions(dynacast: true, adaptiveStream: true), + ); + addTearDown(container.dispose); + + await container.connectRoom(); + + expect(container.room.roomOptions.dynacast, isTrue); + expect(container.room.roomOptions.adaptiveStream, isTrue); + }); + }); +} diff --git a/test/mock/e2e_container.dart b/test/mock/e2e_container.dart index 2d6ef706d..7529c5cd8 100644 --- a/test/mock/e2e_container.dart +++ b/test/mock/e2e_container.dart @@ -37,12 +37,12 @@ class E2EContainer { /// since [connectRoom] returned. Populated only when [captureOutbound] is true. final List capturedDataPackets = []; - E2EContainer() { + E2EContainer({RoomOptions roomOptions = const RoomOptions()}) { wsConnector = MockWebSocketConnector(); client = SignalClient(wsConnector.connect); engine = Engine( connectOptions: const ConnectOptions(), - roomOptions: const RoomOptions(), + roomOptions: roomOptions, signalClient: client, peerConnectionCreate: MockPeerConnection.create, ); @@ -58,8 +58,19 @@ class E2EContainer { /// that value (used to exercise v1 vs v2 caller paths in self-loop tests). /// When [captureOutbound] is true, all DataPackets sent over the reliable /// data channel are recorded in [capturedDataPackets]. - Future connectRoom({int? localClientProtocol, bool captureOutbound = false}) async { - final connectFuture = room.connect(exampleUri, token); + Future connectRoom({ + int? localClientProtocol, + bool captureOutbound = false, + ConnectOptions? connectOptions, + @Deprecated('mirrors the deprecated Room.connect parameter') RoomOptions? roomOptions, + }) async { + final connectFuture = room.connect( + exampleUri, + token, + connectOptions: connectOptions, + // ignore: deprecated_member_use_from_same_package + roomOptions: roomOptions, + ); Future.delayed(const Duration(milliseconds: 1), () { final resp = _buildJoinResponse(localClientProtocol); wsConnector.onData(resp.writeToBuffer());