From 3fb324405831816d64b8d381c4fd9df4345ef437 Mon Sep 17 00:00:00 2001 From: Hiroshi Horie <548776+hiroshihorie@users.noreply.github.com> Date: Thu, 12 Feb 2026 05:08:29 +0900 Subject: [PATCH 1/5] fix: cache connection state read race --- lib/src/core/engine.dart | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/src/core/engine.dart b/lib/src/core/engine.dart index e667659d..528fd782 100644 --- a/lib/src/core/engine.dart +++ b/lib/src/core/engine.dart @@ -487,11 +487,12 @@ class Engine extends Disposable with EventsEmittable { } Future _publisherEnsureConnected() async { - if ((await publisher?.pc.getConnectionState())?.isConnected() != true) { + final state = await publisher?.pc.getConnectionState(); + if (state?.isConnected() != true) { logger.fine('Publisher is not connected...'); // start negotiation - if (await publisher?.pc.getConnectionState() != rtc.RTCPeerConnectionState.RTCPeerConnectionStateConnecting) { + if (state != rtc.RTCPeerConnectionState.RTCPeerConnectionStateConnecting) { await negotiate(); } if (!lkPlatformIsTest()) { From d7f149cc35300c00ec122fe25c0c4ecadd430775 Mon Sep 17 00:00:00 2001 From: Hiroshi Horie <548776+hiroshihorie@users.noreply.github.com> Date: Thu, 12 Feb 2026 05:09:58 +0900 Subject: [PATCH 2/5] fix: reduce debounce to 20ms to match js sdk --- lib/src/constants.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/constants.dart b/lib/src/constants.dart index 3f2a2934..196f7518 100644 --- a/lib/src/constants.dart +++ b/lib/src/constants.dart @@ -31,7 +31,7 @@ class Timeouts { static const Timeouts defaultTimeouts = Timeouts( connection: Duration(seconds: 10), - debounce: Duration(milliseconds: 100), + debounce: Duration(milliseconds: 20), publish: Duration(seconds: 10), subscribe: Duration(seconds: 10), peerConnection: Duration(seconds: 10), From 57181ae4d4bb5706bf1fb0841240885e72823bce Mon Sep 17 00:00:00 2001 From: Hiroshi Horie <548776+hiroshihorie@users.noreply.github.com> Date: Thu, 12 Feb 2026 05:15:06 +0900 Subject: [PATCH 3/5] fix: use completers to cancel orphaned futures --- lib/src/core/engine.dart | 33 ++++++++++++++++++++++++++------- 1 file changed, 26 insertions(+), 7 deletions(-) diff --git a/lib/src/core/engine.dart b/lib/src/core/engine.dart index 528fd782..c9252d14 100644 --- a/lib/src/core/engine.dart +++ b/lib/src/core/engine.dart @@ -166,7 +166,7 @@ class Engine extends Disposable with EventsEmittable { final TTLMap _reliableReceivedState = TTLMap(30000); bool _isReconnecting = false; - Future? _publisherConnectionFuture; + Completer? _publisherConnectionCompleter; String? _reliableParticipantKey(lk_models.DataPacket packet) { if (packet.hasParticipantSid() && packet.participantSid.isNotEmpty) { @@ -507,15 +507,34 @@ class Engine extends Disposable with EventsEmittable { @internal Future ensurePublisherConnected() async { - _publisherConnectionFuture ??= _publisherEnsureConnected(); + if (_publisherConnectionCompleter != null && !_publisherConnectionCompleter!.isCompleted) { + return _publisherConnectionCompleter!.future; + } + final completer = Completer(); + _publisherConnectionCompleter = completer; try { - await _publisherConnectionFuture; - } catch (_) { - _publisherConnectionFuture = null; + await _publisherEnsureConnected(); + if (!completer.isCompleted) { + completer.complete(); + } + } catch (e) { + if (!completer.isCompleted) { + completer.completeError(e); + } + _publisherConnectionCompleter = null; rethrow; } } + void _resetPublisherConnection() { + final completer = _publisherConnectionCompleter; + if (completer != null && !completer.isCompleted) { + completer + .completeError(ConnectException('Publisher connection reset', reason: ConnectionErrorReason.InternalError)); + } + _publisherConnectionCompleter = null; + } + lk_models.EncryptedPacketPayload? asEncryptablePacket(lk_models.DataPacket packet) { if ([ lk_models.DataPacket_Value.sipDtmf, @@ -653,7 +672,7 @@ class Engine extends Disposable with EventsEmittable { rtc.RTCPeerConnectionState.RTCPeerConnectionStateFailed, rtc.RTCPeerConnectionState.RTCPeerConnectionStateDisconnected ].contains(state)) { - _publisherConnectionFuture = null; + _resetPublisherConnection(); } events.emit(EnginePublisherPeerStateUpdatedEvent( state: state, @@ -1131,7 +1150,7 @@ class Engine extends Disposable with EventsEmittable { await publisher?.dispose(); publisher = null; - _publisherConnectionFuture = null; + _resetPublisherConnection(); await subscriber?.dispose(); subscriber = null; From af0a4eb9594eed7cefb24ca02637463f07b37dcb Mon Sep 17 00:00:00 2001 From: Hiroshi Horie <548776+hiroshihorie@users.noreply.github.com> Date: Thu, 12 Feb 2026 17:07:50 +0900 Subject: [PATCH 4/5] changes --- .changes/fix-publisher-connection-negotiation | 1 + 1 file changed, 1 insertion(+) create mode 100644 .changes/fix-publisher-connection-negotiation diff --git a/.changes/fix-publisher-connection-negotiation b/.changes/fix-publisher-connection-negotiation new file mode 100644 index 00000000..b716bec2 --- /dev/null +++ b/.changes/fix-publisher-connection-negotiation @@ -0,0 +1 @@ +patch type="fixed" "Fix publisher connection causing redundant renegotiations on lower-end devices" From bcd172718065eaa06bf6b8fdec5f2f60d6c10a59 Mon Sep 17 00:00:00 2001 From: Hiroshi Horie <548776+hiroshihorie@users.noreply.github.com> Date: Thu, 12 Feb 2026 18:22:57 +0900 Subject: [PATCH 5/5] return single shared future --- lib/src/core/engine.dart | 38 +++++++++++++++++++++++--------------- 1 file changed, 23 insertions(+), 15 deletions(-) diff --git a/lib/src/core/engine.dart b/lib/src/core/engine.dart index c9252d14..2b1794d6 100644 --- a/lib/src/core/engine.dart +++ b/lib/src/core/engine.dart @@ -506,24 +506,32 @@ class Engine extends Disposable with EventsEmittable { } @internal - Future ensurePublisherConnected() async { - if (_publisherConnectionCompleter != null && !_publisherConnectionCompleter!.isCompleted) { - return _publisherConnectionCompleter!.future; + Future ensurePublisherConnected() { + final existing = _publisherConnectionCompleter; + if (existing != null && !existing.isCompleted) { + return existing.future; } + final completer = Completer(); _publisherConnectionCompleter = completer; - try { - await _publisherEnsureConnected(); - if (!completer.isCompleted) { - completer.complete(); - } - } catch (e) { - if (!completer.isCompleted) { - completer.completeError(e); - } - _publisherConnectionCompleter = null; - rethrow; - } + + unawaited( + _publisherEnsureConnected().then((_) { + if (!completer.isCompleted) { + completer.complete(); + } + }, onError: (Object error, StackTrace stackTrace) { + if (!completer.isCompleted) { + completer.completeError(error, stackTrace); + } + }).whenComplete(() { + if (identical(_publisherConnectionCompleter, completer)) { + _publisherConnectionCompleter = null; + } + }), + ); + + return completer.future; } void _resetPublisherConnection() {