From 046bbb482bd2546ddc8ca41c8db87d22d3167a02 Mon Sep 17 00:00:00 2001 From: Hiroshi Horie <548776+hiroshihorie@users.noreply.github.com> Date: Fri, 6 Feb 2026 04:34:17 +0900 Subject: [PATCH 01/15] fix imports --- example/lib/widgets/controls.dart | 2 +- lib/src/agent/agent.dart | 2 +- lib/src/agent/chat/message.dart | 2 +- lib/src/core/engine.dart | 3 ++- lib/src/core/room.dart | 3 +-- lib/src/core/signal_client.dart | 2 +- lib/src/e2ee/e2ee_manager.dart | 4 +++- lib/src/managers/broadcast_manager.dart | 2 +- lib/src/participant/local.dart | 3 ++- lib/src/stats/stats.dart | 2 +- lib/src/support/disposable.dart | 4 +++- lib/src/support/platform/io.dart | 2 +- lib/src/support/platform/web.dart | 2 +- lib/src/track/local/local.dart | 2 +- lib/src/track/local/video.dart | 3 ++- lib/src/track/track.dart | 2 +- lib/src/types/transcription_segment.dart | 2 +- lib/src/utils.dart | 3 ++- lib/src/widgets/screen_select_dialog.dart | 3 ++- lib/src/widgets/video_track_renderer.dart | 2 +- 20 files changed, 29 insertions(+), 21 deletions(-) diff --git a/example/lib/widgets/controls.dart b/example/lib/widgets/controls.dart index e022364b7..fbe8c815e 100644 --- a/example/lib/widgets/controls.dart +++ b/example/lib/widgets/controls.dart @@ -2,7 +2,7 @@ import 'dart:async'; import 'dart:convert'; import 'package:collection/collection.dart'; -import 'package:flutter/foundation.dart'; +import 'package:flutter/foundation.dart' show kIsWeb; import 'package:flutter/material.dart'; import 'package:flutter_background/flutter_background.dart'; import 'package:livekit_client/livekit_client.dart'; diff --git a/lib/src/agent/agent.dart b/lib/src/agent/agent.dart index 4c2ff5bcd..e230829fb 100644 --- a/lib/src/agent/agent.dart +++ b/lib/src/agent/agent.dart @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import 'package:flutter/foundation.dart'; +import 'package:flutter/foundation.dart' show ChangeNotifier; import 'package:collection/collection.dart'; diff --git a/lib/src/agent/chat/message.dart b/lib/src/agent/chat/message.dart index b744fee25..0a01a3743 100644 --- a/lib/src/agent/chat/message.dart +++ b/lib/src/agent/chat/message.dart @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import 'package:flutter/foundation.dart'; +import 'package:meta/meta.dart'; /// A message received from the agent. @immutable diff --git a/lib/src/core/engine.dart b/lib/src/core/engine.dart index 37e846570..e667659d2 100644 --- a/lib/src/core/engine.dart +++ b/lib/src/core/engine.dart @@ -15,8 +15,9 @@ // ignore_for_file: deprecated_member_use_from_same_package import 'dart:async'; +import 'dart:typed_data' show Uint8List; -import 'package:flutter/foundation.dart' hide internal; +import 'package:flutter/foundation.dart' show kDebugMode, kIsWeb; import 'package:collection/collection.dart'; import 'package:connectivity_plus/connectivity_plus.dart'; diff --git a/lib/src/core/room.dart b/lib/src/core/room.dart index 2508063f8..77ede0fb5 100644 --- a/lib/src/core/room.dart +++ b/lib/src/core/room.dart @@ -13,8 +13,7 @@ // limitations under the License. import 'dart:async'; - -import 'package:flutter/foundation.dart' hide internal; +import 'dart:typed_data' show Uint8List; import 'package:collection/collection.dart'; import 'package:http/http.dart' as http; diff --git a/lib/src/core/signal_client.dart b/lib/src/core/signal_client.dart index 3145ddfca..47ce08832 100644 --- a/lib/src/core/signal_client.dart +++ b/lib/src/core/signal_client.dart @@ -15,7 +15,7 @@ import 'dart:async'; import 'dart:collection'; -import 'package:flutter/foundation.dart' hide internal; +import 'package:flutter/foundation.dart' show kIsWeb; import 'package:connectivity_plus/connectivity_plus.dart'; import 'package:fixnum/fixnum.dart'; diff --git a/lib/src/e2ee/e2ee_manager.dart b/lib/src/e2ee/e2ee_manager.dart index b9278d09d..3241a4e81 100644 --- a/lib/src/e2ee/e2ee_manager.dart +++ b/lib/src/e2ee/e2ee_manager.dart @@ -12,7 +12,9 @@ // See the License for the specific language governing permissions and // limitations under the License. -import 'package:flutter/foundation.dart'; +import 'dart:typed_data' show Uint8List; + +import 'package:flutter/foundation.dart' show kDebugMode, kIsWeb; import 'package:flutter_webrtc/flutter_webrtc.dart'; diff --git a/lib/src/managers/broadcast_manager.dart b/lib/src/managers/broadcast_manager.dart index 8d92bb3d1..e42e8eea4 100644 --- a/lib/src/managers/broadcast_manager.dart +++ b/lib/src/managers/broadcast_manager.dart @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import 'package:flutter/foundation.dart' hide internal; +import 'package:flutter/foundation.dart' show ChangeNotifier; import 'package:meta/meta.dart'; diff --git a/lib/src/participant/local.dart b/lib/src/participant/local.dart index 62978adcb..432c7883f 100644 --- a/lib/src/participant/local.dart +++ b/lib/src/participant/local.dart @@ -17,8 +17,9 @@ import 'dart:async'; import 'dart:io'; import 'dart:math'; +import 'dart:typed_data' show Uint8List; -import 'package:flutter/foundation.dart' hide internal; +import 'package:flutter/foundation.dart' show kIsWeb; import 'package:async/async.dart'; import 'package:fixnum/fixnum.dart'; diff --git a/lib/src/stats/stats.dart b/lib/src/stats/stats.dart index a38898467..f75d9ac58 100644 --- a/lib/src/stats/stats.dart +++ b/lib/src/stats/stats.dart @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import 'package:flutter/foundation.dart'; +import 'package:flutter/foundation.dart' show kIsWeb; import 'audio_source_stats.dart'; diff --git a/lib/src/support/disposable.dart b/lib/src/support/disposable.dart index 5d1ebeb5e..70d77015e 100644 --- a/lib/src/support/disposable.dart +++ b/lib/src/support/disposable.dart @@ -14,7 +14,9 @@ import 'dart:async'; -import 'package:flutter/foundation.dart'; +import 'package:flutter/foundation.dart' show ChangeNotifier, VoidCallback; + +import 'package:meta/meta.dart'; import '../extensions.dart'; import '../logger.dart'; diff --git a/lib/src/support/platform/io.dart b/lib/src/support/platform/io.dart index 2a2afa556..af31ba351 100644 --- a/lib/src/support/platform/io.dart +++ b/lib/src/support/platform/io.dart @@ -14,7 +14,7 @@ import 'dart:io'; -import 'package:flutter/foundation.dart'; +import 'package:flutter/foundation.dart' show kIsWeb, kIsWasm; import '../platform.dart'; diff --git a/lib/src/support/platform/web.dart b/lib/src/support/platform/web.dart index f5316dfa0..7fa7f774b 100644 --- a/lib/src/support/platform/web.dart +++ b/lib/src/support/platform/web.dart @@ -15,7 +15,7 @@ import 'dart:js_interop'; import 'dart:js_interop_unsafe'; -import 'package:flutter/foundation.dart'; +import 'package:flutter/foundation.dart' show defaultTargetPlatform, TargetPlatform; import 'package:web/web.dart' as web; diff --git a/lib/src/track/local/local.dart b/lib/src/track/local/local.dart index 981b0717d..2ce82883b 100644 --- a/lib/src/track/local/local.dart +++ b/lib/src/track/local/local.dart @@ -14,7 +14,7 @@ import 'dart:async'; -import 'package:flutter/foundation.dart' hide internal; +import 'package:flutter/foundation.dart' show kIsWeb; import 'package:flutter/material.dart'; import 'package:flutter_webrtc/flutter_webrtc.dart' as rtc; diff --git a/lib/src/track/local/video.dart b/lib/src/track/local/video.dart index 9b8e66b28..b16388895 100644 --- a/lib/src/track/local/video.dart +++ b/lib/src/track/local/video.dart @@ -12,10 +12,11 @@ // See the License for the specific language governing permissions and // limitations under the License. -import 'package:flutter/foundation.dart'; +import 'package:flutter/foundation.dart' show kIsWeb; import 'package:collection/collection.dart'; import 'package:flutter_webrtc/flutter_webrtc.dart' as rtc; +import 'package:meta/meta.dart'; import '../../events.dart'; import '../../exceptions.dart'; diff --git a/lib/src/track/track.dart b/lib/src/track/track.dart index 3c0f09170..92dffc66e 100644 --- a/lib/src/track/track.dart +++ b/lib/src/track/track.dart @@ -14,7 +14,7 @@ import 'dart:async'; -import 'package:flutter/foundation.dart' hide internal; +import 'package:flutter/foundation.dart' show kIsWeb; import 'package:flutter_webrtc/flutter_webrtc.dart' as rtc; import 'package:meta/meta.dart'; diff --git a/lib/src/types/transcription_segment.dart b/lib/src/types/transcription_segment.dart index 37d61aded..0b2b5d0f0 100644 --- a/lib/src/types/transcription_segment.dart +++ b/lib/src/types/transcription_segment.dart @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import 'package:flutter/foundation.dart'; +import 'package:meta/meta.dart'; @immutable class TranscriptionSegment { diff --git a/lib/src/utils.dart b/lib/src/utils.dart index d242e6d90..aa8259ea3 100644 --- a/lib/src/utils.dart +++ b/lib/src/utils.dart @@ -15,8 +15,9 @@ import 'dart:async'; import 'dart:convert'; import 'dart:math' as math; +import 'dart:typed_data' show Uint8List; -import 'package:flutter/foundation.dart' hide internal; +import 'package:flutter/foundation.dart' show kIsWeb, defaultTargetPlatform; import 'package:collection/collection.dart'; import 'package:connectivity_plus/connectivity_plus.dart'; diff --git a/lib/src/widgets/screen_select_dialog.dart b/lib/src/widgets/screen_select_dialog.dart index 16af8b34b..69a69136a 100644 --- a/lib/src/widgets/screen_select_dialog.dart +++ b/lib/src/widgets/screen_select_dialog.dart @@ -13,8 +13,9 @@ // limitations under the License. import 'dart:async'; +import 'dart:typed_data' show Uint8List; -import 'package:flutter/foundation.dart'; +import 'package:flutter/foundation.dart' show kDebugMode; import 'package:flutter/material.dart'; import 'package:flutter_webrtc/flutter_webrtc.dart' as rtc; diff --git a/lib/src/widgets/video_track_renderer.dart b/lib/src/widgets/video_track_renderer.dart index 06f8817bd..95b683bce 100644 --- a/lib/src/widgets/video_track_renderer.dart +++ b/lib/src/widgets/video_track_renderer.dart @@ -15,7 +15,7 @@ import 'dart:async'; import 'dart:math'; -import 'package:flutter/foundation.dart'; +import 'package:flutter/foundation.dart' show kIsWeb; import 'package:flutter/material.dart'; import 'package:flutter_webrtc/flutter_webrtc.dart' as rtc; From 60f04f78a619305a48d6d3023095cfaa6d0652ab Mon Sep 17 00:00:00 2001 From: Hiroshi Horie <548776+hiroshihorie@users.noreply.github.com> Date: Fri, 6 Feb 2026 04:39:18 +0900 Subject: [PATCH 02/15] Create fix-imports --- .changes/fix-imports | 1 + 1 file changed, 1 insertion(+) create mode 100644 .changes/fix-imports diff --git a/.changes/fix-imports b/.changes/fix-imports new file mode 100644 index 000000000..929483765 --- /dev/null +++ b/.changes/fix-imports @@ -0,0 +1 @@ +patch type="fixed" "Use explicit show clauses for foundation imports" From 35d61e4f37d784cf59ebed02fe9575165e976988 Mon Sep 17 00:00:00 2001 From: Hiroshi Horie <548776+hiroshihorie@users.noreply.github.com> Date: Fri, 6 Feb 2026 04:54:47 +0900 Subject: [PATCH 03/15] v13 --- lib/src/core/engine.dart | 7 +++++++ lib/src/extensions.dart | 1 + lib/src/options.dart | 2 +- lib/src/types/other.dart | 1 + 4 files changed, 10 insertions(+), 1 deletion(-) diff --git a/lib/src/core/engine.dart b/lib/src/core/engine.dart index e667659d2..8a6848f22 100644 --- a/lib/src/core/engine.dart +++ b/lib/src/core/engine.dart @@ -1372,9 +1372,16 @@ class Engine extends Disposable with EventsEmittable { await handleReconnect(ClientDisconnectReason.leaveReconnect); break; case lk_rtc.LeaveRequest_Action.RESUME: + fullReconnectOnNext = false; // reconnect immediately instead of waiting for next attempt await handleReconnect(ClientDisconnectReason.leaveReconnect); + break; default: + // Fallback for protocol version < 13: check canReconnect field + if (event.canReconnect) { + fullReconnectOnNext = true; + await handleReconnect(ClientDisconnectReason.leaveReconnect); + } break; } }); diff --git a/lib/src/extensions.dart b/lib/src/extensions.dart index d919da94b..a5d43d7f3 100644 --- a/lib/src/extensions.dart +++ b/lib/src/extensions.dart @@ -66,6 +66,7 @@ extension ProtocolVersionExt on ProtocolVersion { ProtocolVersion.v10: '10', ProtocolVersion.v11: '11', ProtocolVersion.v12: '12', + ProtocolVersion.v13: '13', }[this]!; } diff --git a/lib/src/options.dart b/lib/src/options.dart index af31ce5fd..a18f53a60 100644 --- a/lib/src/options.dart +++ b/lib/src/options.dart @@ -64,7 +64,7 @@ class ConnectOptions { const ConnectOptions({ this.autoSubscribe = true, this.rtcConfiguration = const RTCConfiguration(), - this.protocolVersion = ProtocolVersion.v12, + this.protocolVersion = ProtocolVersion.v13, this.timeouts = Timeouts.defaultTimeouts, }); } diff --git a/lib/src/types/other.dart b/lib/src/types/other.dart index 46f39b07b..35e1cd13d 100644 --- a/lib/src/types/other.dart +++ b/lib/src/types/other.dart @@ -35,6 +35,7 @@ enum ProtocolVersion { v10, v11, v12, + v13, // Regions in leave request, canReconnect obsoleted by action } /// Connection state type used throughout the SDK. From 3c724efa7010ec68e89cfc4efbc162453fd4d9f9 Mon Sep 17 00:00:00 2001 From: Hiroshi Horie <548776+hiroshihorie@users.noreply.github.com> Date: Fri, 6 Feb 2026 14:32:03 +0900 Subject: [PATCH 04/15] update leave request --- lib/src/core/signal_client.dart | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/src/core/signal_client.dart b/lib/src/core/signal_client.dart index 47ce08832..388b40964 100644 --- a/lib/src/core/signal_client.dart +++ b/lib/src/core/signal_client.dart @@ -199,7 +199,11 @@ class SignalClient extends Disposable with EventsEmittable { Future sendLeave() async { _sendRequest(lk_rtc.SignalRequest( - leave: lk_rtc.LeaveRequest(canReconnect: false, reason: lk_models.DisconnectReason.CLIENT_INITIATED))); + leave: lk_rtc.LeaveRequest( + reason: lk_models.DisconnectReason.CLIENT_INITIATED, + // server doesn't process this field, keeping it here to indicate the intent of a full disconnect + action: lk_rtc.LeaveRequest_Action.DISCONNECT, + ))); } // resets internal state to a re-usable state From d18d855417ae000b1bc191ac42a6cb88a016ffaf Mon Sep 17 00:00:00 2001 From: Hiroshi Horie <548776+hiroshihorie@users.noreply.github.com> Date: Fri, 6 Feb 2026 14:52:59 +0900 Subject: [PATCH 05/15] update leave action --- lib/src/core/engine.dart | 47 ++++++++++++++++------------------------ 1 file changed, 19 insertions(+), 28 deletions(-) diff --git a/lib/src/core/engine.dart b/lib/src/core/engine.dart index 8a6848f22..8271c049c 100644 --- a/lib/src/core/engine.dart +++ b/lib/src/core/engine.dart @@ -1355,34 +1355,25 @@ class Engine extends Disposable with EventsEmittable { logger.fine('updating regions'); _regionUrlProvider?.setServerReportedRegions(event.regions!); } - switch (event.action) { - case lk_rtc.LeaveRequest_Action.DISCONNECT: - if (connectionState == ConnectionState.reconnecting) { - logger.warning('[Signal] Received Leave while engine is reconnecting, ignoring...'); - return; - } - await signalClient.cleanUp(); - fullReconnectOnNext = false; - await disconnect(); - events.emit(EngineDisconnectedEvent(reason: event.reason.toSDKType())); - break; - case lk_rtc.LeaveRequest_Action.RECONNECT: - fullReconnectOnNext = true; - // reconnect immediately instead of waiting for next attempt - await handleReconnect(ClientDisconnectReason.leaveReconnect); - break; - case lk_rtc.LeaveRequest_Action.RESUME: - fullReconnectOnNext = false; - // reconnect immediately instead of waiting for next attempt - await handleReconnect(ClientDisconnectReason.leaveReconnect); - break; - default: - // Fallback for protocol version < 13: check canReconnect field - if (event.canReconnect) { - fullReconnectOnNext = true; - await handleReconnect(ClientDisconnectReason.leaveReconnect); - } - break; + if (event.action == lk_rtc.LeaveRequest_Action.RESUME) { + fullReconnectOnNext = false; + // reconnect immediately instead of waiting for next attempt + await handleReconnect(ClientDisconnectReason.leaveReconnect); + } else if (event.action == lk_rtc.LeaveRequest_Action.RECONNECT || + // canReconnect is deprecated in protocol version >= 13 + event.canReconnect) { + fullReconnectOnNext = true; + // reconnect immediately instead of waiting for next attempt + await handleReconnect(ClientDisconnectReason.leaveReconnect); + } else { + if (connectionState == ConnectionState.reconnecting) { + logger.warning('[Signal] Received Leave while engine is reconnecting, ignoring...'); + return; + } + await signalClient.cleanUp(); + fullReconnectOnNext = false; + await disconnect(); + events.emit(EngineDisconnectedEvent(reason: event.reason.toSDKType())); } }); From 6f50e9f8ab004587ab9f985e6758cbdcfd523825 Mon Sep 17 00:00:00 2001 From: Hiroshi Horie <548776+hiroshihorie@users.noreply.github.com> Date: Fri, 6 Feb 2026 14:54:04 +0900 Subject: [PATCH 06/15] changes --- .changes/protocol-v13 | 1 + 1 file changed, 1 insertion(+) create mode 100644 .changes/protocol-v13 diff --git a/.changes/protocol-v13 b/.changes/protocol-v13 new file mode 100644 index 000000000..3f973f1d9 --- /dev/null +++ b/.changes/protocol-v13 @@ -0,0 +1 @@ +minor type="feature" "Support protocol v13 with LeaveRequest action" From 2e066c945066ae386f28762aa61f02bf0f631e8c Mon Sep 17 00:00:00 2001 From: Hiroshi Horie <548776+hiroshihorie@users.noreply.github.com> Date: Fri, 6 Feb 2026 17:59:20 +0900 Subject: [PATCH 07/15] comments and log --- lib/src/core/engine.dart | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/src/core/engine.dart b/lib/src/core/engine.dart index 8271c049c..3babe5123 100644 --- a/lib/src/core/engine.dart +++ b/lib/src/core/engine.dart @@ -1351,21 +1351,25 @@ class Engine extends Disposable with EventsEmittable { token = event.token; }) ..on((event) async { + logger.fine('[Signal] Leave received, action: ${event.action}, reason: ${event.reason}'); if (event.regions != null && _regionUrlProvider != null) { logger.fine('updating regions'); _regionUrlProvider?.setServerReportedRegions(event.regions!); } + // Protocol v13: LeaveRequest.action replaces the deprecated canReconnect boolean. + // canReconnect is still checked for backward compatibility with v12 servers + // (where action defaults to DISCONNECT=0 since it's unset). if (event.action == lk_rtc.LeaveRequest_Action.RESUME) { fullReconnectOnNext = false; // reconnect immediately instead of waiting for next attempt await handleReconnect(ClientDisconnectReason.leaveReconnect); } else if (event.action == lk_rtc.LeaveRequest_Action.RECONNECT || - // canReconnect is deprecated in protocol version >= 13 event.canReconnect) { fullReconnectOnNext = true; // reconnect immediately instead of waiting for next attempt await handleReconnect(ClientDisconnectReason.leaveReconnect); } else { + // DISCONNECT or v12 server with canReconnect=false if (connectionState == ConnectionState.reconnecting) { logger.warning('[Signal] Received Leave while engine is reconnecting, ignoring...'); return; From 5f42b4985ea9e1c23435b23cc3c15b473695ca14 Mon Sep 17 00:00:00 2001 From: Hiroshi Horie <548776+hiroshihorie@users.noreply.github.com> Date: Fri, 6 Feb 2026 18:01:04 +0900 Subject: [PATCH 08/15] format --- lib/src/core/engine.dart | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/src/core/engine.dart b/lib/src/core/engine.dart index 3babe5123..570e3101e 100644 --- a/lib/src/core/engine.dart +++ b/lib/src/core/engine.dart @@ -1363,8 +1363,7 @@ class Engine extends Disposable with EventsEmittable { fullReconnectOnNext = false; // reconnect immediately instead of waiting for next attempt await handleReconnect(ClientDisconnectReason.leaveReconnect); - } else if (event.action == lk_rtc.LeaveRequest_Action.RECONNECT || - event.canReconnect) { + } else if (event.action == lk_rtc.LeaveRequest_Action.RECONNECT || event.canReconnect) { fullReconnectOnNext = true; // reconnect immediately instead of waiting for next attempt await handleReconnect(ClientDisconnectReason.leaveReconnect); From 73cdf40b1e6dba5b0ac0349d15ad3a7c12fb5257 Mon Sep 17 00:00:00 2001 From: Hiroshi Horie <548776+hiroshihorie@users.noreply.github.com> Date: Sat, 21 Feb 2026 19:49:13 +0900 Subject: [PATCH 09/15] fix leave disconnect handling --- lib/src/core/engine.dart | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/lib/src/core/engine.dart b/lib/src/core/engine.dart index edad2fbac..4c03cb916 100644 --- a/lib/src/core/engine.dart +++ b/lib/src/core/engine.dart @@ -1399,18 +1399,15 @@ class Engine extends Disposable with EventsEmittable { await handleReconnect(ClientDisconnectReason.leaveReconnect); } else { // DISCONNECT or v12 server with canReconnect=false - if (connectionState == ConnectionState.reconnecting) { - logger.warning('[Signal] Received Leave while engine is reconnecting, ignoring...'); - return; - } await signalClient.cleanUp(); fullReconnectOnNext = false; - await disconnect(); - events.emit(EngineDisconnectedEvent(reason: event.reason.toSDKType())); + await disconnect(reason: event.reason.toSDKType()); } }); - Future disconnect() async { + Future disconnect({ + DisconnectReason reason = DisconnectReason.clientInitiated, + }) async { _isClosed = true; events.emit(EngineClosingEvent()); if (connectionState == ConnectionState.connected) { @@ -1421,11 +1418,9 @@ class Engine extends Disposable with EventsEmittable { await signalClient.cleanUp(); await _signalListener.cancelAll(); clearPendingReconnect(); - events.emit(EngineDisconnectedEvent( - reason: DisconnectReason.clientInitiated, - )); } await cleanUp(); + events.emit(EngineDisconnectedEvent(reason: reason)); } } From 6bc127e285c07b950d2561e01fe953bb17cd6b3c Mon Sep 17 00:00:00 2001 From: Hiroshi Horie <548776+hiroshihorie@users.noreply.github.com> Date: Sun, 22 Feb 2026 00:26:49 +0900 Subject: [PATCH 10/15] implement v16 --- .changes/protocol-v16 | 1 + example/lib/widgets/controls.dart | 4 +- lib/src/core/engine.dart | 10 +++++ lib/src/core/room.dart | 34 ++++++++++++++++ lib/src/core/signal_client.dart | 28 +++++++++++-- lib/src/events.dart | 10 +++++ lib/src/extensions.dart | 3 ++ lib/src/internal/events.dart | 37 ++++++++++++++++++ lib/src/options.dart | 2 +- lib/src/participant/local.dart | 65 ++++++++++++++++++++++++------- lib/src/types/other.dart | 3 ++ 11 files changed, 177 insertions(+), 20 deletions(-) create mode 100644 .changes/protocol-v16 diff --git a/.changes/protocol-v16 b/.changes/protocol-v16 new file mode 100644 index 000000000..f93c6ae03 --- /dev/null +++ b/.changes/protocol-v16 @@ -0,0 +1 @@ +minor type="feature" "Support protocol v16 with room move and request response handling" diff --git a/example/lib/widgets/controls.dart b/example/lib/widgets/controls.dart index fbe8c815e..3c96b4ebc 100644 --- a/example/lib/widgets/controls.dart +++ b/example/lib/widgets/controls.dart @@ -236,11 +236,11 @@ class _ControlsWidgetState extends State { } if (SimulateScenarioResult.participantMetadata == result) { - widget.room.localParticipant?.setMetadata('new metadata ${widget.room.localParticipant?.identity}'); + await widget.room.localParticipant?.setMetadata('new metadata ${widget.room.localParticipant?.identity}'); } if (SimulateScenarioResult.participantName == result) { - widget.room.localParticipant?.setName('new name for ${widget.room.localParticipant?.identity}'); + await widget.room.localParticipant?.setName('new name for ${widget.room.localParticipant?.identity}'); } await widget.room.sendSimulateScenario( diff --git a/lib/src/core/engine.dart b/lib/src/core/engine.dart index 4c03cb916..2bcb9a128 100644 --- a/lib/src/core/engine.dart +++ b/lib/src/core/engine.dart @@ -1403,6 +1403,16 @@ class Engine extends Disposable with EventsEmittable { fullReconnectOnNext = false; await disconnect(reason: event.reason.toSDKType()); } + }) + ..on((event) async { + events.emit(EngineRequestResponseEvent(response: event.response)); + }) + ..on((event) async { + logger.fine('[Signal] RoomMoved received, room: ${event.response.room.name}'); + if (event.response.hasParticipant()) { + signalClient.participantSid = event.response.participant.sid; + } + events.emit(EngineRoomMovedEvent(response: event.response)); }); Future disconnect({ diff --git a/lib/src/core/room.dart b/lib/src/core/room.dart index b88b2e85e..bbb2c28a5 100644 --- a/lib/src/core/room.dart +++ b/lib/src/core/room.dart @@ -579,6 +579,40 @@ class Room extends DisposableChangeNotifier with EventsEmittable { ..on((event) => _onEngineActiveSpeakersUpdateEvent(event.speakers)) ..on(_onDataMessageEvent) ..on(_onTranscriptionEvent) + ..on((event) { + localParticipant?.handleSignalRequestResponse(event.response); + }) + ..on((event) async { + final response = event.response; + logger.fine('Room moved to: ${response.room.name}'); + + // Update room info + if (response.hasRoom()) { + _metadata = response.room.metadata; + _roomInfo = response.room; + } + + // Disconnect all remote participants + final identities = _remoteParticipants.byIdentity.keys.toList(); + for (final identity in identities) { + await _handleParticipantDisconnect(identity); + } + + // Emit public event + events.emit(RoomMovedEvent(roomName: response.room.name)); + + // Update local participant info + if (response.hasParticipant()) { + await localParticipant?.updateFromInfo(response.participant); + } + + // Add new participants + if (response.otherParticipants.isNotEmpty) { + await _onParticipantUpdateEvent(response.otherParticipants); + } + + notifyListeners(); + }) ..on((event) { _handleAudioPlaybackStarted(); }) diff --git a/lib/src/core/signal_client.dart b/lib/src/core/signal_client.dart index b037960d7..5fb9a07a5 100644 --- a/lib/src/core/signal_client.dart +++ b/lib/src/core/signal_client.dart @@ -57,6 +57,14 @@ class SignalClient extends Disposable with EventsEmittable { int _pingCount = 0; String? participantSid; + int _requestId = 0; + + @internal + int getNextRequestId() { + _requestId += 1; + return _requestId; + } + List _connectivityResult = []; StreamSubscription>? _connectivitySubscription; @@ -338,6 +346,17 @@ class SignalClient extends Disposable with EventsEmittable { case lk_rtc.SignalResponse_Message.reconnect: events.emit(SignalReconnectResponseEvent(response: msg.reconnect)); break; + case lk_rtc.SignalResponse_Message.requestResponse: + logger.fine('received request response: ${msg.requestResponse.reason}'); + events.emit(SignalRequestResponseEvent(response: msg.requestResponse)); + break; + case lk_rtc.SignalResponse_Message.roomMoved: + logger.fine('received room moved: ${msg.roomMoved.room.name}'); + if (msg.roomMoved.token.isNotEmpty) { + events.emit(SignalTokenUpdatedEvent(token: msg.roomMoved.token)); + } + events.emit(SignalRoomMovedEvent(response: msg.roomMoved)); + break; default: logger.warning('received unknown signal message'); } @@ -432,9 +451,12 @@ extension SignalClientRequests on SignalClient { )); @internal - void sendUpdateLocalMetadata(lk_rtc.UpdateParticipantMetadata metadata) => _sendRequest(lk_rtc.SignalRequest( - updateMetadata: metadata, - )); + int sendUpdateLocalMetadata(lk_rtc.UpdateParticipantMetadata metadata) { + final requestId = getNextRequestId(); + metadata.requestId = requestId; + _sendRequest(lk_rtc.SignalRequest(updateMetadata: metadata)); + return requestId; + } @internal void sendUpdateTrackSettings(lk_rtc.UpdateTrackSettings settings) => _sendRequest(lk_rtc.SignalRequest( diff --git a/lib/src/events.dart b/lib/src/events.dart index 7b0eda7e2..2161a7388 100644 --- a/lib/src/events.dart +++ b/lib/src/events.dart @@ -637,3 +637,13 @@ class PreConnectAudioBufferStoppedEvent with RoomEvent { String toString() => '${runtimeType}' '(bufferedSize: ${bufferedSize}, isDataSent: ${isBufferSent})'; } + +/// Fired when the participant has been moved to a different room by the server. +/// Emitted by [Room]. +class RoomMovedEvent with RoomEvent { + final String roomName; + const RoomMovedEvent({required this.roomName}); + + @override + String toString() => '${runtimeType}(roomName: $roomName)'; +} diff --git a/lib/src/extensions.dart b/lib/src/extensions.dart index a5d43d7f3..7c678a86f 100644 --- a/lib/src/extensions.dart +++ b/lib/src/extensions.dart @@ -67,6 +67,9 @@ extension ProtocolVersionExt on ProtocolVersion { ProtocolVersion.v11: '11', ProtocolVersion.v12: '12', ProtocolVersion.v13: '13', + ProtocolVersion.v14: '14', + ProtocolVersion.v15: '15', + ProtocolVersion.v16: '16', }[this]!; } diff --git a/lib/src/internal/events.dart b/lib/src/internal/events.dart index 22fa59709..b4af51238 100644 --- a/lib/src/internal/events.dart +++ b/lib/src/internal/events.dart @@ -543,10 +543,47 @@ class SignalTokenUpdatedEvent with SignalEvent, InternalEvent { String toString() => '${runtimeType}(token: ${token})'; } +@internal +class SignalRequestResponseEvent with SignalEvent, InternalEvent { + final lk_rtc.RequestResponse response; + const SignalRequestResponseEvent({required this.response}); + + @override + String toString() => '${runtimeType}' + '(requestId: ${response.requestId}, reason: ${response.reason})'; +} + +@internal +class SignalRoomMovedEvent with SignalEvent, InternalEvent { + final lk_rtc.RoomMovedResponse response; + const SignalRoomMovedEvent({required this.response}); + + @override + String toString() => '${runtimeType}(room: ${response.room.name})'; +} + // ---------------------------------------------------------------------- // Engine events // ---------------------------------------------------------------------- +@internal +class EngineRequestResponseEvent with EngineEvent, InternalEvent { + final lk_rtc.RequestResponse response; + const EngineRequestResponseEvent({required this.response}); + + @override + String toString() => '${runtimeType}(requestId: ${response.requestId})'; +} + +@internal +class EngineRoomMovedEvent with EngineEvent, InternalEvent { + final lk_rtc.RoomMovedResponse response; + const EngineRoomMovedEvent({required this.response}); + + @override + String toString() => '${runtimeType}(room: ${response.room.name})'; +} + @internal class EngineTrackAddedEvent with EngineEvent, InternalEvent { final rtc.MediaStreamTrack track; diff --git a/lib/src/options.dart b/lib/src/options.dart index a18f53a60..f37cf43de 100644 --- a/lib/src/options.dart +++ b/lib/src/options.dart @@ -64,7 +64,7 @@ class ConnectOptions { const ConnectOptions({ this.autoSubscribe = true, this.rtcConfiguration = const RTCConfiguration(), - this.protocolVersion = ProtocolVersion.v13, + this.protocolVersion = ProtocolVersion.v16, this.timeouts = Timeouts.defaultTimeouts, }); } diff --git a/lib/src/participant/local.dart b/lib/src/participant/local.dart index 2716c703e..fa978ab67 100644 --- a/lib/src/participant/local.dart +++ b/lib/src/participant/local.dart @@ -67,6 +67,9 @@ class LocalParticipant extends Participant { // RPC Pending Responses final Map _pendingResponses = {}; + // Pending signal request responses (keyed by requestId) + final Map> _pendingSignalRequests = {}; + LocalParticipant._({ required Room room, required String sid, @@ -621,30 +624,64 @@ class LocalParticipant extends Participant { /// Sets and updates the metadata of the local participant. /// Note: this requires `CanUpdateOwnMetadata` permission encoded in the token. /// @param metadata - void setMetadata(String metadata) { - room.engine.signalClient.sendUpdateLocalMetadata(lk_rtc.UpdateParticipantMetadata( - name: name, - metadata: metadata, - )); + Future setMetadata(String metadata) { + final requestId = room.engine.signalClient.sendUpdateLocalMetadata( + lk_rtc.UpdateParticipantMetadata( + name: name, + metadata: metadata, + ), + ); + return _waitForRequestResponse(requestId); } /// Sets and updates the attributes of the local participant. /// @attributes key-value pairs to set - void setAttributes(Map attributes) { - room.engine.signalClient.sendUpdateLocalMetadata(lk_rtc.UpdateParticipantMetadata( - attributes: attributes.entries, - )); + Future setAttributes(Map attributes) { + final requestId = room.engine.signalClient.sendUpdateLocalMetadata( + lk_rtc.UpdateParticipantMetadata( + attributes: attributes.entries, + ), + ); + return _waitForRequestResponse(requestId); } /// Sets and updates the name of the local participant. /// Note: this requires `CanUpdateOwnMetadata` permission encoded in the token. /// @param name - void setName(String name) { + Future setName(String name) { super.updateName(name); - room.engine.signalClient.sendUpdateLocalMetadata(lk_rtc.UpdateParticipantMetadata( - name: name, - metadata: metadata, - )); + final requestId = room.engine.signalClient.sendUpdateLocalMetadata( + lk_rtc.UpdateParticipantMetadata( + name: name, + metadata: metadata, + ), + ); + return _waitForRequestResponse(requestId); + } + + Future _waitForRequestResponse(int requestId) { + final completer = Completer(); + _pendingSignalRequests[requestId] = completer; + return completer.future.timeout( + const Duration(seconds: 5), + onTimeout: () { + _pendingSignalRequests.remove(requestId); + }, + ); + } + + @internal + void handleSignalRequestResponse(lk_rtc.RequestResponse response) { + final completer = _pendingSignalRequests.remove(response.requestId); + if (completer != null && !completer.isCompleted) { + if (response.reason != lk_rtc.RequestResponse_Reason.OK) { + completer.completeError( + UnexpectedStateException('Signal request failed: ${response.reason} - ${response.message}'), + ); + } else { + completer.complete(); + } + } } /// A convenience property to get all video tracks. diff --git a/lib/src/types/other.dart b/lib/src/types/other.dart index 35e1cd13d..d6d99d77a 100644 --- a/lib/src/types/other.dart +++ b/lib/src/types/other.dart @@ -36,6 +36,9 @@ enum ProtocolVersion { v11, v12, v13, // Regions in leave request, canReconnect obsoleted by action + v14, + v15, // Non-error signal responses, room move + v16, // Supports moving (full participant move) } /// Connection state type used throughout the SDK. From fea91fda37e1d846cc95e450c46b5947e3b1ee06 Mon Sep 17 00:00:00 2001 From: Hiroshi Horie <548776+hiroshihorie@users.noreply.github.com> Date: Tue, 24 Feb 2026 12:57:03 +0900 Subject: [PATCH 11/15] more optimizations --- lib/src/core/room.dart | 34 +++++++++++++++++----------------- lib/src/participant/local.dart | 13 ++++++++++++- 2 files changed, 29 insertions(+), 18 deletions(-) diff --git a/lib/src/core/room.dart b/lib/src/core/room.dart index bbb2c28a5..d58b116e0 100644 --- a/lib/src/core/room.dart +++ b/lib/src/core/room.dart @@ -391,13 +391,7 @@ class Room extends DisposableChangeNotifier with EventsEmittable { )); }) ..on((event) async { - _metadata = event.room.metadata; - _roomInfo = event.room; - emitWhenConnected(RoomMetadataChangedEvent(metadata: event.room.metadata)); - if (_isRecording != event.room.activeRecording) { - _isRecording = event.room.activeRecording; - emitWhenConnected(RoomRecordingStatusChanged(activeRecording: _isRecording)); - } + _handleRoomUpdate(event.room); }) ..on((event) async { final publication = localParticipant?.trackPublications[event.sid]; @@ -421,17 +415,10 @@ class Room extends DisposableChangeNotifier with EventsEmittable { void _setUpEngineListeners() => _engineListener ..on((event) async { - _roomInfo = event.response.room; - _name = event.response.room.name; - _metadata = event.response.room.metadata; + _handleRoomUpdate(event.response.room); _serverVersion = event.response.serverVersion; _serverRegion = event.response.serverRegion; - if (_isRecording != event.response.room.activeRecording) { - _isRecording = event.response.room.activeRecording; - emitWhenConnected(RoomRecordingStatusChanged(activeRecording: _isRecording)); - } - logger.fine('[Engine] Received JoinResponse, ' 'serverVersion: ${event.response.serverVersion}'); @@ -588,8 +575,7 @@ class Room extends DisposableChangeNotifier with EventsEmittable { // Update room info if (response.hasRoom()) { - _metadata = response.room.metadata; - _roomInfo = response.room; + _handleRoomUpdate(response.room); } // Disconnect all remote participants @@ -1033,6 +1019,20 @@ extension RoomPrivateMethods on Room { _serverRegion = null; } + void _handleRoomUpdate(lk_models.Room room) { + final oldRoom = _roomInfo; + _roomInfo = room; + _name = room.name; + _metadata = room.metadata; + if (oldRoom == null || oldRoom.metadata != room.metadata) { + emitWhenConnected(RoomMetadataChangedEvent(metadata: room.metadata)); + } + if (oldRoom?.activeRecording != room.activeRecording) { + _isRecording = room.activeRecording; + emitWhenConnected(RoomRecordingStatusChanged(activeRecording: _isRecording)); + } + } + @internal void emitWhenConnected(RoomEvent event) { if (connectionState == ConnectionState.connected) { diff --git a/lib/src/participant/local.dart b/lib/src/participant/local.dart index fa978ab67..dcf8b8810 100644 --- a/lib/src/participant/local.dart +++ b/lib/src/participant/local.dart @@ -102,6 +102,15 @@ class LocalParticipant extends Participant { participant.onDispose(() async { BroadcastManager().removeListener(participant._broadcastStateChanged); + // Fail any pending signal requests + for (final completer in participant._pendingSignalRequests.values) { + if (!completer.isCompleted) { + completer.completeError( + UnexpectedStateException('Participant disposed'), + ); + } + } + participant._pendingSignalRequests.clear(); await participant.unpublishAllTracks(); }); @@ -639,6 +648,8 @@ class LocalParticipant extends Participant { Future setAttributes(Map attributes) { final requestId = room.engine.signalClient.sendUpdateLocalMetadata( lk_rtc.UpdateParticipantMetadata( + name: name, + metadata: metadata, attributes: attributes.entries, ), ); @@ -649,7 +660,6 @@ class LocalParticipant extends Participant { /// Note: this requires `CanUpdateOwnMetadata` permission encoded in the token. /// @param name Future setName(String name) { - super.updateName(name); final requestId = room.engine.signalClient.sendUpdateLocalMetadata( lk_rtc.UpdateParticipantMetadata( name: name, @@ -666,6 +676,7 @@ class LocalParticipant extends Participant { const Duration(seconds: 5), onTimeout: () { _pendingSignalRequests.remove(requestId); + throw TimeoutException('Signal request timed out'); }, ); } From 9bb3c424d257f8180e446b2d6484f5064e7aed2e Mon Sep 17 00:00:00 2001 From: Hiroshi Horie <548776+hiroshihorie@users.noreply.github.com> Date: Tue, 24 Feb 2026 22:54:42 +0900 Subject: [PATCH 12/15] fix param reset --- lib/src/core/room.dart | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/src/core/room.dart b/lib/src/core/room.dart index d58b116e0..58f4ae641 100644 --- a/lib/src/core/room.dart +++ b/lib/src/core/room.dart @@ -1013,8 +1013,10 @@ extension RoomPrivateMethods on Room { await NativeAudioManagement.stop(); // reset params + _roomInfo = null; _name = null; _metadata = null; + _isRecording = false; _serverVersion = null; _serverRegion = null; } From 642d7c49e340f2eab8ed1ee708895be9ba16c43a Mon Sep 17 00:00:00 2001 From: Hiroshi Horie <548776+hiroshihorie@users.noreply.github.com> Date: Tue, 24 Feb 2026 22:56:40 +0900 Subject: [PATCH 13/15] update changes --- .changes/protocol-v13 | 1 - .changes/protocol-v16 | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) delete mode 100644 .changes/protocol-v13 diff --git a/.changes/protocol-v13 b/.changes/protocol-v13 deleted file mode 100644 index 3f973f1d9..000000000 --- a/.changes/protocol-v13 +++ /dev/null @@ -1 +0,0 @@ -minor type="feature" "Support protocol v13 with LeaveRequest action" diff --git a/.changes/protocol-v16 b/.changes/protocol-v16 index f93c6ae03..9946efe09 100644 --- a/.changes/protocol-v16 +++ b/.changes/protocol-v16 @@ -1 +1 @@ -minor type="feature" "Support protocol v16 with room move and request response handling" +minor type="feature" "Support up to protocol v16 with room move and request response handling" From f3556dd0fa3effdb63c18b6e609799b759f6739e Mon Sep 17 00:00:00 2001 From: Hiroshi Horie <548776+hiroshihorie@users.noreply.github.com> Date: Fri, 27 Feb 2026 15:13:56 +0900 Subject: [PATCH 14/15] fix --- lib/src/core/room.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/core/room.dart b/lib/src/core/room.dart index 58f4ae641..e81cedf73 100644 --- a/lib/src/core/room.dart +++ b/lib/src/core/room.dart @@ -1026,7 +1026,7 @@ extension RoomPrivateMethods on Room { _roomInfo = room; _name = room.name; _metadata = room.metadata; - if (oldRoom == null || oldRoom.metadata != room.metadata) { + if (oldRoom != null && oldRoom.metadata != room.metadata) { emitWhenConnected(RoomMetadataChangedEvent(metadata: room.metadata)); } if (oldRoom?.activeRecording != room.activeRecording) { From c5c3fbba358e6e3177a921fda2c2efa5e24478f2 Mon Sep 17 00:00:00 2001 From: Hiroshi Horie <548776+hiroshihorie@users.noreply.github.com> Date: Fri, 27 Feb 2026 15:25:58 +0900 Subject: [PATCH 15/15] ref --- lib/src/core/room.dart | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/lib/src/core/room.dart b/lib/src/core/room.dart index e81cedf73..d35317af7 100644 --- a/lib/src/core/room.dart +++ b/lib/src/core/room.dart @@ -390,9 +390,7 @@ class Room extends DisposableChangeNotifier with EventsEmittable { state: publication.subscriptionState, )); }) - ..on((event) async { - _handleRoomUpdate(event.room); - }) + ..on((event) async => _applyRoomUpdate(event.room)) ..on((event) async { final publication = localParticipant?.trackPublications[event.sid]; @@ -415,7 +413,7 @@ class Room extends DisposableChangeNotifier with EventsEmittable { void _setUpEngineListeners() => _engineListener ..on((event) async { - _handleRoomUpdate(event.response.room); + _applyRoomUpdate(event.response.room); _serverVersion = event.response.serverVersion; _serverRegion = event.response.serverRegion; @@ -573,9 +571,9 @@ class Room extends DisposableChangeNotifier with EventsEmittable { final response = event.response; logger.fine('Room moved to: ${response.room.name}'); - // Update room info + // Apply room info from move response if (response.hasRoom()) { - _handleRoomUpdate(response.room); + _applyRoomUpdate(response.room); } // Disconnect all remote participants @@ -1021,7 +1019,9 @@ extension RoomPrivateMethods on Room { _serverRegion = null; } - void _handleRoomUpdate(lk_models.Room room) { + /// Applies room info from server. Skips metadata event on first join + /// since there is no previous state to compare against. + void _applyRoomUpdate(lk_models.Room room) { final oldRoom = _roomInfo; _roomInfo = room; _name = room.name;