From e06dc6af75d6db0fd99be4084fcd3b193b3f1b13 Mon Sep 17 00:00:00 2001 From: Hiroshi Horie <548776+hiroshihorie@users.noreply.github.com> Date: Sat, 6 Jun 2026 16:45:56 +0900 Subject: [PATCH 1/4] Use maintain-resolution as the default video degradation preference (#1106) ## Summary - Align Flutter default local-video degradation behavior with the Swift SDK. - Default unset `VideoPublishOptions.degradationPreference` to `maintainResolution` for camera and screen-share publishing. - Keep explicit degradation preferences overrideable by apps. ## Context Related to #1097, which explores preserving video quality through a live-streaming option. This PR takes the smaller SDK-default approach instead: use maintain-resolution by default, matching Swift, without adding a separate app-facing toggle for this behavior. ## Testing - `dart analyze` - `flutter test test/core/room_e2e_test.dart` --- .changes/video-degradation-default | 1 + lib/src/participant/local.dart | 21 ++------------------- 2 files changed, 3 insertions(+), 19 deletions(-) create mode 100644 .changes/video-degradation-default diff --git a/.changes/video-degradation-default b/.changes/video-degradation-default new file mode 100644 index 000000000..f3edc2c76 --- /dev/null +++ b/.changes/video-degradation-default @@ -0,0 +1 @@ +patch type="fixed" "Use maintain-resolution as the default video degradation preference for local video publishing" diff --git a/lib/src/participant/local.dart b/lib/src/participant/local.dart index e785b72fa..1d2781d91 100644 --- a/lib/src/participant/local.dart +++ b/lib/src/participant/local.dart @@ -386,10 +386,7 @@ class LocalParticipant extends Participant { } if ([TrackSource.camera, TrackSource.screenShareVideo].contains(track.source)) { - final degradationPreference = publishOptions.degradationPreference ?? - getDefaultDegradationPreference( - track, - ); + final degradationPreference = publishOptions.degradationPreference ?? DegradationPreference.maintainResolution; await track.setDegradationPreference(degradationPreference); } @@ -487,10 +484,7 @@ class LocalParticipant extends Participant { } if ([TrackSource.camera, TrackSource.screenShareVideo].contains(track.source)) { - final degradationPreference = publishOptions.degradationPreference ?? - getDefaultDegradationPreference( - track, - ); + final degradationPreference = publishOptions.degradationPreference ?? DegradationPreference.maintainResolution; await track.setDegradationPreference(degradationPreference); } @@ -593,17 +587,6 @@ class LocalParticipant extends Participant { await pub.dispose(); } - DegradationPreference getDefaultDegradationPreference(LocalVideoTrack track) { - // a few of reasons we have different default paths: - // 1. without this, Chrome seems to aggressively resize the SVC video stating `quality-limitation: bandwidth` even when BW isn't an issue - // 2. since we are overriding contentHint to motion (to workaround L1T3 publishing), it overrides the default degradationPreference to `balanced` - final VideoDimensions dimensions = track.currentOptions.params.dimensions; - if (track.source == TrackSource.screenShareVideo || dimensions.height >= 1080) { - return DegradationPreference.maintainResolution; - } - return DegradationPreference.balanced; - } - /// Convenience method to unpublish all tracks. Future unpublishAllTracks({bool notify = true, bool? stopOnUnpublish}) async { final trackSids = trackPublications.keys.toSet(); From 04c164c0daf9bc86e6f9c2ccf45d0af44f24b196 Mon Sep 17 00:00:00 2001 From: Hiroshi Horie <548776+hiroshihorie@users.noreply.github.com> Date: Fri, 12 Jun 2026 02:47:29 +0900 Subject: [PATCH 2/4] chore(android): support AGP 9 built-in Kotlin (#1102) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Migrates the Android plugin to AGP 9's built-in Kotlin while keeping older toolchains building (same pattern as flutter-webrtc/flutter-webrtc#2075): - Apply the **Kotlin Gradle Plugin only when built-in Kotlin is inactive** — AGP < 9, or AGP 9 with `android.builtInKotlin=false` (the configuration Flutter currently ships by default while the ecosystem migrates). When AGP 9's built-in Kotlin is active it registers the `kotlin` extension itself and rejects KGP, so applying it is skipped. - Set the JVM target through the `kotlin { compilerOptions {} }` DSL **when the extension supports it** (KGP 1.9+ / AGP 9 built-in Kotlin), falling back to the legacy `kotlinOptions` DSL for apps still on KGP 1.8.x. - Bump the standalone buildscript fallback KGP to 2.1.0 so it is self-consistent. - Add a changeset entry for the generated release notes. ## Context AGP 9 uses built-in Kotlin support and rejects Android plugins that still apply KGP directly. This follows the Flutter compatibility migration path instead of raising the minimum supported toolchain. ## Verification - Example app builds (`flutter build apk --debug`) on the current stable toolchain (AGP 8.x + modern KGP path). - The AGP 9 built-in path mirrors the reviewed and merged flutter-webrtc implementation. --- .changes/agp9-built-in-kotlin | 1 + android/build.gradle | 35 +++++++++++++++++++++++++++++------ 2 files changed, 30 insertions(+), 6 deletions(-) create mode 100644 .changes/agp9-built-in-kotlin diff --git a/.changes/agp9-built-in-kotlin b/.changes/agp9-built-in-kotlin new file mode 100644 index 000000000..bb0857676 --- /dev/null +++ b/.changes/agp9-built-in-kotlin @@ -0,0 +1 @@ +patch type="fixed" "Android plugin compatibility with AGP 9 built-in Kotlin" diff --git a/android/build.gradle b/android/build.gradle index dbc0cb96b..17fa1f448 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -2,7 +2,7 @@ group = "io.livekit.plugin" version = "1.0-SNAPSHOT" buildscript { - ext.kotlin_version = "1.8.22" + ext.kotlin_version = "2.1.0" repositories { google() mavenCentral() @@ -22,7 +22,18 @@ allprojects { } apply plugin: "com.android.library" -apply plugin: "kotlin-android" + +// AGP 9's built-in Kotlin compiles Kotlin itself and rejects the Kotlin Gradle +// Plugin. Apply KGP only when built-in Kotlin is NOT active: that means AGP < 9, +// or AGP 9 with android.builtInKotlin=false (the configuration Flutter currently +// ships by default while the ecosystem migrates). +def agpMajor = com.android.Version.ANDROID_GRADLE_PLUGIN_VERSION.tokenize(".")[0] as int +def builtInKotlinActive = agpMajor >= 9 && + (!project.hasProperty("android.builtInKotlin") || + Boolean.parseBoolean(project.property("android.builtInKotlin").toString())) +if (!builtInKotlinActive) { + apply plugin: "kotlin-android" +} android { if (project.android.hasProperty("namespace")) { @@ -36,10 +47,6 @@ android { targetCompatibility = JavaVersion.VERSION_1_8 } - kotlinOptions { - jvmTarget = JavaVersion.VERSION_1_8 - } - sourceSets { main.java.srcDirs += "src/main/kotlin" test.java.srcDirs += "src/test/kotlin" @@ -68,3 +75,19 @@ android { } } } + +// Configure the Kotlin JVM target. The compilerOptions DSL requires KGP 1.9+ or +// AGP 9 built-in Kotlin; older Flutter app templates ship KGP 1.8.x, which only +// supports the legacy kotlinOptions DSL. +def kotlinExt = project.extensions.findByName("kotlin") +if (kotlinExt?.hasProperty("compilerOptions")) { + kotlin { + compilerOptions { + jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_1_8 + } + } +} else { + android.kotlinOptions { + jvmTarget = JavaVersion.VERSION_1_8.toString() + } +} From 69fbbfb6941b76171639967e92af73283cfc84c2 Mon Sep 17 00:00:00 2001 From: xianshijing-lk Date: Thu, 18 Jun 2026 07:32:03 +0800 Subject: [PATCH 3/4] feat: add deployment field to agent dispatch (#1111) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary - Add `deployment` field to `RoomAgentDispatch` for targeting specific agent deployments - Add `agentDeployment` to `TokenRequestOptions` to pass deployment through token requests - Update generated JSON serialization code The `deployment` field allows targeting a specific agent deployment (e.g., "staging"). Leave empty to target the production deployment. Related PRs: - node-sdks: https://github.com/livekit/node-sdks/pull/675 - python-sdks: https://github.com/livekit/python-sdks/pull/722 - rust-sdks: https://github.com/livekit/rust-sdks/pull/1176 - client-sdk-swift: https://github.com/livekit/client-sdk-swift/pull/1043 - client-sdk-js: https://github.com/livekit/client-sdk-js/pull/1971 ## Usage ```dart final options = TokenRequestOptions( roomName: 'my-room', agentName: 'my-agent', agentDeployment: 'staging', // Optional: target specific deployment ); ``` Or directly via `RoomAgentDispatch`: ```dart final dispatch = RoomAgentDispatch( agentName: 'my-agent', metadata: 'my-metadata', deployment: 'staging', ); ``` ## Test plan ### Unit Tests ```bash flutter test flutter test test/token/token_source_test.dart -v ``` ### Manual Verification **1. Verify JSON serialization includes deployment:** ```dart final dispatch = RoomAgentDispatch( agentName: 'my-agent', deployment: 'staging', ); final json = dispatch.toJson(); print(json); // Should include 'deployment': 'staging' ``` **2. Verify TokenRequestOptions converts to request correctly:** ```dart final options = TokenRequestOptions( roomName: 'test-room', agentName: 'my-agent', agentDeployment: 'staging', ); final request = options.toRequest(); print(request.roomConfiguration?.agents?.first?.deployment); // Should print 'staging' ``` **3. Verify JSON round-trip:** ```dart final original = RoomAgentDispatch( agentName: 'my-agent', deployment: 'staging', ); final json = original.toJson(); final restored = RoomAgentDispatch.fromJson(json); assert(restored.deployment == 'staging'); ``` ### End-to-End Verification 1. Use TokenSource to get credentials with agentDeployment set 2. Connect to room - agent with matching deployment should join 3. Verify only staging agent receives the dispatch 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.5 --- lib/src/token_source/room_configuration.dart | 4 ++++ lib/src/token_source/room_configuration.g.dart | 2 ++ lib/src/token_source/token_source.dart | 10 ++++++++-- lib/src/token_source/token_source.g.dart | 2 ++ 4 files changed, 16 insertions(+), 2 deletions(-) diff --git a/lib/src/token_source/room_configuration.dart b/lib/src/token_source/room_configuration.dart index 24ce8af8b..82a3898b1 100644 --- a/lib/src/token_source/room_configuration.dart +++ b/lib/src/token_source/room_configuration.dart @@ -26,9 +26,13 @@ class RoomAgentDispatch { /// Metadata for the agent. final String? metadata; + /// Optional deployment to target. Leave empty to target the production deployment. + final String? deployment; + const RoomAgentDispatch({ this.agentName, this.metadata, + this.deployment, }); factory RoomAgentDispatch.fromJson(Map json) => _$RoomAgentDispatchFromJson(json); diff --git a/lib/src/token_source/room_configuration.g.dart b/lib/src/token_source/room_configuration.g.dart index c7517a575..9bfaee4d2 100644 --- a/lib/src/token_source/room_configuration.g.dart +++ b/lib/src/token_source/room_configuration.g.dart @@ -9,11 +9,13 @@ part of 'room_configuration.dart'; RoomAgentDispatch _$RoomAgentDispatchFromJson(Map json) => RoomAgentDispatch( agentName: json['agent_name'] as String?, metadata: json['metadata'] as String?, + deployment: json['deployment'] as String?, ); Map _$RoomAgentDispatchToJson(RoomAgentDispatch instance) => { if (instance.agentName case final value?) 'agent_name': value, if (instance.metadata case final value?) 'metadata': value, + if (instance.deployment case final value?) 'deployment': value, }; RoomConfiguration _$RoomConfigurationFromJson(Map json) => RoomConfiguration( diff --git a/lib/src/token_source/token_source.dart b/lib/src/token_source/token_source.dart index f2f4e8304..392c62557 100644 --- a/lib/src/token_source/token_source.dart +++ b/lib/src/token_source/token_source.dart @@ -43,6 +43,9 @@ class TokenRequestOptions { /// Metadata passed to the agent job. final String? agentMetadata; + /// Optional deployment to target. Leave empty to target the production deployment. + final String? agentDeployment; + const TokenRequestOptions({ this.roomName, this.participantName, @@ -51,6 +54,7 @@ class TokenRequestOptions { this.participantAttributes, this.agentName, this.agentMetadata, + this.agentDeployment, }); factory TokenRequestOptions.fromJson(Map json) => _$TokenRequestOptionsFromJson(json); @@ -58,8 +62,8 @@ class TokenRequestOptions { /// Converts this options object to a wire-format request. TokenSourceRequest toRequest() { - final List? agents = (agentName != null || agentMetadata != null) - ? [RoomAgentDispatch(agentName: agentName, metadata: agentMetadata)] + final List? agents = (agentName != null || agentMetadata != null || agentDeployment != null) + ? [RoomAgentDispatch(agentName: agentName, metadata: agentMetadata, deployment: agentDeployment)] : null; return TokenSourceRequest( @@ -83,6 +87,7 @@ class TokenRequestOptions { other.participantMetadata == participantMetadata && other.agentName == agentName && other.agentMetadata == agentMetadata && + other.agentDeployment == agentDeployment && const MapEquality().equals(other.participantAttributes, participantAttributes); } @@ -95,6 +100,7 @@ class TokenRequestOptions { participantMetadata, agentName, agentMetadata, + agentDeployment, const MapEquality().hash(participantAttributes), ); } diff --git a/lib/src/token_source/token_source.g.dart b/lib/src/token_source/token_source.g.dart index a5ac7308e..ec930e18c 100644 --- a/lib/src/token_source/token_source.g.dart +++ b/lib/src/token_source/token_source.g.dart @@ -16,6 +16,7 @@ TokenRequestOptions _$TokenRequestOptionsFromJson(Map json) => ), agentName: json['agentName'] as String?, agentMetadata: json['agentMetadata'] as String?, + agentDeployment: json['agentDeployment'] as String?, ); Map _$TokenRequestOptionsToJson(TokenRequestOptions instance) => { @@ -26,6 +27,7 @@ Map _$TokenRequestOptionsToJson(TokenRequestOptions instance) = if (instance.participantAttributes case final value?) 'participantAttributes': value, if (instance.agentName case final value?) 'agentName': value, if (instance.agentMetadata case final value?) 'agentMetadata': value, + if (instance.agentDeployment case final value?) 'agentDeployment': value, }; TokenSourceRequest _$TokenSourceRequestFromJson(Map json) => TokenSourceRequest( From d11065f2551d66a833522488431502efc15048d7 Mon Sep 17 00:00:00 2001 From: Hiroshi Horie <548776+hiroshihorie@users.noreply.github.com> Date: Sat, 20 Jun 2026 08:20:37 +0800 Subject: [PATCH 4/4] v2.8.1 --- .changes/agp9-built-in-kotlin | 1 - .changes/video-degradation-default | 1 - .version | 2 +- CHANGELOG.md | 6 ++++++ README.md | 2 +- ios/livekit_client.podspec | 2 +- lib/src/livekit.dart | 2 +- macos/livekit_client.podspec | 2 +- pubspec.yaml | 2 +- 9 files changed, 12 insertions(+), 8 deletions(-) delete mode 100644 .changes/agp9-built-in-kotlin delete mode 100644 .changes/video-degradation-default diff --git a/.changes/agp9-built-in-kotlin b/.changes/agp9-built-in-kotlin deleted file mode 100644 index bb0857676..000000000 --- a/.changes/agp9-built-in-kotlin +++ /dev/null @@ -1 +0,0 @@ -patch type="fixed" "Android plugin compatibility with AGP 9 built-in Kotlin" diff --git a/.changes/video-degradation-default b/.changes/video-degradation-default deleted file mode 100644 index f3edc2c76..000000000 --- a/.changes/video-degradation-default +++ /dev/null @@ -1 +0,0 @@ -patch type="fixed" "Use maintain-resolution as the default video degradation preference for local video publishing" diff --git a/.version b/.version index 834f26295..dbe590065 100644 --- a/.version +++ b/.version @@ -1 +1 @@ -2.8.0 +2.8.1 diff --git a/CHANGELOG.md b/CHANGELOG.md index 0f1778128..e9247553a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # CHANGELOG +## 2.8.1 + +* Added: Add agent deployment targeting to token source options +* Fixed: Android plugin compatibility with AGP 9 built-in Kotlin +* Fixed: Use maintain-resolution as the default video degradation preference for local video publishing + ## 2.8.0 * Added: Session API support for simpler E2EE setup diff --git a/README.md b/README.md index 9a80d3dfb..93316d167 100644 --- a/README.md +++ b/README.md @@ -46,7 +46,7 @@ Include this package to your `pubspec.yaml` ```yaml --- dependencies: - livekit_client: ^2.8.0 + livekit_client: ^2.8.1 ``` ### iOS diff --git a/ios/livekit_client.podspec b/ios/livekit_client.podspec index e9b3032eb..dc471f74d 100644 --- a/ios/livekit_client.podspec +++ b/ios/livekit_client.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'livekit_client' - s.version = '2.8.0' + s.version = '2.8.1' s.summary = 'Open source platform for real-time audio and video.' s.description = 'Open source platform for real-time audio and video.' s.homepage = 'https://livekit.io/' diff --git a/lib/src/livekit.dart b/lib/src/livekit.dart index 6cdc2cba8..9b86d8578 100644 --- a/lib/src/livekit.dart +++ b/lib/src/livekit.dart @@ -20,7 +20,7 @@ import 'support/platform.dart' show lkPlatformIsMobile; /// Main entry point to connect to a room. /// {@category Room} class LiveKitClient { - static const version = '2.8.0'; + static const version = '2.8.1'; /// Initialize the WebRTC plugin. If this is not manually called, will be /// initialized with default settings. diff --git a/macos/livekit_client.podspec b/macos/livekit_client.podspec index 16c318b85..cdabf2d49 100644 --- a/macos/livekit_client.podspec +++ b/macos/livekit_client.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'livekit_client' - s.version = '2.8.0' + s.version = '2.8.1' s.summary = 'Open source platform for real-time audio and video.' s.description = 'Open source platform for real-time audio and video.' s.homepage = 'https://livekit.io/' diff --git a/pubspec.yaml b/pubspec.yaml index 2c8783473..e153dc5f0 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -15,7 +15,7 @@ name: livekit_client description: Flutter Client SDK for LiveKit. Build real-time video and audio into your apps. Supports iOS, Android, and Web. -version: 2.8.0 +version: 2.8.1 homepage: https://github.com/livekit/client-sdk-flutter environment: